diff --git a/.env.example b/.env.example index e2b72d10..94a12608 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 296f9cdf..19c7ea6a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,7 +18,7 @@ jobs: - name: Run checks run: yarn lint - test: + unit-test: runs-on: ubuntu-latest steps: - name: Checkout code @@ -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: @@ -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: diff --git a/.husky/pre-push b/.husky/pre-push index 8708c007..a669dc62 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1 @@ -npm run test --silent +npm run test:unit -- --silent diff --git a/compose.integration.yml b/compose.integration.yml new file mode 100644 index 00000000..ce3d1f86 --- /dev/null +++ b/compose.integration.yml @@ -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 diff --git a/docker-compose.yml b/compose.yml similarity index 100% rename from docker-compose.yml rename to compose.yml diff --git a/eslint.config.mts b/eslint.config.mts index 6bbb29de..4ddb6074 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -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' + } ] } }, diff --git a/package.json b/package.json index c80d38e1..f07b5b92 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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", @@ -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" diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 674e20d5..00000000 --- a/src/app.js +++ /dev/null @@ -1,102 +0,0 @@ -import parser from 'body-parser'; -import cors from 'cors'; -import express from 'express'; -// import rateLimit from 'express-rate-limit'; -import helmet from 'helmet'; -import morgan from 'morgan'; -import swaggerUi from 'swagger-ui-express'; - -import { assets, upload } from './config/directory'; -import swaggerSpec from './config/swagger'; -import errors from './middlewares/erros-middleware'; -import { generatePreview, reportPreview } from './reports/controller'; -import routes from './routes'; - -const app = express(); - -// Security headers -app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", 'data:', 'https:'], - }, - }, - crossOriginEmbedderPolicy: false, // Disable for file uploads -})); - -// // CORS configuration -// const corsOptions = { -// origin(origin, callback) { -// // Allow requests with no origin (mobile apps, curl, etc.) -// if (!origin) return callback(null, true); - -// const allowedOrigins = [ -// 'http://localhost:3000', -// 'http://localhost:3001', -// 'http://localhost:5173', -// 'https://hcf.cm.utfpr.edu.br', -// 'https://dev.hcf.cm.utfpr.edu.br', -// 'https://api.dev.hcf.cm.utfpr.edu.br', -// ]; - -// if (allowedOrigins.includes(origin)) { -// return callback(null, true); -// } -// return callback(new Error('Not allowed by CORS')); - -// }, -// credentials: true, -// methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], -// allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], -// }; -// app.use(cors(corsOptions)); - -app.use(cors()); - -// Rate limiting -// const limiter = rateLimit({ -// windowMs: 15 * 60 * 1000, // 15 minutes -// max: 500, // Limit each IP to 100 requests per windowMs -// message: { -// error: { -// code: 429, -// message: 'Muitas requisições. Tente novamente em 15 minutos.', -// }, -// }, -// standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers -// legacyHeaders: false, // Disable the `X-RateLimit-*` headers -// }); - -// Apply rate limiting to all requests -// app.use(limiter); - -app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -app.use(parser.json()); -app.use(morgan('dev')); - -app.use('/fotos', express.static(upload)); -app.use('/assets', express.static(assets)); - -app.get('/reports/:fileName', reportPreview); -app.post('/reports/:fileName', generatePreview); - -app.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'); - }, - }), -); - -app.use('/api', routes); - -app.use(errors); - -export default app; diff --git a/src/application/create-app.ts b/src/application/create-app.ts new file mode 100644 index 00000000..fa42ee9e --- /dev/null +++ b/src/application/create-app.ts @@ -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 +} diff --git a/src/application/estado/ListaEstadosController.ts b/src/application/estado/ListaEstadosController.ts new file mode 100644 index 00000000..0f7e8435 --- /dev/null +++ b/src/application/estado/ListaEstadosController.ts @@ -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 { + 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 } + } +} diff --git a/src/application/estado/index.ts b/src/application/estado/index.ts new file mode 100644 index 00000000..383f16f0 --- /dev/null +++ b/src/application/estado/index.ts @@ -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: '/v1/paises/:paisSigla/estados' + } + ] +} diff --git a/src/application/index.ts b/src/application/index.ts new file mode 100644 index 00000000..9053c948 --- /dev/null +++ b/src/application/index.ts @@ -0,0 +1,68 @@ +import cluster from 'node:cluster' +import os from 'node:os' + +import { createKnexInstance } from '@/factory/KnexFactory' +import { ConsoleLogger } from '@/infrastructure/ConsoleLogger' + +import legacyRoutes from '../routes' +import { createApp } from './create-app' + +const environment = process.env.NODE_ENV ?? 'development' + +const corsOrigins = process.env.CORS_ORIGINS ?? '*' +const corsMethods = process.env.CORS_METHODS ?? 'HEAD,GET,POST,PUT,PATCH,DELETE' +const corsAllowedHeaders = process.env.CORS_ALLOWED_HEADERS ?? 'Content-Type,Authorization' + +const logger = new ConsoleLogger() +const application = createApp({ + logger, + cors: { + origins: corsOrigins.split(','), + methods: corsMethods.split(','), + allowedHeaders: corsAllowedHeaders.split(',') + }, + knex: createKnexInstance(), + legacyRouter: legacyRoutes +}) + +async function startServer() { + await application.start(Number(process.env.PORT ?? 3000)) +} + +if (cluster.isPrimary) { + logger.info(`Using "${environment}" environment`) + logger.info(`Master ${process.pid} is running`) + + function forkCluster() { + cluster.on('exit', (worker, code, signal) => { + logger.warn(`Worker ${worker.process.pid} died with ${code} code and ${signal} signal.`) + process.exit(1) + }) + + for (const _ of os.cpus()) { + cluster.fork() + } + } + + async function initServer() { + if (environment === 'production') { + forkCluster() + } else { + await startServer() + } + } + + Promise.resolve() + .then(initServer) + .catch(error => { + logger.error('Failed to start server', error) + process.exit(1) + }) +} else { + Promise.resolve() + .then(startServer) + .catch(error => { + logger.error('Failed to start server', error) + process.exit(1) + }) +} diff --git a/src/application/pais/ListaPaisesController.ts b/src/application/pais/ListaPaisesController.ts new file mode 100644 index 00000000..33b6047a --- /dev/null +++ b/src/application/pais/ListaPaisesController.ts @@ -0,0 +1,31 @@ +import { ListaPaisesUseCase } from '@/domain/pais/ListaPaisesUseCase' +import { + HttpRequest, HttpResponse, StatusCode +} from '@/library/http/common' +import { HttpError } from '@/library/http/error/HttpError' +import { InternalServerError } from '@/library/http/error/InternalServerError' +import { NextHandler, RequestHandler } from '@/library/http/Server' + +interface Dependencies { + listaPaisesUseCase: ListaPaisesUseCase +} + +export class ListaPaisesController implements RequestHandler { + private readonly listaPaisesUseCase: ListaPaisesUseCase + + constructor(dependencies: Dependencies) { + this.listaPaisesUseCase = dependencies.listaPaisesUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const result = await this.listaPaisesUseCase.execute({ + nome: request.params.nome as string | undefined + }) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + return { statusCode: StatusCode.Ok, body: result.value } + } +} diff --git a/src/application/pais/index.ts b/src/application/pais/index.ts new file mode 100644 index 00000000..d1255fca --- /dev/null +++ b/src/application/pais/index.ts @@ -0,0 +1,24 @@ +import { type Knex } from 'knex' + +import { ListaPaisesUseCase } from '@/domain/pais/ListaPaisesUseCase' +import { PaisCollectionKnexAdapter } from '@/infrastructure/PaisCollectionKnexAdapter' +import { Method } from '@/library/http/common' +import { Route } from '@/library/http/Router' + +import { ListaPaisesController } from './ListaPaisesController' + +export function routes(knex: Knex): Route[] { + const paisCollection = new PaisCollectionKnexAdapter({ knex }) + + return [ + { + handlers: [ + new ListaPaisesController({ + listaPaisesUseCase: new ListaPaisesUseCase({ paisCollection }) + }) + ], + method: Method.Get, + path: '/v1/paises' + } + ] +} diff --git a/src/database/apply-migration-service.ts b/src/database/apply-migration-service.ts index d86d0814..29a6aa05 100644 --- a/src/database/apply-migration-service.ts +++ b/src/database/apply-migration-service.ts @@ -1,6 +1,6 @@ import path from 'node:path' -import { Logger } from '@/library/logger' +import { Logger } from '@/library/logger/Logger' import { CorruptedDirectoryError } from './error/corrupted-directory-error' import { MigrationFileSystem } from './migration-file-system' diff --git a/src/database/cli.ts b/src/database/cli.ts index bb5b3122..7bb020c6 100644 --- a/src/database/cli.ts +++ b/src/database/cli.ts @@ -2,7 +2,7 @@ import { program } from 'commander' import createKnex from 'knex' import path from 'node:path' -import { ConsoleLogger } from '@/infrastructure/logger' +import { ConsoleLogger } from '@/infrastructure/ConsoleLogger' import { ApplyMigrationService } from './apply-migration-service' import { CreateMigrationService } from './create-migration-service' diff --git a/src/database/migration-repository.ts b/src/database/migration-repository.ts index 9222bba2..428acb67 100644 --- a/src/database/migration-repository.ts +++ b/src/database/migration-repository.ts @@ -1,6 +1,6 @@ import { Knex } from 'knex' -import { Logger } from '@/library/logger' +import { Logger } from '@/library/logger/Logger' interface Dependencies { knex: Knex diff --git a/src/domain/estado/Estado.ts b/src/domain/estado/Estado.ts new file mode 100644 index 00000000..0c242bdb --- /dev/null +++ b/src/domain/estado/Estado.ts @@ -0,0 +1,26 @@ +import { Either } from '@/library/either/Either' + +export interface Attributes { + id: number + nome: string + sigla: string +} + +export class Estado { + readonly id: number + readonly nome: string + readonly sigla: string + + private constructor(attributes: Attributes) { + this.id = attributes.id + this.nome = attributes.nome + this.sigla = attributes.sigla + } + + static create(attributes: Attributes): Either { + if (!attributes.nome.trim()) { + return Either.left(new Error('Nome do estado não pode ser vazio')) + } + return Either.right(new Estado(attributes)) + } +} diff --git a/src/domain/estado/EstadoCollection.ts b/src/domain/estado/EstadoCollection.ts new file mode 100644 index 00000000..175da859 --- /dev/null +++ b/src/domain/estado/EstadoCollection.ts @@ -0,0 +1,11 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './Estado' + +export interface EstadoFilters { + paisSigla?: string +} + +export interface EstadoCollection { + findAll(filters: EstadoFilters): Promise> +} diff --git a/src/domain/estado/ListaEstadosUseCase.ts b/src/domain/estado/ListaEstadosUseCase.ts new file mode 100644 index 00000000..9531dc13 --- /dev/null +++ b/src/domain/estado/ListaEstadosUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './Estado' +import { EstadoCollection, EstadoFilters } from './EstadoCollection' + +interface Dependencies { + estadoCollection: EstadoCollection +} + +export class ListaEstadosUseCase { + private readonly estadoCollection: EstadoCollection + + constructor(dependencies: Dependencies) { + this.estadoCollection = dependencies.estadoCollection + } + + execute(filters: EstadoFilters): Promise> { + return this.estadoCollection.findAll(filters) + } +} diff --git a/src/domain/pais/ListaPaisesUseCase.ts b/src/domain/pais/ListaPaisesUseCase.ts new file mode 100644 index 00000000..1252b0e7 --- /dev/null +++ b/src/domain/pais/ListaPaisesUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './Pais' +import { PaisCollection, PaisFilters } from './PaisCollection' + +interface Dependencies { + paisCollection: PaisCollection +} + +export class ListaPaisesUseCase { + private readonly paisCollection: PaisCollection + + constructor(dependencies: Dependencies) { + this.paisCollection = dependencies.paisCollection + } + + async execute(filters: PaisFilters): Promise> { + return this.paisCollection.findAll(filters) + } +} diff --git a/src/domain/pais/Pais.ts b/src/domain/pais/Pais.ts new file mode 100644 index 00000000..bc78ddb4 --- /dev/null +++ b/src/domain/pais/Pais.ts @@ -0,0 +1,26 @@ +import { Either } from '@/library/either/Either' + +export interface Attributes { + id: number + nome: string + sigla: string | null +} + +export class Pais { + readonly id: number + readonly nome: string + readonly sigla: string | null + + private constructor(attributes: Attributes) { + this.id = attributes.id + this.nome = attributes.nome + this.sigla = attributes.sigla + } + + static create(attributes: Attributes): Either { + if (!attributes.nome.trim()) { + return Either.left(new Error('Nome do país não pode ser vazio')) + } + return Either.right(new Pais(attributes)) + } +} diff --git a/src/domain/pais/PaisCollection.ts b/src/domain/pais/PaisCollection.ts new file mode 100644 index 00000000..a1619deb --- /dev/null +++ b/src/domain/pais/PaisCollection.ts @@ -0,0 +1,10 @@ +import { Attributes } from '@/domain/pais/Pais' +import { Either } from '@/library/either/Either' + +export interface PaisFilters { + nome?: string +} + +export interface PaisCollection { + findAll(filters: PaisFilters): Promise> +} diff --git a/src/factory/EstadoCollectionFactory.ts b/src/factory/EstadoCollectionFactory.ts new file mode 100644 index 00000000..937c4beb --- /dev/null +++ b/src/factory/EstadoCollectionFactory.ts @@ -0,0 +1,8 @@ +import { EstadoCollectionKnexAdapter } from '@/infrastructure/EstadoCollectionKnexAdapter' +import { singleton } from '@/library/singleton' + +import { createKnexInstance } from './KnexFactory' + +export const createEstadoCollection = singleton(() => { + return new EstadoCollectionKnexAdapter({ knex: createKnexInstance() }) +}) diff --git a/src/factory/KnexFactory.ts b/src/factory/KnexFactory.ts new file mode 100644 index 00000000..2754b5ca --- /dev/null +++ b/src/factory/KnexFactory.ts @@ -0,0 +1,25 @@ +import createKnex, { Knex } from 'knex' + +import { singleton } from '@/library/singleton' + +export const createKnexInstance = singleton((): Knex => { + const { + PG_DATABASE, + PG_HOST, + PG_PASSWORD, + PG_PORT = '5432', + PG_USERNAME + } = process.env + + return createKnex({ + client: 'postgres', + connection: { + database: PG_DATABASE, + host: PG_HOST, + password: PG_PASSWORD, + port: parseInt(PG_PORT), + user: PG_USERNAME + }, + pool: { max: 25, min: 2 } + }) +}) diff --git a/src/factory/PaisCollectionFactory.ts b/src/factory/PaisCollectionFactory.ts new file mode 100644 index 00000000..627147b9 --- /dev/null +++ b/src/factory/PaisCollectionFactory.ts @@ -0,0 +1,8 @@ +import { PaisCollectionKnexAdapter } from '@/infrastructure/PaisCollectionKnexAdapter' +import { singleton } from '@/library/singleton' + +import { createKnexInstance } from './KnexFactory' + +export const createPaisCollection = singleton(() => { + return new PaisCollectionKnexAdapter({ knex: createKnexInstance() }) +}) diff --git a/src/index.js b/src/index.js deleted file mode 100644 index fc000f96..00000000 --- a/src/index.js +++ /dev/null @@ -1,90 +0,0 @@ -/* eslint-disable no-console */ -import './setup'; -import cluster from 'cluster'; -import http from 'http'; -import os from 'os'; - -import app from './app'; -import serverConfig from './config/server'; -import { daemonFazRequisicaoReflora } from './herbarium/reflora/main'; -import { daemonSpeciesLink } from './herbarium/specieslink/main'; - -function startServer() { - return new Promise((resolve, reject) => { - http.createServer(app) - .on('listening', () => { - console.info(`Worker ${process.pid} started on port ${serverConfig.port}`); - resolve(); - }) - .on('error', error => { - if (error.syscall !== 'listen') { - reject(error); - return; - } - - switch (error.code) { - case 'EACCES': - console.error(`Port ${serverConfig.port} requires elevated privileges`); - break; - case 'EADDRINUSE': - console.error(`Port ${serverConfig.port} is already in use`); - break; - default: - break; - } - - reject(error); - }) - .listen(serverConfig.port); - }); -} - -if (cluster.isPrimary) { - console.info(`Using "${serverConfig.environment}" environment`); - console.info(`Master ${process.pid} is running`); - - function forkCluster() { - cluster.on('exit', (worker, code, signal) => { - console.warn(`Worker ${worker.process.pid} died with ${code} code and ${signal} signal.`); - process.exit(1); - }); - - for (let i = 0; i < os.cpus().length; i += 1) { - cluster.fork(); - } - } - - function startExternalJobs() { - /** - * Essas duas daemon são utilizadas, ela são iniciadas juntamente - * com o back end. Essas daemon de tempos em tempos verificam - * se é necessário realizar o processo de atualização do serviço. - */ - daemonFazRequisicaoReflora(); - daemonSpeciesLink(); - console.warn('Started reflora and species link jobs'); - } - - async function initServer() { - if (serverConfig.environment === 'production') { - forkCluster(); - } else { - await startServer(); - } - } - - Promise.resolve() - .then(startExternalJobs) - .then(initServer) - .catch(error => { - console.error(error); - process.exit(1); - }); -} else { - Promise.resolve() - .then(startServer) - .catch(error => { - console.error(error); - process.exit(1); - }); -} diff --git a/src/infrastructure/logger.ts b/src/infrastructure/ConsoleLogger.ts similarity index 94% rename from src/infrastructure/logger.ts rename to src/infrastructure/ConsoleLogger.ts index e31b1aa2..48fc39ac 100644 --- a/src/infrastructure/logger.ts +++ b/src/infrastructure/ConsoleLogger.ts @@ -1,4 +1,4 @@ -import { Logger } from '@/library/logger' +import { Logger } from '@/library/logger/Logger' export class ConsoleLogger implements Logger { private formatMessage(message: string, extraInput?: unknown): string { diff --git a/src/infrastructure/EstadoCollectionKnexAdapter.ts b/src/infrastructure/EstadoCollectionKnexAdapter.ts new file mode 100644 index 00000000..03730eb8 --- /dev/null +++ b/src/infrastructure/EstadoCollectionKnexAdapter.ts @@ -0,0 +1,43 @@ +import { Knex } from 'knex' + +import { Attributes } from '@/domain/estado/Estado' +import { EstadoCollection, EstadoFilters } from '@/domain/estado/EstadoCollection' +import { Either } from '@/library/either/Either' + +import { CollectionError } from './error/CollectionError' + +interface Dependencies { + knex: Knex +} + +export class EstadoCollectionKnexAdapter implements EstadoCollection { + private readonly knex: Knex + + constructor(dependencies: Dependencies) { + this.knex = dependencies.knex + } + + async findAll(filters: EstadoFilters): Promise> { + try { + const query = this.knex('estados') + .select([ + 'estados.id', + 'estados.nome', + 'estados.sigla' + ]) + .orderBy('estados.nome') + + if (filters.paisSigla) { + query + .join('paises', 'estados.pais_id', 'paises.id') + .where('paises.sigla', filters.paisSigla) + } + + return Either.right(await query) + } catch (error) { + return Either.left( + new CollectionError({ message: 'Failed to list estados', cause: error }) + ) + } + } +} diff --git a/src/infrastructure/ExpressApplication.ts b/src/infrastructure/ExpressApplication.ts new file mode 100644 index 00000000..89e8b310 --- /dev/null +++ b/src/infrastructure/ExpressApplication.ts @@ -0,0 +1,169 @@ +import parser from 'body-parser' +import express from 'express' +import http from 'node:http' + +import { Application } from '@/library/Application' +import { + Headers, HttpRequest, HttpResponse, Method +} from '@/library/http/common' +import { HttpError } from '@/library/http/error/HttpError' +import { InternalServerError } from '@/library/http/error/InternalServerError' +import { RequestHandler } from '@/library/http/Server' +import { Logger } from '@/library/logger/Logger' + +interface Dependencies { + logger: Logger +} + +export class ExpressApplication implements Application { + private readonly app: express.Application + private readonly logger: Logger + + readonly server: http.Server + + constructor({ logger }: Dependencies) { + this.app = express() + this.app.use(parser.json()) + this.logger = logger + + this.server = http.createServer(this.app) + } + + use(...args: unknown[]): this { + (this.app.use as (...a: any[]) => void)(...args) + return this + } + + endpoint(method: Method, path: string, ...handlers: RequestHandler[]): this { + this.app[method]( + path, + async (expressRequest: express.Request, expressResponse: express.Response) => { + const params = { + ...expressRequest.params, + ...expressRequest.query + } + const headers: Headers = { + ...expressRequest.headers as Record, + 'Content-Type': expressRequest.header('Content-Type') as Headers['Content-Type'], + 'Content-Length': expressRequest.header('Content-Length') + ? Number(expressRequest.header('Content-Length')) + : 0 + } + + const request: HttpRequest = { + method, + path: expressRequest.path, + headers, + params, + body: expressRequest.body + } + + const handlersClone = [...handlers] + const next = async (): Promise => { + const handler = handlersClone.shift() + if (handler) { + return handler.handle(request, next) + } + return new InternalServerError({ + message: 'No next handler found', + report: 'This usually happens when next() is called without any further handler' + }) + } + + const response = await next() + if (response) { + this.processResponse(expressResponse, response) + } + } + ) + return this + } + + get(path: string, ...handlers: RequestHandler[]): this { + return this.endpoint(Method.Get, path, ...handlers) + } + + post(path: string, ...handlers: RequestHandler[]): this { + return this.endpoint(Method.Post, path, ...handlers) + } + + put(path: string, ...handlers: RequestHandler[]): this { + return this.endpoint(Method.Put, path, ...handlers) + } + + delete(path: string, ...handlers: RequestHandler[]): this { + return this.endpoint(Method.Delete, path, ...handlers) + } + + start(port: number): Promise { + return new Promise((resolve, reject) => { + this.server + .on('listening', () => { + this.logger.info(`Server is running on port ${port}`) + resolve() + }) + .on('error', reject) + .listen(port) + }) + } + + shutdown(): Promise { + return new Promise((resolve, reject) => { + this.server.close(error => { + if (error) { + reject(error) + return + } + this.logger.info('Server shutdown successfully') + resolve() + }) + }) + } + + private processResponse( + expressResponse: express.Response, + response: HttpResponse | HttpError + ): void { + try { + if (response instanceof Error) { + this.logger.error(response.stack ?? response.message) + } + + if (response instanceof HttpError) { + expressResponse.status(response.statusCode).json({ + error: { + statusCode: response.statusCode, + name: response.name, + message: response.message, + report: response.report + } + }) + return + } + + const contentType = response.headers?.['Content-Type'] ?? 'application/json' + const body = response.body + + if (body instanceof Error) { + expressResponse.json({ + error: { name: body.name, message: body.message } + }) + return + } + + if (contentType === 'application/json') { + expressResponse.status(response.statusCode).json(body ?? null) + return + } + + expressResponse.status(response.statusCode).json(body ?? null) + } catch (error) { + this.logger.error(error instanceof Error ? (error.stack ?? error.message) : String(error)) + expressResponse.status(500).json({ + error: { + statusCode: 500, name: 'InternalServerError', message: 'Unexpected error' + } + }) + } + } +} diff --git a/src/infrastructure/PaisCollectionKnexAdapter.ts b/src/infrastructure/PaisCollectionKnexAdapter.ts new file mode 100644 index 00000000..17d51059 --- /dev/null +++ b/src/infrastructure/PaisCollectionKnexAdapter.ts @@ -0,0 +1,39 @@ +import { Knex } from 'knex' + +import { Attributes } from '@/domain/pais/Pais' +import { PaisCollection, PaisFilters } from '@/domain/pais/PaisCollection' +import { Either } from '@/library/either/Either' + +import { CollectionError } from './error/CollectionError' + +interface Dependencies { + knex: Knex +} + +export class PaisCollectionKnexAdapter implements PaisCollection { + private readonly knex: Knex + + constructor(dependencies: Dependencies) { + this.knex = dependencies.knex + } + + async findAll(filters: PaisFilters): Promise> { + try { + const query = this.knex('paises') + .select([ + 'id', + 'nome', + 'sigla' + ]) + .orderBy('nome') + + if (filters.nome) { + query.whereILike('nome', `%${filters.nome}%`) + } + + return Either.right(await query) + } catch (error) { + return Either.left(new CollectionError({ message: 'Failed to list países', cause: error })) + } + } +} diff --git a/src/infrastructure/error/CollectionError.ts b/src/infrastructure/error/CollectionError.ts new file mode 100644 index 00000000..733ab859 --- /dev/null +++ b/src/infrastructure/error/CollectionError.ts @@ -0,0 +1,7 @@ +import { InfrastructureError } from './InfrastructureError' + +export class CollectionError extends InfrastructureError { + constructor(params: { message: string; cause?: unknown }) { + super(params) + } +} diff --git a/src/infrastructure/error/InfrastructureError.ts b/src/infrastructure/error/InfrastructureError.ts new file mode 100644 index 00000000..8b53deee --- /dev/null +++ b/src/infrastructure/error/InfrastructureError.ts @@ -0,0 +1,12 @@ +export class InfrastructureError extends Error { + readonly cause?: unknown + + constructor(params: { message: string; cause?: unknown }) { + super(params.message) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + this.name = this.constructor.name + this.cause = params.cause + } +} diff --git a/src/library/Application.ts b/src/library/Application.ts new file mode 100644 index 00000000..3560eeff --- /dev/null +++ b/src/library/Application.ts @@ -0,0 +1,17 @@ +import http from 'node:http' + +import { Method } from './http/common' +import { RequestHandler } from './http/Server' + +export interface Application { + readonly server: http.Server + + use(...args: unknown[]): this + endpoint(method: Method, path: string, ...handlers: RequestHandler[]): this + get(path: string, ...handlers: RequestHandler[]): this + post(path: string, ...handlers: RequestHandler[]): this + put(path: string, ...handlers: RequestHandler[]): this + delete(path: string, ...handlers: RequestHandler[]): this + start(port: number): Promise + shutdown(): Promise +} diff --git a/src/library/BaseError.ts b/src/library/BaseError.ts new file mode 100644 index 00000000..f3b627e6 --- /dev/null +++ b/src/library/BaseError.ts @@ -0,0 +1,12 @@ +export class BaseError extends Error { + readonly cause?: unknown + + constructor(params: { message: string; cause?: unknown }) { + super(params.message) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + this.name = this.constructor.name + this.cause = params.cause + } +} diff --git a/src/library/either/Either.ts b/src/library/either/Either.ts new file mode 100644 index 00000000..29962e53 --- /dev/null +++ b/src/library/either/Either.ts @@ -0,0 +1,50 @@ +interface EitherLike { + readonly value: T + left(): this is Left + right(): this is Right +} + +export class Left implements EitherLike { + readonly value: T + readonly tag = 'left' + + constructor(value: T) { + this.value = value + } + + left(): this is Left { + return true + } + + right(): this is Right { + return false + } +} + +export class Right implements EitherLike { + readonly value: T + readonly tag = 'right' + + constructor(value: T) { + this.value = value + } + + left(): this is Left { + return false + } + + right(): this is Right { + return true + } +} + +export type Either = Left | Right + +export const Either = Object.freeze({ + right(value: T) { + return new Right(value) + }, + left(value: T) { + return new Left(value) + } +}) diff --git a/src/library/enum.ts b/src/library/enum.ts new file mode 100644 index 00000000..3aa669c1 --- /dev/null +++ b/src/library/enum.ts @@ -0,0 +1,5 @@ +declare global { + type EnumOf = T[keyof T] +} + +export {} diff --git a/src/library/http/Router.ts b/src/library/http/Router.ts new file mode 100644 index 00000000..e4f6eb1b --- /dev/null +++ b/src/library/http/Router.ts @@ -0,0 +1,8 @@ +import { Method } from './common' +import { RequestHandler } from './Server' + +export interface Route { + method: Method + path: string + handlers: RequestHandler[] +} diff --git a/src/library/http/Server.ts b/src/library/http/Server.ts new file mode 100644 index 00000000..791f9b7f --- /dev/null +++ b/src/library/http/Server.ts @@ -0,0 +1,19 @@ +import { + HttpRequest, HttpResponse +} from './common' +import { HttpError } from './error/HttpError' + +export interface NextHandler { + (): Promise | HttpError> +} + +export interface RequestHandler< + RequestBody = unknown, + ResponseBody = unknown, + Params extends Record = Record +> { + handle( + request: HttpRequest, + next: NextHandler, + ): Promise | HttpError> +} diff --git a/src/library/http/common.ts b/src/library/http/common.ts new file mode 100644 index 00000000..8bda7739 --- /dev/null +++ b/src/library/http/common.ts @@ -0,0 +1,59 @@ +import '@/library/enum' + +export const Method = { + Get: 'get', + Post: 'post', + Put: 'put', + Delete: 'delete' +} as const + +export type Method = EnumOf + +export const StatusCode = { + Ok: 200, + Created: 201, + NoContent: 204, + BadRequest: 400, + Unauthorized: 401, + Forbidden: 403, + NotFound: 404, + Conflict: 409, + UnprocessableEntity: 422, + InternalServerError: 500 +} as const + +export type StatusCode = EnumOf + +type HeaderValue = string | number | boolean | null | undefined + +type ContentTypeHeaderValue = + | 'application/json' + | 'application/octet-stream' + | 'application/x-www-form-urlencoded' + | 'multipart/form-data' + | 'text/html' + | 'text/plain' + +export interface Headers { + Authorization?: string + 'Content-Length': number + 'Content-Type': ContentTypeHeaderValue + [name: string]: HeaderValue +} + +export interface HttpRequest< + Body = unknown, + Params extends Record = Record +> { + method: Method + path: string + headers: Headers + params: Params + body: Body +} + +export interface HttpResponse { + statusCode: StatusCode + headers?: Partial + body?: Body +} diff --git a/src/library/http/error/BadRequestError.ts b/src/library/http/error/BadRequestError.ts new file mode 100644 index 00000000..f9465bbc --- /dev/null +++ b/src/library/http/error/BadRequestError.ts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError' + +export class BadRequestError extends HttpError { + constructor(params: { message: string; report?: unknown; cause?: unknown }) { + super({ ...params, statusCode: 400 }) + } +} diff --git a/src/library/http/error/HttpError.ts b/src/library/http/error/HttpError.ts new file mode 100644 index 00000000..a06eea3c --- /dev/null +++ b/src/library/http/error/HttpError.ts @@ -0,0 +1,12 @@ +import { BaseError } from '@/library/BaseError' + +export class HttpError extends BaseError { + readonly statusCode: number + readonly report?: unknown + + constructor(params: { statusCode: number; message: string; report?: unknown; cause?: unknown }) { + super(params) + this.statusCode = params.statusCode + this.report = params.report + } +} diff --git a/src/library/http/error/InternalServerError.ts b/src/library/http/error/InternalServerError.ts new file mode 100644 index 00000000..5dfd81b8 --- /dev/null +++ b/src/library/http/error/InternalServerError.ts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError' + +export class InternalServerError extends HttpError { + constructor(params: { message: string; report?: unknown; cause?: unknown }) { + super({ ...params, statusCode: 500 }) + } +} diff --git a/src/library/http/error/NotFoundError.ts b/src/library/http/error/NotFoundError.ts new file mode 100644 index 00000000..4cb78ea3 --- /dev/null +++ b/src/library/http/error/NotFoundError.ts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError' + +export class NotFoundError extends HttpError { + constructor(params: { message: string; report?: unknown; cause?: unknown }) { + super({ ...params, statusCode: 404 }) + } +} diff --git a/src/library/http/error/UnauthorizedError.ts b/src/library/http/error/UnauthorizedError.ts new file mode 100644 index 00000000..2dff42b7 --- /dev/null +++ b/src/library/http/error/UnauthorizedError.ts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError' + +export class UnauthorizedError extends HttpError { + constructor(params: { message: string; report?: unknown; cause?: unknown }) { + super({ ...params, statusCode: 401 }) + } +} diff --git a/src/library/logger/index.ts b/src/library/logger/Logger.ts similarity index 100% rename from src/library/logger/index.ts rename to src/library/logger/Logger.ts diff --git a/src/library/singleton.ts b/src/library/singleton.ts new file mode 100644 index 00000000..d79d6dfd --- /dev/null +++ b/src/library/singleton.ts @@ -0,0 +1,10 @@ +export function singleton(factory: () => T): () => T { + let instance: null | T = null + + return () => { + if (!instance) { + instance = factory() + } + return instance + } +} diff --git a/src/reports/controller.ts b/src/reports/controller.ts index 47f1f07d..fb83d3f6 100644 --- a/src/reports/controller.ts +++ b/src/reports/controller.ts @@ -10,8 +10,9 @@ export function reportPreview(_: Request, response: Response) { export async function generatePreview(request: Request, response: Response) { try { - const fileName = request.params.fileName as string - const { default: ReportTemplate } = await import(path.join(__dirname, `../reports/templates/${fileName}`)) as { default: ComponentType } + const rawName = request.params.fileName + const fileName = Array.isArray(rawName) ? rawName[0] : rawName + const { default: ReportTemplate } = await import(path.join(__dirname, `../reports/templates/${String(fileName)}`)) as { default: ComponentType } const buffer = await generateReport(ReportTemplate, request.body ?? {}) response.setHeader('Content-Type', 'application/pdf') diff --git a/src/setup.ts b/src/setup.ts deleted file mode 100644 index 9a1976a3..00000000 --- a/src/setup.ts +++ /dev/null @@ -1 +0,0 @@ -import 'dotenv/config' diff --git a/src/utils/scripts/update_coordinates.ts b/src/utils/scripts/update_coordinates.ts index 445ee2c7..629413c2 100644 --- a/src/utils/scripts/update_coordinates.ts +++ b/src/utils/scripts/update_coordinates.ts @@ -18,9 +18,9 @@ const { DATABASE_URL } = process.env -const connectionString - = DATABASE_URL - || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` +const connectionString = + DATABASE_URL + || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` function isValidConnectionString(cs: string | undefined): boolean { return typeof cs === 'string' && cs.length > 0 && !/undefined/.test(cs) diff --git a/src/utils/scripts/update_polygons.ts b/src/utils/scripts/update_polygons.ts index 31e61624..5c60f981 100644 --- a/src/utils/scripts/update_polygons.ts +++ b/src/utils/scripts/update_polygons.ts @@ -33,9 +33,9 @@ const { DATABASE_URL } = process.env -const connectionString - = DATABASE_URL - || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` +const connectionString = + DATABASE_URL + || `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${PG_DATABASE}` function isValidConnectionString(cs: string | undefined): boolean { return typeof cs === 'string' && cs.length > 0 && !/undefined/.test(cs) @@ -45,8 +45,8 @@ if (!isValidConnectionString(connectionString)) { process.exit(1) } -const API_IBGE_POLYGON - = 'https://servicodados.ibge.gov.br/api/v3/malhas/municipios/{codigo_ibge}?formato=application/vnd.geo+json&qualidade=minima' +const API_IBGE_POLYGON = + 'https://servicodados.ibge.gov.br/api/v3/malhas/municipios/{codigo_ibge}?formato=application/vnd.geo+json&qualidade=minima' const MAX_CONCURRENCY = 5 const REQUEST_DELAY_MS = 150 diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 00000000..233f21bc --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,84 @@ +# Integration Tests + +Integration tests run against a real PostgreSQL database. **You are responsible for +starting the container and applying migrations before running the tests.** + +## Prerequisites + +- Docker installed and running +- Node.js dependencies installed (`npm install`) + +## First-time setup + +### 1. Start the test database + +```bash +docker compose -f compose.integration.yml up -d +``` + +This starts a PostgreSQL container on port **5433** using the credentials in `.env`. + +### 2. Apply migrations + +```bash +npm run migration:apply +``` + +This runs the full migration stack against the database defined in `.env`. You only need to +re-run this when new migrations are added. + +## Running the tests + +```bash +npm run test:integration +``` + +The test suite connects to the already-running database and executes all tests +in `test/integration/`. No schema changes are made at test time. + +For watch mode (re-runs on file changes): + +```bash +npm run test:integration:watch +``` + +## Stopping the database + +```bash +docker compose -f compose.integration.yml down +``` + +Since the container uses `tmpfs`, all data is lost when it stops. Start fresh +next time with `docker compose up -d` followed by `migration:apply`. + +--- + +## Writing new integration tests + +Each test file must own its data: + +- **`beforeAll`** — insert only the rows your tests need, using a unique prefix in any + identifier column (sigla, nome, etc.) that distinguishes your rows from other test files. +- **`afterAll`** — delete your rows and call `knex.destroy()` to release the connection pool. +- Never use `TRUNCATE` — it would wipe data owned by other test files running in parallel. + +See `test/integration/pais/lista-paises.test.ts` for a concrete example. + +### Seed helpers + +Reusable seed/cleanup functions live in `test/integration/setup/seeds/`. Create one file per +domain entity. Each file should export: + +| Export | Purpose | +|---|---| +| `seed*(knex)` | Inserts rows and returns them with auto-generated IDs | +| `cleanup*(knex)` | Deletes only the rows owned by that seed file | + +### Namespace convention + +Use a short, unique prefix for identifiers to avoid collisions between test files: + +| Test file | Prefix used | +|---|---| +| `lista-paises.test.ts` | `XPBR`/`XPAR` (pais sigla), `XPAI ` (nome prefix) | +| `lista-estados.test.ts` | `XEBR`/`XEAR` (pais sigla), `XEPR`/`XESP`/`XEBA` (estado sigla) | diff --git a/test/integration/estado/lista-estados.test.ts b/test/integration/estado/lista-estados.test.ts new file mode 100644 index 00000000..97273dd9 --- /dev/null +++ b/test/integration/estado/lista-estados.test.ts @@ -0,0 +1,55 @@ +import { + afterAll, describe, expect, test +} from 'vitest' + +import { createTestApp } from '../setup/app-factory' + +type Pais = { id: number; sigla: string } +type Estado = { id: number; nome: string; sigla: string } + +const returningPais = [ + 'id', + 'sigla' +] as const + +const returningEstado = [ + 'id', + 'nome', + 'sigla' +] as const + +describe('GET /api/v1/paises/:paisSigla/estados', () => { + const { agent, knex } = createTestApp() + + afterAll(() => knex.destroy()) + + test('retorna 200 com os estados do país informado, ordenados por nome', async () => { + const [pais] = await knex('paises') + .insert({ nome: 'XEST Brasil', sigla: 'XEBR' }) + .returning(returningPais) + + const estados = await knex('estados') + .insert([ + { + nome: 'Paraná', pais_id: pais.id, sigla: 'XEPR' + }, + { + nome: 'São Paulo', pais_id: pais.id, sigla: 'XESP' + } + ]) + .returning(returningEstado) + + try { + const response = await agent.get(`/api/v1/paises/${pais.sigla}/estados`).expect(200) + expect(response.body).toEqual(estados) + } finally { + await knex('estados').whereIn('sigla', ['XEPR', 'XESP']).delete() + await knex('paises').where({ sigla: 'XEBR' }).delete() + } + }) + + test('retorna array vazio quando o país não possui estados cadastrados', async () => { + const response = await agent.get('/api/v1/paises/XNOMATCH/estados').expect(200) + expect(response.body).toEqual([]) + }) +}) diff --git a/test/integration/pais/lista-paises.test.ts b/test/integration/pais/lista-paises.test.ts new file mode 100644 index 00000000..87256669 --- /dev/null +++ b/test/integration/pais/lista-paises.test.ts @@ -0,0 +1,53 @@ +import { + afterAll, describe, expect, test +} from 'vitest' + +import { createTestApp } from '../setup/app-factory' + +type Pais = { id: number; nome: string; sigla: string } + +const returning = [ + 'id', + 'nome', + 'sigla' +] as const + +describe('GET /api/v1/paises', () => { + const { agent, knex } = createTestApp() + + afterAll(() => knex.destroy()) + + test('retorna 200 com os países que correspondem ao filtro de nome, ordenados por nome', async () => { + const inserted = await knex('paises') + .insert([ + { nome: 'XPAI Argentina', sigla: 'XPAR' }, + { nome: 'XPAI Brasil', sigla: 'XPBR' } + ]) + .returning(returning) + + try { + const response = await agent.get('/api/v1/paises?nome=XPAI').expect(200) + expect(response.body).toEqual(inserted) + } finally { + await knex('paises').whereIn('sigla', ['XPAR', 'XPBR']).delete() + } + }) + + test('filtra pelo parâmetro nome sem distinção entre maiúsculas e minúsculas', async () => { + const [pais] = await knex('paises') + .insert({ nome: 'XPCI Brasil', sigla: 'XPCB' }) + .returning(returning) + + try { + const response = await agent.get('/api/v1/paises?nome=xpci bra').expect(200) + expect(response.body).toEqual([pais]) + } finally { + await knex('paises').where({ sigla: 'XPCB' }).delete() + } + }) + + test('retorna array vazio quando nenhum país corresponde ao filtro', async () => { + const response = await agent.get('/api/v1/paises?nome=XNOMATCH').expect(200) + expect(response.body).toEqual([]) + }) +}) diff --git a/test/integration/setup/app-factory.ts b/test/integration/setup/app-factory.ts new file mode 100644 index 00000000..238d7cfb --- /dev/null +++ b/test/integration/setup/app-factory.ts @@ -0,0 +1,36 @@ +import knex, { type Knex } from 'knex' +import supertest from 'supertest' + +import { createApp } from '@/application/create-app' +import { ConsoleLogger } from '@/infrastructure/ConsoleLogger' + +function createTestKnex(): Knex { + return knex({ + client: 'postgres', + connection: { + host: process.env.PG_HOST, + port: Number(process.env.PG_PORT), + database: process.env.PG_DATABASE, + user: process.env.PG_USERNAME, + password: process.env.PG_PASSWORD + } + }) +} + +export function createTestApp() { + const knexInstance = createTestKnex() + const application = createApp({ + knex: knexInstance, + logger: new ConsoleLogger(), + cors: { + origins: ['*'], + methods: ['GET'], + allowedHeaders: ['Content-Type'] + } + }) + + return { + agent: supertest(application.server), + knex: knexInstance + } +} diff --git a/test/integration/setup/global-setup.ts b/test/integration/setup/global-setup.ts new file mode 100644 index 00000000..b1841471 --- /dev/null +++ b/test/integration/setup/global-setup.ts @@ -0,0 +1,55 @@ +import createKnex, { Knex } from 'knex' +import { loadEnvFile } from 'node:process' + +try { + loadEnvFile('.env.test') +} catch { + // In CI, environment variables are injected directly into the process +} + +const TABLES = [ + 'estados', + 'paises' +] + +async function truncateTables(knex: Knex): Promise { + await knex.raw(`TRUNCATE TABLE ${TABLES.join(', ')} CASCADE`) +} + +export async function setup(): Promise { + const { + PG_DATABASE, + PG_HOST, + PG_PORT = '5432', + PG_USERNAME, + PG_PASSWORD + } = process.env + + const knex = createKnex({ + client: 'postgres', + connection: { + database: PG_DATABASE, + host: PG_HOST, + port: parseInt(PG_PORT, 10), + user: PG_USERNAME, + password: PG_PASSWORD + } + }) + + try { + await knex.raw('SELECT 1') + await truncateTables(knex) + } catch { + throw new Error( + `Cannot connect to the test database (${PG_HOST}:${PG_PORT}/${PG_DATABASE}). ` + + 'Start the container and apply the schema before running e2e tests — ' + + 'see test/integration/README.md' + ) + } finally { + await knex.destroy().catch(() => undefined) + } +} + +export async function teardown(): Promise { + // Container lifecycle is managed by the developer — see test/e2e/README.md +} diff --git a/test/integration/setup/load-env.ts b/test/integration/setup/load-env.ts new file mode 100644 index 00000000..b9608d2f --- /dev/null +++ b/test/integration/setup/load-env.ts @@ -0,0 +1,4 @@ +import { mkdirSync } from 'node:fs' +import path from 'node:path' + +mkdirSync(path.resolve(process.cwd(), 'uploads'), { recursive: true }) diff --git a/test/integration/setup/schema.sql b/test/integration/setup/schema.sql new file mode 100644 index 00000000..f6214b46 --- /dev/null +++ b/test/integration/setup/schema.sql @@ -0,0 +1,2983 @@ +-- +-- PostgreSQL database dump +-- + +\restrict 6jAa2KinsvKEOg1UbHoVMtEdyuVatRpHS5aFz6F6xH4vaNfTZHZIHyPU01e9TJ8 + +-- Dumped from database version 18.2 (Ubuntu 18.2-1.pgdg24.04+1) +-- Dumped by pg_dump version 18.1 + +-- Started on 2026-06-19 09:50:11 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 5000 (class 0 OID 0) +-- Dependencies: 8 +-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON SCHEMA public IS ''; + + +-- +-- TOC entry 7 (class 2615 OID 34988) +-- Name: topology; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA topology; + + +-- +-- TOC entry 5001 (class 0 OID 0) +-- Dependencies: 7 +-- Name: SCHEMA topology; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON SCHEMA topology IS 'PostGIS Topology schema'; + + +-- +-- TOC entry 2 (class 3079 OID 33906) +-- Name: postgis; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; + + +-- +-- TOC entry 5002 (class 0 OID 0) +-- Dependencies: 2 +-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions'; + + +-- +-- TOC entry 3 (class 3079 OID 34989) +-- Name: postgis_topology; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis_topology WITH SCHEMA topology; + + +-- +-- TOC entry 5003 (class 0 OID 0) +-- Dependencies: 3 +-- Name: EXTENSION postgis_topology; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION postgis_topology IS 'PostGIS topology spatial types and functions'; + + +-- +-- TOC entry 1803 (class 1247 OID 30809) +-- Name: alteracoes_status; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.alteracoes_status AS ENUM ( + 'ESPERANDO', + 'APROVADO', + 'REPROVADO' +); + + +-- +-- TOC entry 1806 (class 1247 OID 30816) +-- Name: colecoes_anexas_tipo; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.colecoes_anexas_tipo AS ENUM ( + 'CARPOTECA', + 'XILOTECA', + 'VIA LIQUIDA' +); + + +-- +-- TOC entry 1809 (class 1247 OID 30824) +-- Name: configuracao_periodicidade; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.configuracao_periodicidade AS ENUM ( + 'MANUAL', + 'SEMANAL', + '1MES', + '2MESES' +); + + +-- +-- TOC entry 1812 (class 1247 OID 30834) +-- Name: configuracao_servico; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.configuracao_servico AS ENUM ( + 'REFLORA', + 'SPECIESLINK' +); + + +-- +-- TOC entry 1815 (class 1247 OID 30840) +-- Name: retirada_exsiccata_tombos_tipo; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.retirada_exsiccata_tombos_tipo AS ENUM ( + 'DOACAO', + 'EMPRESTIMO', + 'PERMUTA' +); + + +-- +-- TOC entry 1818 (class 1247 OID 30849) +-- Name: tombos_situacao; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.tombos_situacao AS ENUM ( + 'REGULAR', + 'PERMUTA', + 'EMPRESTIMO', + 'DOACAO' +); + + +-- +-- TOC entry 995 (class 1255 OID 134326) +-- Name: fn_barcodes_tombo(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.fn_barcodes_tombo(p_hcf bigint) RETURNS text + LANGUAGE sql STABLE + AS $$ + SELECT string_agg('[BARCODE=' || codigo_barra || ']', ' , ') + FROM tombos_fotos + WHERE tombo_hcf = p_hcf; + $$; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- TOC entry 222 (class 1259 OID 30857) +-- Name: alteracoes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.alteracoes ( + id bigint NOT NULL, + usuario_id bigint NOT NULL, + status public.alteracoes_status NOT NULL, + observacao text, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + tombo_hcf bigint NOT NULL, + tombo_json text, + ativo boolean, + identificacao boolean +); + + +-- +-- TOC entry 223 (class 1259 OID 30872) +-- Name: alteracoes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.alteracoes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5004 (class 0 OID 0) +-- Dependencies: 223 +-- Name: alteracoes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.alteracoes_id_seq OWNED BY public.alteracoes.id; + + +-- +-- TOC entry 224 (class 1259 OID 30873) +-- Name: autores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.autores ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + observacao character varying(500), + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 225 (class 1259 OID 30883) +-- Name: autores_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.autores_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5005 (class 0 OID 0) +-- Dependencies: 225 +-- Name: autores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.autores_id_seq OWNED BY public.autores.id; + + +-- +-- TOC entry 226 (class 1259 OID 30884) +-- Name: cidades; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.cidades ( + id bigint NOT NULL, + estado_id bigint NOT NULL, + nome character varying(255) NOT NULL, + latitude double precision, + longitude double precision, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + poligono public.geometry(MultiPolygon,4674) DEFAULT NULL::public.geometry +); + + +-- +-- TOC entry 227 (class 1259 OID 30892) +-- Name: cidades_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.cidades_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5006 (class 0 OID 0) +-- Dependencies: 227 +-- Name: cidades_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.cidades_id_seq OWNED BY public.cidades.id; + + +-- +-- TOC entry 228 (class 1259 OID 30893) +-- Name: colecoes_anexas; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.colecoes_anexas ( + tipo public.colecoes_anexas_tipo NOT NULL, + observacoes text, + id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 229 (class 1259 OID 30904) +-- Name: colecoes_anexas_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.colecoes_anexas_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5007 (class 0 OID 0) +-- Dependencies: 229 +-- Name: colecoes_anexas_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.colecoes_anexas_id_seq OWNED BY public.colecoes_anexas.id; + + +-- +-- TOC entry 230 (class 1259 OID 30905) +-- Name: coletores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.coletores ( + id bigint NOT NULL, + nome character varying(255) NOT NULL, + email character varying(200) DEFAULT NULL::character varying, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 231 (class 1259 OID 30915) +-- Name: coletores_complementares; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.coletores_complementares ( + hcf bigint NOT NULL, + complementares character varying(1000) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- TOC entry 232 (class 1259 OID 30924) +-- Name: coletores_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.coletores_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5008 (class 0 OID 0) +-- Dependencies: 232 +-- Name: coletores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.coletores_id_seq OWNED BY public.coletores.id; + + +-- +-- TOC entry 233 (class 1259 OID 30925) +-- Name: configuracao; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.configuracao ( + id bigint NOT NULL, + hora_inicio character varying(19) NOT NULL, + hora_fim character varying(19) DEFAULT NULL::character varying, + periodicidade public.configuracao_periodicidade, + data_proxima_atualizacao character varying(10) DEFAULT NULL::character varying, + nome_arquivo character varying(50) DEFAULT NULL::character varying, + servico public.configuracao_servico +); + + +-- +-- TOC entry 234 (class 1259 OID 30933) +-- Name: configuracao_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.configuracao_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5009 (class 0 OID 0) +-- Dependencies: 234 +-- Name: configuracao_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.configuracao_id_seq OWNED BY public.configuracao.id; + + +-- +-- TOC entry 235 (class 1259 OID 30934) +-- Name: enderecos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.enderecos ( + id bigint NOT NULL, + logradouro character varying(200) NOT NULL, + numero character varying(10) DEFAULT NULL::character varying, + complemento text, + cidade_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 236 (class 1259 OID 30946) +-- Name: enderecos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.enderecos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5010 (class 0 OID 0) +-- Dependencies: 236 +-- Name: enderecos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.enderecos_id_seq OWNED BY public.enderecos.id; + + +-- +-- TOC entry 237 (class 1259 OID 30947) +-- Name: especies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.especies ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + autor_id bigint, + genero_id bigint, + familia_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 238 (class 1259 OID 30957) +-- Name: especies_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.especies_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5011 (class 0 OID 0) +-- Dependencies: 238 +-- Name: especies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.especies_id_seq OWNED BY public.especies.id; + + +-- +-- TOC entry 239 (class 1259 OID 30958) +-- Name: estados; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.estados ( + id bigint NOT NULL, + nome character varying(255) NOT NULL, + sigla character(4) DEFAULT NULL::bpchar, + pais_id integer NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- TOC entry 240 (class 1259 OID 30967) +-- Name: estados_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.estados_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5012 (class 0 OID 0) +-- Dependencies: 240 +-- Name: estados_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.estados_id_seq OWNED BY public.estados.id; + + +-- +-- TOC entry 241 (class 1259 OID 30968) +-- Name: familias; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.familias ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + reino_id bigint NOT NULL +); + + +-- +-- TOC entry 242 (class 1259 OID 30978) +-- Name: familias_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.familias_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5013 (class 0 OID 0) +-- Dependencies: 242 +-- Name: familias_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.familias_id_seq OWNED BY public.familias.id; + + +-- +-- TOC entry 243 (class 1259 OID 30979) +-- Name: fase_sucessional; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.fase_sucessional ( + numero bigint NOT NULL, + nome character varying(200) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 244 (class 1259 OID 30988) +-- Name: generos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.generos ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + familia_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 245 (class 1259 OID 30998) +-- Name: generos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.generos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5014 (class 0 OID 0) +-- Dependencies: 245 +-- Name: generos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.generos_id_seq OWNED BY public.generos.id; + + +-- +-- TOC entry 246 (class 1259 OID 30999) +-- Name: herbarios; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.herbarios ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + caminho_logotipo text, + sigla character varying(80) NOT NULL, + email character varying(200) DEFAULT NULL::character varying, + endereco_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 247 (class 1259 OID 31012) +-- Name: herbarios_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.herbarios_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5015 (class 0 OID 0) +-- Dependencies: 247 +-- Name: herbarios_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.herbarios_id_seq OWNED BY public.herbarios.id; + + +-- +-- TOC entry 248 (class 1259 OID 31013) +-- Name: historico_acessos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.historico_acessos ( + id bigint NOT NULL, + data_criacao timestamp with time zone NOT NULL, + usuario_id bigint NOT NULL +); + + +-- +-- TOC entry 249 (class 1259 OID 31019) +-- Name: historico_acessos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.historico_acessos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5016 (class 0 OID 0) +-- Dependencies: 249 +-- Name: historico_acessos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.historico_acessos_id_seq OWNED BY public.historico_acessos.id; + + +-- +-- TOC entry 250 (class 1259 OID 31020) +-- Name: identificadores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.identificadores ( + id bigint NOT NULL, + nome character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- TOC entry 251 (class 1259 OID 31027) +-- Name: identificadores_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.identificadores_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5017 (class 0 OID 0) +-- Dependencies: 251 +-- Name: identificadores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.identificadores_id_seq OWNED BY public.identificadores.id; + + +-- +-- TOC entry 252 (class 1259 OID 31028) +-- Name: locais_coleta; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.locais_coleta ( + id bigint NOT NULL, + descricao text, + cidade_id bigint, + fase_sucessional_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + fase_numero bigint +); + + +-- +-- TOC entry 253 (class 1259 OID 31038) +-- Name: locais_coleta_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.locais_coleta_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5018 (class 0 OID 0) +-- Dependencies: 253 +-- Name: locais_coleta_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.locais_coleta_id_seq OWNED BY public.locais_coleta.id; + + +-- +-- TOC entry 254 (class 1259 OID 31039) +-- Name: migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.migrations ( + name character varying(300) NOT NULL, + applied_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 255 (class 1259 OID 31045) +-- Name: paises; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.paises ( + id integer NOT NULL, + nome character varying(255) NOT NULL, + sigla character(4) DEFAULT NULL::bpchar, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- TOC entry 256 (class 1259 OID 31053) +-- Name: paises_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.paises_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5019 (class 0 OID 0) +-- Dependencies: 256 +-- Name: paises_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.paises_id_seq OWNED BY public.paises.id; + + +-- +-- TOC entry 257 (class 1259 OID 31054) +-- Name: reflora; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.reflora ( + id bigint NOT NULL, + cod_barra character varying(12) DEFAULT NULL::character varying, + tombo_json text, + ja_comparou boolean DEFAULT false, + ja_requisitou boolean DEFAULT false, + nro_requisicoes bigint DEFAULT '0'::bigint +); + + +-- +-- TOC entry 258 (class 1259 OID 31064) +-- Name: reflora_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.reflora_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5020 (class 0 OID 0) +-- Dependencies: 258 +-- Name: reflora_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.reflora_id_seq OWNED BY public.reflora.id; + + +-- +-- TOC entry 259 (class 1259 OID 31065) +-- Name: reinos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.reinos ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 260 (class 1259 OID 31074) +-- Name: reinos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.reinos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5021 (class 0 OID 0) +-- Dependencies: 260 +-- Name: reinos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.reinos_id_seq OWNED BY public.reinos.id; + + +-- +-- TOC entry 261 (class 1259 OID 31075) +-- Name: relevos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.relevos ( + id bigint NOT NULL, + nome character varying(300) NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 262 (class 1259 OID 31084) +-- Name: relevos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.relevos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5022 (class 0 OID 0) +-- Dependencies: 262 +-- Name: relevos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.relevos_id_seq OWNED BY public.relevos.id; + + +-- +-- TOC entry 263 (class 1259 OID 31085) +-- Name: remessas; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.remessas ( + id bigint NOT NULL, + observacao text, + data_envio timestamp with time zone, + entidade_destino_id bigint NOT NULL, + herbario_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 264 (class 1259 OID 31097) +-- Name: remessas_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.remessas_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5023 (class 0 OID 0) +-- Dependencies: 264 +-- Name: remessas_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.remessas_id_seq OWNED BY public.remessas.id; + + +-- +-- TOC entry 265 (class 1259 OID 31098) +-- Name: retirada_exsiccata_tombos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.retirada_exsiccata_tombos ( + retirada_exsiccata_id bigint NOT NULL, + tombo_hcf bigint NOT NULL, + tipo public.retirada_exsiccata_tombos_tipo NOT NULL, + data_vencimento timestamp with time zone, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + devolvido boolean DEFAULT false +); + + +-- +-- TOC entry 266 (class 1259 OID 31109) +-- Name: solos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.solos ( + id bigint NOT NULL, + nome character varying(300) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 267 (class 1259 OID 31118) +-- Name: solos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.solos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5024 (class 0 OID 0) +-- Dependencies: 267 +-- Name: solos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.solos_id_seq OWNED BY public.solos.id; + + +-- +-- TOC entry 268 (class 1259 OID 31119) +-- Name: specieslink; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.specieslink ( + id bigint NOT NULL, + cod_barra character varying(12) DEFAULT NULL::character varying, + tombo_json text, + ja_comparou boolean DEFAULT false, + ja_requisitou boolean DEFAULT false, + nro_requisicoes bigint DEFAULT '0'::bigint +); + + +-- +-- TOC entry 269 (class 1259 OID 31129) +-- Name: specieslink_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.specieslink_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5025 (class 0 OID 0) +-- Dependencies: 269 +-- Name: specieslink_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.specieslink_id_seq OWNED BY public.specieslink.id; + + +-- +-- TOC entry 270 (class 1259 OID 31130) +-- Name: sub_especies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sub_especies ( + id bigint NOT NULL, + nome character varying(255) NOT NULL, + especie_id bigint NOT NULL, + genero_id bigint, + familia_id bigint NOT NULL, + autor_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 271 (class 1259 OID 31141) +-- Name: sub_especies_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sub_especies_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5026 (class 0 OID 0) +-- Dependencies: 271 +-- Name: sub_especies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.sub_especies_id_seq OWNED BY public.sub_especies.id; + + +-- +-- TOC entry 272 (class 1259 OID 31142) +-- Name: sub_familias; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sub_familias ( + id bigint NOT NULL, + nome character varying(300) NOT NULL, + familia_id bigint NOT NULL, + autor_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 273 (class 1259 OID 31152) +-- Name: sub_familias_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sub_familias_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5027 (class 0 OID 0) +-- Dependencies: 273 +-- Name: sub_familias_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.sub_familias_id_seq OWNED BY public.sub_familias.id; + + +-- +-- TOC entry 274 (class 1259 OID 31153) +-- Name: telefones; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.telefones ( + id bigint NOT NULL, + numero character varying(200) NOT NULL, + herbario_id bigint NOT NULL +); + + +-- +-- TOC entry 275 (class 1259 OID 31159) +-- Name: telefones_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.telefones_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5028 (class 0 OID 0) +-- Dependencies: 275 +-- Name: telefones_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.telefones_id_seq OWNED BY public.telefones.id; + + +-- +-- TOC entry 276 (class 1259 OID 31160) +-- Name: tipos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tipos ( + id bigint NOT NULL, + nome character varying(250) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 277 (class 1259 OID 31169) +-- Name: tipos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tipos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5029 (class 0 OID 0) +-- Dependencies: 277 +-- Name: tipos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.tipos_id_seq OWNED BY public.tipos.id; + + +-- +-- TOC entry 278 (class 1259 OID 31170) +-- Name: tipos_usuarios; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tipos_usuarios ( + id bigint NOT NULL, + tipo character varying(100) DEFAULT NULL::character varying, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 279 (class 1259 OID 31179) +-- Name: tipos_usuarios_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tipos_usuarios_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5030 (class 0 OID 0) +-- Dependencies: 279 +-- Name: tipos_usuarios_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.tipos_usuarios_id_seq OWNED BY public.tipos_usuarios.id; + + +-- +-- TOC entry 280 (class 1259 OID 31180) +-- Name: tombo_alteracoes_antigas; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tombo_alteracoes_antigas ( + sequencia bigint NOT NULL, + data date, + descricao text, + tombo_hcf bigint NOT NULL +); + + +-- +-- TOC entry 281 (class 1259 OID 31187) +-- Name: tombos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tombos ( + hcf bigint NOT NULL, + data_tombo timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + data_coleta_dia bigint, + observacao text, + nomes_populares text, + numero_coleta bigint, + latitude double precision, + longitude double precision, + altitude double precision, + entidade_id bigint, + local_coleta_id bigint, + variedade_id bigint, + tipo_id bigint, + data_identificacao_dia smallint, + data_identificacao_mes smallint, + data_identificacao_ano integer, + situacao public.tombos_situacao DEFAULT 'REGULAR'::public.tombos_situacao, + especie_id bigint, + genero_id bigint, + familia_id bigint, + sub_familia_id bigint, + sub_especie_id bigint, + nome_cientifico text, + colecao_anexa_id bigint, + data_coleta_mes bigint, + data_coleta_ano bigint, + solo_id bigint, + relevo_id bigint, + vegetacao_id bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + ativo boolean DEFAULT true, + taxon character varying(45) DEFAULT NULL::character varying, + rascunho boolean DEFAULT false, + coletor_id bigint, + descricao text, + unicata boolean, + cidade_id bigint, + fase_sucessional_id bigint +); + + +-- +-- TOC entry 282 (class 1259 OID 31202) +-- Name: tombos_fotos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tombos_fotos ( + id bigint NOT NULL, + tombo_hcf bigint NOT NULL, + codigo_barra character varying(45) DEFAULT ''::character varying, + num_barra character varying(45) DEFAULT ''::character varying, + caminho_foto text, + em_vivo boolean DEFAULT false NOT NULL, + sequencia bigint, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 283 (class 1259 OID 31217) +-- Name: tombos_fotos_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tombos_fotos_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5031 (class 0 OID 0) +-- Dependencies: 283 +-- Name: tombos_fotos_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.tombos_fotos_id_seq OWNED BY public.tombos_fotos.id; + + +-- +-- TOC entry 284 (class 1259 OID 31218) +-- Name: tombos_hcf_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tombos_hcf_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5032 (class 0 OID 0) +-- Dependencies: 284 +-- Name: tombos_hcf_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.tombos_hcf_seq OWNED BY public.tombos.hcf; + + +-- +-- TOC entry 285 (class 1259 OID 31219) +-- Name: tombos_identificadores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tombos_identificadores ( + identificador_id bigint NOT NULL, + tombo_hcf bigint NOT NULL, + ordem smallint DEFAULT '1'::smallint +); + + +-- +-- TOC entry 286 (class 1259 OID 31225) +-- Name: usuarios; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.usuarios ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + ra character varying(45) DEFAULT NULL::character varying, + email character varying(200) NOT NULL, + senha character varying(200) NOT NULL, + tipo_usuario_id bigint NOT NULL, + telefone character varying(45) DEFAULT NULL::character varying, + herbario_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + token_troca_senha character varying(255) DEFAULT NULL::character varying, + token_troca_senha_expiracao timestamp with time zone +); + + +-- +-- TOC entry 287 (class 1259 OID 31243) +-- Name: usuarios_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.usuarios_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5033 (class 0 OID 0) +-- Dependencies: 287 +-- Name: usuarios_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.usuarios_id_seq OWNED BY public.usuarios.id; + + +-- +-- TOC entry 288 (class 1259 OID 31244) +-- Name: variedades; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.variedades ( + id bigint NOT NULL, + nome character varying(200) NOT NULL, + autor_id bigint, + especie_id bigint NOT NULL, + genero_id bigint, + familia_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 289 (class 1259 OID 31255) +-- Name: variedades_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.variedades_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5034 (class 0 OID 0) +-- Dependencies: 289 +-- Name: variedades_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.variedades_id_seq OWNED BY public.variedades.id; + + +-- +-- TOC entry 290 (class 1259 OID 31256) +-- Name: vegetacoes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vegetacoes ( + id bigint NOT NULL, + nome character varying(300) NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- TOC entry 291 (class 1259 OID 31265) +-- Name: vegetacoes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vegetacoes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 5035 (class 0 OID 0) +-- Dependencies: 291 +-- Name: vegetacoes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.vegetacoes_id_seq OWNED BY public.vegetacoes.id; + + +-- +-- TOC entry 303 (class 1259 OID 251005) +-- Name: vw_splinker; Type: VIEW; Schema: public; Owner: - +-- + +CREATE VIEW public.vw_splinker AS + SELECT 'Plantae'::text AS "Kingdom", + ''::text AS "Phylum", + ''::text AS "Class", + ''::text AS "Ordem", + CASE + WHEN (lower((f.nome)::text) = 'indeterminada'::text) THEN ''::character varying + ELSE COALESCE(f.nome, ''::character varying) + END AS "Family", + COALESCE(g.nome, ''::character varying) AS "Genus", + COALESCE(e.nome, ''::character varying) AS "Species", + COALESCE(se.nome, ''::character varying) AS "Subspecies", + COALESCE(a.nome, ''::character varying) AS "ScientificNameAuthor", + COALESCE(t.nomes_populares, ''::text) AS "CommonName", + ''::text AS "FieldNumber", + t.hcf AS "CatalogNumber", + ''::text AS "PreviousCatalogNumber", + 'PreservedSpecimen'::text AS "BasisOfRecord", + COALESCE(tp.nome, ''::character varying) AS "TypeStatus", + ''::text AS "PreparationType", + ''::text AS "IndividualCount", + ''::text AS "Sex", + ''::text AS "LifeStage", + COALESCE((t.data_coleta_dia)::text, ''::text) AS "DayCollected", + COALESCE((t.data_coleta_mes)::text, ''::text) AS "MonthCollected", + COALESCE((t.data_coleta_ano)::text, ''::text) AS "YearCollected", + (((COALESCE(col.nome, ''::character varying))::text || ' '::text) || (COALESCE(cc.complementares, ''::character varying))::text) AS "Collector", + COALESCE((t.numero_coleta)::text, ''::text) AS "CollectorNumber", + ''::text AS "Continent", + COALESCE(p.nome, ''::character varying) AS "Country", + COALESCE(TRIM(BOTH FROM est.sigla), ''::text) AS "StateProvince", + COALESCE(c.nome, ''::character varying) AS "County", + COALESCE(lc.descricao, ''::text) AS "Locality", + t.latitude AS "VerbatimLatitude", + t.longitude AS "VerbatimLongitude", + COALESCE((t.altitude)::text, ''::text) AS "VerbatimElevation", + ''::text AS "VerbatimDepth", + COALESCE((t.data_identificacao_dia)::text, ''::text) AS "DayIdentified", + COALESCE((t.data_identificacao_mes)::text, ''::text) AS "MonthIdentified", + COALESCE((t.data_identificacao_ano)::text, ''::text) AS "YearIdentified", + COALESCE(( SELECT string_agg((ident.nome)::text, ';'::text) AS string_agg + FROM (public.tombos_identificadores ti + JOIN public.identificadores ident ON ((ti.identificador_id = ident.id))) + WHERE (ti.tombo_hcf = t.hcf)), ''::text) AS "IdentifiedBy", + ''::text AS "RelatedCatalogItem", + ''::text AS "RelationShipType", + concat_ws(' '::text, public.fn_barcodes_tombo(t.hcf), t.descricao) AS "Notes" + FROM (((((((((((((public.tombos t + LEFT JOIN public.locais_coleta lc ON ((t.local_coleta_id = lc.id))) + LEFT JOIN public.cidades c ON ((t.cidade_id = c.id))) + LEFT JOIN public.estados est ON ((c.estado_id = est.id))) + LEFT JOIN public.paises p ON ((est.pais_id = p.id))) + LEFT JOIN public.familias f ON ((t.familia_id = f.id))) + LEFT JOIN public.reinos r ON ((f.reino_id = r.id))) + LEFT JOIN public.generos g ON ((t.genero_id = g.id))) + LEFT JOIN public.especies e ON ((t.especie_id = e.id))) + LEFT JOIN public.sub_especies se ON ((se.id = t.sub_especie_id))) + LEFT JOIN public.autores a ON ((e.autor_id = a.id))) + LEFT JOIN public.coletores col ON ((t.coletor_id = col.id))) + LEFT JOIN public.coletores_complementares cc ON ((cc.hcf = t.hcf))) + LEFT JOIN public.tipos tp ON ((t.tipo_id = tp.id))); + + +-- +-- TOC entry 4528 (class 2604 OID 32591) +-- Name: alteracoes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alteracoes ALTER COLUMN id SET DEFAULT nextval('public.alteracoes_id_seq'::regclass); + + +-- +-- TOC entry 4531 (class 2604 OID 32590) +-- Name: autores id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.autores ALTER COLUMN id SET DEFAULT nextval('public.autores_id_seq'::regclass); + + +-- +-- TOC entry 4534 (class 2604 OID 32588) +-- Name: cidades id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cidades ALTER COLUMN id SET DEFAULT nextval('public.cidades_id_seq'::regclass); + + +-- +-- TOC entry 4538 (class 2604 OID 32592) +-- Name: colecoes_anexas id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.colecoes_anexas ALTER COLUMN id SET DEFAULT nextval('public.colecoes_anexas_id_seq'::regclass); + + +-- +-- TOC entry 4541 (class 2604 OID 32589) +-- Name: coletores id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.coletores ALTER COLUMN id SET DEFAULT nextval('public.coletores_id_seq'::regclass); + + +-- +-- TOC entry 4547 (class 2604 OID 32593) +-- Name: configuracao id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.configuracao ALTER COLUMN id SET DEFAULT nextval('public.configuracao_id_seq'::regclass); + + +-- +-- TOC entry 4551 (class 2604 OID 32600) +-- Name: enderecos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.enderecos ALTER COLUMN id SET DEFAULT nextval('public.enderecos_id_seq'::regclass); + + +-- +-- TOC entry 4555 (class 2604 OID 32594) +-- Name: especies id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.especies ALTER COLUMN id SET DEFAULT nextval('public.especies_id_seq'::regclass); + + +-- +-- TOC entry 4558 (class 2604 OID 32597) +-- Name: estados id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.estados ALTER COLUMN id SET DEFAULT nextval('public.estados_id_seq'::regclass); + + +-- +-- TOC entry 4562 (class 2604 OID 32601) +-- Name: familias id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.familias ALTER COLUMN id SET DEFAULT nextval('public.familias_id_seq'::regclass); + + +-- +-- TOC entry 4567 (class 2604 OID 32602) +-- Name: generos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.generos ALTER COLUMN id SET DEFAULT nextval('public.generos_id_seq'::regclass); + + +-- +-- TOC entry 4570 (class 2604 OID 32604) +-- Name: herbarios id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.herbarios ALTER COLUMN id SET DEFAULT nextval('public.herbarios_id_seq'::regclass); + + +-- +-- TOC entry 4574 (class 2604 OID 32596) +-- Name: historico_acessos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.historico_acessos ALTER COLUMN id SET DEFAULT nextval('public.historico_acessos_id_seq'::regclass); + + +-- +-- TOC entry 4575 (class 2604 OID 32605) +-- Name: identificadores id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.identificadores ALTER COLUMN id SET DEFAULT nextval('public.identificadores_id_seq'::regclass); + + +-- +-- TOC entry 4578 (class 2604 OID 32618) +-- Name: locais_coleta id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.locais_coleta ALTER COLUMN id SET DEFAULT nextval('public.locais_coleta_id_seq'::regclass); + + +-- +-- TOC entry 4582 (class 2604 OID 32603) +-- Name: paises id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.paises ALTER COLUMN id SET DEFAULT nextval('public.paises_id_seq'::regclass); + + +-- +-- TOC entry 4586 (class 2604 OID 32598) +-- Name: reflora id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reflora ALTER COLUMN id SET DEFAULT nextval('public.reflora_id_seq'::regclass); + + +-- +-- TOC entry 4591 (class 2604 OID 32599) +-- Name: reinos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reinos ALTER COLUMN id SET DEFAULT nextval('public.reinos_id_seq'::regclass); + + +-- +-- TOC entry 4594 (class 2604 OID 32606) +-- Name: relevos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.relevos ALTER COLUMN id SET DEFAULT nextval('public.relevos_id_seq'::regclass); + + +-- +-- TOC entry 4597 (class 2604 OID 32595) +-- Name: remessas id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.remessas ALTER COLUMN id SET DEFAULT nextval('public.remessas_id_seq'::regclass); + + +-- +-- TOC entry 4603 (class 2604 OID 32607) +-- Name: solos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.solos ALTER COLUMN id SET DEFAULT nextval('public.solos_id_seq'::regclass); + + +-- +-- TOC entry 4606 (class 2604 OID 32608) +-- Name: specieslink id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.specieslink ALTER COLUMN id SET DEFAULT nextval('public.specieslink_id_seq'::regclass); + + +-- +-- TOC entry 4611 (class 2604 OID 32611) +-- Name: sub_especies id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies ALTER COLUMN id SET DEFAULT nextval('public.sub_especies_id_seq'::regclass); + + +-- +-- TOC entry 4614 (class 2604 OID 32612) +-- Name: sub_familias id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_familias ALTER COLUMN id SET DEFAULT nextval('public.sub_familias_id_seq'::regclass); + + +-- +-- TOC entry 4617 (class 2604 OID 32613) +-- Name: telefones id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.telefones ALTER COLUMN id SET DEFAULT nextval('public.telefones_id_seq'::regclass); + + +-- +-- TOC entry 4618 (class 2604 OID 32614) +-- Name: tipos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tipos ALTER COLUMN id SET DEFAULT nextval('public.tipos_id_seq'::regclass); + + +-- +-- TOC entry 4621 (class 2604 OID 32616) +-- Name: tipos_usuarios id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tipos_usuarios ALTER COLUMN id SET DEFAULT nextval('public.tipos_usuarios_id_seq'::regclass); + + +-- +-- TOC entry 4625 (class 2604 OID 31293) +-- Name: tombos hcf; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos ALTER COLUMN hcf SET DEFAULT nextval('public.tombos_hcf_seq'::regclass); + + +-- +-- TOC entry 4633 (class 2604 OID 32609) +-- Name: tombos_fotos id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_fotos ALTER COLUMN id SET DEFAULT nextval('public.tombos_fotos_id_seq'::regclass); + + +-- +-- TOC entry 4640 (class 2604 OID 32610) +-- Name: usuarios id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuarios ALTER COLUMN id SET DEFAULT nextval('public.usuarios_id_seq'::regclass); + + +-- +-- TOC entry 4646 (class 2604 OID 32617) +-- Name: variedades id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades ALTER COLUMN id SET DEFAULT nextval('public.variedades_id_seq'::regclass); + + +-- +-- TOC entry 4649 (class 2604 OID 32615) +-- Name: vegetacoes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vegetacoes ALTER COLUMN id SET DEFAULT nextval('public.vegetacoes_id_seq'::regclass); + + +-- +-- TOC entry 4660 (class 2606 OID 31299) +-- Name: alteracoes idx_41012_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alteracoes + ADD CONSTRAINT idx_41012_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4662 (class 2606 OID 31301) +-- Name: autores idx_41023_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.autores + ADD CONSTRAINT idx_41023_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4666 (class 2606 OID 31303) +-- Name: cidades idx_41031_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cidades + ADD CONSTRAINT idx_41031_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4668 (class 2606 OID 31305) +-- Name: colecoes_anexas idx_41038_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.colecoes_anexas + ADD CONSTRAINT idx_41038_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4670 (class 2606 OID 31307) +-- Name: coletores idx_41047_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.coletores + ADD CONSTRAINT idx_41047_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4672 (class 2606 OID 31309) +-- Name: coletores_complementares idx_41054_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.coletores_complementares + ADD CONSTRAINT idx_41054_primary PRIMARY KEY (hcf); + + +-- +-- TOC entry 4674 (class 2606 OID 31311) +-- Name: configuracao idx_41062_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.configuracao + ADD CONSTRAINT idx_41062_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4677 (class 2606 OID 31313) +-- Name: enderecos idx_41070_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.enderecos + ADD CONSTRAINT idx_41070_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4682 (class 2606 OID 31315) +-- Name: especies idx_41080_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.especies + ADD CONSTRAINT idx_41080_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4685 (class 2606 OID 31317) +-- Name: estados idx_41087_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.estados + ADD CONSTRAINT idx_41087_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4687 (class 2606 OID 31319) +-- Name: familias idx_41095_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.familias + ADD CONSTRAINT idx_41095_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4689 (class 2606 OID 31321) +-- Name: fase_sucessional idx_41101_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.fase_sucessional + ADD CONSTRAINT idx_41101_primary PRIMARY KEY (numero); + + +-- +-- TOC entry 4692 (class 2606 OID 31323) +-- Name: generos idx_41107_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.generos + ADD CONSTRAINT idx_41107_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4695 (class 2606 OID 31325) +-- Name: herbarios idx_41114_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.herbarios + ADD CONSTRAINT idx_41114_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4697 (class 2606 OID 31327) +-- Name: historico_acessos idx_41124_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.historico_acessos + ADD CONSTRAINT idx_41124_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4699 (class 2606 OID 31329) +-- Name: identificadores idx_41129_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.identificadores + ADD CONSTRAINT idx_41129_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4704 (class 2606 OID 31331) +-- Name: locais_coleta idx_41136_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.locais_coleta + ADD CONSTRAINT idx_41136_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4706 (class 2606 OID 31333) +-- Name: migrations idx_41144_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.migrations + ADD CONSTRAINT idx_41144_primary PRIMARY KEY (name); + + +-- +-- TOC entry 4708 (class 2606 OID 31335) +-- Name: paises idx_41149_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.paises + ADD CONSTRAINT idx_41149_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4710 (class 2606 OID 31337) +-- Name: reflora idx_41157_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reflora + ADD CONSTRAINT idx_41157_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4712 (class 2606 OID 31339) +-- Name: reinos idx_41168_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reinos + ADD CONSTRAINT idx_41168_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4714 (class 2606 OID 31341) +-- Name: relevos idx_41175_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.relevos + ADD CONSTRAINT idx_41175_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4717 (class 2606 OID 31343) +-- Name: remessas idx_41182_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.remessas + ADD CONSTRAINT idx_41182_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4721 (class 2606 OID 31345) +-- Name: retirada_exsiccata_tombos idx_41190_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.retirada_exsiccata_tombos + ADD CONSTRAINT idx_41190_primary PRIMARY KEY (retirada_exsiccata_id, tombo_hcf); + + +-- +-- TOC entry 4723 (class 2606 OID 31347) +-- Name: solos idx_41197_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.solos + ADD CONSTRAINT idx_41197_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4725 (class 2606 OID 31349) +-- Name: specieslink idx_41204_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.specieslink + ADD CONSTRAINT idx_41204_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4731 (class 2606 OID 31351) +-- Name: sub_especies idx_41215_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies + ADD CONSTRAINT idx_41215_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4735 (class 2606 OID 31353) +-- Name: sub_familias idx_41222_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_familias + ADD CONSTRAINT idx_41222_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4738 (class 2606 OID 31355) +-- Name: telefones idx_41229_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.telefones + ADD CONSTRAINT idx_41229_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4740 (class 2606 OID 31357) +-- Name: tipos idx_41234_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tipos + ADD CONSTRAINT idx_41234_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4742 (class 2606 OID 31359) +-- Name: tipos_usuarios idx_41241_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tipos_usuarios + ADD CONSTRAINT idx_41241_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4762 (class 2606 OID 31361) +-- Name: tombos idx_41249_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT idx_41249_primary PRIMARY KEY (hcf); + + +-- +-- TOC entry 4767 (class 2606 OID 31363) +-- Name: tombos_fotos idx_41263_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_fotos + ADD CONSTRAINT idx_41263_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4771 (class 2606 OID 31365) +-- Name: tombos_identificadores idx_41274_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_identificadores + ADD CONSTRAINT idx_41274_primary PRIMARY KEY (identificador_id, tombo_hcf); + + +-- +-- TOC entry 4745 (class 2606 OID 31367) +-- Name: tombo_alteracoes_antigas idx_41278_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombo_alteracoes_antigas + ADD CONSTRAINT idx_41278_primary PRIMARY KEY (sequencia, tombo_hcf); + + +-- +-- TOC entry 4775 (class 2606 OID 31369) +-- Name: usuarios idx_41284_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuarios + ADD CONSTRAINT idx_41284_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4781 (class 2606 OID 31371) +-- Name: variedades idx_41296_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades + ADD CONSTRAINT idx_41296_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4783 (class 2606 OID 31373) +-- Name: vegetacoes idx_41303_primary; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vegetacoes + ADD CONSTRAINT idx_41303_primary PRIMARY KEY (id); + + +-- +-- TOC entry 4657 (class 1259 OID 31374) +-- Name: idx_41012_fk_alteracoes_tombo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41012_fk_alteracoes_tombo ON public.alteracoes USING btree (tombo_hcf); + + +-- +-- TOC entry 4658 (class 1259 OID 31375) +-- Name: idx_41012_fk_alteracoes_usuario; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41012_fk_alteracoes_usuario ON public.alteracoes USING btree (usuario_id); + + +-- +-- TOC entry 4663 (class 1259 OID 31376) +-- Name: idx_41031_fk_cidades_estado_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41031_fk_cidades_estado_idx ON public.cidades USING btree (estado_id); + + +-- +-- TOC entry 4664 (class 1259 OID 31377) +-- Name: idx_41031_pais_estado_nome; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41031_pais_estado_nome ON public.cidades USING btree (estado_id, nome); + + +-- +-- TOC entry 4675 (class 1259 OID 31378) +-- Name: idx_41070_fk_enderecos_cidade; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41070_fk_enderecos_cidade ON public.enderecos USING btree (cidade_id); + + +-- +-- TOC entry 4678 (class 1259 OID 31379) +-- Name: idx_41080_fk_especies_autor; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41080_fk_especies_autor ON public.especies USING btree (autor_id); + + +-- +-- TOC entry 4679 (class 1259 OID 31380) +-- Name: idx_41080_fk_especies_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41080_fk_especies_familia ON public.especies USING btree (familia_id); + + +-- +-- TOC entry 4680 (class 1259 OID 31381) +-- Name: idx_41080_fk_especies_genero; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41080_fk_especies_genero ON public.especies USING btree (genero_id); + + +-- +-- TOC entry 4683 (class 1259 OID 31382) +-- Name: idx_41087_pais_nome; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41087_pais_nome ON public.estados USING btree (pais_id, nome); + + +-- +-- TOC entry 4690 (class 1259 OID 31383) +-- Name: idx_41107_fk_generos_familias; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41107_fk_generos_familias ON public.generos USING btree (familia_id); + + +-- +-- TOC entry 4693 (class 1259 OID 31384) +-- Name: idx_41114_fk_herbarios_endereco; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41114_fk_herbarios_endereco ON public.herbarios USING btree (endereco_id); + + +-- +-- TOC entry 4700 (class 1259 OID 31385) +-- Name: idx_41136_fk_99i0itontmoklfxmoo8armtnv; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41136_fk_99i0itontmoklfxmoo8armtnv ON public.locais_coleta USING btree (fase_numero); + + +-- +-- TOC entry 4701 (class 1259 OID 31386) +-- Name: idx_41136_fk_locais_coleta_cidades_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41136_fk_locais_coleta_cidades_idx ON public.locais_coleta USING btree (cidade_id); + + +-- +-- TOC entry 4702 (class 1259 OID 31387) +-- Name: idx_41136_fk_locais_coleta_fase_sucessional1_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41136_fk_locais_coleta_fase_sucessional1_idx ON public.locais_coleta USING btree (fase_sucessional_id); + + +-- +-- TOC entry 4715 (class 1259 OID 31388) +-- Name: idx_41182_fk_remessas_herbario; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41182_fk_remessas_herbario ON public.remessas USING btree (herbario_id); + + +-- +-- TOC entry 4718 (class 1259 OID 31389) +-- Name: idx_41190_fk_retirada_exsiccata_tombos_remessa; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41190_fk_retirada_exsiccata_tombos_remessa ON public.retirada_exsiccata_tombos USING btree (retirada_exsiccata_id); + + +-- +-- TOC entry 4719 (class 1259 OID 31390) +-- Name: idx_41190_fk_retirada_exsiccata_tombos_tombo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41190_fk_retirada_exsiccata_tombos_tombo ON public.retirada_exsiccata_tombos USING btree (tombo_hcf); + + +-- +-- TOC entry 4726 (class 1259 OID 31391) +-- Name: idx_41215_fk_sub_especies_autor; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41215_fk_sub_especies_autor ON public.sub_especies USING btree (autor_id); + + +-- +-- TOC entry 4727 (class 1259 OID 31392) +-- Name: idx_41215_fk_sub_especies_especie; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41215_fk_sub_especies_especie ON public.sub_especies USING btree (especie_id); + + +-- +-- TOC entry 4728 (class 1259 OID 31393) +-- Name: idx_41215_fk_sub_especies_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41215_fk_sub_especies_familia ON public.sub_especies USING btree (familia_id); + + +-- +-- TOC entry 4729 (class 1259 OID 31394) +-- Name: idx_41215_fk_sub_especies_genero; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41215_fk_sub_especies_genero ON public.sub_especies USING btree (genero_id); + + +-- +-- TOC entry 4732 (class 1259 OID 31395) +-- Name: idx_41222_fk_sub_familias_autor; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41222_fk_sub_familias_autor ON public.sub_familias USING btree (autor_id); + + +-- +-- TOC entry 4733 (class 1259 OID 31396) +-- Name: idx_41222_fk_sub_familias_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41222_fk_sub_familias_familia ON public.sub_familias USING btree (familia_id); + + +-- +-- TOC entry 4736 (class 1259 OID 31397) +-- Name: idx_41229_fk_telefones_herbario; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41229_fk_telefones_herbario ON public.telefones USING btree (herbario_id); + + +-- +-- TOC entry 4746 (class 1259 OID 31398) +-- Name: idx_41249_fk_tombos_colecao_anexa; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_colecao_anexa ON public.tombos USING btree (colecao_anexa_id); + + +-- +-- TOC entry 4747 (class 1259 OID 31399) +-- Name: idx_41249_fk_tombos_coletor; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_coletor ON public.tombos USING btree (coletor_id); + + +-- +-- TOC entry 4748 (class 1259 OID 31400) +-- Name: idx_41249_fk_tombos_entidade; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_entidade ON public.tombos USING btree (entidade_id); + + +-- +-- TOC entry 4749 (class 1259 OID 31401) +-- Name: idx_41249_fk_tombos_especie; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_especie ON public.tombos USING btree (especie_id); + + +-- +-- TOC entry 4750 (class 1259 OID 31402) +-- Name: idx_41249_fk_tombos_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_familia ON public.tombos USING btree (familia_id); + + +-- +-- TOC entry 4751 (class 1259 OID 31403) +-- Name: idx_41249_fk_tombos_genero; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_genero ON public.tombos USING btree (genero_id); + + +-- +-- TOC entry 4752 (class 1259 OID 31404) +-- Name: idx_41249_fk_tombos_local_coleta; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_local_coleta ON public.tombos USING btree (local_coleta_id); + + +-- +-- TOC entry 4753 (class 1259 OID 31405) +-- Name: idx_41249_fk_tombos_relevo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_relevo ON public.tombos USING btree (relevo_id); + + +-- +-- TOC entry 4754 (class 1259 OID 31406) +-- Name: idx_41249_fk_tombos_solo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_solo ON public.tombos USING btree (solo_id); + + +-- +-- TOC entry 4755 (class 1259 OID 31407) +-- Name: idx_41249_fk_tombos_sub_especie; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_sub_especie ON public.tombos USING btree (sub_especie_id); + + +-- +-- TOC entry 4756 (class 1259 OID 31408) +-- Name: idx_41249_fk_tombos_sub_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_sub_familia ON public.tombos USING btree (sub_familia_id); + + +-- +-- TOC entry 4757 (class 1259 OID 31409) +-- Name: idx_41249_fk_tombos_tipo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_tipo ON public.tombos USING btree (tipo_id); + + +-- +-- TOC entry 4758 (class 1259 OID 31410) +-- Name: idx_41249_fk_tombos_variedade; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_variedade ON public.tombos USING btree (variedade_id); + + +-- +-- TOC entry 4759 (class 1259 OID 31411) +-- Name: idx_41249_fk_tombos_vegetacao; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_fk_tombos_vegetacao ON public.tombos USING btree (vegetacao_id); + + +-- +-- TOC entry 4760 (class 1259 OID 31412) +-- Name: idx_41249_idx_tombos_coletor_id_numero_coleta; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_idx_tombos_coletor_id_numero_coleta ON public.tombos USING btree (coletor_id, numero_coleta); + + +-- +-- TOC entry 4763 (class 1259 OID 31413) +-- Name: idx_41249_tombos_cidade_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41249_tombos_cidade_id_index ON public.tombos USING btree (cidade_id); + + +-- +-- TOC entry 4764 (class 1259 OID 31414) +-- Name: idx_41263_fk_tombos_fotos_tombo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41263_fk_tombos_fotos_tombo ON public.tombos_fotos USING btree (tombo_hcf); + + +-- +-- TOC entry 4765 (class 1259 OID 31415) +-- Name: idx_41263_idx_tombos_fotos_num_barra; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41263_idx_tombos_fotos_num_barra ON public.tombos_fotos USING btree (num_barra); + + +-- +-- TOC entry 4768 (class 1259 OID 31416) +-- Name: idx_41274_fk_tombos_identificadores_identificador; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41274_fk_tombos_identificadores_identificador ON public.tombos_identificadores USING btree (identificador_id); + + +-- +-- TOC entry 4769 (class 1259 OID 31417) +-- Name: idx_41274_fk_tombos_identificadores_tombo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41274_fk_tombos_identificadores_tombo ON public.tombos_identificadores USING btree (tombo_hcf); + + +-- +-- TOC entry 4743 (class 1259 OID 31418) +-- Name: idx_41278_fk_tombo_alteracoes_antigas_tombo; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41278_fk_tombo_alteracoes_antigas_tombo ON public.tombo_alteracoes_antigas USING btree (tombo_hcf); + + +-- +-- TOC entry 4772 (class 1259 OID 31419) +-- Name: idx_41284_fk_usuarios_herbario; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41284_fk_usuarios_herbario ON public.usuarios USING btree (herbario_id); + + +-- +-- TOC entry 4773 (class 1259 OID 31420) +-- Name: idx_41284_fk_usuarios_tipo_usuario; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41284_fk_usuarios_tipo_usuario ON public.usuarios USING btree (tipo_usuario_id); + + +-- +-- TOC entry 4776 (class 1259 OID 31421) +-- Name: idx_41296_fk_variedades_autor; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41296_fk_variedades_autor ON public.variedades USING btree (autor_id); + + +-- +-- TOC entry 4777 (class 1259 OID 31422) +-- Name: idx_41296_fk_variedades_especie; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41296_fk_variedades_especie ON public.variedades USING btree (especie_id); + + +-- +-- TOC entry 4778 (class 1259 OID 31423) +-- Name: idx_41296_fk_variedades_familia; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41296_fk_variedades_familia ON public.variedades USING btree (familia_id); + + +-- +-- TOC entry 4779 (class 1259 OID 31424) +-- Name: idx_41296_fk_variedades_genero; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_41296_fk_variedades_genero ON public.variedades USING btree (genero_id); + + +-- +-- TOC entry 4805 (class 2606 OID 31425) +-- Name: locais_coleta fk_99i0itontmoklfxmoo8armtnv; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.locais_coleta + ADD CONSTRAINT fk_99i0itontmoklfxmoo8armtnv FOREIGN KEY (fase_numero) REFERENCES public.fase_sucessional(numero) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4794 (class 2606 OID 31430) +-- Name: alteracoes fk_alteracoes_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alteracoes + ADD CONSTRAINT fk_alteracoes_tombo FOREIGN KEY (tombo_hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4795 (class 2606 OID 31435) +-- Name: alteracoes fk_alteracoes_usuario; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alteracoes + ADD CONSTRAINT fk_alteracoes_usuario FOREIGN KEY (usuario_id) REFERENCES public.usuarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4796 (class 2606 OID 31440) +-- Name: cidades fk_cidades_estados; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cidades + ADD CONSTRAINT fk_cidades_estados FOREIGN KEY (estado_id) REFERENCES public.estados(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4797 (class 2606 OID 31445) +-- Name: coletores_complementares fk_coletores_complementares_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.coletores_complementares + ADD CONSTRAINT fk_coletores_complementares_tombo FOREIGN KEY (hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4798 (class 2606 OID 31450) +-- Name: enderecos fk_enderecos_cidade; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.enderecos + ADD CONSTRAINT fk_enderecos_cidade FOREIGN KEY (cidade_id) REFERENCES public.cidades(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4799 (class 2606 OID 31455) +-- Name: especies fk_especies_autor; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.especies + ADD CONSTRAINT fk_especies_autor FOREIGN KEY (autor_id) REFERENCES public.autores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4800 (class 2606 OID 31460) +-- Name: especies fk_especies_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.especies + ADD CONSTRAINT fk_especies_familia FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4801 (class 2606 OID 31465) +-- Name: especies fk_especies_genero; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.especies + ADD CONSTRAINT fk_especies_genero FOREIGN KEY (genero_id) REFERENCES public.generos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4802 (class 2606 OID 31470) +-- Name: estados fk_estados_paises; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.estados + ADD CONSTRAINT fk_estados_paises FOREIGN KEY (pais_id) REFERENCES public.paises(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4803 (class 2606 OID 31475) +-- Name: generos fk_generos_familias; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.generos + ADD CONSTRAINT fk_generos_familias FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4804 (class 2606 OID 31480) +-- Name: herbarios fk_herbarios_endereco; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.herbarios + ADD CONSTRAINT fk_herbarios_endereco FOREIGN KEY (endereco_id) REFERENCES public.enderecos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4806 (class 2606 OID 31485) +-- Name: locais_coleta fk_locais_coleta_cidades; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.locais_coleta + ADD CONSTRAINT fk_locais_coleta_cidades FOREIGN KEY (cidade_id) REFERENCES public.cidades(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4807 (class 2606 OID 31490) +-- Name: locais_coleta fk_locais_coleta_fase_sucessional1; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.locais_coleta + ADD CONSTRAINT fk_locais_coleta_fase_sucessional1 FOREIGN KEY (fase_sucessional_id) REFERENCES public.fase_sucessional(numero) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4808 (class 2606 OID 31495) +-- Name: remessas fk_remessas_herbario; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.remessas + ADD CONSTRAINT fk_remessas_herbario FOREIGN KEY (herbario_id) REFERENCES public.herbarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4809 (class 2606 OID 31500) +-- Name: retirada_exsiccata_tombos fk_retirada_exsiccata_tombos_remessa; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.retirada_exsiccata_tombos + ADD CONSTRAINT fk_retirada_exsiccata_tombos_remessa FOREIGN KEY (retirada_exsiccata_id) REFERENCES public.remessas(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4810 (class 2606 OID 31505) +-- Name: retirada_exsiccata_tombos fk_retirada_exsiccata_tombos_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.retirada_exsiccata_tombos + ADD CONSTRAINT fk_retirada_exsiccata_tombos_tombo FOREIGN KEY (tombo_hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4811 (class 2606 OID 31510) +-- Name: sub_especies fk_sub_especies_autor; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies + ADD CONSTRAINT fk_sub_especies_autor FOREIGN KEY (autor_id) REFERENCES public.autores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4812 (class 2606 OID 31515) +-- Name: sub_especies fk_sub_especies_especie; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies + ADD CONSTRAINT fk_sub_especies_especie FOREIGN KEY (especie_id) REFERENCES public.especies(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4813 (class 2606 OID 31520) +-- Name: sub_especies fk_sub_especies_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies + ADD CONSTRAINT fk_sub_especies_familia FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4814 (class 2606 OID 31525) +-- Name: sub_especies fk_sub_especies_genero; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_especies + ADD CONSTRAINT fk_sub_especies_genero FOREIGN KEY (genero_id) REFERENCES public.generos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4815 (class 2606 OID 31530) +-- Name: sub_familias fk_sub_familias_autor; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_familias + ADD CONSTRAINT fk_sub_familias_autor FOREIGN KEY (autor_id) REFERENCES public.autores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4816 (class 2606 OID 31535) +-- Name: sub_familias fk_sub_familias_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sub_familias + ADD CONSTRAINT fk_sub_familias_familia FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4817 (class 2606 OID 31540) +-- Name: telefones fk_telefones_herbario; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.telefones + ADD CONSTRAINT fk_telefones_herbario FOREIGN KEY (herbario_id) REFERENCES public.herbarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4818 (class 2606 OID 31545) +-- Name: tombo_alteracoes_antigas fk_tombo_alteracoes_antigas_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombo_alteracoes_antigas + ADD CONSTRAINT fk_tombo_alteracoes_antigas_tombo FOREIGN KEY (tombo_hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4819 (class 2606 OID 31550) +-- Name: tombos fk_tombos_cidade; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_cidade FOREIGN KEY (cidade_id) REFERENCES public.cidades(id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- TOC entry 4820 (class 2606 OID 31555) +-- Name: tombos fk_tombos_colecao_anexa; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_colecao_anexa FOREIGN KEY (colecao_anexa_id) REFERENCES public.colecoes_anexas(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4821 (class 2606 OID 31560) +-- Name: tombos fk_tombos_coletor; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_coletor FOREIGN KEY (coletor_id) REFERENCES public.coletores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4822 (class 2606 OID 31565) +-- Name: tombos fk_tombos_entidade; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_entidade FOREIGN KEY (entidade_id) REFERENCES public.herbarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4823 (class 2606 OID 31570) +-- Name: tombos fk_tombos_especie; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_especie FOREIGN KEY (especie_id) REFERENCES public.especies(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4824 (class 2606 OID 31575) +-- Name: tombos fk_tombos_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_familia FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4833 (class 2606 OID 31580) +-- Name: tombos_fotos fk_tombos_fotos_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_fotos + ADD CONSTRAINT fk_tombos_fotos_tombo FOREIGN KEY (tombo_hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4825 (class 2606 OID 31585) +-- Name: tombos fk_tombos_genero; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_genero FOREIGN KEY (genero_id) REFERENCES public.generos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4834 (class 2606 OID 31590) +-- Name: tombos_identificadores fk_tombos_identificadores_identificador; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_identificadores + ADD CONSTRAINT fk_tombos_identificadores_identificador FOREIGN KEY (identificador_id) REFERENCES public.identificadores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4835 (class 2606 OID 31595) +-- Name: tombos_identificadores fk_tombos_identificadores_tombo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos_identificadores + ADD CONSTRAINT fk_tombos_identificadores_tombo FOREIGN KEY (tombo_hcf) REFERENCES public.tombos(hcf) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4826 (class 2606 OID 31600) +-- Name: tombos fk_tombos_local_coleta; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_local_coleta FOREIGN KEY (local_coleta_id) REFERENCES public.locais_coleta(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4827 (class 2606 OID 31605) +-- Name: tombos fk_tombos_relevo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_relevo FOREIGN KEY (relevo_id) REFERENCES public.relevos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4828 (class 2606 OID 31610) +-- Name: tombos fk_tombos_solo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_solo FOREIGN KEY (solo_id) REFERENCES public.solos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4829 (class 2606 OID 31615) +-- Name: tombos fk_tombos_sub_especie; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_sub_especie FOREIGN KEY (sub_especie_id) REFERENCES public.sub_especies(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4830 (class 2606 OID 31620) +-- Name: tombos fk_tombos_sub_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_sub_familia FOREIGN KEY (sub_familia_id) REFERENCES public.sub_familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4831 (class 2606 OID 31625) +-- Name: tombos fk_tombos_tipo; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_tipo FOREIGN KEY (tipo_id) REFERENCES public.tipos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4832 (class 2606 OID 31630) +-- Name: tombos fk_tombos_vegetacao; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tombos + ADD CONSTRAINT fk_tombos_vegetacao FOREIGN KEY (vegetacao_id) REFERENCES public.vegetacoes(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4836 (class 2606 OID 31635) +-- Name: usuarios fk_usuarios_herbario; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuarios + ADD CONSTRAINT fk_usuarios_herbario FOREIGN KEY (herbario_id) REFERENCES public.herbarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4837 (class 2606 OID 31640) +-- Name: usuarios fk_usuarios_tipo_usuario; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usuarios + ADD CONSTRAINT fk_usuarios_tipo_usuario FOREIGN KEY (tipo_usuario_id) REFERENCES public.tipos_usuarios(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4838 (class 2606 OID 31645) +-- Name: variedades fk_variedades_autor; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades + ADD CONSTRAINT fk_variedades_autor FOREIGN KEY (autor_id) REFERENCES public.autores(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4839 (class 2606 OID 31650) +-- Name: variedades fk_variedades_especie; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades + ADD CONSTRAINT fk_variedades_especie FOREIGN KEY (especie_id) REFERENCES public.especies(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4840 (class 2606 OID 31655) +-- Name: variedades fk_variedades_familia; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades + ADD CONSTRAINT fk_variedades_familia FOREIGN KEY (familia_id) REFERENCES public.familias(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- TOC entry 4841 (class 2606 OID 31660) +-- Name: variedades fk_variedades_genero; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.variedades + ADD CONSTRAINT fk_variedades_genero FOREIGN KEY (genero_id) REFERENCES public.generos(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- Completed on 2026-06-19 09:50:16 + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict 6jAa2KinsvKEOg1UbHoVMtEdyuVatRpHS5aFz6F6xH4vaNfTZHZIHyPU01e9TJ8 diff --git a/test/integration/setup/seeds/estados.seed.ts b/test/integration/setup/seeds/estados.seed.ts new file mode 100644 index 00000000..d070de75 --- /dev/null +++ b/test/integration/setup/seeds/estados.seed.ts @@ -0,0 +1,45 @@ +import type { Knex } from 'knex' + +/** + * Paises owned by lista-estados integration tests (used only as FK carriers). + * Siglas are exactly 4 chars to fit bpchar(4). + */ +const paises = [ + { nome: 'XEST Brasil', sigla: 'XEBR' }, + { nome: 'XEST Argentina', sigla: 'XEAR' } +] + +export async function seedEstados( + knex: Knex +): Promise> { + const [paisBrasil, paisArgentina] = await knex('paises') + .insert(paises) + .returning>(['id', 'sigla']) + + return knex('estados') + .insert([ + { + nome: 'Paraná', sigla: 'XEPR', pais_id: paisBrasil.id + }, + { + nome: 'São Paulo', sigla: 'XESP', pais_id: paisBrasil.id + }, + { + nome: 'Buenos Aires', sigla: 'XEBA', pais_id: paisArgentina.id + } + ]) + .returning([ + 'id', + 'nome', + 'sigla' + ]) +} + +export async function cleanupEstados(knex: Knex): Promise { + await knex('estados').whereIn('sigla', [ + 'XEPR', + 'XESP', + 'XEBA' + ]).delete() + await knex('paises').whereIn('sigla', paises.map(p => p.sigla)).delete() +} diff --git a/test/integration/setup/seeds/paises.seed.ts b/test/integration/setup/seeds/paises.seed.ts new file mode 100644 index 00000000..4d9bca0a --- /dev/null +++ b/test/integration/setup/seeds/paises.seed.ts @@ -0,0 +1,26 @@ +import type { Knex } from 'knex' + +/** + * Paises owned by lista-paises integration tests. + * Siglas are exactly 4 chars to fit bpchar(4). + * Names are prefixed with 'XPAI' so the test can filter via ?nome=XPAI + * without touching other test files' rows. + */ +export const paises = [ + { nome: 'XPAI Argentina', sigla: 'XPAR' }, + { nome: 'XPAI Brasil', sigla: 'XPBR' } +] + +export async function seedPaises( + knex: Knex +): Promise> { + return knex('paises').insert(paises).returning([ + 'id', + 'nome', + 'sigla' + ]) +} + +export async function cleanupPaises(knex: Knex): Promise { + await knex('paises').whereIn('sigla', paises.map(p => p.sigla)).delete() +} diff --git a/test/unit/application/estado/ListaEstadosController.test.ts b/test/unit/application/estado/ListaEstadosController.test.ts new file mode 100644 index 00000000..00830902 --- /dev/null +++ b/test/unit/application/estado/ListaEstadosController.test.ts @@ -0,0 +1,84 @@ +import { + describe, expect, test, vi +} from 'vitest' + +import { ListaEstadosController } from '@/application/estado/ListaEstadosController' +import { ListaEstadosUseCase } from '@/domain/estado/ListaEstadosUseCase' +import { Either } from '@/library/either/Either' +import { + Headers, HttpRequest, Method, StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' + +describe('ListaEstadosController', () => { + const headers = {} as Headers + + test('returns 200 with body when use case succeeds', async () => { + const lista = [ + { + id: 1, nome: 'PR', sigla: 'PR' + } + ] + + const listaEstadosUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(lista)) + } as unknown as ListaEstadosUseCase + + const controller = new ListaEstadosController({ listaEstadosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { paisSigla: 'BRA' }, + path: '/paises/BRA/estados' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(listaEstadosUseCase.execute).toHaveBeenCalledWith({ paisSigla: 'BRA' }) + expect('statusCode' in response ? response.statusCode : undefined).toBe(StatusCode.Ok) + expect('body' in response ? response.body : undefined).toEqual(lista) + }) + + test('returns BadRequest when paisSigla is missing', async () => { + const listaEstadosUseCase = { + execute: vi.fn() + } as unknown as ListaEstadosUseCase + + const controller = new ListaEstadosController({ listaEstadosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: {}, + path: '/paises//estados' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(listaEstadosUseCase.execute).not.toHaveBeenCalled() + expect(response).toBeInstanceOf(BadRequestError) + }) + + test('returns InternalServerError when use case fails', async () => { + const listaEstadosUseCase = { + execute: vi.fn().mockResolvedValue(Either.left(new Error('boom'))) + } as unknown as ListaEstadosUseCase + + const controller = new ListaEstadosController({ listaEstadosUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get, + params: { paisSigla: 'BRA' }, + path: '/paises/BRA/estados' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect((response as Error).name).toBe('InternalServerError') + }) +}) diff --git a/test/unit/application/pais/ListaPaisesController.test.ts b/test/unit/application/pais/ListaPaisesController.test.ts new file mode 100644 index 00000000..2ec799ca --- /dev/null +++ b/test/unit/application/pais/ListaPaisesController.test.ts @@ -0,0 +1,64 @@ +import { + describe, expect, test, vi +} from 'vitest' + +import { ListaPaisesController } from '@/application/pais/ListaPaisesController' +import { ListaPaisesUseCase } from '@/domain/pais/ListaPaisesUseCase' +import { Either } from '@/library/either/Either' +import { Method, StatusCode } from '@/library/http/common' +import type { Headers } from '@/library/http/common' +import type { HttpRequest } from '@/library/http/common' +import type { InternalServerError } from '@/library/http/error/InternalServerError' + +describe('ListaPaisesController', () => { + const headers = {} as Headers + + test('returns 200 with body when use case succeeds', async () => { + const lista = [ + { + id: 1, nome: 'Brasil', sigla: 'BRA' as string | null + } + ] + + const listaPaisesUseCase = { + execute: vi.fn().mockResolvedValue(Either.right(lista)) + } as unknown as ListaPaisesUseCase + + const controller = new ListaPaisesController({ listaPaisesUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get as Method, + params: { nome: 'Bra' }, + path: '/paises' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect(listaPaisesUseCase.execute).toHaveBeenCalledWith({ nome: 'Bra' }) + expect('statusCode' in response && response.statusCode).toBe(StatusCode.Ok) + + expect('body' in response ? response.body : undefined).toEqual(lista) + }) + + test('returns InternalServerError when use case fails', async () => { + const listaPaisesUseCase = { + execute: vi.fn().mockResolvedValue(Either.left(new Error('boom'))) + } as unknown as ListaPaisesUseCase + + const controller = new ListaPaisesController({ listaPaisesUseCase }) + + const request = { + body: {}, + headers, + method: Method.Get as Method, + params: {}, + path: '/paises' + } satisfies HttpRequest + + const response = await controller.handle(request, vi.fn()) + + expect((response as InternalServerError).name).toBe('InternalServerError') + }) +}) diff --git a/test/database/apply-migration-service.test.ts b/test/unit/database/apply-migration-service.test.ts similarity index 99% rename from test/database/apply-migration-service.test.ts rename to test/unit/database/apply-migration-service.test.ts index a1a0d384..2ba2a32c 100644 --- a/test/database/apply-migration-service.test.ts +++ b/test/unit/database/apply-migration-service.test.ts @@ -5,7 +5,7 @@ import { import { ApplyMigrationService } from '@/database/apply-migration-service' import { MigrationFileSystem } from '@/database/migration-file-system' import { MigrationRepository } from '@/database/migration-repository' -import { Logger } from '@/library/logger' +import { Logger } from '@/library/logger/Logger' const logger: Logger = { debug: vi.fn(), diff --git a/test/database/create-migration-service.test.ts b/test/unit/database/create-migration-service.test.ts similarity index 100% rename from test/database/create-migration-service.test.ts rename to test/unit/database/create-migration-service.test.ts diff --git a/test/database/migration-file-system.test.ts b/test/unit/database/migration-file-system.test.ts similarity index 100% rename from test/database/migration-file-system.test.ts rename to test/unit/database/migration-file-system.test.ts diff --git a/test/database/migration-repository.test.ts b/test/unit/database/migration-repository.test.ts similarity index 96% rename from test/database/migration-repository.test.ts rename to test/unit/database/migration-repository.test.ts index 56d3132b..7eef7f27 100644 --- a/test/database/migration-repository.test.ts +++ b/test/unit/database/migration-repository.test.ts @@ -3,9 +3,8 @@ import { vi, describe, expect, test } from 'vitest' -import { Logger } from '@/library/logger' - -import { MigrationRepository } from '../../src/database/migration-repository' +import { MigrationRepository } from '@/database/migration-repository' +import { Logger } from '@/library/logger/Logger' const logger: Logger = { debug: vi.fn(), diff --git a/test/unit/domain/estado/ListaEstadosUseCase.test.ts b/test/unit/domain/estado/ListaEstadosUseCase.test.ts new file mode 100644 index 00000000..dfd2af30 --- /dev/null +++ b/test/unit/domain/estado/ListaEstadosUseCase.test.ts @@ -0,0 +1,65 @@ +import { + describe, expect, test, vi +} from 'vitest' + +import { EstadoCollection } from '@/domain/estado/EstadoCollection' +import { ListaEstadosUseCase } from '@/domain/estado/ListaEstadosUseCase' +import { Either } from '@/library/either/Either' + +const makeMockCollection = (overrides?: Partial): EstadoCollection => ({ + findAll: vi.fn().mockResolvedValue(Either.right([])), + ...overrides +}) + +describe('ListaEstadosUseCase', () => { + test('returns estados for a given paisSigla', async () => { + const expected = [ + { + id: 1, nome: 'Paraná', sigla: 'PR' + } + ] + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.right(expected)) + }) + + const useCase = new ListaEstadosUseCase({ estadoCollection: collection }) + const result = await useCase.execute({ paisSigla: 'BRA' }) + + expect(result.right()).toBe(true) + expect(result.value).toEqual(expected) + }) + + test('forwards filters to the collection', async () => { + const collection = makeMockCollection() + + const useCase = new ListaEstadosUseCase({ estadoCollection: collection }) + await useCase.execute({ paisSigla: 'BRA' }) + + expect(collection.findAll).toHaveBeenCalledWith({ paisSigla: 'BRA' }) + }) + + test('returns empty list when país has no estados', async () => { + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.right([])) + }) + + const useCase = new ListaEstadosUseCase({ estadoCollection: collection }) + const result = await useCase.execute({ paisSigla: 'XYZ' }) + + expect(result.right()).toBe(true) + expect(result.value).toHaveLength(0) + }) + + test('propagates collection error as Either.left', async () => { + const error = new Error('DB failure') + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.left(error)) + }) + + const useCase = new ListaEstadosUseCase({ estadoCollection: collection }) + const result = await useCase.execute({ paisSigla: 'BRA' }) + + expect(result.left()).toBe(true) + expect(result.value).toBe(error) + }) +}) diff --git a/test/unit/domain/pais/ListaPaisesUseCase.test.ts b/test/unit/domain/pais/ListaPaisesUseCase.test.ts new file mode 100644 index 00000000..0a482ee4 --- /dev/null +++ b/test/unit/domain/pais/ListaPaisesUseCase.test.ts @@ -0,0 +1,65 @@ +import { + describe, expect, test, vi +} from 'vitest' + +import { ListaPaisesUseCase } from '@/domain/pais/ListaPaisesUseCase' +import { PaisCollection } from '@/domain/pais/PaisCollection' +import { Either } from '@/library/either/Either' + +const makeMockCollection = (overrides?: Partial): PaisCollection => ({ + findAll: vi.fn().mockResolvedValue(Either.right([])), + ...overrides +}) + +describe('ListaPaisesUseCase', () => { + test('returns países from the collection', async () => { + const expected = [ + { + id: 1, nome: 'Brasil', sigla: 'BRA' + } + ] + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.right(expected)) + }) + + const useCase = new ListaPaisesUseCase({ paisCollection: collection }) + const result = await useCase.execute({}) + + expect(result.right()).toBe(true) + expect(result.value).toEqual(expected) + }) + + test('passes nome filter to the collection', async () => { + const collection = makeMockCollection() + + const useCase = new ListaPaisesUseCase({ paisCollection: collection }) + await useCase.execute({ nome: 'Brasil' }) + + expect(collection.findAll).toHaveBeenCalledWith({ nome: 'Brasil' }) + }) + + test('returns empty list when no países match', async () => { + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.right([])) + }) + + const useCase = new ListaPaisesUseCase({ paisCollection: collection }) + const result = await useCase.execute({ nome: 'Inexistente' }) + + expect(result.right()).toBe(true) + expect(result.value).toHaveLength(0) + }) + + test('propagates collection error as Either.left', async () => { + const error = new Error('DB failure') + const collection = makeMockCollection({ + findAll: vi.fn().mockResolvedValue(Either.left(error)) + }) + + const useCase = new ListaPaisesUseCase({ paisCollection: collection }) + const result = await useCase.execute({}) + + expect(result.left()).toBe(true) + expect(result.value).toBe(error) + }) +}) diff --git a/test/infrastructure/logger.test.ts b/test/unit/infrastructure/ConsoleLogger.test.ts similarity index 98% rename from test/infrastructure/logger.test.ts rename to test/unit/infrastructure/ConsoleLogger.test.ts index c0cad423..683afeef 100644 --- a/test/infrastructure/logger.test.ts +++ b/test/unit/infrastructure/ConsoleLogger.test.ts @@ -3,7 +3,7 @@ import { } from 'vitest' import type { MockInstance } from 'vitest' -import { ConsoleLogger } from '@/infrastructure/logger' +import { ConsoleLogger } from '@/infrastructure/ConsoleLogger' describe('Infrastructure > ConsoleLogger', () => { let consoleDebugSpy: MockInstance diff --git a/test/unit/infrastructure/EstadoCollectionKnexAdapter.test.ts b/test/unit/infrastructure/EstadoCollectionKnexAdapter.test.ts new file mode 100644 index 00000000..4ee37472 --- /dev/null +++ b/test/unit/infrastructure/EstadoCollectionKnexAdapter.test.ts @@ -0,0 +1,83 @@ +import type { Knex } from 'knex' +import { + describe, expect, test, vi +} from 'vitest' + +import { EstadoCollectionKnexAdapter } from '@/infrastructure/EstadoCollectionKnexAdapter' + +function stubKnex(promise: Promise): Knex & { builder: Record } { + const builder = {} as Record + builder.select = vi.fn().mockImplementation(() => builder) + builder.orderBy = vi.fn().mockImplementation(() => builder) + builder.join = vi.fn().mockImplementation(() => builder) + builder.where = vi.fn().mockImplementation(() => builder) + builder.then = ( + onResolved: (v: TResult) => unknown, + onRejected?: (e: unknown) => unknown + ): Promise => promise.then(onResolved, onRejected) + + const knex = vi.fn(() => builder) as unknown as Knex & { + builder: Record + } + + knex.builder = builder + return knex as Knex & { builder: Record } +} + +describe('EstadoCollectionKnexAdapter', () => { + test('findAll applies paisSigla filter and returns rows', async () => { + const rows = [ + { + id: 1, nome: 'Paraná', sigla: 'PR' + } + ] + + const knex = stubKnex(Promise.resolve(rows)) + const adapter = new EstadoCollectionKnexAdapter({ knex }) + + const result = await adapter.findAll({ paisSigla: 'BRA' }) + + expect(result.right()).toBe(true) + expect(result.value).toEqual(rows) + expect(knex).toHaveBeenCalledWith('estados') + expect(knex.builder.select).toHaveBeenCalledWith([ + 'estados.id', + 'estados.nome', + 'estados.sigla' + ]) + expect(knex.builder.join).toHaveBeenCalledWith('paises', 'estados.pais_id', 'paises.id') + expect(knex.builder.where).toHaveBeenCalledWith('paises.sigla', 'BRA') + expect(knex.builder.orderBy).toHaveBeenCalledWith('estados.nome') + }) + + test('findAll returns all rows when no filters provided', async () => { + const rows = [ + { + id: 1, nome: 'Paraná', sigla: 'PR' + }, + { + id: 2, nome: 'São Paulo', sigla: 'SP' + } + ] + + const knex = stubKnex(Promise.resolve(rows)) + const adapter = new EstadoCollectionKnexAdapter({ knex }) + + const result = await adapter.findAll({}) + + expect(result.right()).toBe(true) + expect(result.value).toHaveLength(2) + expect(knex.builder.join).not.toHaveBeenCalled() + expect(knex.builder.where).not.toHaveBeenCalled() + }) + + test('findAll returns Either.left on failure', async () => { + const knex = stubKnex(Promise.reject(new Error('DB down'))) + const adapter = new EstadoCollectionKnexAdapter({ knex }) + + const result = await adapter.findAll({ paisSigla: 'BRA' }) + + expect(result.left()).toBe(true) + expect(result.value).toBeInstanceOf(Error) + }) +}) diff --git a/test/unit/infrastructure/PaisCollectionKnexAdapter.test.ts b/test/unit/infrastructure/PaisCollectionKnexAdapter.test.ts new file mode 100644 index 00000000..7c324e49 --- /dev/null +++ b/test/unit/infrastructure/PaisCollectionKnexAdapter.test.ts @@ -0,0 +1,73 @@ +import type { Knex } from 'knex' +import { + describe, expect, test, vi +} from 'vitest' + +import type { Attributes } from '@/domain/pais/Pais' +import { PaisCollectionKnexAdapter } from '@/infrastructure/PaisCollectionKnexAdapter' + +function stubKnex(promise: Promise): Knex & { builder: Record } { + const builder = {} as Record + builder.orderBy = vi.fn().mockImplementation(() => builder) + builder.select = vi.fn().mockImplementation(() => builder) + builder.then = ( + onResolved: (v: TResult) => unknown, + onRejected?: (e: unknown) => unknown + ): Promise => promise.then(onResolved, onRejected) + builder.whereILike = vi.fn().mockImplementation(() => builder) + + const knex = vi.fn(() => builder) as unknown as Knex & { + builder: Record + } + + knex.builder = builder + return knex as Knex & { builder: Record } +} + +describe('PaisCollectionKnexAdapter', () => { + test('findAll returns rows from knex', async () => { + const rows: Attributes[] = [ + { + id: 1, nome: 'Brasil', sigla: 'BRA' + } + ] + + const knex = stubKnex(Promise.resolve(rows)) + const adapter = new PaisCollectionKnexAdapter({ knex }) + + const result = await adapter.findAll({}) + + expect(result.right()).toBe(true) + expect(result.value).toEqual(rows) + expect(knex).toHaveBeenCalledWith('paises') + expect(knex.builder.select).toHaveBeenCalledWith([ + 'id', + 'nome', + 'sigla' + ]) + expect(knex.builder.whereILike).not.toHaveBeenCalled() + }) + + test('findAll passes nome filter to whereILike', async () => { + const rows: Attributes[] = [] + const knex = stubKnex(Promise.resolve(rows)) + const adapter = new PaisCollectionKnexAdapter({ knex }) + + await adapter.findAll({ nome: 'Bra' }) + + expect(knex.builder.whereILike).toHaveBeenCalledWith( + 'nome', + '%Bra%' + ) + }) + + test('findAll returns Either.left on failure', async () => { + const knex = stubKnex(Promise.reject(new Error('DB down'))) + const adapter = new PaisCollectionKnexAdapter({ knex }) + + const result = await adapter.findAll({}) + + expect(result.left()).toBe(true) + expect(result.value).toBeInstanceOf(Error) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index cf1cdce6..fd0eb904 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "es2020", + "target": "es2022", "lib": [ - "es2020" + "es2022" ], "jsx": "react", - "module": "es2020", + "module": "es2022", "strict": true, "skipLibCheck": true, "noUnusedLocals": true, diff --git a/vitest.config.mts b/vitest.config.mts index 4af3cf3a..24b72341 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -2,12 +2,12 @@ import path from 'node:path' import { defineConfig } from 'vitest/config' export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, test: { - globals: true, - environment: 'node', - mockReset: true, - clearMocks: true, - include: ['test/**/*.test.ts', 'test/**/*.spec.ts'], coverage: { provider: 'v8', reporter: [ @@ -17,11 +17,30 @@ export default defineConfig({ ], include: ['src/**/*.ts'], exclude: ['src/database/migration/'] - } - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src') - } + }, + projects: [ + { + extends: true, + test: { + name: 'unit', + globals: true, + environment: 'node', + mockReset: true, + clearMocks: true, + include: ['test/unit/**/*.test.ts', 'test/unit/**/*.spec.ts'] + } + }, + { + extends: true, + test: { + name: 'integration', + globals: true, + environment: 'node', + include: ['test/integration/**/*.test.ts', 'test/integration/**/*.spec.ts'], + setupFiles: ['test/integration/setup/load-env.ts'], + globalSetup: ['test/integration/setup/global-setup.ts'] + } + } + ] } }) diff --git a/yarn.lock b/yarn.lock index 99394392..426779ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1600,6 +1600,13 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/cors@2.8.19": + version "2.8.19" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + "@types/deep-eql@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" @@ -1610,6 +1617,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== +"@types/estree@^1.0.0", "@types/estree@^1.0.6": + version "1.0.9" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + "@types/express-serve-static-core@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz#1a77faffee9572d39124933259be2523837d7eaa" @@ -1620,7 +1632,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@5.0.5": +"@types/express@*", "@types/express@5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.5.tgz#3ba069177caa34ab96585ca23b3984d752300cdc" integrity sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ== @@ -1659,6 +1671,13 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/morgan@1.9.10": + version "1.9.10" + resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz#725c15d95a5e6150237524cd713bc2d68f9edf1a" + integrity sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA== + dependencies: + "@types/node" "*" + "@types/node@*": version "25.9.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.3.tgz#11dfe7a33e68fa5c560f0aa76cc5595621ef26b9" @@ -1712,7 +1731,7 @@ "@types/mime" "^1" "@types/node" "*" -"@types/serve-static@^1": +"@types/serve-static@*", "@types/serve-static@^1": version "1.15.10" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== @@ -1721,6 +1740,16 @@ "@types/node" "*" "@types/send" "<1" +"@types/superagent@^8.1.0": + version "8.1.10" + resolved "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz#aa1f600eeee83a7b129e06f8e7fec78fb0ac980a" + integrity sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + "@types/superagent@^8.1.7": version "8.1.10" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.10.tgz#aa1f600eeee83a7b129e06f8e7fec78fb0ac980a" @@ -1731,6 +1760,22 @@ "@types/node" "*" form-data "^4.0.0" +"@types/supertest@7.2.0": + version "7.2.0" + resolved "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz#9620bc998d3e26bcbedba7d31da8fe4c82fc1620" + integrity sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + +"@types/swagger-ui-express@^4.1.8": + version "4.1.8" + resolved "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz" + integrity sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g== + dependencies: + "@types/express" "*" + "@types/serve-static" "*" + "@types/yauzl@^2.9.1": version "2.10.3" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" @@ -2591,7 +2636,7 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@^1.2.1: +cookie-signature@^1.2.1, cookie-signature@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== @@ -3506,7 +3551,7 @@ fs.realpath@^1.0.0: fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: @@ -6134,7 +6179,7 @@ super-regex@^0.2.0: function-timeout "^0.1.0" time-span "^5.1.0" -superagent@^10.0.0: +superagent@^10.0.0, superagent@^10.3.0: version "10.3.0" resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.3.0.tgz#ff1e39e7976b63f8084291d65f5bfbbbbd156989" integrity sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ== @@ -6149,6 +6194,15 @@ superagent@^10.0.0: mime "2.6.0" qs "^6.14.1" +supertest@7.2.2: + version "7.2.2" + resolved "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz#dac3ee25a2aa59942a7f641e50c838a7c8819204" + integrity sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA== + dependencies: + cookie-signature "^1.2.2" + methods "^1.1.2" + superagent "^10.3.0" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"