Anne  de Morel

Anne de Morel

1658375160

Comment Utiliser DataLoader Avec NestJS

Dans toute application Web complète, la réduction du nombre de trajets que nous devons effectuer vers la source de données (généralement des bases de données) peut améliorer considérablement la vitesse de l'application. Le package DataLoader est un utilitaire générique qui fournit une API cohérente que nous pouvons utiliser pour accélérer le chargement des données à partir de n'importe quelle source de données distante. Pour ce faire, il fournit des fonctionnalités de traitement par lots et de mise en cache.

Dans cet article, je montrerai comment le package DataLoader peut nous aider à minimiser le nombre de trajets que nous devons effectuer vers la source de données. J'expliquerai les fonctionnalités qu'il offre et comment nous pouvons l'utiliser pour résoudre un problème courant avec les serveurs GraphQL, le problème N+1.

Traitement par lots et mise en cache avec DataLoader

Le traitement par lots est la principale fonctionnalité du DataLoader. Cela nous aide à charger plusieurs ressources en même temps alors qu'elles auraient été chargées indépendamment par défaut. Cela peut arriver lorsque nous devons charger plusieurs ressources similaires, mais que leurs charges sont indépendantes les unes des autres. Ces ressources peuvent provenir d'une base de données, d'une API REST ou de n'importe où ailleurs.

DataLoader fonctionne en fusionnant toutes les charges individuelles se produisant dans le même cadre d'exécution (un seul tick de la boucle d'événements) et en regroupant les charges en une seule opération.

DataLoader fournit également un cache de mémorisation pour toutes les charges qui se produisent dans une seule requête à votre application. Cela ne remplace pas un cache partagé au niveau de l'application comme Redis, Memcache ou autres. Voici une clause de non-responsabilité de la documentation DataLoader à ce sujet :

DataLoader est avant tout un mécanisme de chargement de données, et son cache ne sert qu'à ne pas charger plusieurs fois les mêmes données dans le cadre d'une seule requête à votre application.

L'un des endroits où le DataLoader brille le plus est avec les requêtes GraphQL , mais un problème courant auquel il est confronté est le problème N + 1. Nous couvrirons cela dans la section suivante.

Quel est le problème N+1 dans GraphQL ?

GraphQL résout un problème important dans l'API REST : la surrécupération. Pour ce faire, il permet de définir un schéma et les clients peuvent demander les champs spécifiques dont ils ont besoin.

Disons que nous avons un serveur API GraphQL qui permet aux clients de rechercher des enregistrements dans une studentstable stockée dans notre base de données. Et disons que pour les besoins de l'explication, il y a aussi un friendstableau, stockant les détails des amis de chaque étudiant qui ne sont pas étudiants :

{
  students {
    name
  }
}

Cette requête demande le namechamp sur les studentobjets qui doivent être définis par le serveur GraphQL. GraphQL résout chaque champ en appelant la Resolverfonction attachée au champ. Chaque champ déclaré dans le schéma doit avoir sa Resolverfonction correspondante.

Le Resolverpour les étudiants peut effectuer une seule requête dans la base de données pour récupérer les étudiants. Supposons maintenant que nous ayons cette requête GraphQL :

 

   query {                      
     students {                 
       name       
       friends {                
         name
       }
     }
   }  

Nous avons des données imbriquées dans la studentsrequête. Pour exécuter cela, une requête de base de données est exécutée pour récupérer la liste de students, et comme chaque champ du schéma GraphQL a un résolveur utilisé pour résoudre sa valeur, une requête de base de données est exécutée pour obtenir les amis de chacun des étudiants.

Donc s'il y a N étudiants, nous devons faire une requête pour obtenir la liste des étudiants, puis N requêtes (pour chaque étudiant) pour obtenir les amis. C'est de là que vient le nom N+1.

Dans le diagramme ci-dessous, supposons qu'il y a trois étudiants dans la base de données qui sont résolus par le studentsrésolveur. La requête de base de données unique nécessaire pour les récupérer a été colorée en bleu. Pour chaque élève, les trois requêtes de la base de données pour récupérer leurs amis reçoivent une couleur différente. Dans ce cas, nous en avons quatre.

Visualisation du problème N+1

Le problème N + 1 peut sembler trivial lorsque nous n'avons que trois enregistrements d'étudiants au total, mais en supposant que nous ayons des milliers d'étudiants, les requêtes supplémentaires dans la base de données peuvent ralentir considérablement les requêtes GraphQL.

Comment DataLoader résout le problème N+1

Le problème N+1 découle de l'implémentation intuitive des serveurs GraphQL : chaque champ doit avoir sa propre fonction de résolution. En continuant avec l' studentsexemple de requête présenté ci-dessus, il serait utile de pouvoir différer d'une manière ou d'une autre la résolution du friendschamp de chaque étudiant jusqu'à ce que nous ayons le dernier objet étudiant, puis nous pouvons faire une seule requête de base de données pour récupérer les amis de chaque étudiant.

Si nous utilisons SQL, nous pouvons faire une requête comme celle-ci :

SELECT * from friends WHERE studentId IN (firststudentId, secondstudentId, ...) 

DataLoader peut nous aider à atteindre cet objectif grâce à sa fonction de traitement par lots. Laissez-nous travailler sur un exemple de projet NestJS pour voir cela en action.

Configurer NestJS avec GraphQL

J'ai mis en place un projet GraphQL en suivant l'approche "code first" décrite dans la documentation NestJS . Voici la structure des fichiers que j'ai créés (vous pouvez le voir sur GitHub ici ):

structure du fichier de projet

Voici le contenu de friend.entity.ts:

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Friend {
  @Field()
  id: number;

  @Field()
  name: string;

  studentId: number;
}

Et le contenu de student.entity.ts:

import { Field, ObjectType } from '@nestjs/graphql';
import { Friend } from 'src/friend/friend.entity';

@ObjectType()
export class Student {
  @Field()
  id: number;

  @Field()
  name: string;

  @Field()
  class: string;

  @Field(() => [Friend])
  friends?: Friend[];
}

J'ai utilisé un magasin de données en mémoire au lieu d'une base de données réelle pour faciliter l'exécution du projet sans se soucier de la configuration de la base de données, comme vous pouvez le voir dans le data.tsfichier :

import { Friend } from './friend/friend.entity';
import { Student } from './student/student.entity';

export const students: Student[] = [
  { id: 1, name: 'John', class: '1A' },
  ...
  { id: 10, name: 'Mary', class: '1J' },
];

export const friends: Friend[] = [
  { id: 1, name: 'Sam', studentId: 1 },
  ...
  { id: 7, name: 'Seyi', studentId: 4 },
];

Les référentiels accèdent à ce magasin de données et effectuent des opérations de base de données fictives. Par exemple, à l'intérieur student.repository.tsde , nous avons :

import { Injectable } from '@nestjs/common';
import { students } from '../data';
import { Student } from './student.entity';

@Injectable()
export class StudentRepository {
  public async getAll(): Promise<Student[]> {
    console.log('SELECT * FROM students');
    return students;
  }
}

Je me déconnecte de ce à quoi ressemblerait la requête de la base de données si nous utilisions une base de données réelle.
Le contenu de friend.repository.tsest le suivant :

import { Injectable } from '@nestjs/common';
import { friends } from '../data';
import { Friend } from './friend.entity';

@Injectable()
export class FriendRepository {
  public async getStudentFriends(studentId: number): Promise<Friend[]> {
    console.log(`SELECT * FROM friends WHERE studentId = ${studentId}`);
    return friends.filter((friend) => friend.studentId === studentId);
  }
}

Le student.resolver.tscontient des méthodes pour résoudre les requêtes :

import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Friend } from '../friend/friend.entity';
import { FriendService } from 'src/friend/friend.service';
import { Student } from './student.entity';
import { StudentService } from './student.service';

@Resolver(Student)
export class StudentResolver {
  constructor(
    private readonly studentService: StudentService,
    private readonly friendService: FriendService,
  ) {}
  @Query(() => [Student])
  async students() {
    return await this.studentService.getAll();
  }

  @ResolveField('friends', () => [Friend])
  async getFriends(@Parent() student: Student) {
    const { id: studentId } = student;
    return await this.friendService.getStudentFriends(studentId);
  }
}

À l'aide du @ResolveFielddécorateur, nous spécifions comment le friendschamp des Studentobjets parents doit être résolu.

Nous pouvons exécuter l'application avec npm run start:devet visiter le terrain de jeu qui sera sur http://localhost:3000/graphql pour le tester :

aire de jeux de l'application nestjs graphql

La requête renvoie chaque étudiant avec ses amis. Voici le journal du terminal :

Journal des requêtes de base de données

Nous pouvons voir le problème N+1 sur l'écran. La première requête, SELECT * FROM students, récupère les étudiants, et comme il y a dix étudiants dans le magasin de données, une requête est exécutée pour obtenir chacun de leurs amis. Nous avons donc 11 requêtes au total !

Configuration de DataLoader

Nous allons maintenant configurer DataLoader afin que nous puissions utiliser sa fonction de traitement par lots pour résoudre ce problème N+1. D'abord, nous l'installons comme ceci:

npm install dataloader -S

Pour mieux résumer les choses, créons un dataloadermodule :

nest g mo dataloader

Nous allons également créer un service et une interface DataLoader, dataloader.interface.ts, pour spécifier les chargeurs que nous utiliserons.

Le contenu de l' dataloader.interface.tsintérieur du dataloaderdossier du module est le suivant :

import DataLoader from 'dataloader';
import { Friend } from '../friend/friend.entity';

export interface IDataloaders {
  friendsLoader: DataLoader<number, Friend>;
}

Ici, nous avons déclaré l' IDataloadersinterface et spécifié le friendsLoadercomme un champ dans celle-ci. Si nous avons besoin de plus de chargeurs, nous pouvons les déclarer ici.

Nous avons d'abord besoin d'une méthode pour récupérer friendspar lot à partir d'un tableau d'identifiants d'étudiants. Mettons à jour le friend.repository.tspour inclure une nouvelle méthode :

export class FriendRepository {
  ...
  public async getAllFriendsByStudentIds(
    studentIds: readonly number[],
  ): Promise<Friend[]> {
    console.log(
      `SELECT * FROM friends WHERE studentId IN (${studentIds.join(',')})`,
    );
    return friends.filter((friend) => studentIds.includes(friend.studentId));
  }
}

La getAllFriendsByStudentIdsméthode reçoit un tableau de studentIdset renvoie un tableau des amis de tous les étudiants fournis.

Mettons également à jour le friend.service.tsfichier :

export class FriendService {
  ...
  public async getAllFriendsByStudentIds(
    studentIds: readonly number[],
  ): Promise<Friend[]> {
    return await this.friendRepository.getAllFriendsByStudentIds(studentIds);
  }

