Guia completo para usar TypeORM e TypeScript para persistência de dados no módulo Node.js

David Herron Segue 18 de jul · 25 min ler

O TypeORM é um módulo avançado de gerenciamento de relações de objeto que é executado no Node.js. Como o nome indica, o TypeORM deve ser usado com o TypeScript. Neste artigo, aprenderemos a usar o TypeORM para configurar objetos Entity para armazenar dados em um banco de dados, como usar uma instância CustomRepository para manipular uma tabela de banco de dados e usar Relações entre instâncias de Entidade para simular junções de banco de dados.

Para começar, é necessário ler os dois artigos anteriores desta série:

  1. Escolhendo o TypeScript vs JavaScript: Technology, Popularity
  2. Como configurar o compilador do Typescript e o ambiente de edição com o Node.js ( também no TechSparx )
  3. Como criar módulos do Node.js usando o Typescript

Este artigo foi extraído do meu livro Quick Start para usar o Typescript e o TypeORM em aplicativos Node.js

Neste artigo, criaremos um módulo TypeScript simples para o Node.js manipular o armazenamento de dados em um banco de dados para um aplicativo. O conceito que estamos seguindo é que um escritório de registrador universitário precisa de um banco de dados e aplicativos correspondentes para armazenar dados sobre os alunos e os cursos oferecidos.

Como estamos usando o TypeScript, é natural usar o TypeORM para simplificar o gerenciamento do banco de dados. Estaremos criando duas entidades – Student e OfferedClass – com uma instância CustomRepository correspondente para cada uma. A classe CustomRepository fornecerá funções de alto nível para o banco de dados.

Inicializando o módulo package.json e o diretório de teste

Como fazemos em todos os projetos do Node.js, primeiro usamos o npm init ou yarn init para inicializar o diretório e fazer outra inicialização.

O que estamos inicializando é um módulo para manipular o banco de dados para esse aplicativo da Universidade registrada. Vamos criar o código para isso mais tarde, nesta fase, estamos apenas estabelecendo as bases.

Crie um diretório ts-example e dentro dele um registrar diretório.

 $ mkdir ts-example 
$ cd ts-example
$ mkdir registrador
$ cd registrador
$ npm init
.. responder a perguntas

Isso inicializa o package.json .

 $ npm install @ types / node --save-dev 
$ npm install typescript ts-node --save-dev
$ npm instalar typeorm sqlite3 reflect-metadata --save

A maioria desses pacotes foi discutida nos artigos anteriores. O pacote typeorm do curso fornece a biblioteca TypeORM. Para este exemplo, usaremos o SQLite3 para armazenar o banco de dados, daí o pacote sqlite3 . Finalmente, o pacote reflect-metadata é requerido pelo TypeORM.

