Introdução

O AdonisJS é um framework full MVC inspirado no Laravel, framework consolidado da linguagem PHP, feito em NodeJS. Ele nos permite diversas abstrações no desenvolvimento de um sistema completo, o que agiliza o processo de programação. Com ele temos diversas ferramentas como: Validadores, Autenticação, WebSocket, Transações, Middlewares, Routing, Seeds, Factories, Hooks, CORS e diversas outras. No post é dado início em um projeto para conhecermos um pouco deste incrível framework.

Proposta de Projeto

Nada melhor do que aprender uma nova tecnologia na prática. Por isso vamos construir nossa primeira aplicação utilizando AdonisJS ao longo desse texto.

O sistema deve permitir ao usuário (nesse caso, uma aplicação front-end) realizar chamadas REST utilizando JSON para a realização da lógica do nosso sistema de chamados. Sendo assim, construiremos uma REST API utilizando o AdonisJS, tornando a View independente do projeto do back-end.

Mas o AdonisJS não é um framework full MVC? Com certeza, porém devido a cisão do desenvolvimento WEB entre back-end e front-end, boa parte por conta da ramificação entre as técnicas e tecnologias utilizadas nesses segmentos, atualmente, essa é a escolha  trivial realizada pelas empresas no começo de um projeto. Além disso, essa divisão torna a aplicação independente, por exemplo, com repositórios totalmente diferentes, possibilitando melhor manutenção e escalabilidade.

Help Desk

Faremos um sistema de Help Desk simplificado como prática!

Requisitos

  • O sistema deve permitir a um usuário cadastrar um chamado. A exclusão de um chamado só pode ser feita se ele não foi atendido.
  • O sistema deve permitir a um técnico atender um chamado. O atendimento se dá pela mudança de situação.
  • O técnico pode encaminhar um chamado para outro técnico ou setor.
  • Um técnico deve pertencer a um setor.
  • Um setor pode ter diversos problemas pré-cadastrados.
  • O sistema deve listar os problemas do setor no ato do cadastro.
  • Toda mudança de situação (status) de um chamado deve ser enviada ao usuário que o abriu.

Adonis CLI

Para dar o pontapé inicial no nosso projeto Adonis, precisamos de sua CLI (command-line interface). Para realizar a instalação digite no seu terminal npm i -g @adonisjs/cli, se estiver utilizando outro gerenciador de pacotes Node, substitua adequadamente no comando.

Verifique sua instalação com o seguinte comando: adonis -h. Agora você deve visualizar todos os comandos possíveis com o Adonis CLI, e sim, essa ferramenta é incrivelmente útil e essencial no desenvolvimento de uma aplicação Adonis.

Vamos lá, agora utilize adonis new help-desk-nave. Esse comando vai criar uma nova aplicação Adonis dentro do diretório especificado, que nesse caso é help-desk-nave. Quando o comando terminar, abra seu editor favorito no diretório citado e let's code!

Estrutura de diretórios

O Adonis tem uma estrutura de diretórios própria e que deve ser respeitada ao máximo, porque minimiza a necessidade de um novo desenvolvedor do projeto ter que aprendê-la, visto que a estrutura padrão está bem documentada pelo framework.

  • app: regras de negócio da aplicação (Models, Controllers e etc)
  • config: configuração dos módulos instalados
  • database: arquivos sobre banco de dados, como: relações (tabelas)
  • start: arquivos executados na inicialização da aplicação

Conectando com o banco de dados

O AdonisJS suporta apenas banco de dados relacionais, para a permanência e recuperação da camada de Model. Clique aqui para visualizá-los.

No nosso projeto, utilizaremos o PostgreSQL, para isso, você precisa executar npm i pg. Navegue em config/database.js e configure adequadamente, deve ficar mais ou menos assim:

/** @type {import('@adonisjs/framework/src/Env')} */
const Env = use("Env");