  public async getStudentsFriendsByBatch(
    studentIds: readonly number[],
  ): Promise<(Friend | any)[]> {
    const friends = await this.getAllFriendsByStudentIds(studentIds);
    const mappedResults = this._mapResultToIds(studentIds, friends);
    return mappedResults;
  }

  private _mapResultToIds(studentIds: readonly number[], friends: Friend[]) {
    return studentIds.map(
      (id) =>
        friends.filter((friend: Friend) => friend.studentId === id) || null,
    );
  }
}

La getStudentsFriendsByBatchest la méthode qui exécute l'opération par lots que nous venons de définir dans le friend.repository.ts, mais elle inclut également la méthode _mapResultToIds. Ceci est important pour que DataLoader fonctionne correctement.

DataLoader a certaines contraintes sur la fonction batch. Tout d'abord, la longueur du tableau renvoyé doit être identique à la longueur des clés fournies. C'est pourquoi dans (friend: Friend) => friend*.*studentId === id) || null, nous revenons nullsi un ami n'est pas trouvé pour une carte d'étudiant donnée.

Deuxièmement, l'index des valeurs renvoyées doit suivre le même ordre que l'index des clés fournies. Autrement dit, si les clés sont [1, 3, 4], la valeur renvoyée doit avoir ce format [friendsOfStudent1, friendsOfStudent3, friendsOfStudent4]. La source de données peut ne pas les renvoyer dans le même ordre, nous devons donc les réorganiser, ce que nous faisons dans la fonction transmise à l' studentIds*.map*intérieur de _mapResultToIds.

Création du service DataLoader

Maintenant, nous mettons à jour le dataloader.service.tscomme suit :

import { Injectable } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { Friend } from '../friend/friend.entity';
import { FriendService } from '../friend/friend.service';
import { IDataloaders } from './dataloader.interface';

@Injectable()
export class DataloaderService {
  constructor(private readonly friendService: FriendService) {}

  getLoaders(): IDataloaders {
    const friendsLoader = this._createFriendsLoader();
    return {
      friendsLoader,
    };
  }

  private _createFriendsLoader() {
    return new DataLoader<number, Friend>(
      async (keys: readonly number[]) =>
        await this.friendService.getStudentsFriendsByBatch(keys as number[]),
    );
  }
}

La getLoadersméthode renvoie un objet qui contient des objets de chargeur. Actuellement, nous avons un chargeur pour seulement friends.

La _createFriendsLoaderméthode instancie une nouvelle instance DataLoader. Nous passons une fonction batch, qui attend un tableau de clés comme argument unique. Nous effectuons l'opération par lots en faisant un appel à friendService.

Ajouter des chargeurs au contexte

Nous devons maintenant fournir les chargeurs de données au contexte Apollo créé par NestJS dans le module GraphQL. L'argument de contexte peut être utilisé pour transmettre des informations à n'importe quel résolveur, telles que la portée d'authentification, les connexions à la base de données et les fonctions de récupération personnalisées. Dans notre cas, nous l'utiliserons pour passer nos chargeurs aux résolveurs.

Mettre à jour app.module.tscomme ceci :

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { StudentModule } from './student/student.module';
import { FriendModule } from './friend/friend.module';
import { DataloaderModule } from './dataloader/dataloader.module';
import { DataloaderService } from './dataloader/dataloader.service';

@Module({
  imports: [
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      driver: ApolloDriver,
      imports: [DataloaderModule],
      useFactory: (dataloaderService: DataloaderService) => {
        return {
          autoSchemaFile: true,
          context: () => ({
            loaders: dataloaderService.getLoaders(),
          }),
        };
      },
      inject: [DataloaderService],
    }),
    StudentModule,
    FriendModule,
  ],
})
export class AppModule {}

Nous les importons DataLoaderet DataServiceles utilisons maintenant dans GraphQLModule. Nous déclarons également le contextet fournissant le loadersreçu du dataloaderservice dans le cadre du contexte. Maintenant, nous pouvons accéder à ce contexte dans le résolveur.

Ensuite, mettons à jour la façon dont nous résolvons le friendschamp dansstudent.resolver.ts :

@Resolver(Student)
export class StudentResolver {
  ...
  @ResolveField('friends', () => [Friend])
  getFriends(
    @Parent() student: Student,
    @Context() { loaders }: { loaders: IDataloaders },
  ) {
    const { id: studentId } = student;
    return loaders.friendsLoader.load(studentId);
  }
}

Le @Contextdécorateur donne accès au contexte où nous pouvons accéder aux chargeurs précédemment ajoutés. Nous utilisons maintenant le friendsLoaderpour charger les amis d'un étudiant donné.

Nous pouvons maintenant redémarrer l'application et tester la requête depuis le terrain de jeu GraphQL. Le résultat est le même qu'auparavant, mais lorsque nous vérifions les journaux du terminal, nous voyons ce qui suit :

Journal des requêtes de base de données effectuées après l'introduction de DataLoader

Fantastique! Désormais, seules deux requêtes de base de données sont en cours d'exécution. Auparavant, sans DataLoader, nous en exécutions onze ; imaginez récupérer 1 000 objets d'élèves ! Avec DataLoader, nous n'effectuerons toujours que deux requêtes de base de données, au lieu de 1 001 requêtes sans lui. Cela peut avoir un impact considérable sur les performances de notre application.

Conclusion

Dans cet article, j'ai expliqué ce qu'est le package DataLoader et les fonctionnalités qu'il offre. J'ai également montré comment utiliser le package dans NestJS avec un exemple d'API GraphQL. Ensuite, nous avons expliqué comment résoudre le problème N+1 dans GraphQL avec le package DataLoader. Certaines personnalisations sont disponibles pour DataLoader, sur lesquelles vous pouvez en savoir plus dans la documentation .

Comme je l'ai mentionné précédemment, le package DataLoader est un utilitaire générique et il n'est pas nécessaire de l'utiliser uniquement avec les serveurs GraphQL. Vous pouvez appliquer les idées de cet article pour les adapter à votre propre usage.

Le dépôt du projet NestJS créé dans cet article est ici . La mainbranche contient la solution avec DataLoader, tandis que la without-dataloaderbranche montre l'état de l'application avant l'introduction de DataLoader.

Source : https://blog.logrocket.com/use-dataloader-nestjs/

#nestjs 

What is GEEK

Buddha Community

Comment Utiliser DataLoader Avec NestJS
Anne  de Morel

Anne de Morel

1658375160

Comment Utiliser DataLoader Avec NestJS

Dans toute application Web complète, la réduction du nombre de trajets que nous devons effectuer vers la source de données (généralement des bases de données) peut améliorer considérablement la vitesse de l'application. Le package DataLoader est un utilitaire générique qui fournit une API cohérente que nous pouvons utiliser pour accélérer le chargement des données à partir de n'importe quelle source de données distante. Pour ce faire, il fournit des fonctionnalités de traitement par lots et de mise en cache.

Dans cet article, je montrerai comment le package DataLoader peut nous aider à minimiser le nombre de trajets que nous devons effectuer vers la source de données. J'expliquerai les fonctionnalités qu'il offre et comment nous pouvons l'utiliser pour résoudre un problème courant avec les serveurs GraphQL, le problème N+1.

Traitement par lots et mise en cache avec DataLoader

Le traitement par lots est la principale fonctionnalité du DataLoader. Cela nous aide à charger plusieurs ressources en même temps alors qu'elles auraient été chargées indépendamment par défaut. Cela peut arriver lorsque nous devons charger plusieurs ressources similaires, mais que leurs charges sont indépendantes les unes des autres. Ces ressources peuvent provenir d'une base de données, d'une API REST ou de n'importe où ailleurs.

DataLoader fonctionne en fusionnant toutes les charges individuelles se produisant dans le même cadre d'exécution (un seul tick de la boucle d'événements) et en regroupant les charges en une seule opération.

DataLoader fournit également un cache de mémorisation pour toutes les charges qui se produisent dans une seule requête à votre application. Cela ne remplace pas un cache partagé au niveau de l'application comme Redis, Memcache ou autres. Voici une clause de non-responsabilité de la documentation DataLoader à ce sujet :

DataLoader est avant tout un mécanisme de chargement de données, et son cache ne sert qu'à ne pas charger plusieurs fois les mêmes données dans le cadre d'une seule requête à votre application.

L'un des endroits où le DataLoader brille le plus est avec les requêtes GraphQL , mais un problème courant auquel il est confronté est le problème N + 1. Nous couvrirons cela dans la section suivante.

Quel est le problème N+1 dans GraphQL ?

GraphQL résout un problème important dans l'API REST : la surrécupération. Pour ce faire, il permet de définir un schéma et les clients peuvent demander les champs spécifiques dont ils ont besoin.

Disons que nous avons un serveur API GraphQL qui permet aux clients de rechercher des enregistrements dans une studentstable stockée dans notre base de données. Et disons que pour les besoins de l'explication, il y a aussi un friendstableau, stockant les détails des amis de chaque étudiant qui ne sont pas étudiants :

{
  students {
    name
  }
}

Cette requête demande le namechamp sur les studentobjets qui doivent être définis par le serveur GraphQL. GraphQL résout chaque champ en appelant la Resolverfonction attachée au champ. Chaque champ déclaré dans le schéma doit avoir sa Resolverfonction correspondante.

Le Resolverpour les étudiants peut effectuer une seule requête dans la base de données pour récupérer les étudiants. Supposons maintenant que nous ayons cette requête GraphQL :

 

   query {                      
     students {                 
       name       
       friends {                
         name
       }
     }
   }  

Nous avons des données imbriquées dans la studentsrequête. Pour exécuter cela, une requête de base de données est exécutée pour récupérer la liste de students, et comme chaque champ du schéma GraphQL a un résolveur utilisé pour résoudre sa valeur, une requête de base de données est exécutée pour obtenir les amis de chacun des étudiants.

Donc s'il y a N étudiants, nous devons faire une requête pour obtenir la liste des étudiants, puis N requêtes (pour chaque étudiant) pour obtenir les amis. C'est de là que vient le nom N+1.

Dans le diagramme ci-dessous, supposons qu'il y a trois étudiants dans la base de données qui sont résolus par le studentsrésolveur. La requête de base de données unique nécessaire pour les récupérer a été colorée en bleu. Pour chaque élève, les trois requêtes de la base de données pour récupérer leurs amis reçoivent une couleur différente. Dans ce cas, nous en avons quatre.

Visualisation du problème N+1

Le problème N + 1 peut sembler trivial lorsque nous n'avons que trois enregistrements d'étudiants au total, mais en supposant que nous ayons des milliers d'étudiants, les requêtes supplémentaires dans la base de données peuvent ralentir considérablement les requêtes GraphQL.

Comment DataLoader résout le problème N+1

