DEV Community

Cover image for 04 - Estudo de Microsserviços ingresso.x - O Catálogo
José Antonio
José Antonio

Posted on • Edited on

04 - Estudo de Microsserviços ingresso.x - O Catálogo

🔓 Esse artigo é o quarto de uma sequência de estudo em andamento.

github linkedin twitter Instagram Badge Status


Documento
Um documento é qualquer registro de informações, independentemente do formato ou suporte utilizado para registrá-las.

Fonte: Wikipédia

O texto abaixo é referente à branch feature/initial-main-000 do repositório de estudo. Ao final do artigo teremos desenvolvido até o ponto da branch feature/catalog-service-001


📚 Diagramas, ADRs e Documentação

Um bom inicio para microsserviços é um diagrama simples, um Diagrama de Fluxo de Processo, onde podemos definir as ações e quais entidades do sistema elas afetam.

Existem inúmeras ferramentas que permitem gerar diagramas, inclusive de maneira colaborativa, Miro Board, Draw.io etc... mas a abordagem de arquivos readme.md ricos (os quais falarei em outro artigo) permite que documentação sobre códigos fique próxima do programador, mesmo existindo Wikis, GithubPages ou Coda do projeto. O Mermaid que gera diagramas a partir de código, posteriormente renderizados direto de arquivos markdown no GitHub, foi a escolha.

Documentação próxima ao código "induz" (ou deveria "induzir") o desenvolvedor a mantê-la atualizada, isso poderia ser parte da sua Definição de Pronto pois:

"Documentação é um dos principios de disponibilidade." - Susan Fowler

O problema da "sala de cinema" já é "velho conhecido" para aqueles que estudaram UML, mesmo que esse não seja seu caso, é simples compreender como as coisas se relacionam.

"Nada é mais didático do que desenhar para comunicar." - Bruno Nardini

Image description

A partir da iteração desse fluxo, similar ao caso de uso, fica fácil entender que um Employee User tem as responsabilidades de manutenção de nosso catálogo de filmes, salas de cinema, Endereços, sessões, assentos e nosso produto de fato, o ticket. Nada mais do que um assento de uma sala de cinema que está reservado para o horário x de uma sessão.

A partir da dinâmica representada pelo diagrama anterior e com algum conhecimento de base de dados relacionais avancei em direção ao Diagrama de Entidade-Relacionamento (ER) abaixo:


Image description

Isso nos dá sólidas garantias de que todos os envolvidos compreendam o que será feito. Mas e quanto a como será feito? Quais as decisões de arquitetura serão tomadas para que o projeto de prosseguimento? Para isso uma boa prática e definirmos essas decisões de arquitetura em outro documento e não em nosso readme rico. Vamos definir uma ADR - Architectural Decision Records

Deixo aqui o contexto com as principais decisões do documento, mas sugiro a você dar uma olhada no ADR completo no repositório.

"Estamos projetando um Microsserviço de Catálogo de ingressos de cinema. Decidimos usar o framework Express com TypeScript para criar a API em arquitetura MVC. Faremos testes de unidade e integração com Jest e usaremos Eslint para linting. Adotaremos principios SOLID onde se aplicar. O ORM Sequelize será usado para acessar o banco de dados PostgreSQL. Utilizaremos Swagger para documentação da API. O desenvolvimento acontecerá em um Monorepo NX, permitindo a organização de múltiplos projetos em um único repositório. Ceph será usado usado como storage local para os posters, simulando um Amazon S3. Não integraremos autenticação agora, mas consideramos Keycloak no futuro. Cache Redis está sendo considerado para o futuro"


👷 Mãos à Obra ou Show-me the code

Agora as coisas vão ficar um pouco mais sérias. Estou partindo do pressuposto de que o leitor possui alguma familiaridade com o VSCode e o terminal. Nosso ambiente é padronizado e o gerenciador de monorepos NX nos permite um conjunto de facilidades para criar e manter nossos microsserviços, pacotes dentro de do seu diretorio packages. Para utilizar esse poder, temos que possuir a última versão estável do Node.js instalada e do gerenciador de Monorepos NX

Node.js

Logo após, instalar nosso gerenciador de Monorepo.

NX

Pronto, com isso agora podemos finalmente por a "Mão na Massa" e codar algo que seja aderente as nossas guidelines.