Crie um arquivo chamado tsconfig.json contendo:

 { 
"compiladorOptions": {
"lib": ["es5", "es6", "es7",
"es2015", "es2016",
"es2017", "es2018",
"esnext"],
"target": "es2017",
"módulo": "commonjs",
"moduleResolution": "Node",
"outDir": "./dist",
"rootDir": "./lib",
"declaração": verdade,
"declarationMap": verdadeiro
"inlineSourceMap": verdadeiro
"inlineSources": true
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}

Isso é apenas um pouco diferente do que em “ Como criar módulos do Node.js usando o Typescript ”. Os parâmetros significam:

  • A linha de target diz para a saída do código ES2017, que precisamos porque essa versão suporta as funções async / await.
  • A linha do module descreve o formato do módulo de saída que será usado, correspondendo commonjs à decisão de usar o formato do módulo CommonJS.
  • O parâmetro moduleResolution diz para procurar os módulos nos diretórios node_modules , assim como o NodeJS.
  • O parâmetro outDir diz para compilar arquivos para o diretório nomeado, e o parâmetro rootDir diz para compilar os arquivos no diretório nomeado.
  • A declaration e os parâmetros declarationMap diz para gerar arquivos de declaração.
  • O inlineSourceMap e inlineSources dizem para gerar dados do mapa de origem dentro dos arquivos de origem do JavaScript.
  • O emitDecoratorMetadata e experimentalDecorators são usados pelo TypeORM ao gerar código.

O que isso significa é que nosso código fonte estará em lib e o TypeScript irá compilá-lo em dist .

Nós temos uma pequena mudança para fazer no package.json :

 { 
...
"main": "dist / index.js",
"tipos": "./dist/index.d.ts",
"tipo": "commonjs",
"scripts": {
"build": "tsc",
"watch": "tsc - watch",
"teste": "teste de cd && npm teste de execução"
}
...
}

O script de build simplesmente executa o tsc para compilar as fontes. O script de test é alterado para o diretório de test para executar o conjunto de testes.

A main deste pacote é o módulo de index gerado, especificamente dist/index.js . Com o tsconfig.json mostrado anteriormente, a fonte TypeScript está no diretório lib e, portanto, a interface principal para o módulo estaria em lib/index.ts . O compilador TypeScript compila lib/index.ts para dist/index.js . O atributo type é novo no NodeJS 12.x e nos permite declarar o tipo de módulos usados neste pacote. Se dist/index.js estivesse no formato do módulo ES6, o valor do atributo seria module vez disso.

O campo types declara ao mundo que este módulo contém definições de tipos. É uma boa forma gerar definições de tipo automaticamente, o que o arquivo tsconfig.json mostrado anteriormente faz e, em seguida, certificar-se de que o mundo saiba que as definições de tipo estão incluídas.

Configurar diretório de teste

É útil criar um conjunto de testes de unidade ao lado do código do aplicativo. Existem muitas abordagens para testes unitários, portanto, considere isso como a opinião de uma pessoa.

No diretório ts-example/registrar , crie um diretório chamado test e inicialize um novo package.json .

 teste $ mkdir 
$ cd test
$ npm init
.. responder a perguntas
$ npm instalar o chai mocha --save-dev

Nós vamos usar Chai e Mocha para escrever os testes. Como configuramos o TypeScript para gerar um módulo do CommonJS, usaremos o Mocha da maneira padrão. Atualmente, o Mocha suporta o teste de módulos CommonJS e o uso do Mocha para testar os módulos do ES6 exige que você salte alguns aros.

Agora edite o package.json no diretório de test e faça com que pareça:

 { 
"nome": "registro-teste",
"version": "1.0.0",
"description": "Suíte de testes para a biblioteca de registradores de alunos",
"main": "index.js",
"scripts": {
"pretest": "cd .. && npm run build",
"teste": "rm -f registrardb.sqlite && mocha ./index"
}
"autor": "David Herron <david@davidherron.com>",
"licença": "ISC",
"devDependencies": {
"chai": "^ 4.2.0",
"mocha": "^ 6.1.4"
}
"dependências": {}
}

Para os números de versão Mocha e Chai, use o que for mais recente. O importante aqui é os dois scripts. Para executar os testes, usamos o comando mocha , mas antes de executar os testes, queremos garantir que o código-fonte seja reconstruído. Portanto, o script de pretestpretest vai para o diretório pai e executa o script de construção.

Como não escrevemos código de teste ou código de aplicativo, ainda não podemos executar nada. Paciência, estaremos executando testes antes do final do artigo.

index.ts – Interface programática principal para o módulo Registrar

Tendo estabelecido as bases, podemos começar a escrever código para lidar com o banco de dados. No diretório lib , crie um arquivo chamado index.ts . Lembre-se que lib/index.ts é compilado para dist/index.js e servirá como o principal ponto de entrada para este módulo.

 import "reflect-metadata"; 
import {createConnection, Connection} de "typeorm";
import {Student} de './entities/Student';
exportar {Estudante} de './entities/Student';
import {StudentRepository} de './StudentRepository';
exportar {StudentRepository} de './StudentRepository';
import {OfferedClassRepository} de './OfferedClassRepository';
export {OfferedClassRepository} de './OfferedClassRepository';
import {OfferedClass} de './entities/OfferedClass';
export {OfferedClass} de './entities/OfferedClass';
var _connection: conexão; exportar função assínica connect (databaseFN: string) {
_connection = await createConnection ({
tipo: "sqlite",
banco de dados: databaseFN,
sincronizar: verdadeiro
logging: false,
entidades: [
Estudante, OferecidoClasse
]
});
}
export function connected () {
return typeof _connection! == 'indefinido';
}
função de exportação getStudentRepository (): StudentRepository {
return _connection.getCustomRepository (StudentRepository);
}
função de exportação getOfferedClassRepository (): OfferedClassRepository {
return _connection.getCustomRepository (OfferedClassRepository);
}

Se isso não parecer muito, considere que toda a funcionalidade reside nos arquivos importados aqui. Pense em como estruturar a API cobrindo um grande banco de dados. Você quer trazer cada última função em um único módulo? Não, esse módulo seria pesado. Em vez disso, você modularia as coisas, como visto aqui. Tudo o que está aqui é funções para gerenciar a conexão com o banco de dados. As classes denominadas StudentRepository e OfferedClassRepository contêm as operações CRUD para as tabelas correspondentes.

Essas duas classes devem ser instâncias da classe TypeORM CustomRepository. O próprio CustomRepository é uma instância do Repositório, fornecendo funcionalidade básica para manipular a tabela de banco de dados correspondente a uma Entidade TypeORM.

Normalmente você usaria a classe Repository base da seguinte forma:

 const studentRepository = getRepository (aluno); 

Mas nós queríamos adicionar algumas funções personalizadas ao nosso aplicativo. Portanto, vamos implementar o whats chamado de classe Custom Repository . A função getStudentRepository manipula a geração da implementação do repositório customizado.

A outra parte importante do código é a função de conexão que configura a conexão com o banco de dados. Isso deve ser chamado logo após o lançamento do aplicativo. Ele simplesmente inicializa a conexão do banco de dados usando createConnection . Essa função usa um objeto de descritor usado para conectar-se ao banco de dados real e fazer outra configuração.

O TypeORM, obviamente, suporta muitos tipos de bancos de dados e, para simplificar, estamos usando o SQLite3 porque não requer configuração de um servidor de banco de dados.

A matriz de entities é como informamos ao TypeORM os tipos de objetos disponíveis. Ele cria uma tabela de banco de dados correspondente a cada Entidade, definindo o esquema da tabela nos campos Entidade.

Nesse caso, a matriz de entities contém as classes de entidade que serão definidas posteriormente. Toda vez que adicionamos outra classe Entity, temos que lembrar de atualizar esse arquivo para que o TypeORM saiba sobre a Entity. Também é possível passar um nome de arquivo curinga para que o TypeORM atraia automaticamente todas as Entidades.

É possível gastar muito tempo na depuração se você atrapalhar a configuração das entities . O TypeORM lhe dirá que não é possível encontrar metadados para a entidade, e ficará muito claro porque esse é o caso. Se vamos fazer como mostrado aqui e passar Entidade na matriz de entities , devemos fazer isso com cuidado. Deve ser a referência da classe Entity e não algo como o módulo que contém a classe Entity.

Criando classes de Entidade TypeORM e métodos CRUD

Na seção anterior, implementamos a API principal no banco de dados do registrador. Mas nós referenciamos várias classes que fornecem a maior parte da funcionalidade.

Neste aplicativo, estamos usando dois recursos principais do TypeORM:

  1. Classes de entidade – Mapeia uma definição de objeto simples para uma tabela de banco de dados
  2. Classes de repositório customizadas – Funções úteis da API para manipular as tabelas do banco de dados

Cada classe Repository é associada a uma classe Entity e suas funções, portanto, manipulam a tabela associada a essa Entity. Ao implementar uma classe Custom Repository, criaremos funções adicionais.

Entidades TypeORM

Em TypeORM, uma entidade mapeia entre uma classe TypeScript e uma tabela de banco de dados. Você cria uma entidade adicionando a anotação @Entity() no topo da definição da classe e, em seguida, para cada campo da classe, adicionando uma @Column (ou similar). Essas anotações fornecem ao TypeORM as informações necessárias para configurar a tabela do banco de dados.

Parece que não devemos adicionar nenhuma função à classe Entity. Em vez disso, simplesmente adicionamos definições de campo. Nos bastidores, o TypeORM deve estar configurando as funções getter e setter e outras funções de suporte.

A entidade estudantil

No diretório lib , crie um diretório chamado entities . Colocaremos todas as definições de classe de entidade nesse diretório. Em seguida, crie um arquivo chamado lib/entities/Student.ts

 importar { 
Entity, Column, PrimaryGeneratedColumn,
ManyToMany, JoinTable
} de "typeorm";
import {OfferedClass} de './OfferedClass';
@Entidade()
classe de exportação Student {
@PrimaryGeneratedColumn () id: number;
@Coluna({
comprimento: 100
}) nome: string;
@Column ("int") introduzido: number;
@Column ("int") grade: number;
@Column () gender: string;
@ManyToMany (() => OfferedClass, oclass => oclass.students)
@JoinTable ()
classes: OfferedClass [];
}

Como dissemos anteriormente, @Entity() diz que a classe será tratada pelo TypeORM como uma entidade. Para obter o TypeORM para reconhecer a Entidade, devemos configurar o array de entities no descritor de conexão de forma que o TypeORM encontre a classe Entity.

Nós temos cinco campos:

  • id é a chave primária para a tabela do aluno
  • name é o name do aluno
  • entered é o ano em que o aluno entrou na universidade
  • grade é o ano em que eles estão atualmente
  • gender indica se são homens ou mulheres. Isso deveria ser um enum chamado Gender mas o SQLite3 não parece suportar campos enum.

Relacionamentos de entidade no TypeORM – ManyToMany

Existe um campo adicional, os students , que requer uma explicação mais profunda. Tem duas anotações, @ManyToMany e @JoinTable . Este é um exemplo de um Entity Relationship, um dos recursos do TypeORM.

Com relacionamentos de entidade, o TypeORM manipula automaticamente as junções entre tabelas. Neste caso, queremos apoiar o caso em que

  1. Um aluno pode se inscrever em várias classes
  2. Um AvailableClass pode ter vários alunos inscritos na turma

Em outras palavras, muitas instâncias de Student referem-se a muitas instâncias de OfferClass e vice-versa. Isso é o que TypeORM chama um relacionamento bidirecional ManyToMany.

A anotação @JoinTable aqui indica que a classe Student é o proprietário do relacionamento. Nos bastidores, essa anotação causa a criação de uma tabela, a Join Table , que ajuda a conectar instâncias de Student e OfferedClass juntas.

Com ManyToMany, a anotação @ManyToMany é necessária para ambas as Entidades. Vamos agora olhar para a entidade OfferedClass.

A entidade OfferedClass

Obviamente, nossos alunos irão gostar de poder se inscrever para as aulas. Até agora, tudo o que temos é uma lista de alunos e não há meios para eles se inscreverem em nada. Que tipo de operação de registrador somos nós?

Para corrigir isso, precisamos configurar uma tabela ou tabelas adicionais para armazenar informações sobre as classes. Para isso, poderíamos extrair uma cópia do Enterprise Architect e criar uma hierarquia de classes inteira. Por uma questão de brevidade, vamos fazer a coisa simples. Incluiremos uma Entity, denominada OfferedClass , e usaremos anotações TypeORM para atribuir relacionamentos entre as instâncias do Student e as ocorrências do OfferedClass .

Crie o arquivo chamado lib/entities/OfferedClass.ts

 importar { 
Entidade,
Coluna,
PrimaryColumn,
Um para muitos,
Muitos para muitos
} de "typeorm";
import {Student} de './Student';
@Entidade()
classe de exportação oferecidaClasse {
@PrimaryColumn ({
comprimento: 10
}) código: string;
@Coluna({
comprimento: 100
}) nome: string;
@Column ("int") horas: número;
@ManyToMany (type => Student, student => student.classes)
alunos: aluno [];
}

Esta entidade registra dados sobre as classes oferecidas pela universidade. Os campos são:

  • code é o número da classe, por exemplo, CS101
  • name é o nome descritivo da classe, como Computer Science 101
  • students são o outro lado do relacionamento @ManyToMany

A anotação @ManyToMany no campo de students é a outra metade da anotação correspondente na Student Entity. A relação ManyToMany requer que ambos os lados sejam declarados.

Classes personalizadas do Repository para gerenciar entidades Student e OfferedClass – operações CRUD e mais

Agora que temos as entidades de banco de dados definidas, precisamos de algumas funções úteis para gerenciar essas entidades. Como dissemos anteriormente, TypeORM fora da caixa fornece uma classe de Repositório padrão com um monte de funções úteis. Mas queremos que o RegistrarDB forneça uma camada mais alta de abstração.

Em index.ts nós já declaramos que haverá duas classes, StudentRepository e OfferedClassRepository . Essas são as instâncias do CustomRepository que fornecem as funções que desejamos.

StudentRepository – operações CRUD para a entidade Student

Portanto, a classe StudentRepository implementará as funções CRUD (criar, ler, atualizar e excluir) para gerenciar as instâncias do aluno, juntamente com algumas outras funções úteis.

Crie um arquivo chamado lib/StudentRepository.ts :

 importar { 
EntityRepository, Repository, getRepository
} de "typeorm";
import {Student} de "./entities/Student";
import * como util de 'util';
tipo de exportação GenderType = "male" | "fêmea"; enum de exportação Gênero {
masculino = "masculino", feminino = "feminino"
}
@EntityRepository (estudante)
classe de exportação StudentRepository extends Repository <Student> {
...
}

Nós estaremos adicionando muito mais a isso, mas esta é a estrutura inicial. Nós importamos algumas coisas do typeorm assim como da entidade Student. Como dissemos, o campo de gender em Student era para ser um enum chamado Gênero, e nós o definimos aqui.

A principal característica deste módulo é a classe StudentRepository. As instruções para implementar um Repositório customizado são estender a definição da classe Repository dessa maneira e usar a anotação EntityRepository conforme mostrado aqui.

Ao estender a classe Repository , a classe StudentRepository tem acesso automaticamente às funções da classe Repository . Assim, nossas funções serão criadas com base na API do repositório e seguirão o padrão definido por essa API.

Adicione esta função dentro da classe StudentRepository :

 async createAndSave (student: Student): Promessa <number> { 
let stud = new Student ();
stud.name = student.name;
stud.entered = normalizeNumber (student.entered, 'Bad year entered');
stud.grade = normalizeNumber (student.grade, 'Bad grade');
stud.gender = student.gender;
aguarde this.save (stud);
return stud.id;
}

O nome createAndSave é sugerido na documentação do TypeORM. Conforme implícito, essa função manipula a criação de um objeto Student e, em seguida, salva-o no banco de dados. Esta é a primeira das operações CRUD.

Criamos um novo objeto Student para garantir que o objeto salvo no banco de dados tenha apenas os campos definidos em Student . o objeto que passou pode facilmente ter outros campos e ainda ser compatível com o Student .

Depois de configurar o objeto Student, nós o salvamos no banco de dados. A função do TypeORM é converter isso para a conexão de banco de dados subjacente que foi configurada.

Adicione estas funções dentro da classe StudentRepository :

 async allStudents (): Promise <Student []> { 
deixe os alunos = aguardarem isso.
retornar estudantes;
}
async findOneStudent (id: number):
Prometer <Estudante> {
deixe estudante = aguarde isso.findOne ({
onde: {id: id}
});
if (! StudentRepository.isStudent (student)) {
lançar novo erro (`id do estudante $ {util.inspect (id)} não recuperou um Student`);
}
retornar aluno;
}

Em findOneStudent , temos a próxima operação CRUD, lendo um objeto Student do banco de dados. O método findOne é uma maneira de pesquisar o banco de dados para recuperar dados. A cláusula where nos permite descrever como selecionar no banco de dados os itens que queremos recuperar.

Adicione esta função dentro da classe StudentRepository :

 async updateStudent (id: number, aluno: Student): 
Promessa <número> {
if (typeof student.entered! == 'undefined') {
student.entered = normalizeNumber (student.entered, 'Bad year entered');
}
if (typeof student.grade! == 'undefined') {
student.grade = normalizeNumber (student.grade, 'Bad grade');
}
if (! StudentRepository.isStudentUpdater (student)) {
throw new Error (Erro de atualização do aluno $ {util.inspect (id)} não recebeu um atualizador de Student $ {util.inspect (student)} `);
}
aguarde this.manager.update (Student, id, student);
return id;
}

No updateStudent , temos a próxima operação CRUD, atualizando um objeto Student. Precisamos do ID para o aluno atualizar, então um objeto que permitirá atualizar o objeto. No TypeORM, o método de update nos permite especificar um objeto parcial e atualizará os campos que estão definidos. O método isStudentUpdater verifica se há campos válidos para atualizar objetos Student.

Adicione esta função dentro da classe StudentRepository :

 async deleteStudent (student: number | Student) { 
if (tipo de aluno! == 'numero'
&&! StudentRepository.isStudent (student)) {
throw new Error ('Objeto de estudante fornecido não é aluno');
}
aguarde this.manager.delete (Aluno,
tipo de aluno === 'número'
? estudante: student.id);
}

No deleteStudent , temos nossa última operação CRUD, excluindo um aluno. Podemos usar o número de ID ou o objeto Student (do qual obtemos o número de ID).

Adicione esta função fora do corpo da classe StudentRepository :

 função de exportação normalizeNumber ( 
num: number | string, errorIfNotNumber: string)
: número {
if (typeof num === 'indefinido') {
lançar novo erro (`$ {errorIfNotNumber} - $ {num}`);
}
if (typeof num === 'number') retornar num;
vamos ret = parseInt (num);
if (isNaN (ret)) {
lançar novo erro (`$ {errorIfNotNumber} $ {ret} - $ {num}`);
}
return ret !;
}

Esta função é usada em alguns lugares. Dependendo da fonte de dados, por exemplo, um envio de FORM a partir de um navegador da web, podemos receber uma string que é na verdade um número. Esta função acomoda essa possibilidade usando parseInt .

Funções de verificação de tipo para estudantes

O TypeScript não suporta a verificação do tipo de tempo de execução. Para fazer a verificação do tipo de tempo de execução, correspondendo à verificação do tipo em tempo de compilação que o TypeScript nos fornece, temos que implementá-lo por conta própria. Essa é outra diferença entre o TypeScript e linguagens como Java ou C #.

Para implementar a verificação de tipo de tempo de execução, o TypeScript nos obriga a implementar funções chamadas de proteções de tipo . Essas funções pegam um objeto e devem testar as características do objeto para ver se ele é do tipo correto e, em seguida, retornar um indicador booleano.

Adicione estas funções estáticas à classe StudentRepository:

 static isStudent (student: any): o aluno é o aluno { 
return typeof student === 'objeto'
&& typeof student.name === 'string'
&& typeof student.entered === 'número'
&& typeof student.grade === 'número'
&& StudentRepository.isGender (student.gender);
}
Static isStudentUpdater (atualizador: any): boolean {
vamos ret = true;
if (tipo de atualizador! == 'objeto') {
throw new Error ('isStudentUpdater deve pegar o objeto');
}
if (tipo de atualizador.name! == 'indefinido') {
if (typeof updater.name! == 'string') ret = false;
}
if (typeof updater.entered! == 'undefined') {
if (typeof updater.entered! == 'number') ret = false;
}
if (typeof updater.grade! == 'indefinido') {
if (tipo de updater.grade! == 'number') ret = false;
}
if (tipo de updater.gender! == 'undefined') {
if (! StudentRepository.isGender (updater.gender)) ret = false;
}
retorno ret;
}
Static isGender (gênero: any): sexo é Gender {
return typeof gender === 'string'
&& (gender === 'male' || gender === 'female');
}

Lembre-se de que dissemos anteriormente que o campo de gender deveria ser um enum . Temos o enum Gender definido no topo e a função isGender verifica se uma string corresponde aos valores permitidos em enum Gender .

Olhe de perto e você verá que isGender tem um tipo de retorno gender is Gender . Que outra linguagem tem um tipo de retorno como esse? Isso é o que o TypeScript descreve como um predicado de tipo . A documentação do TypeScript não explica isso ainda mais. Mas fica claro a partir do contexto que o predicado " object is Class " é usado para indicar o que as palavras dizem, significando uma indicação de que o objeto corresponde a essa classe.

A função isStudent verifica um objeto para ver se ele corresponde à forma do objeto Student . Isso deve ser usado quando esperamos que o objeto tenha todos os campos correspondentes ao Student.

Por contraste, o isStudentUpdater deve ser usado em um contexto onde o objeto terá alguns dos campos da classe Student. Em ambos os casos, estamos determinando que os tipos de campo correspondam aos tipos de campo na classe Student.

OfferedClassRepository – operações CRUD para, e teste, a entidade OfferedClass

Para seguir o padrão já definido, crie um arquivo chamado lib/OfferedClassRepository.ts . Isso manipulará a tabela OfferedClass.

 importar { 
EntityRepository,
Repositório,
getRepository
} de "typeorm";
importar {
OferecidoClasse
} de './entities/OfferedClass';
importar {
normalizeNumber,
StudentRepository
} de './StudentRepository';
importar {
getStudentRepository
} de './index';
import * como util de 'util';
import * como yaml de 'js-yaml';
import * como fs de 'fs-extra';
@EntityRepository (OfferedClass)
classe de exportação OffersClassRepository extends Repository <OfferedClass> {
...
}

É como o que colocamos no topo do StudentRepository.ts mas com algumas adições. Ou seja, estamos importando o js-yaml e o fs-extra . Vamos usá-los para ler em um banco de dados de classe. Para suportar estes tipos de módulos:

 $ npm install --save js-yaml fs-extra 
$ npm install --save-dev @ tipos / js-yaml

O primeiro caso de teste que escreveremos inicializará o RegistrDB com descrições de classe provenientes de um arquivo YAML. A teoria que seguiremos é que ocasionalmente o Registrador adicionará uma classe, excluirá uma classe ou alterará algo sobre uma classe. Embora eles pudessem fazer isso a partir da API da CRUD, eles também poderiam usar um arquivo YAML.

Na classe OfferedClassRepository adicione estas funções:

 async updateClasses (classFN: string) { const yamlText = aguarda fs.readFile (classFN, 'utf8'); 
const oferecido = yaml.safeLoad (yamlText);
if (typeof oferecido! == 'objeto'
|| ! Array.isArray (offered.classes)) {
throw new Error (`updateClasses leu arquivo de dados incorreto de $ {classFN}`);
}
deixe todos = aguardar this.allClasses ();
para (deixa cls de todos) {
deixe stillOffered = false;
para (let ofrd of offered.classes) {
if (ofrd.code === cls.code) {
stillOffered = true;
pausa;
}
}
if (! stillOffered) {
this.deleteOfferedClass (cls.code);
}
}
para (deixe o atualizador de offered.classes) {
if (! OfferedClassRepository
.isOfferedClassUpdater (updater)) {
throw new Error (`updateClasses encontrou a entrada de classes que não é um OfferedClassUpdater $ {util.inspect (atualizador)}`);
}
deixe cls;
experimentar {
cls = espera this.findOneClass (updater.code);
} catch (e) {cls = indefinido}
if (cls) {
aguarde this.updateOfferedClass (updater.code, updater)
} outro {
aguarde this.createAndSave (atualizador)
}
}

}
estático isOfferedClass (offeredClass: any): offeredClass is OfferedClass {
return typeof offeredClass === 'objeto'
&& typeof offeredClass.code === 'string'
&& typeof offeredClass.name === 'string'
&& typeof offeredClass.hours === 'número';
}
estático isOfferedClassUpdater (updater: any): boolean {
vamos ret = true;
if (tipo de atualizador! == 'objeto') {
throw new Error ('isOfferedClassUpdater deve pegar o objeto');
}
if (tipo de updater.code! == 'undefined') {
if (tipo de updater.code! == 'string') ret = false;
}
if (tipo de atualizador.name! == 'indefinido') {
if (typeof updater.name! == 'string') ret = false;
}
if (typeof updater.hours! == 'indefinido') {
if (tipo de updater.hours! == 'number') ret = false;
}
retorno ret;
}

A função updateClasses lê em um arquivo YAML que deve ter uma matriz de classes . A matriz terá objetos que correspondem ao padrão OfferedClassUpdater . Há também duas funções de proteção de tipo, isOfferedClass e isOfferedClassUpdater para testar objetos.

Como um arquivo YAML pode ser qualquer coisa, temos que testar se o arquivo contém o que esperamos e, caso contrário, lançar um erro.

Como objetos em um arquivo YAML podem ser qualquer coisa, não temos informações de tipo para ajudar. Mas é por isso que escrevemos isOfferedClassUpdater . Com isso, podemos testar os objetos que chegam do arquivo YAML.

O primeiro estágio é detectar instâncias de classes que não estão listadas no arquivo YAML. Essa classe seria, portanto, excluída do catálogo de cursos, porque não existe mais. Detectamos essas classes fazendo um loop em todas as classes existentes e, se elas não estiverem listadas no YAML, sabemos que elas devem ser excluídas.

O segundo estágio é adicionar uma nova classe ou atualizar uma classe existente a partir dos dados no arquivo YAML.

BTW, nós tínhamos a intenção de testar essa função por conta própria, mas como ela se refere às outras funções do CRUD, teremos que implementá-las.

Na classe OfferedClassRepository adicione esta função:

 allClasses async (): Promise <OfferedClass []> { 
deixe classes = aguardar isso.
relações: ["estudantes"]
});
retornar classes;
}

Com allClasses , recuperamos todas as instâncias de OfferedClass.

O bit com relations diz ao TypeORM para carregar os dados no relacionamento, se houver. Por algum motivo, o TypeORM parece acreditar que os dados em um relacionamento são opcionais e nem sempre precisam ser carregados. Para fazer com que os dados do relacionamento sejam carregados, você passa esse campo de relations contendo uma matriz de strings que nomeia as relações com as quais você está interessado em carregar.

Ao carregar um OfferedClass , queremos sempre carregar o relacionamento do Aluno e, portanto, temos que lembrar de sempre listar esse campo de relações.

A diferença é:

  • Sem relations : somente os campos code e name e hours são carregados.
  • Com relations : Esses campos e o campo de students são uma matriz contendo alunos associados à dada classe OfferedClass.

Na classe OfferedClassRepository adicione esta função:

 async createAndSave (offeredClass: OfferedClass) 
: Promessa <qualquer> {
let cls = new OfferedClass ();
cls.code = offeredClass.code;
cls.name = offeredClass.name;
cls.hours = normalizeNumber (offeredClass.hours, 'Número inválido de horas');
if (! OfferedClassRepository
.isOfferedClass (cls)) {
throw new Error (`Não é uma classe oferecida $ {util.inspect (offeredClass)}`);
}
aguarde this.save (cls);
return cls.code;
}

Com o createAndSave , adicionamos um novo OfferedClass ao banco de dados. Isso é semelhante à mesma função no StudentsRepository.

Na classe OfferedClassRepository adicione esta função:

 findOneClass assíncrono (código: string) 
: Promise <OfferedClass> {
vamos cls = aguardar this.findOne ({
onde: {code: code},
relações: ["estudantes"]
});
if (! OfferedClassRepository.isOfferedClass (cls)) {
throw new Error (`OferecidoClass id $ {util.inspect (code)} não recuperou um OfferedClass`);
}
retornar cls;
}

Com findOneClass , encontramos um OfferedClass pelo nome do código. Adicionamos novamente o campo de relations às opções para garantir e extrair dados de relacionamento. Isso é semelhante à função correspondente no StudentsRepository.

Na classe OfferedClassRepository adicione esta função:

 async updateOfferedClass ( 
código: string,
offerClass: OfferedClass)
: Promessa <qualquer> {
if (typeof offeredClass.hours! == 'indefinido') {
offeredClass.hours = normalizeNumber (
offerClass.hours, 'Bad number of hours');
}
if (! OfferedClassRepository
.isOfferedClassUpdater (offeredClass)) {
throw new Error (o id de atualização `OfferClass $ {util.inspect (code)} não recebeu um atualizador OfferedClass $ {util.inspect (offeredClass)}`);
}
aguarde this.manager.update (OfferedClass,
código, offeredClass);
Código de retorno;
}

Com o updateOfferedClass pegamos um objeto OfferedClassUpdater e o usamos para atualizar a entrada no banco de dados. A função de update TypeORM cuida da atualização seletiva de um item com base nos campos que estão definidos ou não. Isso é semelhante à função correspondente no StudentsRepository.

Na classe OfferedClassRepository adicione esta função:

 async deleteOfferedClass ( 
offeredClass: string | OfferedClass) {
if (typeof offeredClass! == 'string'
&&! OfferedClassRepository.isOfferedClass (offeredClass)) {
throw new Error ('Fornecido o objeto offerClass não um OfferedClass');
}
aguarde this.manager.delete (OfferedClass,
typeof offeredClass === 'string'
? offeredClass: offeredClass.code);
}

Com deleteOfferedClass tentamos excluir uma ocorrência de OfferedClass. Isso é semelhante à função correspondente no StudentsRepository.

Em seguida, queremos apoiar a inscrição de um aluno em uma classe Offered.

Na classe OfferedClassRepository adicione esta função:

 asyncStudentInClass assíncrona ( 
studentid: any, code: string) {
let offered = aguardar this.findOneClass (código);
if (! OfferedClassRepository
.isOfferedClass (oferecido)) {
throw new Error (`enrollStudentInClass não encontrou OfferedClass para $ {util.inspect (code)}`);
}
let student = espera getStudentRepository ()
.findOneStudent (studentid);
if (! StudentRepository.isStudent (student)) {
lançar novo erro (`enrollStudentInClass não encontrou Student para $ {util.inspect (studentid)}`);
}

if (! student.classes) student.classes = [];
student.classes.push (oferecido);
aguarde getStudentRepository (). manager.save (aluno);
}

A primeira seção disso verifica que recebemos bons identificadores para um aluno conhecido e uma classe conhecida.

Então, adicionar o aluno à classe oferecida é fácil. Basta adicionar a ocorrência OfferedClass à matriz de classes no objeto Student e salvá-la no banco de dados. Nos bastidores, o TypeORM cuida de tudo.

Outra operação desejada é inscrever um aluno em várias aulas ao mesmo tempo. Em vez de se inscrever um de cada vez, talvez seja mais eficiente receber uma série de códigos de turma e garantir que o aluno esteja inscrito em cada um deles. Além disso, se o aluno estiver matriculado em uma turma que não esteja nessa matriz, devemos desinscrever o aluno dessa turma.

Na classe OfferedClassRepository adicione esta função:

 async updateStudentEnrolledClasses ( 
studentid: any, codes: string []) {
let student = espera getStudentRepository ()
.findOneStudent (studentid);
if (! StudentRepository.isStudent (student)) {
lançar novo erro (`enrollStudentInClass não encontrou Student para $ {util.inspect (studentid)}`);
}
let newclasses = [];
para (let sclazz of student.classes) {
para (let code of codes) {
if (sclazz.code === código) {
newclasses.push (sclazz);
}
}
}
para (let code of codes) {
deixe encontrado = falso;
para (let nclazz of newclasses) {
if (nclazz.code === código) {
encontrado = verdadeiro;
}
}
se achado) {
newclasses.push (aguarde isso.findOneClass (code));
}
}
student.classes = newclasses;
aguardar getStudentRepository () salvar (aluno);
}

A abordagem seguida é primeiro recuperar a instância do aluno, manipular as classes o aluno está inscrito e, finalmente, salvar o aluno.

Criamos um array vazio que conterá o novo conjunto de classes no qual o aluno está inscrito. Em seguida, usamos um par for loops para definir esse array corretamente. No primeiro, estamos copiando as instâncias de OfferedClass que ainda estão listadas na matriz de códigos de classe. No segundo, passamos pelo array de classes disponíveis, e para qualquer classe que não esteja no array, enviamos sua ocorrência de OfferedClass para o array.

Como resultado newclasses tem os casos OfferedClass correspondentes aos códigos que foram dadas. Nós simplesmente salvamos a instância do aluno.

Unidade testando as entidades Student e OfferedClass

No diretório de test , já configuramos os bones de um projeto Node.js.

Nesse diretório, crie um arquivo chamado index.js . Isto irá conter um conjunto de testes Mocha / Chai. Como queremos testar a interface JavaScript no módulo do Registrador, o conjunto de testes é escrito em JavaScript.

 const util = require ('util'); 
const path = require ('caminho');
const assert = require ('chai') assert;
const {
conectar,
conectado,
Aluna,
getStudentRepository,
StudentRepository,
getOfferedClassRepository,
OferecidoClassRepository
} = require ('../ dist / index');
describe ('Initialize Registrar', function () {
before (função assíncrona () {
experimentar {
aguardar connect ("registrardb.sqlite");
} pegar (e) {
console.error (o `Initialize Registrar falhou com`, e);
jogue e;
}
});
ele ('deve inicializar com sucesso o Registrador', função assíncrona () {
assert.isTrue (connected ());
});
});

Isso configura os módulos necessários e implementa um caso de teste inicial.

Como o diretório de test é um subdiretório do módulo registrar , podemos carregar o módulo usando uma referência de caminho relativa como aqui. O código-fonte gerado está no diretório dist e, portanto, estamos carregando módulos a partir dele.

A função before aqui é usada para inicializar a conexão do REGISTRADB. Não há muito a testar porque tudo o que estamos fazendo é instanciar o banco de dados. A finalidade principal desse teste é, portanto, inicializar o banco de dados, mas verifica-se um pouco que o banco de dados foi configurado com êxito.

O teste pode ser executado desta maneira:

 Teste $ npm > registrador@1.0.0 teste / Volumes / Extra / e-books / typescript-nodejs / examples / registrador 
> teste de cd && npm teste de execução
> registrar-test@1.0.0 pretest / Volumes / Extra / e-books / typescript-nodejs / examples / registrar / test
> cd .. && npm run build
> registrador@1.0.0 compilação / Volumes / Extra / ebooks / typescript-nodejs / exemplos / registrador
> tsc
> registrador-teste@1.0.0 teste / Volumes / Extra / ebooks / typescript-nodejs / exemplos / registrador / teste
> mocha ./index
Initialize Registrar
? deve inicializar com sucesso o registrador
1 passando (13ms)

Testando o objeto Student

Vamos detalhar um pouco a suíte de testes. Não temos espaço neste livro para testar tudo, então vamos mostrar alguns casos de teste.

 describe ('Adicionar alunos ao registro', function () { 
deixe stud1 = {
nome: "John Brown",
entrou: 1997, grau: 4,
género masculino"
};
deixe stud2 = {
nome: "John Brown",
inserido: "trump1", nota: "senior",
género masculino"
};
deixe studentid1;
deixe studentid2;
ele ('deve adicionar um aluno ao registro', função assíncrona () {
studentid1 = espera getStudentRepository (). createAndSave (stud1);
let student = espera getStudentRepository (). findOneStudent (studentid1);
assert.exists (aluno);
assert.isObject (aluno);
assert.isString (student.name);
assert.equal (student.name, stud1.name);
assert.isNumber (student.entered);
assert.equal (student.entered, stud1.entered);
assert.isNumber (student.grade);
assert.equal (student.grade, stud1.grade);
assert.isString (student.gender);
assert.equal (student.gender, stud1.gender);
});
ele ('não deve adicionar um aluno com dados inválidos', função assíncrona () {
let sawError = false;
experimentar {
aguarde getStudentRepository (). createAndSave (stud2);
} pegar (err) {
sawError = true;
}
assert.isTrue (sawError);
});
});

Os dois objetos, stud1 e stud2 , são destinados a testes positivos (esperam sucesso) e testes negativos (esperam falha). No primeiro, chamamos addStudent , recuperamos o objeto Student e verificamos seus valores em relação ao objeto.

Nas afirmações de Chai é um método muito útil, deepEqual , que teria sido preferível aqui. Mas, o objeto retornado de getStudent(studentid1) é um objeto de TypeScript. Eu descobri de inspecionar o objeto que tem campos chamados _name etc, que você vai lembrar são os campos privados que mantêm os dados reais. Os métodos getter não são reconhecidos como nomes de campos e, portanto, não foi possível fazer isso: assert.deepEqual(student, stud1) , porque os nomes dos campos não correspondem.

Portanto, acabamos usando as verificações assert.isString e assert.isNumber individuais. Ou podemos simplesmente usar o isStudent tipo isStudent para verificar se o objeto retornado é o esperado.

No caso de teste negativo ( não deve adicionar ), estamos passando no stud2 . Este objeto possui cadeias em vez dos campos numéricos e essas cadeias não são conversíveis em números. A função createStudent detecta esse problema e gera um erro. Nós sawError esse erro e definimos o sinalizador sawError como true , e a asserção será bem-sucedida. Se o erro não for lançado, o sinalizador permanecerá false e a declaração falhará.

 Teste $ npm > registrador@1.0.0 teste / Volumes / Extra / e-books / typescript-nodejs / examples / registrador 
> teste de cd && npm teste de corrida
> registrar-test@1.0.0 pretest / Volumes / Extra / e-books / typescript-nodejs / examples / registrar / test
> cd .. && npm run build
> registrador@1.0.0 build / Volumes / Extra / e-books / typescript-nodejs / examples / registrador
> tsc
> registrar-test@1.0.0 test / Volumes / Extra / ebooks / typescript-nodejs / exemplos / registrador / teste
> mocha ./index
Initialize Registrar
? deve inicializar com sucesso o registrador
Adicionar alunos ao registrador vazio
? deve adicionar um aluno ao registrador
? deve deixar de adicionar um aluno com dados ruins
3 passando (46ms)

Depois de mais algum trabalho, os resultados do teste podem parecer:

 > registry-test@1.0.0 test / Volumes / Extra / ebooks / typescript-nodejs-quick-start / registrador / teste 
> rm -f registrardb.sqlite && mocha ./index
Initialize Registrar
? deve inicializar com sucesso o Registrador
Adicionar alunos ao registro
? deve adicionar um aluno ao registro
? deve deixar de adicionar um aluno com dados ruins
Atualizar aluno no registro
? deve atualizar aluno (44ms)
? deve falhar ao atualizar o aluno com dados incorretos
Excluir aluno do registro
? não deve deixar de excluir o aluno usando uma identificação incorreta
? deve excluir o aluno usando uma boa identificação
7 passando (861ms)

Testando a entidade OfferedClass

Tendo escrito alguns testes da entidade Student, precisamos validar a entidade OfferedClass. Ao contrário da entidade Student, precisamos inicializar uma lista de objetos OfferedClass.

O primeiro passo é criar um arquivo de dados no formato YAML. Crie um arquivo chamado test/students.yaml :

 classes: 
- código: BW101
nome: Introdução ao Basket Weaving
horas: 3
- código: BW102
nome: Tecelagem Subaquática de Cestos
horas: 3
- código: BW103
nome: Basket Weaving enquanto Sky Diving
horas: 3
- código: BW201
nome: Fundamentos de tecelagem de cesta
horas: 3
- código: BW202
nome: Tecelagem Histórica
horas: 3
- código: BW203
nome: Desenvolvimento de Tecelagem Moderna de Cestos
horas: 3
- código: BW301
nome: Tópicos sobre Tecelagem Contemporânea de Cestos
horas: 3
- código: BW302
nome: Teoria de Cestaria
horas: 3
- código: BW303
nome: Tecelagem de Cestos e Teoria dos Gráficos
horas: 3
- código: BW401
nome: Advanced Basket Weaving
horas: 3
- código: BW402
nome: Practicum Research Tecelagem Basket
horas: 3

Este arquivo de dados contém uma lista de classes de uma universidade hipotética interessada em tecelagem de cestos.

Em seguida, adicione este grupo de teste:

 describe ('Inicializar Classes Oferecidas no Registro', function () { 
before (função assíncrona () {
aguarde getOfferedClassRepository ()
.updateClasses (path.join (__ dirname, 'classes.yaml'));
});
ele ('deveria ter oferecido classes', função assíncrona () {
deixar classes = aguardar getOfferedClassRepository ()
.allClasses ();
assert.exists (classes);
assert.isArray (classes);
para (ofertado de classes) {
assert.isTrue (OfferedClassRepository
.isOfferedClass (oferecido));
}
});
});

A função before insere o que estiver no arquivo YAML no banco de dados. Isso inicializa nossos dados de teste. E podemos finalmente viver esse sonho da faculdade de ver Underwater Basket Weaving no catálogo do curso.

No caso de teste, lemos em todas as classes e verificamos que tudo é realmente uma classe Offered.

 > rm -f registrardb.sqlite && mocha ./index Initialize Registrar 
? deve inicializar com sucesso o Registrador
Adicionar alunos ao registro
? deve adicionar um aluno ao registro
? deve deixar de adicionar um aluno com dados ruins
Atualizar aluno no registro
? deve atualizar aluno
? deve falhar ao atualizar o aluno com dados incorretos
Excluir aluno do registro
? não deve deixar de excluir o aluno usando uma identificação incorreta
? deve excluir o aluno usando uma boa ID
Inicializar Classes Ofertadas no registro
? deveria ter oferecido aulas
8 passagens (369ms)

Legal, nós verificamos que as instâncias de OfferedClass existem e temos o tipo correto. Agora precisamos testar a adição de alunos às aulas e muito mais.

 deixe stud1 = { 
nome: "Mary Brown",
entrou: 2010, nota: 2,
gênero feminino"
};
deixe studentid1;
ele ('deve adicionar aluno a uma classe', função assíncrona () {
studentid1 = espera getStudentRepository ()
.createAndSave (stud1);
aguarde getOfferedClassRepository ()
.enrollStudentInClass (studentid1, "BW102");
let student = espera getStudentRepository ()
.findOneStudent (studentid1);
assert.isTrue (StudentRepository
.isStudent (aluno));
assert.isArray (student.classes);
vamos foundbw102 = false;
para (let of student.classes) {
assert.isTrue (OfferedClassRepository
.isOfferedClass (oferecido));
if (offered.code === "BW102") foundbw102 = verdadeiro;
}
assert.isTrue (foundbw102);
});

Como antes, temos um objeto estático a partir do qual criar um aluno. Então, inscrevemos o aluno em uma aula. E então, recuperamos o aluno novamente e verificamos a matriz de classes para garantir que ela está usando o BW102.

Depois de algum desenvolvimento de caso de teste adicional, temos:

 > rm -f registrardb.sqlite && mocha ./index Initialize Registrar 
? deve inicializar com sucesso o Registrador
Adicionar alunos ao registro
? deve adicionar um aluno ao registro
? deve deixar de adicionar um aluno com dados ruins
Atualizar aluno no registro
? deve atualizar aluno
? deve falhar ao atualizar o aluno com dados incorretos
Excluir aluno do registro
? não deve deixar de excluir o aluno usando uma identificação incorreta
? deve excluir o aluno usando uma boa ID
Inicializar Classes Ofertadas no registro
? deveria ter oferecido aulas
? deve adicionar aluno a uma turma
? deve adicionar aluno a três classes
? deve mostrar alunos registrados na classe
11 passando (372ms)

Este artigo foi publicado originalmente no TechSparx .

Texto original em inglês.