Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .config/cspell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
language: en,pt-BR
words:
- distro
- caprover
- certbot
- lineinfile
- blockinfile
- familias
- subfamilia
- subfamilias
- genero
- generos
- subespecie
- subespecies
11 changes: 10 additions & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: pull-request
run-name: 'Validate #${{github.event.pull_request.number}} ${{github.event.pull_request.title}}'

on:
- pull_request
Expand All @@ -14,3 +13,13 @@ jobs:
node-version: 'lts/jod'
- run: yarn install
- run: yarn lint

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/jod'
- run: yarn install
- run: yarn test --silent test/**/*.test.ts
3 changes: 0 additions & 3 deletions .husky/pre-commit
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.16.0
lts/jod
8 changes: 0 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@
"files.insertFinalNewline": true,
"files.eol": "\n",
"liveServer.settings.port": 5501,
"cSpell.words": [
"distro",
"caprover",
"certbot",
"lineinfile",
"blockinfile",
"pipeable"
],
"[javascript]": {
"editor.tabSize": 4
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Pré-requisitos

- [NodeJS >= 18.16](https://nodejs.org/en)
- [NodeJS >= 22](https://nodejs.org/en)
- [Yarn](https://yarnpkg.com)
- [Docker](https://www.docker.com)
- [Docker Compose](https://docs.docker.com/compose)
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ services:
MYSQL_ROOT_PASSWORD: $MYSQL_PASSWORD
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/initdb:/docker-entrypoint-initdb.d
ports:
- ${MYSQL_PORT}:3306
command:
Expand Down
35 changes: 20 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"node": "^22"
},
"scripts": {
"migration:create": "tsx --env-file=.env src/database/cli.ts migration:create",
"migration:apply": "tsx --env-file=.env src/database/cli.ts migration:apply",
"lint:typescript": "tsc --noEmit",
"lint:eslint": "eslint --cache --fix --ext '.js,.ts,.tsx' .",
"lint": "run-p lint:typescript lint:eslint",
Expand All @@ -18,17 +20,18 @@
"start": "nodemon --ext 'js,ts,tsx' --exec babel-node --extensions '.js,.ts,.tsx' ./src/index.js",
"build": "run-s clean build:app",
"test": "jest",
"prepare": "husky install"
"prepare": "husky"
},
"devDependencies": {
"@babel/cli": "7.22.10",
"@babel/core": "7.22.11",
"@babel/node": "7.22.10",
"@babel/cli": "7.27.0",
"@babel/core": "7.26.10",
"@babel/node": "7.26.0",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/preset-env": "7.22.10",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.22.15",
"@types/express": "5.0.1",
"@types/jest": "30.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"@typescript-eslint/eslint-plugin": "6.7.0",
Expand All @@ -42,31 +45,32 @@
"eslint-import-resolver-typescript": "3.6.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-import-helpers": "1.3.1",
"husky": "8.0.3",
"husky": "9.1.7",
"jest": "29.7.0",
"nodemon": "3.0.1",
"nodemon": "3.1.9",
"npm-run-all": "4.1.5",
"typescript": "5.2.2"
"typescript": "5.8.3"
},
"dependencies": {
"axios": "^1.10.0",
"bcrypt": "5.1.1",
"bluebird": "3.7.2",
"body-parser": "1.20.2",
"body-parser": "2.2.0",
"commander": "13.1.0",
"cors": "2.8.5",
"date-fns": "^2.30.0",
"date-fns": "4.1.0",
"dotenv": "16.3.1",
"ejs": "^2.6.1",
"express": "4.18.2",
"express-validator": "^5.3.0",
"express": "5.1.0",
"express-validator": "7.2.1",
"handlebars": "^4.7.8",
"jsonwebtoken": "9.0.1",
"jsonwebtoken": "9.0.2",
"knex": "2.5.1",
"moment": "^2.24.0",
"moment-timezone": "^0.5.21",
"morgan": "1.10.0",
"multer": "1.4.5-lts.1",
"mysql2": "3.6.0",
"multer": "1.4.5-lts.2",
"mysql2": "3.14.0",
"node-wkhtmltopdf": "^2.0.0",
"puppeteer": "24.6.0",
"q": "^1.5.1",
Expand All @@ -77,7 +81,8 @@
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"throttled-queue": "^1.0.5",
"uuid": "9.0.0",
"tsx": "4.19.3",
"uuid": "11.1.0",
"wkhtmltopdf": "^0.4.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
Expand Down
1 change: 0 additions & 1 deletion src/controllers/pendencias-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1822,16 +1822,16 @@
if (objetoAlterado.genero_nome) parametros.genero = objetoAlterado.genero_nome;
if (objetoAlterado.especie_nome) parametros.especie = objetoAlterado.especie_nome;

if (objetoAlterado.identificadores) parametros.identificador = await Usuario.findOne({ where: { id: objetoAlterado.identificadores }, raw: true, nest: true });

Check warning on line 1825 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 167. Maximum allowed is 150
if (objetoAlterado.fase_sucessional_id) parametros.faseSucessional = await FaseSucessional.findOne({ where: { numero: objetoAlterado.fase_sucessional_id }, raw: true, nest: true });

Check warning on line 1826 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 189. Maximum allowed is 150
if (objetoAlterado.vegetacao_id) parametros.vegetacao = await Vegetacao.findOne({ where: { id: objetoAlterado.vegetacao_id }, raw: true, nest: true });

Check warning on line 1827 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 159. Maximum allowed is 150
if (objetoAlterado.relevo_id) parametros.relevo = await Relevo.findOne({ where: { id: objetoAlterado.relevo_id }, raw: true, nest: true });
if (objetoAlterado.solo_id) parametros.solo = await Solo.findOne({ where: { id: objetoAlterado.solo_id }, raw: true, nest: true });
if (objetoAlterado.cidade_id) parametros.cidade = await Cidade.findOne({ where: { id: objetoAlterado.cidade_id }, raw: true, nest: true });
if (objetoAlterado.tipo_id) parametros.tipo = await Tipo.findOne({ where: { id: objetoAlterado.tipo_id }, raw: true, nest: true });
if (objetoAlterado.entidade_id) parametros.entidade = await Herbario.findOne({ where: { id: objetoAlterado.entidade_id }, raw: true, nest: true });

Check warning on line 1832 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 155. Maximum allowed is 150
if (objetoAlterado.variedade_id) parametros.variedade = await Variedade.findOne({ where: { id: objetoAlterado.variedade_id }, raw: true, nest: true });

Check warning on line 1833 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 159. Maximum allowed is 150
if (objetoAlterado.sub_especie_id) parametros.subespecie = await Subespecie.findOne({ where: { id: objetoAlterado.sub_especie_id }, raw: true, nest: true });

Check warning on line 1834 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 165. Maximum allowed is 150

const tombo = await Tombo.findOne({
where: { hcf: alteracao.dataValues.tombo_hcf, ativo: true },
Expand Down Expand Up @@ -1922,16 +1922,16 @@
jsonRetorno.push({ key: '17', campo: 'Solo', antigo: tombo?.locais_coletum?.solo?.nome || '', novo: parametros.solo.nome });
}
if (parametros.descricao && tombo?.locais_coletum?.descricao !== parametros.descricao) {
jsonRetorno.push({ key: '18', campo: 'Descrição do relevo', antigo: tombo?.locais_coletum?.descricao || '', novo: parametros.descricao });

Check warning on line 1925 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 154. Maximum allowed is 150
}
if (parametros.relevo && (!tombo?.locais_coletum?.relevo?.id || tombo.locais_coletum.relevo.id !== parametros.relevo.id)) {
jsonRetorno.push({ key: '19', campo: 'Relevo', antigo: tombo?.locais_coletum?.relevo?.nome || '', novo: parametros.relevo.nome });
}
if (parametros.vegetacao && (!tombo?.locais_coletum?.vegetaco?.id || tombo.locais_coletum.vegetaco.id !== parametros.vegetacao.id)) {
jsonRetorno.push({ key: '20', campo: 'Vegetação', antigo: tombo?.locais_coletum?.vegetaco?.nome || '', novo: parametros.vegetacao.nome });

Check warning on line 1931 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 154. Maximum allowed is 150
}
if (parametros.faseSucessional && (!tombo?.locais_coletum?.fase_sucessional?.numero || tombo.locais_coletum.fase_sucessional.numero !== parametros.faseSucessional.id)) {

Check warning on line 1933 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 181. Maximum allowed is 150
jsonRetorno.push({ key: '21', campo: 'Fase sucessional', antigo: tombo?.locais_coletum?.fase_sucessional?.nome || '', novo: parametros.faseSucessional.nome });

Check warning on line 1934 in src/controllers/pendencias-controller.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 175. Maximum allowed is 150
}
}

Expand All @@ -1945,7 +1945,6 @@
nest: true,
});


if (identificadores?.length) {
const identificadorAntigo = identificadores.find(i => i.ordem === 1)?.identificadore;
if (identificadorAntigo && identificadorAntigo.identificador_id !== parametros.identificador.id) {
Expand Down
57 changes: 57 additions & 0 deletions src/database/apply-migration-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import path from 'node:path'

import { CorruptedDirectoryError } from './error/corrupted-directory-error'
import { MigrationFileSystem } from './migration-file-system'
import { MigrationRepository } from './migration-repository'

interface Dependencies {
migrationFileSystem: MigrationFileSystem
migrationRepository: MigrationRepository
}

export class ApplyMigrationService {

private readonly migrationFileSystem: MigrationFileSystem
private readonly migrationRepository: MigrationRepository

constructor(readonly dependencies: Dependencies) {
this.migrationFileSystem = dependencies.migrationFileSystem
this.migrationRepository = dependencies.migrationRepository
}

async execute() {
const migrationFilePaths = await this.migrationFileSystem.listMigrationFiles()
const migrationNames = migrationFilePaths.map(file => path.basename(file, path.extname(file)))

await this.migrationRepository.ensureMigrationTableExists()
const appliedMigrations = await this.migrationRepository.getAppliedMigrations()

const missingMigrations = appliedMigrations.filter(appliedMigration => !migrationNames.includes(appliedMigration.name))
if (missingMigrations.length) {
const missingMigrationsNames = missingMigrations.map(migration => migration.name)
throw new CorruptedDirectoryError('The migration directory is missing some files', missingMigrationsNames)
}

const unappliedMigrations = migrationNames
.filter(migrationFileName => !appliedMigrations.some(appliedMigration => appliedMigration.name === migrationFileName))

if (!unappliedMigrations.length) {
console.warn('No pending migrations to apply')
return
}

/* eslint-disable no-restricted-syntax, no-await-in-loop */
for (const migrationName of unappliedMigrations) {
try {
await this.migrationFileSystem.runMigrationFile(migrationName)
await this.migrationRepository.applyMigration(migrationName)
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error applying migration ${migrationName}`, error)
throw error
}
}
/* eslint-enable no-restricted-syntax */
}

}
52 changes: 52 additions & 0 deletions src/database/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { program } from 'commander'
import createKnex from 'knex'
import path from 'node:path'

import { ApplyMigrationService } from './apply-migration-service'
import { CreateMigrationService } from './create-migration-service'
import { MigrationFileSystem } from './migration-file-system'
import { MigrationRepository } from './migration-repository'

const {
MYSQL_DATABASE,
MYSQL_HOST,
MYSQL_PORT = '3306',
MYSQL_MIGRATION_USERNAME,
MYSQL_MIGRATION_PASSWORD
} = process.env

const migrationKnex = createKnex({
client: 'mysql2',
connection: {
database: MYSQL_DATABASE,
host: MYSQL_HOST,
port: parseInt(MYSQL_PORT),
user: MYSQL_MIGRATION_USERNAME,
password: MYSQL_MIGRATION_PASSWORD,
multipleStatements: true
}
})

const migrationFileSystem = new MigrationFileSystem({ knex: migrationKnex, migrationsPath: path.join(__dirname, 'migrations') })
const migrationDataSource = new MigrationRepository({ knex: migrationKnex, tableName: 'migrations' })

async function createMigration(name: string) {
const createMigrationService = new CreateMigrationService({ migrationFileSystem })
await createMigrationService.execute(name)
}

async function applyMigrations() {
const applyMigrationsService = new ApplyMigrationService({ migrationFileSystem, migrationRepository: migrationDataSource })
await applyMigrationsService.execute()
await migrationKnex.destroy()
}

program
.command('migration:create')
.argument('<name>', 'The name of the migration')
.action(createMigration)

program.command('migration:apply')
.action(applyMigrations)

program.parse(process.argv)
19 changes: 19 additions & 0 deletions src/database/create-migration-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationFileSystem } from './migration-file-system'

interface Dependencies {
migrationFileSystem: MigrationFileSystem
}

export class CreateMigrationService {

private readonly migrationFileSystem: MigrationFileSystem

constructor(readonly dependencies: Dependencies) {
this.migrationFileSystem = dependencies.migrationFileSystem
}

async execute(name: string): Promise<string> {
return this.migrationFileSystem.createMigrationFile(name)
}

}
13 changes: 13 additions & 0 deletions src/database/error/corrupted-directory-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class CorruptedDirectoryError extends Error {

override readonly message: string
readonly missingFiles: string[]

constructor(message: string, missingFiles: string[]) {
super(message)
this.message = message
this.name = 'CorruptedDirectoryError'
this.missingFiles = missingFiles
}

}
68 changes: 68 additions & 0 deletions src/database/migration-file-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Knex } from 'knex'
import fs from 'node:fs'
import path from 'node:path'

interface Dependencies {
knex: Knex
migrationsPath: string
}

const MIGRATION_TEMPLATE = `import { Knex } from 'knex'

export async function run(knex: Knex): Promise<void> {
await knex.raw('')
}
`

export class MigrationFileSystem {

private readonly knex: Knex
private readonly migrationsPath: string

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

async createMigrationFile(name: string): Promise<string> {
const timestamp = new Date().toISOString()
.substring(0, 19) // 2025-04-12T16:39:06.000Z => 20250412163906
.replace(/\D/g, '')
const fileName = `${timestamp}_${name}`

const upFilePath = path.join(this.migrationsPath, `${fileName}.ts`)

fs.writeFileSync(upFilePath, MIGRATION_TEMPLATE)

return upFilePath
}

async listMigrationFiles(): Promise<string[]> {
const files = fs.readdirSync(this.migrationsPath)
return files
.filter(file => /\.[jt]s$/.test(file))
.sort()
.map(file => path.join(this.migrationsPath, file))
}

private async importMigrationModule(name: string): Promise<{ run: (knex: Knex) => Promise<void> }> {
const filePathWithoutExtension = path.join(this.migrationsPath, name)

let migrationModule: { run: (knex: Knex) => Promise<void> }
if (fs.existsSync(`${filePathWithoutExtension}.ts`)) {
migrationModule = await import(`${filePathWithoutExtension}.ts`)
} else if (fs.existsSync(`${filePathWithoutExtension}.js`)) {
migrationModule = await import(`${filePathWithoutExtension}.js`)
} else {
throw new Error(`Migration file ${name} not found`)
}

return migrationModule
}

async runMigrationFile(name: string): Promise<void> {
const migration = await this.importMigrationModule(name)
await migration.run(this.knex)
}

}
Loading