module.exports = {
  connection: Env.get("DB_CONNECTION", "pg"),

  pg: {
    client: "pg",
    connection: {
      host: Env.get("DB_HOST", "localhost"),
      port: Env.get("DB_PORT", ""),
      user: Env.get("DB_USER", "root"),
      password: Env.get("DB_PASSWORD", ""),
      database: Env.get("DB_DATABASE", "adonis")
    },
    debug: Env.get("NODE_ENV", "development") === "development"
  }
}
Configuração da conexão com o BD

Todas as entradas definidas no primeiro argumento da função Env.get devem estar propriamente preenchidas no arquivo .env na raiz do projeto, se não será usado o segundo argumento como resultado da chamada. Exemplo de .env:

NODE_ENV=development
PORT=3000
DB_CONNECTION=pg
DB_HOST=hdu-postgres
DB_PORT=5432
DB_PORT_EXPOSED=5432
DB_USER=root
DB_ALLOW_EMPTY_PASSWORD=yes
DB_PASSWORD=123456
DB_DATABASE=hdu
.env

Migrations

Agora, temos que definir nossas relações (tabelas). Para este fim, utilizaremos as migrations que são alterações no banco de dados codificadas em Javascript. Além disso, elas nos possibilita, nativamente, o retorno (rollback) a partir de checkpoint. A cada comando adonis migration:run, que fará as migrations serem gravadas no banco dados, é feito um commit, ou seja, na execução do comando adonis migration:rollback retornaremos para um estado anterior ao último commit.

Nas próximas seções, apresentarei as migrations que descreverão o nosso banco de dados relacional. Toda migration é colocada no diretório database/migrations pelo framework. Para cada migration especificada, assumo que você tenha utilizado o comando adonis make:model -m -c model, esse comando criará o Modelo, Controlador e Migration deste model. Exemplo:

╰─➤  adonis make:model -m -c Test 
✔ create  app/Models/Test.js 
✔ create  database/migrations/1575204853018_test_schema.js 
✔ create  app/Controllers/Http/TestController.js 
Criação de modelo, controlador e migration para a classe Test.
Modelo lógico BD

Usuário (Técnico)

No diretório de migrations, por padrão, temos duas criadas, tanto a de Usuário  quanto a de Tokens. O AdonisJS parte do pressuposto que toda aplicação terá pelo menos essas duas tabelas. Sendo assim, apenas editaremos a migration de usuário para preencher nossos requisitos. O usuário ficará estruturado do seguinte modo:

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class UserSchema extends Schema {
  up() {
    this.create("users", table => {
      table.increments();
      table
        .string("username", 80)
        .notNullable()
        .unique();
      table
        .string("email", 254)
        .notNullable()
        .unique();
      table.string("password", 60).notNullable();
      table.string("first_name", 30).notNullable();
      table.string("last_name", 30).notNullable();
      table.string("telephone", 20).notNullable();
      table.enu("role", ["Técnico", "Gerente", "Admin"]).defaultTo("Técnico");
      table.timestamps(); // campos created_at e updated_at 
    });
  }

  down() {
    this.drop("users");
  }
}
Tabela de Usuários

Setor

Chamados devem ser direcionados para um determinado Setor da empresa. Segue a descrição da tabela:

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class SetorSchema extends Schema {
  up() {
    this.create("sectors", table => {
      table.increments();
      table
        .string("name", 30)
        .notNullable()
        .unique();
      table.string("telephone",20).notNullable();
      table
        .string("email")
        .notNullable()
        .unique();
      table.timestamps();
    });
  }

  down() {
    this.drop("sectors");
  }
}
Tabela de Setor

Problema

Cada setor terá problemas pré-cadastrados, que podem ou não ser atrelados ao Chamado.

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class ProblemSchema extends Schema {
  up() {
    this.create("problems", table => {
      table.increments();
      table.text("description").notNullable();
      table
        .integer("sector_id")
        .unsigned()
        .references("id")
        .inTable("sectors")
        .notNullable();
      table.timestamps();
    });
  }

  down() {
    this.drop("problems");
  }
}
Tabela de Problema

Usuário (Caller)

