AGENT SKILL

volontariapp-grpc-microservice

Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers et tests.

License 许可
MIT
所属仓库
Volontariapp/meta
最近更新
2026-11-10
Stars
★ 0
Forks
0

01 前言 Front Matter

name
volontariapp-grpc-microservice
tags
[ "microservice", "grpc", "nestjs", "ddd", "cqrs", "playbook" ]
type
Playbook
paths
[ "ms-user/src/**", "ms-event/src/**", "ms-post/src/**", "ms-social/src/**", "scripts/cli/commands/sync-migrations.mjs" ]
title
Implémenter ou Modifier un RPC gRPC
status
stable
sources
[ { "id": "agent-md", "title": "AGENT.md (structure de module, DTO, contrôleurs, tests)", "resource": "AGENT.md" }, { "id": "main-user", "title": "Bootstrap hybride (gRPC + HTTP /health)", "resource": "ms-user/src/main.ts" }, { "id": "app-event", "title": "AppModule ms-event (GrpcValidationPipe, APP_GUARD)", "resource": "ms-event/src/app.module.ts" }, { "id": "base-cmd", "title": "BaseCommandController.withFallback", "resource": "ms-event/src/modules/event/controllers/commands/base.command.controller.ts" }, { "id": "sync-mig", "title": "sync-migrations.mjs", "resource": "scripts/cli/commands/sync-migrations.mjs" }, { "id": "validation", "title": "GrpcValidationPipe", "resource": "npm-packages/packages/validation-nest" } ]
verified
[ { "at": "2026-10-30T12:29:16Z", "by": "claude-code/agent", "digest": "37f4a83af8a6d3d1" }, { "at": "2026-11-03T20:31:03Z", "by": "claude-code/agent", "digest": "79cb461c249d3c2e" }, { "at": "2026-11-04T17:53:23Z", "by": "claude-code/agent", "digest": "a4e563bea3a17189" }, { "at": "2026-11-06T14:04:01Z", "by": "claude-code/agent", "digest": "90966abbcd8b04bd" }, { "at": "2026-11-08T12:02:22Z", "by": "claude-code/agent", "digest": "e1f929df232e29f5" } ]
generated
{ "at": "2026-10-06T08:54:04Z", "by": "claude-code/claude-opus-5-5" }
mesh_keys
[ "ms-user", "ms-event", "ms-post", "ms-social", "basecommandcontroller", "grpcvalidationpipe", "grpcinternalguard", "transformer" ]
description
Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers et tests.

02 资源文件 9

03 SKILL.md 正文

Playbook : Implémenter ou Modifier un RPC gRPC dans un Microservice

Dans l'architecture Volontariapp, un microservice ms-* est une coquille NestJS purement dédiée à l'exposition gRPC (port 3000) et HTTP /health[^main-user] : il valide les messages entrants, applique la garde du token interne de sécurité, et délègue l'exécution métier au package de domaine partagé @volontariapp/domain-<domaine>.

La documentation d'architecture détaillée et les conventions structurelles sont disponibles dans Architecture Microservice et AGENT.md^agent-md.


Trigger

Ce playbook doit être exécuté dès qu'une tâche nécessite :

  • L'ajout d'une nouvelle méthode RPC rpc Method(Command) returns (Response) dans un microservice existant.
  • La modification d'un endpoint gRPC existant (nouveau champ reçu ou retourné, ajustement de validation).
  • L'ajout d'un appel gRPC sortant vers un autre microservice du mesh.

Étape 1 : Vérification des Contrats Amont & Règle du STOP

Avant d'écrire la moindre ligne de code dans ms-* :

  1. Localiser la méthode dans le graphe gRPC :
    analyze_grpc({ "target": "<MethodName>" })
    
  2. Contrôler la présence du contrat dans @volontariapp/contracts-nest :
    • L'interface de commande/requête, l'interface de réponse et les constantes de service (GRPC_SERVICES, <SERVICE>_METHODS) doivent être exportées par le package publié.

[!CAUTION]

🛑 RÈGLE CRITIQUE DU STOP IMMÉDIAT