Navegue Até a raiz do projeto em seu terminal, para instalar suas dependencias. Digite o seguinte comando:

$ npm install
Enter fullscreen mode Exit fullscreen mode

Com isso, teremos as dependências do projeto disponíveis em nosso monorepo NX. Agora, podemos criar nosso primeiro microserviço. Como estamos em um ambiente JavaScript, o próprio gerenciador nos auxiliará nessa tarefa. Basta digitar:

$ nx generate @nrwl/express:app catalog-service
Enter fullscreen mode Exit fullscreen mode

Agora sua pasta packages vai se parecer com a estrutura abaixo:

  $ tree
  .
  └── packages
  |    ├── catalog-service
  |    ├── catalog-service-e2e
  |    ├── cine-ticket-front-site
  |    └── cine-ticket-nginx
  .
Enter fullscreen mode Exit fullscreen mode

O NX criou dois novos packages: catalog-service já com um backend mínimo e catalog-service-e2e, seus respectivos testes e2e. Mas como o projeto esta configurado para typescript, o arquivo packages/catalog-service/src/main.ts acusa erro na linha 13 por conta das definições do typescript e pela declaração da variável req que não esta sendo utilizada, basta substituir por _ que o código estará correto.

Image description

Agora rode o seguinte comando para testar seu "microsserviço mínimo":

$ nx serve catalog-service
Enter fullscreen mode Exit fullscreen mode

A saída deve ser similar a:

chunk (runtime: main) main.js (main) 1.87 KiB [entry] [rendered]
webpack compiled successfully (eb148a07024bd3a0)
Debugger listening on ws://localhost:9229/75f60138-7e65-4fbb-95b9-23c6553bfdf8
For help, see: https://nodejs.org/en/docs/inspector

Listening at http://localhost:3333/api
Enter fullscreen mode Exit fullscreen mode

Acesando a url que esta sendo servida, Listening, no comando o seu browser deve ser algo parecido com isso:

Image description

Embora pareça estar tudo funcional, percebi que os arquivos transpilados que devem ser distribuídos, os dist, estavam sendo gerados na raiz do projeto e gostaria que nesse momento eles fossem gerados dentro de seus respectivos pacakges.

A solução foi simples, bastando acessar o arquivo /packages/catalog-service/project.json e alterar o valor de build.options.outputPath para packages/catalog-service/dist e resolvido. Interrompa o processo do catalog-service digitando ctrl + c no terminal do seu VSCode, remova a pasta dist da raiz e rode o comando $ nx serve catalog-service novamente. A pasta dist deve ser criada dentro do catalog-service agora, o comportamento esperado.

💱 Trocando WebPack por ESbuild

Por padrão, o NX usa o WebPack para gerar seus builds. No entanto, optei por uma abordagem mais moderna e adotei o EsBuild para essa tarefa. Precisaremos instalar suas dependências e fazer algumas configurações adicionais. Na raiz do projeto, execute o seguinte comando:

$ npm install --save-dev @nrwl/esbuild
$ npm install --save-dev @nx/esbuild
Enter fullscreen mode Exit fullscreen mode

E então, devemos informar ao NX que o nosso pacote catalog-microsservice deve agora utilizar as configurações do esbuild. Você pode fazer isso com o seguinte comando:

$ nx g @nx/esbuild:configuration catalog-service \
--main=packages/catalog-service/src/main.ts \
--tsConfig=packages/catalog-service/tsconfig.custom.json --skipValidation
Enter fullscreen mode Exit fullscreen mode

Mas não é apenas isso; na verdade, serão necessários mais dois ajustes. Para isso, abra o arquivo package.json que deve ter sido criado na raiz de packages/catalog-microsservice. Ele deve se parecer com a estrutura vista abaixo:

{
  "name": "@cine-ticket-microsservices/catalog-service",
  "version": "0.0.1"
}
Enter fullscreen mode Exit fullscreen mode

Abaixo do campo "version": "0.0.1", coloque a seguinte entrada "type": "module" no documento json informando ao Node.js que os arquivos JavaScript no projeto devem ser tratados como módulos ESM. Isso significa que podemos usar a sintaxe import e export para carregar módulos em vez da sintaxe require e module.exports usada em módulos CommonJS.

