O Silo é um aplicativo para fornecer informações mais rápido e eficiente dos processos e atividades realizadas no supercomputador, monitoradas pelo Grupo de Produtos e Processos (PP) do CPTEC/INPE.
Neste projeto serão utilizados nomes de variáveis, funções, comentários e classes somente em inglês. A tabulação está sendo feita dois espaços. Obrigatório o uso do plugin Prettier no Visual Studio Code.
A API será desenvolvida para dar suporte ao front-end do projeto.
1 - Instalar todas as dependências de uma só vez:
> npm install
2 - Ou instalar as dependências isoladamente:
> npm install express --save
> npm install cors --save
> npm install body-parser --save
> npm install dotenv --save
> npm install sequelize sqlite3 --save
> npm install bcrypt --save
> npm install jsonwebtoken --save
> npm install swagger-ui-express --save
E também as dependências de desenvolvimento:
> npm install sequelize-cli --save-dev
> npm install nodemon --save-dev
> npm install jest --save-dev
> npm install supertest --save-dev
> npm install swagger-autogen --save-dev
Foi criado manualmente o arquivo .env no root do projeto, e configurado as variáveis de ambiente:
NODE_ENV=development
PORT=3030
Ao alterar o ambiente para produção deve usar NODE_ENV=production. Os valores possíveis são: development, test ou production, conforme o arquivo ./config/config.json criado pelo sequelize.
O projeto está dividido em diretórios e arquivos com responsabilidades diferentes.
silo-api/
├─ .git
├─ assets/
│ └─ img/
├─ config/
│ └─ config.json
├─ controllers/
│ ├─ problemcategories.controller.js
│ ├─ problems.controller.js
│ ├─ problemsvsproblemcategories.controller.js
│ ├─ problemsvssolutions.controller.js
│ ├─ services.controller.js
│ ├─ solutions.controller.js
│ ├─ tasks.controller.js
│ └─ users.controller.js
├─ database/
│ ├─ mer.png
│ └─ silo.sqlite
├─ middlewares/
│ ├─ problemcategories.middleware.js
│ ├─ problems.middleware.js
│ ├─ services.middleware.js
│ ├─ solutions.middleware.js
│ ├─ tasks.middleware.js
│ ├─ users.middleware.js
├─ migrations/
├─ models/
│ ├─ index.js
│ ├─ problemcategories.js
│ ├─ problems.js
│ ├─ problemsvsproblemcategories.js
│ ├─ problemsvssolutions.js
│ ├─ services.js
│ ├─ solutions.js
│ ├─ tasks.js
│ ├─ users.js
├─ node_modules/
├─ routes/
│ ├─ auth.js
│ └─ index.js
├─ seeders/
├─ tests/
├─ .env
├─ .gitignore
├─ config.js
├─ index.js
├─ package.json
├─ README.md
├─ swagger-gen.js
├─ swagger-gen.json
└─ swagger.json
A estrutura abaixo pode ser obtida inserindo o comando tree no terminal do Windows. É o arquivo ./index.js que contém os scripts para inicializar o servidor.
A documentação completa será feita com a especificação do padrão OpenAPI 3.1.0, usando o Swagger. Pode-se editar a documentação através do o Swagger Editor online. A rota para a documentação está em /api/docs/. A documentação deve ser editada no arquivo swagger.json.
Para gerar a documentação básica automaticamente com o Swagger irei utilizar o Swagger Autogen. Para isso é só executar o arquivo swagger-gen.js que adicionei ao projeto. Esse arquivo pega as rotas e gera a documentação automaticamente para cada uma delas, mas sem detalhes e descrição:
> node swagger-gen
Depois é só pegar o que foi gerado, substituir e adaptar no arquivo ./swagger.json.
Caso no projeto adicione novos arquivos de rotas, é preciso adicionar o caminho desse arquivo no array endpointsFiles do arquivo ./swagger-gen.js, rodar novamente o comando acima e modificar o arquivo ./swagger.json com as novas alterações.
O Diagrama de Entidade Relacionamento (DER) a seguir descreve as entidades e relacionamentos do projeto.
Observação: O diagrama acima foi construído rapidamente usando o brModelo, ferramenta para modelagem de dados online e gratuita.
1 - Inicializar o sequelize, criando o arquivo ./config/config.json na raíz do projeto:
> npx sequelize-cli init
Em ./config/config.json alterar o development para:
"development": {
"database": "silo_development",
"storage": "./database/silo.sqlite",
"dialect": "sqlite"
},
2 - Criar o banco de dados:
> npx sequelize-cli db:create
Observação: Se o banco de dados for do tipo SQLite é preciso criar o arquivo ./database/silo.sqlite manualmente através do comando touch silo.sqlite ou em novo arquivo no VSCode.
3 - Criar as entidades do banco de dados:
> npx sequelize-cli model:generate --name Users --attributes name:string,email:string,password:string
> npx sequelize-cli model:generate --name Services --attributes name:string
> npx sequelize-cli model:generate --name Tasks --attributes serviceId:integer,name:string,description:string
> npx sequelize-cli model:generate --name Problems --attributes taskId:integer,title:string,description:string
> npx sequelize-cli model:generate --name ProblemCategories --attributes name:string
> npx sequelize-cli model:generate --name Solutions --attributes description:string
> npx sequelize-cli model:generate --name ProblemsVsSolutions --attributes problemId:integer,solutionId:integer
> npx sequelize-cli model:generate --name ProblemsVsProblemCategories --attributes problemId:integer,problemCategoryId:integer
Observação: Insira vírgulas sem espaços.
4 - Executar as migrations para aplicar as alterações, toda vez que um model do Sequelize acima:
> npx sequelize-cli db:migrate
5 - Alterações e modificações em tabelas
Se no futuro quiser alterar a coluna de uma tabela, criar uma nova migration com o comando, por exemplo:
> npx sequelize-cli migration:create --name alter-users
Depois editar o arquivo criado com o migration é possível alterar a estrutura. Por exemplo, para fazer com que o arquivo 20240506121018-alter-users.js (criado pelo comando acima) altere a coluna password para password_hash na tabela Users, é só editar o arquivo para deixá-lo da seguinte forma:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.renameColumn("Users", "password", "password_hash");
},
async down (queryInterface, Sequelize) {
await queryInterface.renameColumn("Users", "password_hash", "password");
}
};
Depois rodar o comando abaixo para atualizar:
> npx sequelize-cli db:migrate
Entretanto, irei deixar do jeito que está.
6 - Para criar relacionamentos entre tabelas, editar por exemplo o arquivo criado com o comando npx sequelize-cli model:generate ... e adicionar references. Por exemplo:
serviceId: {
type: Sequelize.INTEGER,
references: {
model: "Services",
key: "id",
},
onUpdate: "CASCADE",
onDelete: "CASCADE",
},
Ficando assim, por exemplo no arquivo 20240506123928-create-tasks.js:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("Tasks", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
serviceId: {
type: Sequelize.INTEGER,
references: {
model: "Services",
key: "id",
},
onUpdate: "CASCADE",
onDelete: "CASCADE",
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("Tasks");
}
};
Por fim rodar o comando abaixo para atualizar:
> npx sequelize-cli db:migrate
É necessário alterar também o arquivo do diretório ./models, pois precisa estar definido com o relacionamento. Por exemplo, para o arquivo ./models/tasks.js alterar assim:
static associate(models) {
this.hasOne(models.Services, { foreignKey: "serviceId" });
}
Para o arquivo ./models/problemsvssolutions.js, deixar assim:
static associate(models) {
this.hasMany(models.Problems, { foreignKey: "problemId" });
this.hasMany(models.Solutions, { foreignKey: "solutionId" });
}
Fazer isso para cada tabela que tiver um relacionamento.
Para criar uma nova coluna em uma tabela após a tabela já ter sido criada, rodar por exemplo, o seguinte comando para criar uma nova migration:
> npx sequelize-cli migration:create --name alter-tasks
O arquivo de migração ficaria assim:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("Tasks", "name", {
type: Sequelize.STRING
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("Tasks", "name");
}
};
E então, execute para aplicar as alterações no banco de dados:
> npx sequelize-cli db:migrate
Acesse o link da documentação oficial da Query Interface do Sequelize para mais informações.
As rotas estão divididas da seguinte forma:
Usuários: /api/users
[GET] /api/users (Listar os usuários)
[GET] /api/users?page=1&limit_per_page=10&order_by=id&order_sort=ASC&filter=mario
[POST] /api/users (Cadastrar um novo usuário)
[GET] /api/users/:id (Obter dados de um usuário pelo ID)
[PUT] /api/users/:id (Alterar dados de um usuário pelo ID)
[DELETE] /api/users/:id (Apagar um usuário pelo ID)
Para cadastrar um novo usuário é necessário enviar por body:
{
"name": "Mario",
"email": "[email protected]",
"password": "123456",
"roles": ["admin", "editor", "viewer"]
}
É feito uma validação com o construtor de schemas Yup para cada coluna utilizando middlewares:
const schema = {
name: yup.string().trim().required(),
email: yup.string().trim().email().required(),
password: yup.string().min(6).max(30).required(),
roles: yup.array().min(1).required(),
};
Isso também vale para as demais rotas.
Serviços: /api/services
[GET] /api/services (Listar os serviços)
[GET] /api/services?page=1&limit_per_page=30&order_by=id&order_sort=ASC&filter=brams
[POST] /api/services (Cadastrar um novo serviço)
[GET] /api/services/:id (Obter dados de um serviço pelo ID)
[PUT] /api/services/:id (Alterar dados de um serviço pelo ID)
[DELETE] /api/services/:id (Apagar um serviço pelo ID)
{
"name": "BAM"
}
const schema = {
name: yup.string().trim().required(),
};
Tarefas: /api/tasks
[GET] /api/tasks (Listar as tarefas)
[GET] /api/tasks?page=1&limit_per_page=30&order_by=id&order_sort=ASC&serviceId=1&filter=pos
[POST] /api/tasks (Cadastrar uma nova tarefa)
[GET] /api/tasks/:id (Obter dados de uma tarefa pelo ID)
[PUT] /api/tasks/:id (Alterar dados de uma tarefa pelo ID)
[DELETE] /api/tasks/:id (Apagar uma tarefa pelo ID)
Informações: /api/problems
[GET] /api/problems (Listar os problemas)
[GET] /api/problems?page=1&limit_per_page=30&order_by=id&order_sort=ASC&taskId=1&filter=
[POST] /api/problems (Cadastrar um novo problema)
[GET] /api/problems/:id (Obter dados de um problema pelo ID)
[PUT] /api/problems/:id (Alterar dados de um problema pelo ID)
[DELETE] /api/problems/:id (Apagar um problema pelo ID)
Categorias de problemas: /api/problemcategories
[GET] /api/problemcategories (Listar as categorias de problemas)
[GET] /api/problemcategories?page=1&limit_per_page=30&order_by=id&order_sort=ASC&filter=
[POST] /api/problemcategories (Cadastrar um novo problema)
[GET] /api/problemcategories/:id (Obter dados de um problema pelo ID)
[PUT] /api/problemcategories/:id (Alterar dados de um problema pelo ID)
[DELETE] /api/problemcategories/:id (Apagar um problema pelo ID)
Soluções: /api/solutions
[GET] /api/solutions (Listar as soluções)
[GET] /api/solutions?page=1&limit_per_page=30&order_by=id&order_sort=ASC&filter=
[POST] /api/solutions (Cadastrar uma nova solução)
[GET] /api/solutions/:id (Obter dados de uma solução pelo ID)
[PUT] /api/solutions/:id (Alterar dados de uma solução pelo ID)
[DELETE] /api/solutions/:id (Apagar uma solução pelo ID)
Problemas x soluções (relacionamento): /api/problemsvssolutions
[POST] /api/problemsvssolutions (Cadastrar um relacionamento de problemas x solução)
[GET] /api/problemsvssolutions?problemId=1 (Obter dados de um relacionamento de problemas x solução pelo ID do problema)
[GET] /api/problemsvssolutions?solutionId=1 (Obter dados de um relacionamento de problemas x solução pelo ID da solução)
[DELETE] /api/problemsvssolutions?problemId=1&solutionId=1 (Apagar um relacionamento de problemas x solução pelo ID do problema e da solução)
Problemas x Categorias de problemas (relacionamento): /api/problemsvsproblemcategories
[POST] /api/problemsvsproblemcategories (Cadastrar um relacionamento de problemas x categoria de problemas)
[GET] /api/problemsvsproblemcategories?problemId=1 (Obter dados de um relacionamento de problemas x categoria de problemas pelo ID do problema)
[GET] /api/problemsvsproblemcategories?problemCategoryId=1 (Obter dados de um relacionamento de problemas x categoria de problemas pelo ID da solução)
[DELETE] /api/problemsvsproblemcategories?problemId=1&problemCategoryId=1 (Apagar um relacionamento de problemas x categoria de problemas pelo ID do problema e da solução)
Documentação (usando Swagger): /api/docs
[GET] /api/docs (Documentação de toda a API)
O projeto utiliza o JWT, pois tem a vantagem de transmitir os dados do usuário por meio de token. É melhor do que a utilização por Session, pois não precisa armazenar sessions no servidor, apenas utiliza o token do lado do cliente em uma localStorage, por exemplo. Utilizando esta abordagem, seguimos os padrões do RESTful.
[POST] /api/auth (Login com e-mail e senha para obter o token que será enviado em cada requisição)
Se o usuário possuir autorização (roles), ele poderá acessar a rota. As roles permitidas são: admin, editor e viewer.
Para acessar deve inserir no Bearer da requisição no Insomnia ou Postman e o Bearer valor-do-token obtido através do login.
Algumas tabelas podem ser já pré-preenchidas, tanto para teste quanto para situações reais.
A tabela Users já terá algumas informações pré-preenchidas. Para isso vamos popular com alguns dados a tabela:
> npx sequelize-cli seed:generate --name Users
Realizar as seguintes alterações no arquivo criado, por exemplo, o arquivo ./seeders/20240510121749-Users.js:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert("Users", [
{ name: "Mario", email: "[email protected]", password: "viewe$2a$08$5YiRHW/o6.aW.ErN0lBx.uIA1zIl1cQ.S0xOKdlRlsipiMOzAAJFKr", roles: '["admin", "editor", "viewer"]', createdAt: "2024-05-07 13:42:14.060 +00:00", updatedAt: "2024-05-07 13:42:14.060 +00:00" },
{ name: "Lucas", email: "[email protected]", password: "viewe$2a$08$5YiRHW/o6.aW.ErN0lBx.uIA1zIl1cQ.S0xOKdlRlsipiMOzAAJFKr", roles: '["editor", "viewer"]', createdAt: "2024-05-07 13:42:14.060 +00:00", updatedAt: "2024-05-07 13:42:14.060 +00:00" },
], {});
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete("Users", null, {});
}
};
E em seguida aplicar as alterações ao seeder específico:
> npx sequelize-cli db:seed --seed 20240510121749-Users.js
Para aplicar as alterações a todos os seeders, seria este o comando:
> npx sequelize-cli db:seed:all
Para desfazer um seed específico:
> npx sequelize db:seed:undo --seed 20240510121749-Users.js
Para desfazer todos os seeders gerados até o momento:
> npx sequelize db:seed:undo:all
Todas as rotas devem ser adicionadas no arquivo ./routes/index.js. A rota de login está no arquivo ./routes/auth.js.
Para os testes de servidor, usando supertest é preciso alterar a variável de ambiente de execução para test e usar o banco de dados de teste:
> npx sequelize-cli db:create --env test
> npx sequelize-cli db:migrate --env test
> npx sequelize-cli db:seed:all --env test
O servidor deve ser exportado, adicionar o seguinte no final do arquivo ./index.js:
module.exports = app;
Para executar todas as rotinas de teste usando jest e supertest:
> npm test
Essa rotina irá executar todos os arquivos com a extensão ".test.js" do diretório ./tests.
Estou utilizando o Insomnia para testar as rotas, mas utilize o Postman ou a extensão Boomerang no Google Chrome se você quiser.