Si le RPC, le message ou le champ n'existe pas encore dans les contrats TypeScript :

  1. INTERDICTION FORMELLE de créer des interfaces locales temporaires ou d'injecter des types arbitraires.
  2. Dérouler d'abord le playbook volontariapp-proto-contract-evolution dans proto-registry/.
  3. Attendre la publication de @volontariapp/contracts-nest par la CI.
  4. Mettre à jour les dépendances du microservice : yarn up @volontariapp/contracts-nest.

Étape 2 : Création des DTOs de Requête (Validation)

Emplacement : src/modules/<domaine>/dto/request/{command,query}/.

Chaque DTO de commande ou requête doit impérativement implémenter l'interface TypeScript du contrat gRPC et définir des règles class-validator strictes :

import { IsString, IsNotEmpty, IsUUID, IsEnum, IsOptional } from 'class-validator';
import type { CreateEventCommand, EventType } from '@volontariapp/contracts-nest';

export class CreateEventCommandDTO implements CreateEventCommand {
  @IsString()
  @IsNotEmpty()
  title!: string;

  @IsEnum(EventType)
  type!: EventType;

  @IsUUID()
  @IsOptional()
  coverFileId?: string;
}
  • Règles de validation automatique : GrpcValidationPipe est configuré en APP_PIPE global avec enumMaps[^app-event][^validation]. Il convertit les chaînes vides en undefined, les entiers Int64 en number, et les noms d'enums sous forme de string en valeurs numériques conformes Protobuf.
  • Défense en profondeur : Utiliser l'assertion d'assignation définie ! pour les champs obligatoires du contrat (aucun any).
  • Pagination des requêtes de liste : ne jamais déclarer pagination avec le seul @IsOptional() (aucune valeur n'est alors validée). Motif commun : @IsOptional() @ValidateNested() @Type(() => PaginationRequestDTO) pagination: PaginationRequestDTO | undefined;, avec PaginationRequestDTO dans src/common/dto/pagination.dto.ts (page : @IsInt @Min(1), limit : @IsInt @Min(1) @Max(MAX_PAGINATION_LIMIT)). Le plafond MAX_PAGINATION_LIMIT dépend du service (relevé le 2026-11-04) : 100 dans ms-post (#69) et ms-event (#83), qui l'ont abaissé de 1000 à 100, et 1000 dans ms-user et ms-social (la gateway écrête à 100 sur ses routes, et lit les ensembles d'ids par pages de 100 avec fetchAllPages depuis api-gateway#90, au lieu d'un appel limit: 1000). Ne pas supposer 1000 : lire la constante du service. Pagination absente : défaut page 1 / limit 10 dans le contrôleur (DEFAULT_PAGE, DEFAULT_LIMIT), jamais une lecture de toute la table. Listes d'ids en entrée : @IsArray @ArrayMaxSize(100) (1000 pour ids/excludedIds de SearchEvents, que la gateway remplit avec les événements recommandés).
  • Lectures par lot : pas de Promise.all(ids.map(findById)). Utiliser le findByIds du domaine (une requête, 100 ids maximum, tronqué en silence au-delà donc découper par lots de 100) puis remettre le résultat dans l'ordre des ids d'entrée, car cet ordre vient de ms-social pour les listes de profils et d'événements. Exemples : ms-user/src/modules/user/helpers/find-active-users-in-order.ts, ms-event/src/modules/event/utils/events-by-ids.util.ts. Côté domaine, filtrer en SQL avant limit/offset (UserService.findPage) et toujours trier avec un départage par id (offset instable sinon).

Étape 3 : Câblage Métier dans @volontariapp/domain-*

Les microservices ms-* n'ont aucune logique métier propre :

  1. Les entités TypeORM, modèles, règles de gestion et repositories résident dans le package partagé @volontariapp/domain-<domaine>.
  2. Si la nouvelle méthode RPC requiert une nouvelle règle de calcul, une transaction ou une transition d'état :
    • Modifier d'abord npm-packages/packages/domain-<domaine>/.
    • Appliquer le playbook volontariapp-shared-npm-package-change avec bump changeset et publication snapshot.
    • Lancer yarn up @volontariapp/domain-<domaine> dans le microservice consommateur.
  3. Injecter le service de domaine comme provider dans <Domaine>Module.
  4. Cas d'une lecture qui agrège plusieurs tables avec un catalogue partagé (progression des badges) : un service local au microservice, src/modules/<domaine>/services/<nom>.service.ts, qui injecte les repositories TypeORM par @InjectRepository (le DatabaseModule les expose en global) et délègue le calcul à une fonction pure de @volontariapp/shared. Précédent : BadgeProgressService de ms-user (computeBadgeProgress). Les mocks des repositories vont dans un fichier *.mock.ts séparé.

Étape 4 : Implémentation du Controller gRPC (CQRS)

Les contrôleurs sont strictement scindés selon le pattern CQRS :

  • Écriture : src/modules/<domaine>/controllers/commands/<entite>.command.controller.ts
  • Lecture : src/modules/<domaine>/controllers/queries/<entite>.query.controller.ts

Contrôleur de Commande (avec Fallback éventuel) :

import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { Logger } from '@volontariapp/logger';
import { GRPC_SERVICES, EVENT_COMMAND_METHODS } from '@volontariapp/contracts-nest';
import { CurrentUser, type AuthUser } from '@volontariapp/auth';
import { BaseCommandController } from './base.command.controller.js';
import { CreateEventCommandDTO } from '../../dto/request/command/create-event.command.dto.js';
import { CreateEventResponseDTO } from '../../dto/response/create-event.response.dto.js';
import { EventResponseMapper } from '../../mappers/event-response.mapper.js';
import { EventDomainService } from '@volontariapp/domain-event';

@Controller()
export class EventCommandController extends BaseCommandController {
  private readonly logger = new Logger({ context: EventCommandController.name });

  constructor(private readonly eventService: EventDomainService) {
    super();
  }

  @GrpcMethod(GRPC_SERVICES.EVENT_COMMAND_SERVICE, EVENT_COMMAND_METHODS.CREATE_EVENT)
  async createEvent(
    data: CreateEventCommandDTO,
    @CurrentUser() user: AuthUser,
  ): Promise<CreateEventResponseDTO> {
    this.logger.info(`gRPC CreateEvent: ${data.title} par user ${user.id}`);

    // Exécution avec fallback pour absorption des pannes de dépendances
    const entity = await this.withFallback(
      JobMessagingType.FALLBACK_CREATE_EVENT,
      user.id,
      data,
      async () => this.eventService.createEvent(user.id, data),
    );

    return EventResponseMapper.toCreateResponseDTO(entity);
  }
}
  • Identification de l'acteur : L'identité provient exclusivement du décorateur @CurrentUser() injecté par GrpcInternalGuard depuis l'INTERNAL_TOKEN. Ne jamais lire l'acteur dans le corps du message DTO[^app-event].
  • Gestion withFallback : Héritée de BaseCommandController[^base-cmd], cette méthode ne déclenche aucun job en cas d'erreur client (400, 404, 409, et aussi 403 dans ms-event ; vérifier les autres ms-* avant de supposer le même comportement). Sur panne système ou dépendance injoignable, elle insère un job dans jobs_outbox et renvoie la réponse typée FALLBACK_ACTIVATED (traduite en HTTP 206 par l'API Gateway).