Le problème N+1 découle de l'implémentation intuitive des serveurs GraphQL : chaque champ doit avoir sa propre fonction de résolution. En continuant avec l' studentsexemple de requête présenté ci-dessus, il serait utile de pouvoir différer d'une manière ou d'une autre la résolution du friendschamp de chaque étudiant jusqu'à ce que nous ayons le dernier objet étudiant, puis nous pouvons faire une seule requête de base de données pour récupérer les amis de chaque étudiant.

Si nous utilisons SQL, nous pouvons faire une requête comme celle-ci :

SELECT * from friends WHERE studentId IN (firststudentId, secondstudentId, ...) 

DataLoader peut nous aider à atteindre cet objectif grâce à sa fonction de traitement par lots. Laissez-nous travailler sur un exemple de projet NestJS pour voir cela en action.

Configurer NestJS avec GraphQL

J'ai mis en place un projet GraphQL en suivant l'approche "code first" décrite dans la documentation NestJS . Voici la structure des fichiers que j'ai créés (vous pouvez le voir sur GitHub ici ):

structure du fichier de projet

Voici le contenu de friend.entity.ts:

import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Friend {
  @Field()
  id: number;

  @Field()
  name: string;

  studentId: number;
}

Et le contenu de student.entity.ts:

import { Field, ObjectType } from '@nestjs/graphql';
import { Friend } from 'src/friend/friend.entity';

@ObjectType()
export class Student {
  @Field()
  id: number;

  @Field()
  name: string;

  @Field()
  class: string;

  @Field(() => [Friend])
  friends?: Friend[];
}

J'ai utilisé un magasin de données en mémoire au lieu d'une base de données réelle pour faciliter l'exécution du projet sans se soucier de la configuration de la base de données, comme vous pouvez le voir dans le data.tsfichier :

import { Friend } from './friend/friend.entity';
import { Student } from './student/student.entity';

export const students: Student[] = [
  { id: 1, name: 'John', class: '1A' },
  ...
  { id: 10, name: 'Mary', class: '1J' },
];

export const friends: Friend[] = [
  { id: 1, name: 'Sam', studentId: 1 },
  ...
  { id: 7, name: 'Seyi', studentId: 4 },
];

Les référentiels accèdent à ce magasin de données et effectuent des opérations de base de données fictives. Par exemple, à l'intérieur student.repository.tsde , nous avons :

import { Injectable } from '@nestjs/common';
import { students } from '../data';
import { Student } from './student.entity';

@Injectable()
export class StudentRepository {
  public async getAll(): Promise<Student[]> {
    console.log('SELECT * FROM students');
    return students;
  }
}

Je me déconnecte de ce à quoi ressemblerait la requête de la base de données si nous utilisions une base de données réelle.
Le contenu de friend.repository.tsest le suivant :

import { Injectable } from '@nestjs/common';
import { friends } from '../data';
import { Friend } from './friend.entity';

@Injectable()
export class FriendRepository {
  public async getStudentFriends(studentId: number): Promise<Friend[]> {
    console.log(`SELECT * FROM friends WHERE studentId = ${studentId}`);
    return friends.filter((friend) => friend.studentId === studentId);
  }
}

Le student.resolver.tscontient des méthodes pour résoudre les requêtes :

import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Friend } from '../friend/friend.entity';
import { FriendService } from 'src/friend/friend.service';
import { Student } from './student.entity';
import { StudentService } from './student.service';

@Resolver(Student)
export class StudentResolver {
  constructor(
    private readonly studentService: StudentService,
    private readonly friendService: FriendService,
  ) {}
  @Query(() => [Student])
  async students() {
    return await this.studentService.getAll();
  }

  @ResolveField('friends', () => [Friend])
  async getFriends(@Parent() student: Student) {
    const { id: studentId } = student;
    return await this.friendService.getStudentFriends(studentId);
  }
}

À l'aide du @ResolveFielddécorateur, nous spécifions comment le friendschamp des Studentobjets parents doit être résolu.

Nous pouvons exécuter l'application avec npm run start:devet visiter le terrain de jeu qui sera sur http://localhost:3000/graphql pour le tester :

aire de jeux de l'application nestjs graphql

La requête renvoie chaque étudiant avec ses amis. Voici le journal du terminal :

Journal des requêtes de base de données

Nous pouvons voir le problème N+1 sur l'écran. La première requête, SELECT * FROM students, récupère les étudiants, et comme il y a dix étudiants dans le magasin de données, une requête est exécutée pour obtenir chacun de leurs amis. Nous avons donc 11 requêtes au total !

Configuration de DataLoader

Nous allons maintenant configurer DataLoader afin que nous puissions utiliser sa fonction de traitement par lots pour résoudre ce problème N+1. D'abord, nous l'installons comme ceci:

npm install dataloader -S

Pour mieux résumer les choses, créons un dataloadermodule :

nest g mo dataloader

Nous allons également créer un service et une interface DataLoader, dataloader.interface.ts, pour spécifier les chargeurs que nous utiliserons.

Le contenu de l' dataloader.interface.tsintérieur du dataloaderdossier du module est le suivant :

import DataLoader from 'dataloader';
import { Friend } from '../friend/friend.entity';

export interface IDataloaders {
  friendsLoader: DataLoader<number, Friend>;
}

Ici, nous avons déclaré l' IDataloadersinterface et spécifié le friendsLoadercomme un champ dans celle-ci. Si nous avons besoin de plus de chargeurs, nous pouvons les déclarer ici.

Nous avons d'abord besoin d'une méthode pour récupérer friendspar lot à partir d'un tableau d'identifiants d'étudiants. Mettons à jour le friend.repository.tspour inclure une nouvelle méthode :

export class FriendRepository {
  ...
  public async getAllFriendsByStudentIds(
    studentIds: readonly number[],
  ): Promise<Friend[]> {
    console.log(
      `SELECT * FROM friends WHERE studentId IN (${studentIds.join(',')})`,
    );
    return friends.filter((friend) => studentIds.includes(friend.studentId));
  }
}

La getAllFriendsByStudentIdsméthode reçoit un tableau de studentIdset renvoie un tableau des amis de tous les étudiants fournis.

Mettons également à jour le friend.service.tsfichier :

export class FriendService {
  ...
  public async getAllFriendsByStudentIds(
    studentIds: readonly number[],
  ): Promise<Friend[]> {
    return await this.friendRepository.getAllFriendsByStudentIds(studentIds);
  }

  public async getStudentsFriendsByBatch(
    studentIds: readonly number[],
  ): Promise<(Friend | any)[]> {
    const friends = await this.getAllFriendsByStudentIds(studentIds);
    const mappedResults = this._mapResultToIds(studentIds, friends);
    return mappedResults;
  }

  private _mapResultToIds(studentIds: readonly number[], friends: Friend[]) {
    return studentIds.map(
      (id) =>
        friends.filter((friend: Friend) => friend.studentId === id) || null,
    );
  }
}

La getStudentsFriendsByBatchest la méthode qui exécute l'opération par lots que nous venons de définir dans le friend.repository.ts, mais elle inclut également la méthode _mapResultToIds. Ceci est important pour que DataLoader fonctionne correctement.

DataLoader a certaines contraintes sur la fonction batch. Tout d'abord, la longueur du tableau renvoyé doit être identique à la longueur des clés fournies. C'est pourquoi dans (friend: Friend) => friend*.*studentId === id) || null, nous revenons nullsi un ami n'est pas trouvé pour une carte d'étudiant donnée.

Deuxièmement, l'index des valeurs renvoyées doit suivre le même ordre que l'index des clés fournies. Autrement dit, si les clés sont [1, 3, 4], la valeur renvoyée doit avoir ce format [friendsOfStudent1, friendsOfStudent3, friendsOfStudent4]. La source de données peut ne pas les renvoyer dans le même ordre, nous devons donc les réorganiser, ce que nous faisons dans la fonction transmise à l' studentIds*.map*intérieur de _mapResultToIds.

Création du service DataLoader

Maintenant, nous mettons à jour le dataloader.service.tscomme suit :

import { Injectable } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { Friend } from '../friend/friend.entity';
import { FriendService } from '../friend/friend.service';
import { IDataloaders } from './dataloader.interface';

@Injectable()
export class DataloaderService {
  constructor(private readonly friendService: FriendService) {}

  getLoaders(): IDataloaders {
    const friendsLoader = this._createFriendsLoader();
    return {
      friendsLoader,
    };
  }

  private _createFriendsLoader() {
    return new DataLoader<number, Friend>(
      async (keys: readonly number[]) =>
        await this.friendService.getStudentsFriendsByBatch(keys as number[]),
    );
  }
}

La getLoadersméthode renvoie un objet qui contient des objets de chargeur. Actuellement, nous avons un chargeur pour seulement friends.

La _createFriendsLoaderméthode instancie une nouvelle instance DataLoader. Nous passons une fonction batch, qui attend un tableau de clés comme argument unique. Nous effectuons l'opération par lots en faisant un appel à friendService.

Ajouter des chargeurs au contexte

Nous devons maintenant fournir les chargeurs de données au contexte Apollo créé par NestJS dans le module GraphQL. L'argument de contexte peut être utilisé pour transmettre des informations à n'importe quel résolveur, telles que la portée d'authentification, les connexions à la base de données et les fonctions de récupération personnalisées. Dans notre cas, nous l'utiliserons pour passer nos chargeurs aux résolveurs.

Mettre à jour app.module.tscomme ceci :

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { StudentModule } from './student/student.module';
import { FriendModule } from './friend/friend.module';
import { DataloaderModule } from './dataloader/dataloader.module';
import { DataloaderService } from './dataloader/dataloader.service';

@Module({
  imports: [
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      driver: ApolloDriver,
      imports: [DataloaderModule],
      useFactory: (dataloaderService: DataloaderService) => {
        return {
          autoSchemaFile: true,
          context: () => ({
            loaders: dataloaderService.getLoaders(),
          }),
        };
      },
      inject: [DataloaderService],
    }),
    StudentModule,
    FriendModule,
  ],
})
export class AppModule {}

Nous les importons DataLoaderet DataServiceles utilisons maintenant dans GraphQLModule. Nous déclarons également le contextet fournissant le loadersreçu du dataloaderservice dans le cadre du contexte. Maintenant, nous pouvons accéder à ce contexte dans le résolveur.

Ensuite, mettons à jour la façon dont nous résolvons le friendschamp dansstudent.resolver.ts :

@Resolver(Student)
export class StudentResolver {
  ...
  @ResolveField('friends', () => [Friend])
  getFriends(
    @Parent() student: Student,
    @Context() { loaders }: { loaders: IDataloaders },
  ) {
    const { id: studentId } = student;
    return loaders.friendsLoader.load(studentId);
  }
}

