3. NestJS et TypeOrm
Tu vas apprendre à utiliser l'ORM intégrée à NestJS. Elle te permettra d'interagir avec la base de données directement par l'intermédiaire d'objets d'entités.
Objectifs
- Initialiser TypeORM
- Créer une entité
- Gérer les migrations
- Créer le service qui gère cette entité
- Binder le controller de l'API de l'entité
Initialiser Nest avec TypeORM
Installer TypeORM
Tu vas utiliser l'ORM TypeORM dans ton projet cependant elle n'est pas directement incluse dans un projet NestJS lors de sa création. Tu dois l'ajouter :
npm install --save @nestjs/typeorm typeorm mysql2
Tu viens d'installer TypeORM ainsi que le driver mysql2.
Configurer TypeORM
Qui dit ORM dit base de données et donc credentials pour se connecter à la base de données. Comme tu le sais, tu ne dois jamais exposer un mot de passe. Pour faire les choses correctement, tu vas faire un peu de configuration pour ne pas mettre le nom de base ainsi que l'utilisateur et le mot de passe de connexion en dure.
Tu dois au préalable créer la base de données sur mariadb ou mysql
Tout d'abord, crée un fichier .env à la racine de ton projet et complète les variables :
DATABASE_HOST=
DATABASE_PORT=
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_DB=
Ensuite, dans un dossier config de ton répertoire src crée un fichier database.config.ts. Il contiendra la configuration de ta base de données dans un format compréhensif par TypeORM.
// src/config/database.config.ts
import { DataSourceOptions } from 'typeorm';
import 'dotenv/config';
export const databaseConfig: DataSourceOptions = {
type: 'mariadb',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT || '3000'),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_DB,
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: false,
};
Tu observeras 2 points importants dans cette configuration :
ligne 13: tous les fichiers se terminant par.entity.tssont référencés en tant queentities. Nous en parlerons un peu plus loin.ligne 14: si vrai, toutes modifications dans les entités s'appliqueront directement dans la base de données (ajout de colonnes, création/suppression de tables...). En développement ce flag peut resté surtruemais jamais en production. Si tu travailles à plusieurs sur un projet, il est recommandé de mettre le flag àfalseet de gérer les modifications de la structure de ta base de données avec des migrations que nous verrons ensuite.
Tu dois maintenant utiliser la configuration que tu viens de déclarer. Pour cela, tu dois ajouter le module d'import de configuration à ton projet :
npm install @nestjs/config --save
Il faut dire à Nest quels seront les modules qui utiliseront une connexion avec la base de données. Dans notre cas, tu vas définir la connexion sur le AppModule; elle sera ainsi disponible dans tous les modules de l'application.
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { databaseConfig } from './config/database.config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: () => databaseConfig,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Tu as importé le module TypeOrmModule et tu lui as fourni la configuration de ta base de données databaseConfig. Tu importes dans ce module TypeOrmModule le module ConfigModule car c'est lui qui permet de lire dans le fichier .env
Scripts TypeORM
TypeORM s'utilise avec un CLI. Si tu veux, tu peux ajouter quelques raccourcis dans la section scripts de ton package.json :
// package.json
{
...
"scripts": {
...
"migration:create": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:create",
"migration:generate": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:generate -d src/config/migration.config.ts",
"migration:up": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:run -d src/config/migration.config.ts",
"migration:down": "./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:revert -d src/config/migration.config.ts"
},
...
}
Tu as maintenant accès à ces lignes de commandes :
-
npm run migration:createpour créer une nouvelle migration vide -
npm run migration:generatepour génerer une nouvelle migration -
npm run migration:uppour exécuter les migrations non exécutées -
npm run migration:downpour rolllback la dernière migration
Tu vas voir comment utiliser toutes ces lignes de commandes par la suite.
Entité de base de données
Une entité de base de données est une classe qui représente une table. Les champs de la table correspondent à des propriétés de la classe. Tu dois définir quelle propriété correspond à la clé primaire de ta table, ainsi que les caractéristiques de chaque champs.
Tout d'abord, tu vas créer un nouveau module pour gérer les utilisateurs de ton application. A terme, ce module contiendra ton entité User, le UserService ainsi que le UserController mais tu vas procéder par étape.
Nest fournit une CLI qui permet de générer les classes dont tu as besoin et de les lier au bon endroit.
Module utilisateur
Les utilisateurs sont stockés dans une table user de ta base de données. Cette table est reliée à une classe Entity. Tu auras besoin d'un service UserService pour manipuler les entités User et donc les données de la base de données.
Tu dois donc créer ce module, ce service et le controller associé.
nest g module users
nest g service users
nest g controller users
Tu viens de générer :
-
un module
users: il contiendra la gestion des utilisateurs -
un
UserService: il contiendra les méthodes d'interaction avec la base de données -
un
UsersController: il contiendra les méthodes correspondant aux routes de l'api/users
Entité
Tu vas créer une entité User que tu mettras dans un fichier user.entity.ts.
nest g class users/user.entity
Tu renommeras la classe UserEntity en User pour plus de simplicité (renomme dans le fichier .spec.ts aussi).
Le décorateur @Entity() permet de définir une classe comme une entité de base de données.
Un User est composé de :
-
id: autogénéré -
email: string 50 caractères -
password: string 50 caractères -
firstname: string 80 caractères -
lastname: string 80 caractères
// app/src/users/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50 })
email:string;
@Column({ length: 255 })
password:string;
@Column({ length: 80 })
firstname:string;
@Column({ length: 80 })
lastname: string;
}
Tu dois spécifier à TypeORM que tu utilises une entité dans ce module :
// app/src/users/users.module.ts
import { User } from './user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
...
@Module({
imports: [TypeOrmModule.forFeature([User])],
...
})
export class UsersModule {}
Migration
Tu as créé la classe d'entité mais la table user correspondante n'existe pas encore dans mysql. Tu vas devoir créer une migration.
Les migrations sont des fichiers javascript auto générés. Lors de la création d'une nouvelle migration, TypeORM traduit en SQL les modifications que tu as apporté à tes entités afin de les appliquer dans la base de données. Tu auras donc une multitude de fichiers de migrations qui correspondront à toutes les modifications successives que tu as faites sur ta base de données.
Lors d'une nouvelle mise en production, tu n'auras qu'à exécuter les migrations sur ta base de production avec une simple ligne de commande.
De plus, un fichier de migration possède 2 fonctions : up() et down() qui respectivement applique les modifications ou les enlève. Il est donc possible de revenir facilement à une version antérieure de la base de données. Toutes les données de migrations appliquées à la base se trouvent dans une table nommée migrations qui est directement gérée par TypeOrm.
Pour utiliser correctement les migrations, tu dois définir une configuration. Crée un fichier migration.config.ts dans le répertoire src/config/
// src/config/migration.config.ts
import { DataSource } from 'typeorm';
import { databaseConfig } from './database.config';
export default new DataSource({
...databaseConfig,
migrations: [__dirname + '/../migrations/*.ts'],
});
Dans ce fichier, tu définis que la connexion à la base de données et la même que celle de ton application. Tu ajoutes le chemin de l'emplacement du dossier de migrations que tu dois créer dans src/migrations
Dans ce fichier, tu définis que la connexion à la base de données est la même que celle de ton application. Tu ajoutes le chemin de l'emplacement du dossier de migrations que tu dois créer dans src/migrations
Tu dois d'abord lancer ton serveur car les migrations seront générées en TS, mais devront être transpilées en JS.
npm run start:dev
Pour créer la migration correspondante à la création de la table user :
npm run migration:generate src/migrations/CreateUserTable
Tu dois spécifier dans quel dossier tu veux que ta migration soit générée.
Tu peux regarder le contenu du fichier de migration.
Maintenant que tu as généré une migration, tu l'appliques :
npm run migration:up
TypeORM va récupérer toutes les migrations de ton dossier de migrations et va appliquer celles qu'il n'a pas encore appliqué. A la fin, TypeORM renseigne dans sa table migrations tous les fichiers de migrations qu'il a exécuté.
Tu verras sur le terminal les requêtes SQL qui ont été exécutées. Tu peux vérifier dans ta base de données que tes modifications ont bien été appliquées.
A chaque fois que tu modifieras ta base de données, tu devras générer une migration et l'appliquer. Tes camarades récupèreront les migrations via git et pourront les appliquer à leur tour.
Il te reste 2 étapes :
- Créer les méthodes de manipulation des entités
Userdans leUserService - Créer les méthodes de l'api dans le
UserController. Elles utiliseront leUserService
Manipulation des entités
Les interactions avec la base de données se font exclusivement dans les services. Pour cela, tu dois injecter le Repository correspondant à ton entité User. Un Repository est une classe qui contient toutes les méthodes dont tu auras besoin pour interagir avec la base. Il se charge de traduire tes demandes en SQL.
Un Repository s'injecte dans le constructeur de ton service presque comme un service lambda.
constructor(@InjectRepository(User) private usersRepository: Repository<User>) { }
Comme tu le vois, tu n'as pas besoin de créer une classe de Repository par entité. Il te suffit de le typer à l'aide des chevrons <User>
Ta classe UsersService devrait ressembler à cela :
// src/app/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private usersRepository: Repository<User>,
) {}
async getUsers(): Promise<User[]> {
return await this.usersRepository.find();
}
async getUser(id: number): Promise<User[]> {
return await this.usersRepository.find({
// Properties to return. We don't want the password property.
select: ['firstname', 'lastname', 'email'],
where: [{ id: id }],
});
}
saveUser(user: User): Promise<User> {
return this.usersRepository.save(user);
}
deleteUser(user: User): void {
this.usersRepository.delete(user);
}
}
Création de l'API
UsersController
Les controllers gèrent les routes suivant une méthode/verbe HTTP. Ce sont eux qui constitueront l'API de ton application. Le décorateur @Controller('users') sera chargé de gérer la route http://localhost:3000/users peu importe la méthode.
Tu créeras ensuite une méthode par appel d'API que tu veux gérer. Tu devras préciser quelle méthode HTTP est gérée par quelle méthode de la classe avec les décorateurs : @Get(), @Post(), @Put(), @Delete().
Tu peux également définir des paramètres : @Get(':id').
N'oublie pas d'injecter le UsersService dans ton constructeur. Tu vas en avoir besoin pour interagir avec ta base de données.
Ton UsersController devrait ressembler à cela :
// src/app/users/users.controller.ts
import {
Controller,
Post,
Body,
Get,
Put,
Delete,
Param,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';
@Controller('users')
export class UsersController {
constructor(private service: UsersService) {}
@Get(':id')
get(@Param() params) {
return this.service.getUser(params.id);
}
@Post()
create(@Body() user: User) {
return this.service.saveUser(user);
}
@Put()
update(@Body() user: User) {
return this.service.saveUser(user);
}
@Delete(':id')
deleteUser(@Param() params) {
this.service.deleteUser(params.id);
return;
}
}
Tu peux récupérer le body ou/et les paramètres en paramétrant les arguments de tes méthodes avec @Param() et @Body().
Tu peux maintenant ouvrir Postman et lancer cette requête :
POST http://localhost:3000/users
{
"firstname": "Pierre",
"lastname": "Dupont",
"email": "dupont@example.com",
"password": "azerty"
}
Amuse-toi à tester toutes les requêtes avec Postman ou curl.
Ressources partagées sur cette quête
- Cela m'a permis de resoudre le problème sur windows des commande migrations. Voir dans discutions de cette quête.https://stackoverflow.com/questions/57601796/cannot-generate-the-migration