Pièges du contrôleur de lecture :

  • Constante de méthode manquante. BADGE_QUERY_METHODS, USER_QUERY_METHODS... dans contracts-nest/src/<d>/grpc-helpers.ts sont écrites à la main : un nouveau RPC n'y figure pas automatiquement. Utiliser le littéral du .proto : @GrpcMethod(BADGE_SERVICE_NAME, 'GetMyBadgeProgress') (même approche que 'GetUsersByIds'), ou ajouter la constante dans npm-packages si plusieurs consommateurs la veulent.
  • Route « me ». Aucun identifiant dans le message : @UseGuards(GrpcInternalGuard) et @CurrentUser() user: AuthUser, comme getUser. Un userId fourni par le client ne doit jamais être lu.

Étape 5 : Mappers & DTOs de Réponse

  1. Définir le DTO de réponse sous src/modules/<domaine>/dto/response/<entite>.response.dto.ts en implémentant l'interface du contrat (ex. CreateEventResponse).
  2. Implémenter la transformation dans src/modules/<domaine>/mappers/<domaine>-response.mapper.ts :
    • Conversion des dates en chaînes ISO 8601 UTC ou timestamps conformes au .proto.
    • Transformation des enums numériques et normalisation des relations.
    • Ne jamais exposer directement l'entité TypeORM brute sur le bus gRPC.