Le @Contextdécorateur donne accès au contexte où nous pouvons accéder aux chargeurs précédemment ajoutés. Nous utilisons maintenant le friendsLoaderpour charger les amis d'un étudiant donné.

Nous pouvons maintenant redémarrer l'application et tester la requête depuis le terrain de jeu GraphQL. Le résultat est le même qu'auparavant, mais lorsque nous vérifions les journaux du terminal, nous voyons ce qui suit :

Journal des requêtes de base de données effectuées après l'introduction de DataLoader

Fantastique! Désormais, seules deux requêtes de base de données sont en cours d'exécution. Auparavant, sans DataLoader, nous en exécutions onze ; imaginez récupérer 1 000 objets d'élèves ! Avec DataLoader, nous n'effectuerons toujours que deux requêtes de base de données, au lieu de 1 001 requêtes sans lui. Cela peut avoir un impact considérable sur les performances de notre application.

Conclusion

Dans cet article, j'ai expliqué ce qu'est le package DataLoader et les fonctionnalités qu'il offre. J'ai également montré comment utiliser le package dans NestJS avec un exemple d'API GraphQL. Ensuite, nous avons expliqué comment résoudre le problème N+1 dans GraphQL avec le package DataLoader. Certaines personnalisations sont disponibles pour DataLoader, sur lesquelles vous pouvez en savoir plus dans la documentation .

Comme je l'ai mentionné précédemment, le package DataLoader est un utilitaire générique et il n'est pas nécessaire de l'utiliser uniquement avec les serveurs GraphQL. Vous pouvez appliquer les idées de cet article pour les adapter à votre propre usage.

Le dépôt du projet NestJS créé dans cet article est ici . La mainbranche contient la solution avec DataLoader, tandis que la without-dataloaderbranche montre l'état de l'application avant l'introduction de DataLoader.

Source : https://blog.logrocket.com/use-dataloader-nestjs/

#nestjs 

Thierry  Perret

Thierry Perret

1657550479

Comment Créer Une Application NestJS MVC Avec YugabyteDB

Dans cet article, nous en apprendrons davantage sur l'architecture du contrôleur de vue de modèle (MVC) en créant une application NestJS MVC avec YugabyteDB . Nous allons coder un projet de démonstration de magasin de livres électroniques. Le code de ce tutoriel est disponible sur mon dépôt Github . N'hésitez pas à le cloner au fur et à mesure que vous suivez les étapes. Commençons!

Conception du contrôleur de vue modèle

MVC est un paradigme architectural qui divise une application en trois composants logiques de base : modèle, vue et contrôleur. Chacun de ces composants est conçu pour gérer des parties particulières du développement d'une application. MVC est un cadre de développement Web standard populaire pour la création de projets évolutifs et flexibles.

Les trois composants logiques de MVC sont les suivants :

  • Modèle : Comparé à la Vue et au Contrôleur, ce niveau est considéré comme le plus bas. Il représente les données transférées entre les composants View et Controller et détermine le stockage de tous les éléments de données dans l'application.
  • Vue : Ce composant est en charge de l'interface utilisateur de l'application. Il gère également les données de l'utilisateur final ainsi que la communication entre l'utilisateur et le contrôleur.
  • Contrôleur : le contrôleur complète la boucle en recevant les entrées de l'utilisateur, en les transformant en messages appropriés, en les transmettant aux vues et en gérant les gestionnaires de requêtes.

Le modèle MVC présente les avantages suivants :

  • Il permet une organisation facile des grandes applications Web
  • Il permet de modifier facilement n'importe quelle partie de l'application sans nécessairement affecter d'autres parties de l'application
  • Il simplifie le processus de test de code
  • Il permet une collaboration facile entre les équipes de développeurs
  • Il aide les développeurs à créer un code facilement maintenu
  • Il permet aux développeurs de créer et d'utiliser leurs moteurs de vue préférés

Conditions préalables

Ce tutoriel est une démonstration pratique. Pour suivre, assurez-vous d'avoir installé les éléments suivants :

Le code de ce tutoriel est disponible sur mon dépôt Github . N'hésitez pas à le cloner au fur et à mesure que vous suivez les étapes.

Qu'est-ce que Nest JS ?

NestJS est un framework Node.js permettant de créer des applications côté serveur rapides, testables, évolutives et faiblement couplées qui utilisent TypeScript. Il tire parti de puissants frameworks de serveur HTTP tels que Express ou Fastify. Nest ajoute une couche d'abstraction aux frameworks Node.js et expose ses API aux développeurs. Il prend en charge les systèmes de gestion de base de données tels que PostgreSQL, MySQL et, dans ce didacticiel, yugabyteDB . NestJS propose également des injections de dépendances prêtes à l'emploi.

Pourquoi utiliser NestJS ?

NestJS est l'un des frameworks Node.JS les plus populaires depuis sa sortie en 2017. Certaines des raisons pour lesquelles les développeurs utilisent Nestjs sont les suivantes :

  • Il est hautement évolutif et facile à entretenir
  • Il a une grande communauté de développeurs et un système de support
  • Nest a trouvé une intersection unique de programmation frontale et middleware que de nombreux langages ont eu du mal à découvrir
  • La prise en charge de TypeScript par NestJS garantit qu'il restera pertinent dans le monde JavaScript en constante évolution et offre aux développeurs moins de changements de contexte.
  • Il dispose d'une documentation complète
  • Tests unitaires faciles
  • Il est conçu pour les applications d'entreprise à grande échelle
  • Nest fournit une architecture d'application prête à l'emploi qui permet aux développeurs et aux équipes de créer des applications hautement testables, évolutives, faiblement couplées et facilement maintenables

Configuration du projet

Avant de plonger dans le codage, configurons notre projet NestJS et configurons notre structure de projet. Nous allons commencer par créer le dossier du projet. Ouvrez votre terminal et exécutez la commande suivante :

mkdir nestmvcapp && cd nestmvcapp

Création du dossier du projet

Installez ensuite la CLI NestJS avec la commande ci-dessous :

npm i -g @nestjs/cli

Installer NestJS

Une fois l'installation terminée, exécutez la commande ci-dessous pour échafauder un projet NestJS.

nest new bookapi

Échafaudage d'un projet NestJS

Choisissez votre gestionnaire de packages npm préféré. Pour ce didacticiel, nous utiliserons npm et attendrons que les packages nécessaires soient installés. Une fois l'installation terminée, procédons en créant notre table de base de données avec Arctype .

Configurer une base de données YugabyteDB

Pour commencer à utiliser la base de données Yugabyte dans notre application, nous devons l'installer sur notre machine. Voyons comment procéder, étape par étape. Tout d'abord, vérifiez que vous avez Python.

# Ubuntu 20.04
sudo apt install python-is-python3

Vérifier que Python est installé

Ensuite, vérifiez que vous avez installé wget . Vous pouvez le faire avec la commande ci-dessous.

sudo apt install wget

Installation de wget

Ensuite, téléchargez et extrayez la base de données Yugabyte :

# download
wget https://downloads.yugabyte.com/releases/2.11.2.0/yugabyte-2.11.2.0-b89-linux-x86_64.tar.gz

# extract
tar xvfz yugabyte-2.11.2.0-b89-linux-x86_64.tar.gz && cd yugabyte-2.11.2.0/

Téléchargement et installation de Yugabyte

Ensuite, configurez YugabyteDB avec la commande ci-dessous.

./bin/post_install.sh

Configuration de Yugabyte

Enfin, démarrez votre base de données Yugabyte.

./bin/yugabyted start

Démarrage de la base de données Yugabyte

Maintenant, nous allons connecter Arctype à Yugabyte. Ouvrez Arctype , cliquez sur l' onglet YugabyteDB et connectez-vous à la base de données Yugabyte en complétant les informations comme indiqué dans la capture d'écran ci-dessous :

Capture d'écran d'Arctype

Connecter Arctype à Yugabyte

Notez que dans la capture d'écran ci-dessus, nous avons laissé l'entrée de la base de données vide. C'est parce que nous n'en avons pas encore créé. Alors, créons-en un. Cliquez sur le bouton Nouvelle requête et exécutez la commande SQL ci-dessous :

CREATE DATABASE books_db;

Création de la base de donnéesCapture d'écran d'ArctypeLa requête dans Arctype

Installer les dépendances

Avec notre configuration de base de données Yugabyte, installons les dépendances de notre application. Installez typeorm, pg et ejs avec la commande ci-dessous :

npm install --save @nestjs/typeorm typeorm pg ejs

Installation des dépendances

Cela prendra un peu de temps à installer, alors attendez qu'il se termine. Ensuite, nous pouvons procéder à la création de notre application.

Créer un module de livres

Un module est une classe qui a été annotée avec le décorateur @Module() . Nest utilise les métadonnées fournies par le décorateur @Module() pour organiser la structure de l'application. Nous allons créer un module de livres avec la commande ci-dessous :

nest generate module books

Génération du booksmodule

La commande ci-dessus créera un dossier de livres dans le dossier src avec un fichier books.module.ts et l'enregistrera dans le fichier du module d'application racine (app.module.ts).

Créer une classe de modèle de livres

Avec notre module de livres créé, créons un modèle pour créer et lire les données de notre base de données.

Créez une classe de modèle de livre avec la commande ci-dessous :

nest generate class /books/model/book --flat

Création d'une classe de modèle de livre

La commande ci-dessus créera le fichier model/book.ts dans le répertoire du module livres. L'indicateur --flat garantit que Nest ne générera pas de dossier pour le modèle de livres.

Ensuite, définissons notre modèle de base de données en utilisant Typeorm. Nous avons besoin des champs id , title , author , quantity , description et createdAt pour notre modèle de livres. Nous utiliserons le décorateur d' entité typeorm pour définir notre classe de modèle, le décorateur de colonne pour définir nos champs, PrimaryGeneratedColumn pour créer des identifiants générés aléatoirement pour nos livres en utilisant uuid et le décorateur CreatedDateColumn pour enregistrer la date-heure actuelle à laquelle un livre a été créé. . Ouvrez le fichier model/book.ts et ajoutez l'extrait de code ci-dessous :