Para finalizar, remova os comentários e as linhas desnecessárias do seu arquivo packages/catalog-microsservice/main.ts. Isso inclui as linhas de 1 a 5, o import de path na linha 7 e a linha 11, já que não faremos uso de assets.

packages/catalog-microsservice/main.ts

1    /**
2     * This is not a production server yet!
3     * This is only a minimal backend to get started.
4     */
5    
6    import express from 'express';
7    import * as path from 'path';
8    
9    const app = express();
10   
11    app.use('/assets', express.static(path.join(__dirname, 'assets')));
12   
13    app.get('/api', (_, res) => {
14      res.send({ message: 'Welcome to catalog-microsservice!' });
15    });
16   
17    const port = process.env.PORT || 3333;
18    const server = app.listen(port, () => {
19      console.log(`Listening at http://localhost:${port}/api`);
20    });
21    server.on('error', console.error);
Enter fullscreen mode Exit fullscreen mode

Pronto, mudamos nosso builder e tudo deve continuar como era antes.


🎲 Banco de Dados, Models e o Serviço a partir dai

Temos um serviço mínimo rodando; é hora de começar a construir funcionalidades reais e robustas. Vamos dar vida às entidades e relações que mapeamos no Diagrama ER a partir do banco de dados. No terminal, na raiz do projeto, saia do prompt do nx serve digitando ctrl + c e digite os comandos para criar a pasta onde armazenaremos os dados de volume de nosso banco de dados

mkdir packages/catalog-service-db
mkdir packages/catalog-service-db/data
Enter fullscreen mode Exit fullscreen mode

Adicione as seguintes linhas no seu docker-compose.yml visando criar o banco de dados Postgres que nosso microsservico Utilizará.

docker-compose.yml

  catalog-service-db:
    image: postgres:latest
    container_name: catalog-service-db
    restart: always
    environment:
      POSTGRES_DB: catalog_service_db
      POSTGRES_USER: catalog_service_user
      POSTGRES_PASSWORD: catalog_service_password
    ports:
      - "5432:5432"
    volumes:
      - ./packages/catalog-service-db/data:/var/lib/postgresql/data
    networks:
      - cine-ticket-network

Enter fullscreen mode Exit fullscreen mode

Rode a partir da raiz um docker compose up --build para subir e reconstruir as imagens de nossos serviços em conjunto com o Postgres, isso deve demorar um pouco. Note que ainda não configuramos o catalog-service para subir junto, apenas o seu banco.

Agora que temos um banco de dados, vamos criar a conexão e os modelos correspondentes aos nossos dados, conforme decidido no ADR. Abra um novo terminal e crie a pasta /packages/catalog-service/src/models executando o seguinte comando em um novo terminal:

mkdir packages/catalog-service-db/src/models
Enter fullscreen mode Exit fullscreen mode

Sua árvore de diretório deve ficar similar a:

  $ tree
  .
  └── packages
  |    ├── catalog-service
  |        ├── src
  |            ├── assets
  |            ├── models
  .
Enter fullscreen mode Exit fullscreen mode

E então, instalaremos as dependências necessárias para criar nossos modelos. Em uma nova janela do terminal insira os comandos:

npm install sequelize pg pg-hstore
npm install @types/sequelize --save-dev
Enter fullscreen mode Exit fullscreen mode
  1. sequelize: ORM (Object-Relational Mapping) que você usará para interagir com o banco de dados.
  2. pg: driver do PostgreSQL para o Node.js, permitindo que o Sequelize se conecte ao PostgreSQL.
  3. pg-hstore: pacote que lida com o mapeamento de tipos de dados JSON para o PostgreSQL, usado pelo Sequelize.
  4. types/sequelize: fornece definições de tipos TypeScript para a biblioteca Sequelize

✍️ Nota:
Tentei usar o sequelize-cli para automatizar a geração das models, mas ele ainda não oferece suporte ao TypeScript. Portanto, criei as models manualmente. Existe um pacote chamado sequelize-typescript, mas como seu mantenedor não é a equipe oficial do Sequelize, não o utilizei.

🔌 Criando a conexao:

touch packages/catalog-service/src/models/db.ts
code packages/catalog-service/src/models/db.ts
Enter fullscreen mode Exit fullscreen mode