Étape 6 : Stratégie de Tests Obligatoires

Chaque nouveau RPC ou modification doit être couvert par des tests unitaires stricts :

  1. Isolation des Mocks & Factories :
    • Mocks obligatoires dans un fichier dédié : src/modules/<d>/__tests__/<nom>.mock.ts.
    • Données de test générées par factories dédiées : src/modules/<d>/__tests__/<nom>.factory.ts.
    • Interdiction formelle de déclarer des objets as unknown as Type ou any inline dans les tests.
  2. Utilisation rigoureuse de Spies :
    • Toujours espionner les appels de dépendances avec jest.spyOn().
    • Nettoyage systématique dans afterEach(() => { jest.restoreAllMocks(); }).
  3. Cas de test obligatoires :
    • Cas nominal : renvoi du DTO de réponse avec mapping exact.
    • Cas d'erreur métier : validation des exceptions typées (@volontariapp/errors ou @volontariapp/errors-nest).
    • Cas de refus d'authentification : rejet si x-internal-token est manquant.

Checklist d'Assurance Qualité & Critères d'Arrêt

Avant de déclarer la tâche terminée :

  • [ ] Le RPC et ses DTOs proviennent exclusivement de @volontariapp/contracts-nest (aucun type inventé).
  • [ ] Aucun bout de logique métier n'a été inséré dans src/ (délégué à domain-<domaine>).
  • [ ] Aucun cast as unknown as Type ni any n'est présent.
  • [ ] L'acteur est extrait via @CurrentUser(), jamais depuis le body.
  • [ ] Les mappers transforment proprement les entités en DTOs gRPC.
  • [ ] yarn test passe avec succès sur le microservice.
  • [ ] yarn tsc et yarn lint ne produisent aucune erreur.
  • [ ] En cas de migration de base de données associée : exécuter yarn meta sync-migrations si commune[^sync-mig].

[^main-user]: Bootstrap hybride (gRPC + HTTP /health) [^app-event]: AppModule ms-event (GrpcValidationPipe, APP_GUARD) [^validation]: GrpcValidationPipe [^base-cmd]: BaseCommandController.withFallback [^sync-mig]: scripts/cli/commands/sync-migrations.mjs

04 同仓库更多 Skills

Volontariapp
Volontariapp/meta

volontariapp-user-account-deletion

Suppression de compte en deux temps (PENDING_DELETION): requestDeletionWithUserDeleted, finalizeDeletion, fan-out de user.deleted vers les domaines et gather ws. A utiliser pour tout changement de domain-user touchant la suppression, le statut ou le cycle de vie d'un user.

★ 0
Volontariapp
Volontariapp/meta

volontariapp-proto-contract-evolution

Modifier un contrat Protobuf de proto-registry (champ, message, enum, RPC) sans casser la compatibilité wire, puis suivre la cascade proto-registry, PR automatique dans npm-packages, publication, consommateurs. À utiliser avant toute édition d'un .proto.

律师与法务
★ 0
Volontariapp
Volontariapp/meta

volontariapp-meta-scripts

Utiliser et faire evoluer la CLI Node de meta (setup, doctor, migrate, certs, repos, bump) et le manifeste repos.json, source unique des repos et unites du workspace.

Skill与智能体开发
★ 0
Volontariapp
Volontariapp/meta

volontariapp-implement-async-event-flow

Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).

前端开发
★ 0
Volontariapp
Volontariapp/meta

research

Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.

资料检索与研究
★ 0
Volontariapp
Volontariapp/meta

prototype

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

设计师
★ 0