import { Entity, Column, PrimaryGeneratedColumn, PrimaryColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class Book {

   @PrimaryGeneratedColumn("uuid")
   id: number;

   @Column()
   title: string;

   @Column()
   author: string;

   @Column()
   quantity: number

   @Column()
   description: String

   @CreateDateColumn()
   createdAt: Date;
}

Définition du modèle de base de données

Lorsque nous exécuterons notre application, Typeorm générera l'équivalent SQL du modèle, pour créer une table de livres dans notre base de données Yugabyte.

Ensuite, nous allons connecter notre application à notre base de données Yugabyte dans le fichier src/app.module.ts . Tout d'abord, importez le module Nest TypeOrmModule et la classe de modèle Books avec l'extrait de code ci-dessous :

import { TypeOrmModule } from '@nestjs/typeorm'
import { Book } from './movie/model/book';

Connexion de l'application à la base de données Yugabyte

Ensuite, connectez-vous à la base de données à l'aide de la méthode forRoot avec nos informations d'identification de base de données avec l'extrait de code ci-dessous :

imports: [
   …
   TypeOrmModule.forRoot({
     type: 'postgres',
     host: 'localhost',
     username: 'yugabyte',
     port: 5433,
     password: '',
     database: 'books_db',
     entities: [Book],
     synchronize: true,
   }),
 ],

Connexion à la base de données

Nous devons également exporter notre classe de modèle Books dans notre fichier books.module.ts, pour la rendre accessible. Tout d'abord, nous allons importer le module TypeOrmModule et la classe de modèle Books .

import { TypeOrmModule } from '@nestjs/typeorm';
import { Book } from './model/book';

Importation du TypeOrmModulemodule

Ensuite, nous rendrons la classe de modèle Book disponible à l'aide de la forFeatureméthode TypeOrmModule.

…
@Module({
 imports: [TypeOrmModule.forFeature([Book])],
 …

En utilisant la forFeatureméthode

Créer des vues

Avec notre modèle de livres défini, créons la vue de notre application. Créez un dossier Views dans le répertoire du module livres. Nous allons créer les modèles de vue pour notre application et nous utiliserons ejs, que nous avons installé dans une section précédente, comme moteur de modèle . Pour commencer, différencions le code passe-partout dans notre fichier main.ts et le code ci-dessous pour configurer notre moteur de modèle et notre répertoire de fichiers statiques.

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create<NestExpressApplication>(
   AppModule,
 );
 app.useStaticAssets(join(__dirname, '..', '/src/public'));
 app.setBaseViewsDir(join(__dirname, '..', '/src/books/views'));
 app.setViewEngine('ejs');

 await app.listen(3000);
}
bootstrap();

Tirer dans le passe-partout

Maintenant, nous pouvons créer nos fichiers modèles. Nous allons commencer par le header.ejs et le footer.ejs , qui seront créés dans le dossier books/views/partials . Ensuite, les fichiers books.ejs et book-detail.ejs seront créés dans le dossier books/views . Ouvrez le modèle header.ejs et ajoutez l'extrait de code ci-dessous :

<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Bootstrap CSS -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
      crossorigin="anonymous"
    />
    <title>Hello, world!</title>
  </head>
  <body>
    <nav class="navbar navbar-light bg-light">
      <div class="container-fluid">
        <a class="navbar-brand">Book Store</a>
        <% if (page ==="book"){ %>
        <form class="d-flex">
          <input
            class="form-control me-2"
            type="search"
            placeholder="Search"
            aria-label="Search"
          />
          <button class="btn btn-outline-success" type="submit">Search</button>
        </form>
        <button
          type="button"
          class="btn btn-primary"
          data-bs-toggle="modal"
          data-bs-target="#staticBackdrop"
        >
          Add New
        </button>
        <% } %>
      </div>
    </nav>
  </body>
</html>

En-tête HTML

Notre modèle d'en-têtes ressemblera à la capture d'écran ci-dessous :

Capture d'écran du modèle d'en-tête rendu

Le modèle d'en-tête rendu

Ensuite, ouvrez le modèle footer.ejs et référencez notre fichier javascript, ainsi que le CDN bootstrap avec l'extrait de code ci-dessous :

<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
<script src="/js/app.js"></script>
</body>
</html>

HTML de pied de page

Les vues que nous avons créées dans les modèles header.ejs et footer.ejs seront incluses dans nos modèles books.ejs et book-detail.ejs .

Ensuite, nos livres auront un formulaire HTML modal pour ajouter de nouveaux livres à la base de données, et la liste de tous les livres de notre base de données. Dans le modèle de livres , nous aurons un formulaire de saisie pour envoyer une demande à notre backend pour enregistrer un livre dans la base de données. Nous inclurons également les modèles d'en- tête et de pied de page dans notre modèle de livres. Ouvrez le modèle books.ejs et ajoutez l'extrait de code ci-dessous.

<%- include('partials/header.ejs') %>

<div class="container-fluid mt-3">
 <h4>Book Store</h4>
 <ol class="list-group list-group-numbered">
   <% books.forEach(data=>{ %>

   <li
     class="list-group-item d-flex justify-content-between align-items-start"
   >
     <div class="ms-2 me-auto">
       <div class="fw-bold">
         <a href="book/<%= data.id %>"><%= data.title %></a>
       </div>
     </div>
     <span class="badge bg-primary rounded-pill"><%= data.quantity %></span>
   </li>
   <% }) %>
 </ol>
</div>
<!-- Modal -->
<div
 class="modal fade"
 id="staticBackdrop"
 data-bs-backdrop="static"
 data-bs-keyboard="false"
 tabindex="-1"
 aria-labelledby="staticBackdropLabel"
 aria-hidden="true"
>
 <div class="modal-dialog">
   <div class="modal-content">
     <div class="modal-header">
       <h5 class="modal-title" id="staticBackdropLabel">Add Book</h5>
       <button
         type="button"
         class="btn-close"
         data-bs-dismiss="modal"
         aria-label="Close"
       ></button>
     </div>
     <div class="modal-body">
       <form action="" id="createForm" method="post" action="/movie">
         <div class="mb-3">
           <label for="title" class="form-label">Title</label>
           <input
             required
             type="text"
             class="form-control"
             id="title"
             placeholder="Javascript Cookbook"
             name="title"
           />
         </div>
         <div class="mb-3">
           <label for="author" class="form-label">Author</label>
           <input
             required
             type="text"
             class="form-control"
             id="author"
             name="author"
             placeholder="Nelson Doe"
           />
         </div>
         <div class="mb-3">
           <label for="quantity" class="form-label">Quantity</label>
           <input
             required
             type="number"
             class="form-control"
             id="quantity"
             name="quantity"
             placeholder="40"
           />
         </div>
         <div class="mb-3">
           <label for="description" class="form-label">Description</label>
           <textarea
             class="form-control"
             id="description"
             name="description"
             required
             rows="3"
           ></textarea>
         </div>
         <button type="submit" class="btn btn-primary">Add</button>
       </form>
     </div>
   </div>
 </div>
</div>
<%- include('partials/footer.ejs') %>

Le books.ejsfichier

Le modèle de livres ressemblera à la capture d'écran ci-dessous :

Capture d'écran du modèle de livres

Le modèle de livres

Ensuite, notre modèle de détail de livre aura également un formulaire html modal pour mettre à jour un livre et un bouton de suppression pour supprimer un livre de la base de données . Nous inclurons également le modèle d'en- tête et de pied de page dans notre modèle de détail de livre . Ensuite, nous affichons dynamiquement les détails de chaque livre dans notre base de données.

Ajoutez l'extrait de code ci-dessous au modèle de détail du livre :

<%- include('partials/header.ejs') %>
<div class="container-fluid">
 <table class="table">
   <thead>
     <tr>
       <th scope="col">Item</th>
       <th scope="col">Details</th>
       <th scope="col">Action</th>
     </tr>
   </thead>
   <tbody>
     <tr>
       <td>Title</td>
       <td colspan="2"><%= book.title %></td>
     </tr>
     <tr>
       <td>Author</td>
       <td colspan="2"><%= book.author %></td>
     </tr>
     <tr>
       <td>Quantity</td>
       <td colspan="2"><%= book.quantity %></td>
     </tr>
     <tr>
       <td>Description</td>
       <td colspan="2"><%= book.description %></td>
     </tr>
     <tr>
       <td colspan="2"></td>
       <td>
         <button
           type="button"
           class="btn btn-primary"
           data-bs-toggle="modal"
           data-bs-target="#update"
         >
           Update
         </button>
         <button
           type="button"
           class="btn btn-danger"
           onclick="deleteBook('<%= book.id %>')"
         >
           Delete
         </button>
       </td>
     </tr>
   </tbody>
 </table>
 <div
   class="modal fade"
   id="update"
   data-bs-backdrop="static"
   data-bs-keyboard="false"
   tabindex="-1"
   aria-labelledby="staticBackdropLabel"
   aria-hidden="true"
 >
   <div class="modal-dialog">
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" id="update">Update Book</h5>
         <button
           type="button"
           class="btn-close"
           data-bs-dismiss="modal"
           aria-label="Close"
         ></button>
       </div>
       <div class="modal-body">
         <div
           class="alert alert-success alert-dismissible fade show"
           role="alert"
           hidden
         >
           <strong>Succcess!</strong> Record Updated!.
           <button
             type="button"
             class="btn-close"
             data-bs-dismiss="alert"
             aria-label="Close"
           ></button>
         </div>
         <form action="" id="form">
           <div class="mb-3">
             <label for="title" class="form-label">Title</label>
             <input
               type="text"
               class="form-control"
               id="title"
               name="title"
               value="<%= book.title %>"
             />
           </div>
           <div class="mb-3">
             <label for="author" class="form-label">Author</label>
             <input
               type="text"
               class="form-control"
               id="author"
               name="author"
               value="<%= book.author %>"
             />
           </div>
           <div class="mb-3">
             <label for="quantity" class="form-label">Quantity</label>
             <input
               type="text"
               class="form-control"
               id="quantity"
               name="quantity"
               value="<%= book.quantity %>"
             />
           </div>
           <div class="mb-3">
             <label for="description" class="form-label">Description</label>
             <textarea
               class="form-control"
               id="description"
               name="description"
               rows="3"
             >
<%= book.description %></textarea
             >
           </div>
           <input type="hidden" class="" name="id" value="<%= book.id %>" />
           <button type="submit" class="btn btn-primary">Add</button>
         </form>
       </div>
     </div>
   </div>
 </div>
</div>

<%- include('partials/footer.ejs') %>

Le modèle de détail du livre

Notre modèle de détail de livre ressemblera à la capture d'écran ci-dessous :

Capture d'écran du modèle de détail du livre

Capture d'écran du modèle de détail du livre

Maintenant, créons notre dossier de fichiers statiques pour enregistrer nos fichiers statiques (CSS, JS et image). Pour ce tutoriel, nous allons uniquement créer un dossier JS pour notre code javascript. Créez donc un dossier public dans le répertoire src et créez le fichier js/app.js dans le dossier public. Ajoutez ensuite l'extrait de code ci-dessous au fichier app.js.

function updateBook() {
 const createForm = document.getElementById('form');
 createForm.addEventListener('submit', async (e) => {
   e.preventDefault();
   const id = createForm['id'].value;
   await fetch(`http://localhost:3000/book/${id}`, {
     method: 'Put',
     headers: {
       'Content-Type': 'application/json',
     },
     body: JSON.stringify({
       title: createForm['title'].value,
       author: createForm['author'].value,
       quantity: createForm['quantity'].value,
       description: createForm['description'].value,
     }),
   })
     .then((data) => data.json())
     .then((res) => {
       if (res) {
         document.querySelector('.alert').removeAttribute('hidden');
         setTimeout(() => {
           window.location.reload();
         }, 3000);
       }
     });
 });
}

async function deleteBook(id) {
 console.log(id);
 await fetch(`http://localhost:3000/book/${id}`, {
   method: 'DELETE',
 }).then(() => (window.location.href = '/book'));
}

updateBook();

Le app.jsfichier

Dans l'extrait de code ci-dessus, nous avons créé deux fonctions pour mettre à jour et supprimer un livre de notre base de données. Dans ces fonctions, nous utiliserons l'API Fetch pour envoyer une requête à notre point de terminaison backend, que nous créerons plus tard.

Créer un contrôleur

Continuons avec nos contrôleurs. Générez un contrôleur Nest avec la commande ci-dessous :

nest generate controller /books/controller/book --flat

Générer un contrôleur NestJS

La commande ci-dessus créera un fichier controller/book.controller.ts dans le dossier du module livres avec du code passe-partout. Ouvrez le fichier controller/book.controller.ts  et importez les décorateurs Nest nécessaires à nos itinéraires, importez notre classe de modèle Book et la classe BookService , que nous créerons plus tard. Créez ensuite notre méthode constructeur BookController et liez notre classe BookService pour la rendre accessible dans d'autres méthodes.

import { Controller, Render, Get, Post, Put, Delete, Param, Body, Res } from '@nestjs/common';
import { Book } from '../model/book';
import { BookService } from '../service/book.service'

@Controller('book')
export class BookController {
   constructor(private readonly bookService: BookService) {}
...

LaBookController

Ensuite, nous allons créer notre route allBook qui écoutera la requête GET, pour rendre notre modèle de livre à l'aide du décorateur @Render avec notre liste de livres à partir de la base de données avec l'extrait de code ci-dessous :

…
@Get()
   @Render('book')
   async allBook(): Promise<object> {
       const books = await this.bookService.getAllBook();
       return { books, page: "book" }
   }
…

Le allBookparcours

Ensuite, nous allons créer notre route createBook . Nous utiliserons le décorateur @Body pour obtenir l'entrée de l'utilisateur à partir du corps de la requête, et les données du corps de la requête doivent correspondre au schéma du livre. Ensuite, nous redirigerons l'utilisateur vers la même page en utilisant la méthode de redirection @Res .

…
@Post()
   async createBook(@Body() book: Book, @Res() res): Promise<any> {
       await this.bookService.createBook(book);
       return res.redirect('/book')
   }
…

Le createBookparcours

Ensuite, nous allons créer les routes getBook , updateBook et deleteBook . Nous obtiendrons l'identifiant du livre à partir des paramètres de la requête à l'aide du décorateur @Param , et récupérerons , mettrons à jour ou supprimerons l'enregistrement à l'aide de l'identifiant. Sur la route updateBook , nous utiliserons également le décorateur @Body pour obtenir les nouveaux détails du livre à partir du corps de la requête.

…
@Get(':id')
   @Render('book-detail')
   async getBook(@Param() params): Promise<object> {
       const book = await this.bookService.getBook(params.id)
       return { book, page: "detail" }
   }
   @Put(':id')
   async updateBook(@Param() params, @Body() book: Book): Promise<Book> {
       return this.bookService.updateBook(params.id, book);
   }

   @Delete(':id')
   async deleteBook(@Param() params): Promise<Book> {
       return this.bookService.deleteBook(params.id)
   }
}

Gestion des fonctions de lecture, de mise à jour et de suppression

Créer un service

À ce stade, notre contrôleur est réglé. Configurons notre service d'application en exécutant la commande ci-dessous :

nest generate service /books/service/book --flat

Configuration du service d'application

La commande ci-dessus créera un fichier service/book.service.ts dans notre dossier de module livres. Créons maintenant nos fonctions de gestionnaire de route dans le fichier de service. Tout d'abord, nous allons importer les dépendances suivantes :

  • Injectable : Pour rendre notre BookService disponible dans d'autres fichiers de notre projet.
  • HttpException : pour créer des erreurs HTTP personnalisées
  • HttpStatus : pour envoyer un code d'état personnalisé
  • InjectRepository : pour injecter notre classe de modèle Book dans notre BookService.

Nous allons également importer notre classe de modèle de livre. Faites-le avec l'extrait de code ci-dessous :

import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Book } from '../model/book';

Importer des dépendances

Ensuite, nous ajouterons une méthode constructeur à notre classe BookService . Injectez notre modèle de livre avec l'extrait de code ci-dessous :

...
constructor(@InjectRepository(Book) private readonly bookRepository: Repository<Book>) { }
...

Ajout du constructeur

Ensuite, nous allons créer les fonctions de gestion getAllBooks et createBooks . Le gestionnaire getAllBooks interrogera la base de données pour tous les livres de la base de données et les renverra par ordre décroissant à notre contrôleur. Alors que le gestionnaire createBook créera un nouveau avec les données de l'objet livre en utilisant la méthode save .

…
async getAllBook(): Promise<Book[]> {
       return await this.bookRepository.find({ order: { createdAt: "DESC" } })
   }

   async createBook(book: Book): Promise<Book> {
       return await this.bookRepository.save(book)
   }
…

Création des fonctions de gestionnaire

Enfin, nous allons créer les gestionnaires getBook , updateBook et deleteBook avec l'extrait de code ci-dessous. Ces gestionnaires utiliseront l'identifiant du livre pour obtenir, mettre à jour ou supprimer un livre de notre base de données.

…
async getBook(id: string): Promise<Book> {
       return await this.bookRepository.findOne(id);
   }
   async updateBook(id: string, book: Book): Promise<Book> {
       const updateBook = await this.bookRepository.update(id, book)
       if (!updateBook) {
           throw new HttpException('Book id not found', HttpStatus.NOT_FOUND)
       }
       return await this.bookRepository.findOne(id);
   }
   async deleteBook(id: string): Promise<any> {
       if (await this.bookRepository.delete(id)) {
           return null
       }
       throw new HttpException('Book not found', HttpStatus.NOT_FOUND)
   }

Fonctions du gestionnaire final

Conclusion

Grâce à ce didacticiel, vous avez appris à structurer une application NestJS MVC avec une base de données Yugabyte en créant un projet de démonstration de librairie. Vous avez appris ce qu'est l'architecture MVC, comment configurer une application NestJS et comment configurer et créer une base de données Yugabyte. Pour en savoir plus, vous pouvez également en savoir plus sur NestJS et YugabyteDB . Pour un défi supplémentaire, vous pouvez étendre l'application en protégeant les routes de suppression et de mise à jour. Qu'allez-vous construire ensuite ?

Lien : https://arctype.com/blog/nestjs-mvc/

#nestjs #yugabytedb 

Thierry  Perret

Thierry Perret

1659497335

Comment Gérer Les Téléchargements De Fichiers Avec NestJS Et MySQL

Dans ce blog, nous allons vous apprendre à créer une fonctionnalité de téléchargement de fichiers à l'aide de NestJS et MySQL.

Introduction

De nombreux développeurs méprisent la gestion des téléchargements de fichiers. Cela peut être attribué à un manque de connaissances sur la meilleure approche à adopter ou à des difficultés à déterminer comment configurer leur application Nest.js pour gérer les téléchargements de fichiers. De nombreuses personnes peuvent souhaiter enregistrer leurs fichiers directement dans une base de données MySQL, ou enregistrer des noms d'image et enregistrer l'image sur un stockage sur disque : tout dépend de leurs préférences et des objectifs qu'ils souhaitent atteindre. Ce didacticiel vous apprendra à créer une fonctionnalité de téléchargement de fichiers à l'aide de Nestjs et MySQL.

Conditions préalables

Avant de commencer à suivre ce didacticiel, assurez-vous que votre système répond aux exigences suivantes :

  • Votre système exécute Node.js avec la version 14 ou ultérieure.
  • Votre système a une base de données MySQL installée.
  • Vous avez installé Postman .

Configurer NestJS

Une fois les exigences mentionnées ci-dessus remplies, procédez à l'installation de la CLI Nestjs et créez un nouveau projet en exécutant les commandes suivantes :

$ npm i -g @nestjs/cli
$ nest new file-upload

Ces commandes installeront la CLI Nestjs et créeront un nouveau projet Nestjs avec la structure de dossiers ci-dessous.

📦file-upload
┣ 📂src
┃ ┣ 📜app.controller.spec.ts
┃ ┣ 📜app.controller.ts
┃ ┣ 📜app.module.ts
┃ ┣ 📜app.service.ts
┃ ┣ 📜image.entity.ts
┃ ┗ 📜main.ts
┣ 📂test
┃ ┣ 📜app.e2e-spec.ts
┃ ┗ 📜jest-e2e.json
┣ 📜.eslintrc.js
┣ 📜.gitignore
┣ 📜.prettierrc
┣ 📜README.md
┣ 📜nest-cli.json
┣ 📜package-lock.json
┣ 📜package.json
┣ 📜tsconfig.build.json
┗ 📜tsconfig.json

Une fois le projet Nestjs créé, passez à l'étape suivante : installez les dépendances requises pour votre application en exécutant la commande suivante :

npm install --save @nestjs/typeorm typeorm mysql2

Dans la commande ci-dessus, vous avez installé les modules TypeORM et mysql2 : ils vont vous permettre de connecter votre application à une base de données MySQL et d'y effectuer des opérations.

Configurer la base de données MySQL

Une fois les dépendances ci-dessus installées, procédez à la configuration et à la connexion à votre base de données MySQL. Pour commencer, ajoutez le code dans le app.module.tsfichier avec l'extrait de code ci-dessous.

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { Image } from './image.entity';

@Module({
  imports: [TypeOrmModule.forRoot({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: '1234',
    database: 'blog',
    entities: [Image],
    synchronize: true,
  }),
  TypeOrmModule.forFeature([Image])
  ],
  ...
})
...

Dans l'extrait de code ci-dessus, nous avons importé TypeOrmModuledu module typeorm que nous avons installé précédemment. Nous avons utilisé la forRootméthode pour connecter l'application à une base de données MySQL et transmettre les informations d'identification de la base de données. Une autre chose à souligner ici est que entitiesles propriétés, qui nous ont permis de spécifier les entités dans notre module et qui nous donneront accès à l' Imageentité que vous allez créer prochainement : nous avons également la synchronizepropriété définie sur truepour migrer automatiquement la base de données.

Créer une entité image

Ensuite, créons l'entité Image que nous avons mentionnée précédemment. Pour commencer, créez un fichier image.entity.ts dans le répertoire src et ajoutez l'extrait de code ci-dessous.

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity()
export class Image {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @CreateDateColumn()
    dateCreated: Date;

    @UpdateDateColumn()
    dateUpdated: Date;
}

Dans l'extrait de code ci-dessus, nous avons importé les décorateurs dont nous avons besoin pour créer une entité. En utilisant ces décorateurs, nous avons défini les propriétés de l'entité. Nous avons le idchamp pour générer des identifiants aléatoires pour chaque enregistrement dans la base de données à l'aide du @PrimaryGeneratedColumn()décorateur, le namechamp pour stocker les noms des images qui seront téléchargées à l'aide du @Columndécorateur, les champs dateCreated et dateUpdate pour enregistrer la date à laquelle un enregistrement a été créé et mis à jour en utilisant @CreateDateColumn()et @UpdateDateColumn().

Création du service de téléchargement

Une fois l'entité Image créée, créons un service pour effectuer les opérations CRUD afin de gérer les téléchargements de fichiers. Dans le app.service.tsfichier, ajoutez l'extrait de code ci-dessous.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Image } from './image.entity';

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(Image)
    private readonly imageRepository: Repository<Image>,
  ) {}

  async getImages(): Promise<Image[]> {
    return this.imageRepository.find();
  }

  async createImage(image: Image): Promise<Image> {
    return this.imageRepository.save(image);
  }

  async getImage(id: number): Promise<Image> {
    return this.imageRepository.findOneBy({ id });
  }

  async deleteImage(id: number): Promise<void> {
    await this.imageRepository.delete(id);
  }
}