Esses comandos criam e abrem no seu VsCode o arquivo packages/catalog-service/models/db.ts que vai conectar nossa API ao banco, veja abaixo uma Implantação simples de conexão ao banco que você deve digitar no arquivo:

packages/catalog-service/models/db.ts

1   import { Sequelize } from 'sequelize';
2  
3   const pool = {
4     max: 5,
5     min: 0,
6     acquire: 130000,
7     idle: 10000
8    };
9  
10   const sequelize = new Sequelize({
11     dialect: 'postgres',
12     host: 'localhost',
13     database: 'catalog_service_db',
14     username: 'catalog_service_user',
15     password: 'catalog_service_password',
16     pool
17   });
18  
19   export default sequelize;

Enter fullscreen mode Exit fullscreen mode

✍️ Nota:
Atenção especial entre as linhas 3 a 8. Esse é um problema clássico para iniciantes e aborda uma das "Falácias" - a da "Composição" - que afirma: "se um componente individual funciona bem, todos eles funcionarão bem em conjunto". Sua aplicação não escala em conjunto com seu banco; é necessário que tenhamos um pool de conexões que evite gargalos. Já vi isso acontecer em uma aplicação na qual microserviços individuais eram bem otimizados, mas os gargalos frequentemente ocorriam devido a bloqueios no banco de dados central, limitando a escalabilidade horizontal.

Instanciamos com os parâmetros de conexão e exportamos o sequelize.

Com isso efetivamente vinculamos o banco de dados incluído em nosso docker-compose.yml ao projeto. Como software gerenciador de banco de dados, estou utilizando o DBeaver e sugiro sua instalação.

DBeaver

Com o DBeaver instalado, crie uma nova conexão nele usando os mesmos dados de acesso usados em nosso arquivo db.ts para verificar o banco, mesmo que não tenhamos as tabelas ainda:

Image description

Vamos agora criar nossa primeira entidade "filme", estando na raiz do projeto digite os seguintes comandos:

$ touch packages/catalog-service/src/models/film.ts
$ code packages/catalog-service/src/models/film.ts
Enter fullscreen mode Exit fullscreen mode

Criamos e abrimos no nosso vscode a entidade filme, a qual digitaremos o seguinte código typescript baseado na entidade film de nosso DER:

packages/catalog-service/models/film.ts

1  import { Model, DataTypes, Sequelize } from 'sequelize';
2 
3  class Film extends Model {
4    public id!: number;
5    public uuid!: string;
6    public name!: string;
7    public description!: string;
8    public age_rating!: number;
9    public subtitled!: boolean;
10   public poster!: string;
11 
12   static associate() {
13   //static associate(models: any) {
14     // Define as associações aqui...
15   }
16 }
17
18 export default (sequelize: Sequelize) => {
19   Film.init({
20     id: {
21       type: DataTypes.INTEGER,
22       autoIncrement: true,
23       primaryKey: true,
24     },
25     uuid: {
26       type: DataTypes.UUID,
27       allowNull: false,
28     },
29     name: {
30       type: DataTypes.STRING(500),
31       allowNull: false,
32     },
33     description: {
34       type: DataTypes.TEXT,
35       allowNull: false,
36     },
37     age_rating: {
38       type: DataTypes.INTEGER,
39       allowNull: false,
40     },
41     subtitled: {
42       type: DataTypes.BOOLEAN,
43       allowNull: false,
44     },
45     poster: {
46       type: DataTypes.STRING(500),
47       allowNull: false,
48     },
49   }, {
50     sequelize,
51     modelName: 'Film',
52     tableName: 'film',
53   });
54
55   return Film;
56 };
57
Enter fullscreen mode Exit fullscreen mode

Perceba que na linha 3, estendemos a classe Model do Sequelize ao definir nossa classe Film. Estamos "abertos para extensão e fechados para modificação". Dentro do código de definição da classe, informamos que nossos atributos são públicos, seguidos pelo nome do atributo e pelo caractere de exclamação !, indicando que não podem receber valores null e nem undefined, e também indica que os valores dos atributos serão definidos após a criação do objeto. Nessa classe, na linha 22, também são definidas as associações que Film terá com outras entidades. Como ela é única no momento, nenhuma associação foi definida.

A partir da linha 18, estamos exportando nossa entidade Film e descrevendo os tipos referentes aos DataTypes do Sequelize a quais esses atributos pertencem.