Aquele quem abre o chamado. Este usuário não terá senha, seu email servirá como autenticação.

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class CallerSchema extends Schema {
  up() {
    this.create("callers", table => {
      table.increments();
      table
        .string("cpf", 15)
        .unique()
        .notNullable();
      table
        .string("email")
        .unique()
        .notNullable();
      table.string("telephone", 20).notNullable();
      table.timestamps();
    });
  }

  down() {
    this.drop("callers");
  }
}
Tabela de Caller

Chamado

Peça fundamental do nosso sistema, com diversas relações. Segue sua descrição:

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class CallSchema extends Schema {
  up() {
    this.create("calls", table => {
      table.increments();
      table.text("description").notNullable();
      table.string("asset_cod", 15);
      table
        .integer("problem_id")
        .unsigned()
        .references("id")
        .inTable("problems");
      table
        .integer("user_id")
        .unsigned()
        .references("id")
        .inTable("users")
        .notNullable();
      table
        .integer("caller_id")
        .unsigned()
        .references("id")
        .inTable("callers")
        .notNullable();
      table
        .integer("sector_id")
        .unsigned()
        .references("id")
        .inTable("sectors")
        .notNullable();
      table.timestamps();
    });
  }

  down() {
    this.drop("calls");
  }
}
Tabela de Chamados

Alteração

Um técnico pode atender um chamado e, por questões de segurança e praticidade, todo chamado terá várias alterações. O conjunto dessas alterações formarão o ciclo de vida do chamado, assim como um log. Aqui pode ser visto um novo tipo de dado enum nas colunas status e priority.

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class ModificationSchema extends Schema {
  up() {
    this.create("modifications", table => {
      table.increments();
      table.timestamp("date");
      table.string("description");
      table
        .integer("user_id")
        .unsigned()
        .references("id")
        .inTable("users")
        .notNullable();
      table
        .integer("call_id")
        .unsigned()
        .references("id")
        .inTable("calls")
        .notNullable();
      table
        .enu("situation", 
        	["Opened", "Processing", "Pending", "Transfered", "Finished"]
        )
        .defaultTo("Opened")
      table
        .enu("priority", ["Low", "Medium", "High", "Urgent"])
        .defaultTo("Low");
      table.timestamps();
    });
  }

  down() {
    this.drop("modifications");
  }
}
Tabela de Alteração

Migration:run

Com todas os arquivos prontamente criados e configurados, perceba que há uma regra temporal nos arquivos de migrations, definido pelo prefixo do seu nome com um timestamp. Sendo assim, a ordem da execução deles importa.

Por isso, utilizaremos não uma criação, mas uma alteração com o comando adonis make:migration, para editar a tabela de Usuário e adicionar uma relação com Setor . Essa relação não era possível ser definida logo na migration de criação de Usuário, visto que a tabela Setor não existia.

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use("Schema");

class UserSchema extends Schema {
  up() {
    this.table("users", table => {
      // alter table
      table
        .integer("sector_id")
        .unsigned()
        .references("id")
        .inTable("sectors")
        .notNullable();
    });
  }

  down() {
    this.table("users", table => {
      // reverse alternations
      table.dropColumn("sector_id");
    });
  }
}
Adição de relação entre Usuário e Setor

É de suma importância a definição de ambas as funções, tanto up quanto down. Visto que a up será chamada no migration:run e a down no migration:rollback ou migration:reset. Após isso, execute adonis migration:run.

PgAdmin

Como decidi utilizar o PostgreSQL, é natural a utilização da ferramenta de administração web PgAdmin.  Nela conecte-se ao banco de dados (utilize as mesmas configurações realizadas no config/database.js) e navegue até a seção de tabelas, lá você poderá visualizar o adonis_schema, meta-tabela utilizada pelo framework, além de todas as tabelas específicas da aplicação, especificadas nas migrations acima.

Tabelas no RDBMS

Conclusão

Sendo assim, criamos uma aplicação NodeJS utilizando o framework MVC AdonisJS. Nesse ponto chegamos até a criação das tabelas no banco de dados PostgreSQL. No próximo post veremos um pouco sobre a criação de Controladores e também de Relações (no Model).