Dans l'extrait de code ci-dessus, nous avons importé le injectRepositorydécorateur à injecter imageRepositorydans AppServiceet Repositoryqui vous fournit les méthodes nécessaires pour effectuer certaines opérations sur votre base de données. Donc, pour le createImageservice d'image, nous enregistrons le nom de l'image qui a été téléchargée et qui sera transmise au contrôleur.

Création du contrôleur de téléchargement

Créons maintenant les contrôleurs pour utiliser les services. Dans le app.controller.tsfichier et ajoutez l'extrait de code ci-dessous.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Image } from './image.entity';

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(Image)
    private readonly imageRepository: Repository<Image>,
  ) {}

  async getImages(): Promise<Image[]> {
    return this.imageRepository.find();
  }

  async createImage(image: Image): Promise<Image> {
    return this.imageRepository.save(image);
  }

  async getImage(id: number): Promise<Image> {
    return this.imageRepository.findOneBy({ id });
  }

  async deleteImage(id: number): Promise<void> {
    await this.imageRepository.delete(id);
  }
}

Dans l'extrait de code ci-dessus, nous avons importé quelques décorateurs comme FileInterceptor, UploadedFileet UseInterceptors. L' FileInterceptor()intercepteur du gestionnaire de route extrait le fichier de la requête à l'aide du @UploadedFile()décorateur. Le FileInterceptor()décorateur est exporté depuis le @nestjs/platform-expresspackage. Le @UploadedFile()décorateur est exporté depuis @nestjs/common. Le FileInterceptor()décorateur prend deux arguments, fieldNamequi est la chaîne qui fournit le nom du champ du formulaire HTML qui contient un fichier, et optionsqui est un objet facultatif de type MulterOptions. C'est le même objet utilisé par le constructeur multer.