Retorne ao seu arquivo packages/catalog-service/models/db.ts e adicione o import da nossa entidade film. Em seguida, crie uma instância dela, passando o objeto sequelize, como pode ser visto nas linhas 2 e 22 abaixo:

packages/catalog-service/models/db.ts

1   import { Sequelize } from 'sequelize';
2   import Film from './film';
3 
4   const pool = {
5     max: 5,
6     min: 0,
7     acquire: 130000,
8     idle: 10000
9   };
10 
11  const sequelize = new Sequelize({
12    dialect: 'postgres',
13    host: 'localhost',
14    database: 'catalog_service_db',
15    username: 'catalog_service_user',
16    password: 'catalog_service_password',
17    pool
18  });
19 
20  Film(sequelize);
21 
22  export default sequelize;
23 
Enter fullscreen mode Exit fullscreen mode

Com isso a entidade esta quase pronta para ser espelhada em nosso banco, para tanto um utltimo ajuste em nosso packages\catalog-microsservice\src\main.ts sera necessario:

packages\catalog-microsservice\src\main.ts

1   import express from 'express';
2   import sequelize from './models/db';
3 
4   const app = express();
5 
6   app.get('/api', (_, res) => {
7     res.send({ message: 'Welcome to catalog-microsservice!' });
8   });
9 
10  const port = process.env.PORT || 3333;
11  const server = app.listen(port, () => {
12    console.log(`Listening at http://localhost:${port}/api`);
13  });
14  server.on('error', console.error);
15 
16  async function syncDB() {
17    await sequelize.sync();
18    console.log('Models synchronized with the database');
19  }
20 
21  syncDB();
22 
Enter fullscreen mode Exit fullscreen mode

Fizemos o import do sequelize que acabamos de configurar na linha 2. Entre as linhas 16 a 21, criamos uma função assíncrona com a finalidade de criar as tabelas definidas como entidades em nosso packages/catalog-microsservices/src/models. Atualmente, apenas film está ali.

Insira o comando para iniciar nossa API novamente, utilizando o comando nx serve catalog-service. Devemos obter um resultado semelhante a este:

$ nx serve catalog-service

> nx run catalog-service:serve:development

[ watch ] build succeeded, watching for changes...
Debugger listening on ws://localhost:9229/a5fe752f-36bd-469c-9361-24432ba30c1a
For help, see: https://nodejs.org/en/docs/inspector

