Skip to content

sessojunior/silo-api

Repository files navigation

Silo API (silo-api)

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.

Login Dashboard Modal 1 Modal 2

Instalação

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

Variáveis de ambiente

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.

Arquivos do projeto

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.

Documentação

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.

Configuração do banco de dados

O Diagrama de Entidade Relacionamento (DER) a seguir descreve as entidades e relacionamentos do projeto.

Diagrama de Entidade Relacionamento

Observação: O diagrama acima foi construído rapidamente usando o brModelo, ferramenta para modelagem de dados online e gratuita.

Sequelize CLI:

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.

Rotas

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)

Autorização com JWT

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.

Criando seeders

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.

Rotinas de teste

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.

About

Silo API

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published