En ce qui concerne la createImagefonction, nous avons utilisé les décorateurs susmentionnés pour gérer le téléchargement du fichier en FileInterceptor()passant le nom du champ pour l'image et nous avons modifié la FileInterceptor()fonction pour télécharger l'image sur le disque en spécifiant la storagepropriété à l'aide de la diskStoragefonction disponible dans multer. Ensuite, nous avons spécifié l'emplacement des images et généré des noms aléatoires pour les images. De plus, nous avons ajouté une filterpropriété pour restreindre le téléchargement de certains formats d'image. Maintenant, nous obtenons le fichier extrait à l'aide du @UploadedFile()décorateur et obtenons le nom et l'enregistrons dans la base de données. De cette façon, nous pouvons utiliser le nom de chaque image pour obtenir l'image de l'emplacement de stockage.

Pour que le code ci-dessus fonctionne, vous devez installer multer en exécutant la commande ci-dessous dans votre terminal :

npm i -D @types/multer


Ensuite, vous devez enregistrer le module multer dans le tableau des importations dans le app.module.tsfichier :

...
import { MulterModule } from '@nestjs/platform-express';


@Module({
  ...
  MulterModule.register({
    dest: './files',
  }),],
  ...

La configuration ci-dessus indique à multer de gérer le téléchargement du fichier et l'emplacement vers lequel télécharger le fichier. Enfin, nous devons créer un filesdossier dans le srcrépertoire pour stocker les fichiers.

Servir des fichiers

Pour réellement servir les images téléchargées sur votre application à l'utilisateur, vous devez installer le serve-staticmodule en exécutant la commande ci-dessous.

npm install --save @nestjs/serve-static

Ensuite, enregistrez le ServeStaticModuledans le tableau des importations dans le app.module.tsfichier avec l'extrait de code ci-dessous.

...
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

@Module({
  ...
  ServeStaticModule.forRoot({
    rootPath: join(__dirname, '..', 'files')
  }),],
  ...


Dans l'extrait de code ci-dessus, vous avez spécifié l'emplacement où se trouvent les fichiers et à partir duquel ils peuvent être servis.

Tester l'API

Ouvrez maintenant Postman et testez l'application en envoyant une requête POST au point de terminaison localhost:4000/imageset transmettez la charge utile dans le corps de la requête en tant que données de formulaire.

Notre dossier

Si vous regardez maintenant le dossier des fichiers, vous devriez voir le fichier que vous avez téléchargé. N'hésitez pas à aller de l'avant : testez et jouez également avec d'autres itinéraires.

Conclusion

Grâce à ce didacticiel, vous avez appris à gérer le téléchargement de fichiers avec NestJS et MySQL. Vous avez appris à vous connecter à une base de données MySQL à l'aide de TypeORM et vous avez également créé une entité et téléchargé des images dans l'application Nestjs.

Lien : https://arctype.com/blog/nestjs-mysql-tutorial-fileuploads/

#nestjs #node #mysql #typeorm #database 

Thierry  Perret

Thierry Perret

1657313040

Comment Créer Une Application De Chat En Temps Réel Avec Nestjs

Qu'est-ce que Nest JS ?

NestJS est un framework Node.js permettant de créer des applications côté serveur rapides, testables, évolutives et faiblement couplées qui utilisent TypeScript. Il tire parti de puissants frameworks de serveur HTTP tels que Express ou Fastify. Nest ajoute une couche d'abstraction aux frameworks Node.js et expose ses API aux développeurs. Il prend en charge les systèmes de gestion de bases de données tels que PostgreSQL et MySQL. NestJS propose également des injections de dépendances Websockets et APIGetaways.

Qu'est-ce qu'un Websocket ?

Un WebSocket est un protocole de communication informatique qui fournit des canaux de communication en duplex intégral sur une seule connexion TCP. L'IETF a normalisé le protocole WebSocket en tant que RFC 6455 en 2011. La spécification actuelle est connue sous le nom de HTML Living Standard. Contrairement à HTTP/HTTPS, les Websocket sont des protocoles avec état, ce qui signifie que la connexion établie entre le serveur et le client sera active à moins qu'elle ne soit interrompue par le serveur ou le client ; une fois qu'une connexion WebSocket est fermée par une extrémité, elle s'étend jusqu'à l'autre extrémité.

Conditions préalables

Ce tutoriel est une démonstration pratique. Pour suivre, assurez-vous d'avoir installé les éléments suivants :

Configuration du projet

Avant de plonger dans le codage, configurons notre projet NestJS et notre structure de projet. Nous allons commencer par créer le dossier du projet. Ensuite, ouvrez votre terminal et exécutez la commande suivante :

mkdir chatapp && cd chatapp

Création du dossier du projet

Installez ensuite la CLI NestJS avec la commande ci-dessous :

npm i -g @nestjs/cli

 

Une fois l'installation terminée, exécutez la commande ci-dessous pour échafauder un projet NestJS.

nest new chat

 

Choisissez votre gestionnaire de packages npm préféré. Pour ce didacticiel, nous utiliserons npm et attendrons que les packages nécessaires soient installés. Une fois l'installation terminée, installez WebSocket et Socket.io avec la commande ci-dessous :

npm i --save @nestjs/websockets @nestjs/platform-socket.io

 

Ensuite, créez une application passerelle avec la commande ci-dessous :

nest g gateway app

 

Démarrons maintenant notre serveur en exécutant la commande ci-dessous :

npm run start:dev

Mise en place d'une base de données Postgres

Nous pouvons maintenant configurer notre base de données Postgres pour stocker nos enregistrements d'utilisateurs avec notre configuration de serveur. Tout d'abord, nous allons utiliser TypeORM (Object Relational Mapper) pour connecter notre base de données à notre application. Pour commencer, nous devrons créer une base de données en suivant les étapes suivantes. Tout d'abord, passez au compte d'utilisateur Postgres du système.

sudo su - postgres

Ensuite, créez un nouveau compte utilisateur avec la commande ci-dessous.

createuser --interactive

Ensuite, créez une nouvelle base de données. Vous pouvez le faire avec la commande suivante :

createdb chat

Maintenant, nous allons nous connecter à la base de données que nous venons de créer. Tout d'abord, ouvrez le fichier app.module.ts et ajoutez l'extrait de code suivant ci-dessous dans le tableau d' importations[] :

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { Chat } from './chat.entity';
imports: [
   TypeOrmModule.forRoot({
     type: 'postgres',
     host: 'localhost',
     username: '<USERNAME>',
     password: '<PASSWORD>',
     database: 'chat',
     entities: [Chat],
     synchronize: true,
   }),
   TypeOrmModule.forFeature([Chat]),
 ],
...

 

Dans l'extrait de code ci-dessus, nous avons connecté notre application à une base de données PostgresSQL à l'aide de la méthode TypeOrmModule forRoot et transmis nos informations d'identification de base de données. Remplacez <USERNAME> et <PASSWORD> par l'utilisateur et le mot de passe que vous avez créés pour la base de données de chat .

Création de notre entité de chat

Maintenant que nous avons connecté l'application à votre base de données, créez une entité de discussion pour enregistrer les messages de l'utilisateur. Pour ce faire, créez un fichier chat.entity.ts dans le dossier src et ajoutez l'extrait de code ci-dessous :

import {
 Entity,
 Column,
 PrimaryGeneratedColumn,
 CreateDateColumn,
} from 'typeorm';
 
@Entity()
export class Chat {
 @PrimaryGeneratedColumn('uuid')
 id: number;
 
 @Column()
 email: string;
 
 @Column({ unique: true })
 text: string;
 
 @CreateDateColumn()
 createdAt: Date;
}

 

Dans l'extrait de code ci-dessus, nous avons créé les colonnes de nos chats à l'aide des décorateurs Entity , Column , CreatedDateColumn et PrimaryGenerateColumn fournis par TypeOrm.

Mise en place d'une prise web

Configurons une connexion socket Web dans notre serveur pour envoyer des messages en temps réel. Tout d'abord, nous allons importer le module requis dont nous avons besoin avec un extrait de code ci-dessous.

import {
 SubscribeMessage,
 WebSocketGateway,
 OnGatewayInit,
 WebSocketServer,
 OnGatewayConnection,
 OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { AppService } from './app.service';
import { Chat } from './chat.entity';

 

Dans l'extrait de code ci-dessus, nous avons importé SubscribeMessage() pour écouter les événements du client, WebSocketGateway() , qui donnera accès à socket.io ; nous avons également importé les instances OnGatewayInit , OnGatewayConnection et OnGatewayDisconnect . Cette instance WebSocket vous permet de connaître l'état de votre application. Par exemple, nous pouvons demander à notre serveur de faire des choses lorsqu'un serveur rejoint ou se déconnecte du chat. Ensuite, nous avons importé l' entité Chat et l' AppService qui expose les méthodes dont nous avons besoin pour enregistrer les messages de notre utilisateur.

@WebSocketGateway({
 cors: {
   origin: '*',
 },
})

 

Pour permettre à notre client de communiquer avec le serveur, nous activons CORS en initialisant WebSocketGateway.

export class AppGateway
 implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
 constructor(private appService: AppService) {}
 
 @WebSocketServer() server: Server;
 
 @SubscribeMessage('sendMessage')
 async handleSendMessage(client: Socket, payload: Chat): Promise<void> {
   await this.appService.createMessage(payload);
   this.server.emit('recMessage', payload);
 }
 
 afterInit(server: Server) {
   console.log(server);
   //Do stuffs
 }
 
 handleDisconnect(client: Socket) {
   console.log(`Disconnected: ${client.id}`);
   //Do stuffs
 }
 
 handleConnection(client: Socket, ...args: any[]) {
   console.log(`Connected ${client.id}`);
   //Do stuffs
 }
}

 

Ensuite, dans notre classe AppGateWay , nous avons implémenté les instances WebSocket que nous avons importées ci-dessus. Nous avons créé une méthode de constructeur et lié notre AppService pour avoir accès à ses méthodes. Nous avons créé une instance de serveur à partir des décorateurs WebSocketServer .

Ensuite, nous créons un handleSendMessage en utilisant l' instance @SubscribeMessage() et une méthode handleMessage() pour envoyer des données à notre côté client.

Lorsqu'un message est envoyé à cette fonction depuis le client, nous l'enregistrons dans notre base de données et renvoyons le message à tous les utilisateurs connectés côté client. Nous avons également de nombreuses autres méthodes que vous pouvez expérimenter, comme afterInit, qui se déclenche après la connexion d'un client, handleDisconnect, qui se déclenche lorsqu'un utilisateur se déconnecte. La méthode handleConnection démarre lorsqu'un utilisateur rejoint la connexion.

Créer un contrôleur/service

Créons maintenant notre service et notre contrôleur pour enregistrer le chat et rendre notre page statique. Ouvrez le fichier app.service.ts et mettez à jour le contenu avec l'extrait de code ci-dessous :

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Chat } from './chat.entity';
 
@Injectable()
export class AppService {
 constructor(
   @InjectRepository(Chat) private chatRepository: Repository<Chat>,
 ) {}
 async createMessage(chat: Chat): Promise<Chat> {
   return await this.chatRepository.save(chat);
 }
 
 async getMessages(): Promise<Chat[]> {
   return await this.chatRepository.find();
 }
}

 

Mettez ensuite à jour le fichier app.controller.ts avec l'extrait de code ci-dessous :

import { Controller, Render, Get, Res } from '@nestjs/common';
import { AppService } from './app.service';
import { Chat } from './chat.entity';
 
@Controller()
export class AppController {
 constructor(private readonly appService: AppService) {}
 
 @Get('/chat')
 @Render('index')
 Home() {
   return;
 }
 
 @Get('/api/chat')
 async Chat(@Res() res) {
   const messages = await this.appService.getMessages();
   res.json(messages);
 }
}

 

Dans l'extrait de code ci-dessus, nous avons créé deux itinéraires pour afficher notre page statique et les messages de l'utilisateur.

Servir notre page statique

Configurons maintenant l'application pour afficher le fichier statique et nos pages. Pour ce faire, nous allons implémenter le rendu côté serveur. Tout d'abord, dans votre fichier main.ts , configurez l'application en fichiers serveur statiques avec la commande ci-dessous :

async function bootstrap() {
 ...
 app.useStaticAssets(join(__dirname, '..', 'static'));
 app.setBaseViewsDir(join(__dirname, '..', 'views'));
 app.setViewEngine('ejs');
 ...
}

 

Ensuite, créez un dossier statique et un dossier de vues dans votre répertoire src . Dans le dossier des vues, créez un fichier index.ejs et ajoutez l'extrait de code ci-dessous :

<!DOCTYPE html>
<html lang="en">
 
<head>
 <!-- Required meta tags -->
 <meta charset="utf-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 
 <!-- Bootstrap CSS -->
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
   integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" />
 
 <title>Let Chat</title>
</head>
 
<body>
 <nav class="navbar navbar-light bg-light">
   <div class="container-fluid">
     <a class="navbar-brand">Lets Chat</a>
   </div>
 </nav>
 <div class="container">
   <div class="mb-3 mt-3">
     <ul style="list-style: none" id="data-container"></ul>
   </div>
   <div class="mb-3 mt-4">
     <input class="form-control" id="email" rows="3" placeholder="Your Email" />
   </div>
   <div class="mb-3 mt-4">
     <input class="form-control" id="exampleFormControlTextarea1" rows="3" placeholder="Say something..." />
   </div>
 </div>
 <script src="https://cdn.socket.io/4.3.2/socket.io.min.js"
   integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQNs"
   crossorigin="anonymous"></script>
 <script src="app.js"></script>
 <!-- Option 1: Bootstrap Bundle with Popper -->
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
   integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
   crossorigin="anonymous"></script>
</body>
</html>

 

Pour accélérer les choses dans nos modèles, nous avons utilisé Bootstrap pour ajouter des styles. Ensuite, nous avons ajouté deux champs de saisie et une liste non ordonnée pour afficher les messages de l'utilisateur. Nous avons également inclus notre fichier app.js que nous créerons plus tard dans cette section et un lien vers le client socket.io .

Créez maintenant un fichier app.js et ajoutez l'extrait de code ci-dessous :

const socket = io('http://localhost:3002');
const msgBox = document.getElementById('exampleFormControlTextarea1');
const msgCont = document.getElementById('data-container');
const email = document.getElementById('email');
 
//get old messages from the server
const messages = [];
function getMessages() {
 fetch('http://localhost:3002/api/chat')
   .then((response) => response.json())
   .then((data) => {
     loadDate(data);
     data.forEach((el) => {
       messages.push(el);
     });
   })
   .catch((err) => console.error(err));
}
getMessages();
 
//When a user press the enter key,send message.
msgBox.addEventListener('keydown', (e) => {
 if (e.keyCode === 13) {
   sendMessage({ email: email.value, text: e.target.value });
   e.target.value = '';
 }
});
 
//Display messages to the users
function loadDate(data) {
 let messages = '';
 data.map((message) => {
   messages += ` <li class="bg-primary p-2 rounded mb-2 text-light">
      <span class="fw-bolder">${message.email}</span>
      ${message.text}
    </li>`;
 });
 msgCont.innerHTML = messages;
}
 
//socket.io
//emit sendMessage event to send message
function sendMessage(message) {
 socket.emit('sendMessage', message);
}
//Listen to recMessage event to get the messages sent by users
socket.on('recMessage', (message) => {
 messages.push(message);
 loadDate(messages);
})

 

Dans l'extrait de code ci-dessus, nous avons créé une instance socket.io et écouté les événements sur notre serveur pour envoyer et recevoir un message du serveur. Nous voulons que les anciens chats soient disponibles lorsqu'un utilisateur rejoint le chat par défaut. Notre application devrait ressembler à la capture d'écran ci-dessous :

Affichage des données utilisateur avec Arctype

Nous avons maintenant créé avec succès notre application de chat. Examinons d'abord les données des utilisateurs avec Arctype. Pour commencer, lancez Arctype, cliquez sur l'onglet MySQL et entrez les informations d'identification MySQL suivantes, comme indiqué dans la capture d'écran ci-dessous :

Ensuite, cliquez sur le tableau de discussion pour afficher les messages de discussion de l'utilisateur, comme indiqué dans la capture d'écran ci-dessous :

Tester l'application

Ouvrez maintenant l'application dans deux onglets ou fenêtres différents et essayez d'envoyer un message avec une adresse e-mail différente, comme indiqué dans la capture d'écran ci-dessous :

De plus, lorsque vous regardez votre console, vous voyez des journaux lorsqu'un utilisateur rejoint et se déconnecte du serveur qui est géré par les méthodes handleDisconnect et handleConnection .

Conclusion

Tout au long de ce didacticiel, nous avons exploré comment créer une application de chat en temps réel avec Nestjs et PostgreSQL. Nous avons commencé par une brève introduction à Nestjs et WebSockets. Ensuite, nous avons créé une application de démonstration pour démontrer la mise en œuvre. J'espère que vous avez les connaissances que vous recherchez. Peut-être pouvez-vous en savoir plus sur l'implémentation de WebSocket à partir de la documentation Nestjs et ajouter l'extension de l'application.++

#nestjs #noeud #postgres 

Moriah  Fisher

Moriah Fisher

1595504520

All You Need To Know About Comments in Ruby

It is well known that the ruby comments are used to leave notes or micro-docs within the code and is ignored by the Ruby Interpreter. But can we do other things with comments?

There are several other functions that comments can offer. In this article, we shall dive into some of the use cases of comments that can have certain desirable impacts on the Ruby interpreter. Each use case has been explained in detail making it easier for even beginners to understand.


Shebang Comments

Shebang is a “comment” that indicates the interpreter directive for the interpreter to be used when the file is run as an executable in *nix operating systems. This specific comment is not unique to Ruby. Many scripting languages make use of the shebang to make scripts executable in a simple manner.

in addition to specifying the interpreter directive, you can also specify the flags like — jit or -w which will automatically be passed on to the interpreter when the executable is run. Let’s take a look at how this can be done:

#magic-comments #ruby #frozen-string-literals #comment #encoding