Listening at http://localhost:3333/api
Executing (default): SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'film'
Executing (default): CREATE TABLE IF NOT EXISTS "film" ("id"  SERIAL , "uuid" UUID NOT NULL, "name" VARCHAR(500) NOT NULL, "description" TEXT NOT NULL, "age_rating" INTEGER NOT NULL, "subtitled" BOOLEAN NOT NULL, "poster" VARCHAR(500) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'film' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Models synchronized with the database

Enter fullscreen mode Exit fullscreen mode

Isso indica que seu modelo/entidade foi sincronizado com o banco que subimos com o docker-compose up --build que fizemos no inicio do artigo, agora se revisitarmos o DBeaver e atualizarmos sua visualização clicando F5 com nosso database selecionado, veremos a tabela criada por nosso ultimo comando.

Image description

Não deverá ser um fator complicador criar as outras entidades a partir do que já foi exposto aqui e utilizando o DER como guideline. Sugiro tentar mesmo sem os relacionamentos no momento para praticar e só depois olhar o gabarito em forma de lista abaixo:

packages/catalog-service/src/models/room.ts


// Importe os módulos necessários do Sequelize
import { Model, DataTypes, Sequelize } from 'sequelize';

// Defina a entidade "Room"
class Room extends Model {
  public id!: number;
  public uuid!: string;
  public name!: string;
  public capacity!: number;

  // Defina as associações
  static associate(models: any) {
    // Uma sala pode ter várias sessões
    Room.hasMany(models.Session, { foreignKey: 'room_id' });

    // Uma sala pode ter apenas um endereço
    Room.belongsTo(models.Address, {
      foreignKey: 'address_id',
    });
  }
}

// Exporte a função que define a entidade e suas configurações
export default (sequelize: Sequelize) => {
  Room.init({
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    uuid: {
      type: DataTypes.UUID,
      allowNull: false,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    capacity: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
  }, {
    sequelize,
    modelName: 'Room',
    tableName: 'room',
  });

  return Room;
};

packages/catalog-service/src/models/session.ts


// Importe os módulos necessários do Sequelize
import { Model, DataTypes, Sequelize } from 'sequelize';

// Defina a entidade "session"
class Session extends Model {
  public id!: number;
  public uuid!: string;
  public film_id!: number;
  public room_id!: number;
  public description!: string;
  public date!: Date;
  public start_time!: Date;
  public end_time!: Date;
  public time!: string;

  // Defina as associações
  static associate(models: any) {
    // Uma sessão pertence a um filme
    Session.belongsTo(models.Film, { foreignKey: 'film_id' });
    
    // Uma sessão ocorre em uma sala
    Session.belongsTo(models.Room, { foreignKey: 'room_id' });

    // Uma sessão pode ter vários tickets
    Session.hasMany(models.Ticket, { foreignKey: 'session_id' });
  }
}

// Exporte a função que define a entidade e suas configurações
export default (sequelize: Sequelize) => {
  Session.init({
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    uuid: {
      type: DataTypes.UUID,
      allowNull: false,
    },
    film_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    room_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    description: {
      type: DataTypes.STRING,
      allowNull: true,
    },
    date: {
      type: DataTypes.DATEONLY,
      allowNull: false,
    },
    start_time: {
      type: DataTypes.TIME,
      allowNull: false,
    },
    end_time: {
      type: DataTypes.TIME,
      allowNull: false,
    },
    time: {
      type: DataTypes.STRING,
      allowNull: true,
    },
  }, {
    sequelize,
    modelName: 'Session',
    tableName: 'session',
  });

  return Session;
};

packages/catalog-service/src/models/seat.ts


import { DataTypes, Model, Sequelize } from 'sequelize';

class Seat extends Model {
  public id!: number;
  public uuid!: string;
  public room_id!: number;
  public code!: string;

  static associate(models: any) {
    Seat.belongsTo(models.Room, {
      foreignKey: 'room_id',
      as: 'room',
    });
    Seat.hasMany(models.Ticket, {
      foreignKey: 'seat_id',
      as: 'tickets',
    });
  }
}

export default (sequelize: Sequelize) => {
  Seat.init(
    {
      id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      uuid: {
        type: DataTypes.UUID,
        allowNull: false,
      },
      room_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
      },
      code: {
        type: DataTypes.STRING,
        allowNull: false,
      },
    },
    {
      sequelize,
      modelName: 'Seat',
      tableName: 'seat',
    }
  );

  return Seat;
};

packages/catalog-service/src/models/ticket.ts


import { DataTypes, Model, Sequelize } from 'sequelize';

class Ticket extends Model {
  public id!: number;
  public uuid!: string;
  public session_id!: number;
  public seat_id!: number;

  static associate(models: any) {
    Ticket.belongsTo(models.Session, {
      foreignKey: 'session_id',
      as: 'session',
    });
    Ticket.belongsTo(models.Seat, {
      foreignKey: 'seat_id',
      as: 'seat',
    });
  }
}

export default (sequelize: Sequelize) => {
  Ticket.init(
    {
      id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      uuid: {
        type: DataTypes.UUID,
        allowNull: false,
      },
      session_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
      },
      seat_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
      },
    },
    {
      sequelize,
      modelName: 'Ticket',
      tableName: 'ticket',
    }
  );

  return Ticket;
};

packages/catalog-service/src/models/address.ts


import { DataTypes, Model, Sequelize } from 'sequelize';

class Address extends Model {
  public id!: number;
  public uuid!: string;
  public country!: string;
  public state!: string;
  public zip_code!: string;
  public telephone!: string;
  public description!: string;
  public postal_code!: string;
  public name!: string;

  static associate(models: any) {
    Address.belongsToMany(models.Room, {
      through: models.RoomAddress,
      foreignKey: 'address_id',
      as: 'rooms',
    });
    models.Room.belongsToMany(Address, {
      through: models.RoomAddress,
      foreignKey: 'room_id',
      as: 'addresses',
    });
  }
}

export default (sequelize: Sequelize) => {
  Address.init(
    {
      id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      uuid: {
        type: DataTypes.UUID,
        allowNull: false,
      },
      country: {
        type: DataTypes.STRING,
        allowNull: false,
      },
      state: {
        type: DataTypes.STRING,
        allowNull: false,
      },
      zip_code: {
        type: DataTypes.STRING,
        allowNull: false,
      },
      telephone: {
        type: DataTypes.STRING,
        allowNull: false,
      },
      description: {
        type: DataTypes.STRING,
        allowNull: true,
      },
      postal_code: {
        type: DataTypes.STRING,
        allowNull: true,
      },
      name: {
        type: DataTypes.STRING,
        allowNull: true,
      },
    },
    {
      sequelize,
      modelName: 'Address',
      tableName: 'address',
    }
  );

  return Address;
};

packages/catalog-service/src/models/roomAddress.ts


import { DataTypes, Model, Sequelize } from 'sequelize';

class RoomAddress extends Model {
  public id!: number;
  public uuid!: string;
  public room_id!: number;
  public address_id!: number;

  static associate(models: any) {
    RoomAddress.belongsTo(models.Room, {
      foreignKey: 'room_id',
      as: 'room',
    });
    RoomAddress.belongsTo(models.Address, {
      foreignKey: 'address_id',
      as: 'address',
    });
  }
}

export default (sequelize: Sequelize) => {
  RoomAddress.init(
    {
      id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
      },
      uuid: {
        type: DataTypes.UUID,
        allowNull: false,
      },
      room_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
      },
      address_id: {
        type: DataTypes.INTEGER,
        allowNull: false,
      },
    },
    {
      sequelize,
      modelName: 'RoomAddress',
      tableName: 'roomAddress',
    }
  );

  return RoomAddress;
};

Por fim, iremos criar as novas tabelas ao "injetar" a instancia do sequelize, que abstrai nossa conexao, nas instancias das models/entidades recem criadas no nosso packages/catalog-microsservice/db.ts

import { Sequelize } from 'sequelize';
import Film from './film';
import Session from './session';
import Room from './room';
import Seat from './seat';
import Ticket from './ticket';
import Address from './address';
import RoomAddress from './roomAddress';

const pool = {
  max: 5,
  min: 0,
  acquire: 130000,
  idle: 10000
};

const sequelize = new Sequelize({
  dialect: 'postgres',
  host: 'localhost',
  database: 'catalog_service_db',
  username: 'catalog_service_user',
  password: 'catalog_service_password',
  pool
});

Film(sequelize);
Session(sequelize);
Room(sequelize);
Seat(sequelize);
Ticket(sequelize);
Address(sequelize);
RoomAddress(sequelize);

export default sequelize;

Enter fullscreen mode Exit fullscreen mode

Ao se criar essas models e criar o vínculo em nosso packages/catalog-microsservice/src/models/db.ts e rodar novamemte o comando $ nx serve catalog-service devemos ter a seguinte estrutura de tabelas criadas em nosso postgres:

Image description


🤓 Conclusão (por hora)

Vimos a importância da criação de diagramas colaborativos para iniciar o projeto e demos uma olhada em um ADR simples que definia nossas escolhas técnicas e arquiteturais.

Em um projeto da vida real, a partir dessas documentações, expandiríamos nosso projeto em direção à criação de tarefas menores, issues no GitHub ou tasks no Jira, para que pudéssemos desenvolver. Mas aqui, por finalidade puramente didática, já colocamos a mão na massa para criar um projeto mínimo utilizando as facilidades do NX. Criamos o microsserviço de catálogo, atualizamos o seu builder e a partir daí criamos nosso banco, conexão e entidades.

Não falamos ainda de testes, embora tenha a forte convicção de que devemos seguir a abordagem TDD (Test-Driven Development) neste projeto. Do meu ponto de vista, não há nenhuma lógica de negócio que valha a pena ser testada unitariamente no momento.


👣 Próximos passos

Vamos continuar seguindo na produção do nosso catálogo. Agora, com as entidades já relacionadas, iremos construir rotas dentro do padrão Swagger, respeitando o REST, e a partir daí criar uma suíte de testes que garantam as operações dos nossos endpoints.

Continue por aqui, pois o projeto só tende a melhorar!


👈 Anterior Próximo 👉
Monorepo & Docker [Em Breve] REST e e2e

Top comments (0)