Skip to content

Latest commit

 

History

History
1051 lines (759 loc) · 42.4 KB

CONTRIBUTING.md

File metadata and controls

1051 lines (759 loc) · 42.4 KB

Contribuer à Trackdéchets

Mise en route

Pré-requis

  1. Installer Node.js
  2. Installer Docker et Docker Compose

Installation

  1. Cloner le dépôt sur votre machine.

    git clone [email protected]:MTES-MCT/trackdechets.git
    cd trackdechets
    git checkout --track origin/dev
  2. Configurer les variables d'environnements :

    1. Renommer le ficher .env.model en .env et le compléter en demandant les infos à un développeur de l'équipe
    2. Créer un fichier .env dans front/ en s'inspirant du fichier .env.recette
  3. Mapper les différentes URLs sur localhost dans votre fichier host

    127.0.0.1 api.trackdechets.local
    127.0.0.1 trackdechets.local
    127.0.0.1 developers.trackdechets.local
    127.0.0.1 es.trackdechets.local
    127.0.0.1 notifier.trackdechets.local
    127.0.0.1 storybook.trackdechets.local
    

    Pour rappel, le fichier host est dans C:\Windows\System32\drivers\etc sous windows, /etc/hosts ou /private/etc/hosts sous Linux et Mac

    La valeur des URLs doit correspondre aux variables d'environnement API_HOST, NOTIFIER_HOST, UI_HOST, DEVELOPERS_HOST, STORYBOOK_HOST et ELASTIC_SEARCH_HOST

  4. Démarrer les containers de bases de données

    docker compose docker-compose.yml up -d

    NB: Pour éviter les envois de mails intempestifs, veillez à configurer la variable EMAIL_BACKEND sur console.

  5. Installez les dépendances de l'application localement

    npm install
  6. Synchroniser la base de données avec le schéma prisma.

    Les modèles de données sont définis dans les fichiers libs/back/prisma/src/schema.prisma. Afin de synchroniser les tables PostgreSQL, il faut lancer une déploiement prisma

    npx prisma db push
  7. Initialiser l'index Elastic Search.

    Les données sont indexées dans une base de donnée Elastic Search pour la recherche. Il est nécessaire de créer l'index et l'alias afin de commencer à indexer des documents. À noter que ce script peut aussi être utiliser pour indexer tous les documents en base de donnée.

    npx nx run back:reindex-all-bsds-bulk -- -f
  8. Lancer les services.

    Il est conseiller de lancer les services dans différents terminaux pour plus de lisibilité:

    > npx nx run api:serve # API
    > npx nx run front:serve # Frontend
    > npx nx run-many --parallel=4 -t serve --projects=tag:backend:background # Services annexes: notifier & queues
  9. Accéder aux différents services.

    C'est prêt ! Rendez-vous sur l'URL UI_HOST configurée dans votre fichier .env (par ex: http://trackdechets.local) pour commencer à utiliser l'application ou sur API_HOST (par ex http://api.trackdechets.local) pour accéder au playground GraphQL.

Développement

Lors du développement, vous aurez sûrement besoin de pull des mises à jour depuis le repo. Après avoir pull le repo localement, vous pouvez utiliser la commande :

npm run afterpull

Qui automatise les tâches redondantes (mettre à jour les packages, appliquer les nouvelles migrations, générer les types back et front).

Installation alternative sans docker sur MacOS avec puce Apple

L'utilisation de Docker sur MacOS avec puce Apple est problématique car il n'existe pas d'image officielle pour Elasticsearch@6. Par ailleurs des problèmes de networking existe sur l'image Docker utilisée pour le back.

  1. Installer postgres, redis, elasticsearch@6, nginx et mongodb
brew install postgresql
brew install redis
brew install nginx
brew tap mongodb/brew
brew update
brew install [email protected]
brew install elasticsearch@6
  1. Installer PostgreSQL 14 avec Postgres.app. Par défaut un utilisateur est crée avec votre nom d'user MacOS et un mot de passe vide.

  2. Se connecter à la base PostgresSQL avec la commande psql puis créer la DB : create database prisma.

  3. Lancer les différents services :

brew services start redis
brew services start nginx
brew services start mongodb-community
brew services start elasticsearch@6

puis vérifier qu'ils tournent avec brew services list. Vous pouvez vérifier également que Nginx est bien démarré en allant sur http://localhost:8080.

En cas d'erreur à l'exécution d'elasticsearch (jdk.app corrupted ou autre), il est possible de faire l'installation en téléchargeant directement les binaires depuis Le site d'elasticsearch. Vous pouvez ensuite ajouter le dossier bin/ à votre PATH ou démarrer elasticsearch en vous rendant dans le dossier directement.

  1. Configurer Nginx pour servir l'API, l'UI et le notifier en créant les fichiers suivants :
# fichier /opt/homebrew/etc/nginx/servers/api.trackdechets.local

server {
   listen 80;
   listen [::]:80;
   server_name api.trackdechets.local;

   location / {
      proxy_pass http://localhost:4000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
   }
}

#  /opt/homebrew/etc/nginx/servers/notifier.trackdechets.local
server {
   listen 80;
   listen [::]:80;
   server_name notifier.trackdechets.local;

   location / {
      proxy_pass http://localhost:4001;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_set_header Connection '';
      proxy_http_version 1.1;
      chunked_transfer_encoding off;
      proxy_buffering off;
      proxy_cache off;
      proxy_read_timeout 4h;
   }
}

# /opt/homebrew/etc/nginx/servers/trackdechets.local
server {
   listen 80;
   listen [::]:80;

   server_name trackdechets.local;

   location / {
      proxy_pass http://localhost:3000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
   }
}

# /opt/homebrew/etc/nginx/servers/storybook.trackdechets.local
server {
   listen 80;
   listen [::]:80;

   server_name storybook.trackdechets.local;

   location / {
      proxy_pass http://localhost:6006;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
   }
}

Si vous voulez utiliser le domaine es.trackdechets.local pour elasticsearch, ajouter:

# /opt/homebrew/etc/nginx/servers/es.trackdechets.local
server {
   listen 80;
   listen [::]:80;
   server_name es.trackdechets.local;

   location / {
      proxy_pass http://localhost:9200;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
   }
}

Re-charger la config et redémarrer NGINX

brew services reload nginx
brew services restart nginx
  1. Mapper les différentes URLs sur localhost dans votre fichier host
# /etc/hosts
127.0.0.1 api.trackdechets.local
127.0.0.1 trackdechets.local
127.0.0.1 notifier.trackdechets.local
127.0.0.1 storybook.trackdechets.local

Si vous avez mappé le domaine es.trackdechets.local dans la config nginx:

127.0.0.1 es.trackdechets.local
  1. Installer nvm
brew install nvm
nvm install 20 // version pour le back
echo v20 > .nvmrc
nvm use && npm install && npm nx run back:codegen
  1. Ajouter un fichier .env dans le répertoire racine en copiant le fichier .env.model et un fichier .env dans le répertoire front en copiant le fichier front/.env.model. (demander à un dev)

  2. Pousser le schéma de la base de données dans la table prisma et ajouter des données de tests en ajoutant un fichier seed.dev.ts dans le répertoire back/prisma (demander à un dev) :

npx prisma db push
npx prisma db seed
  1. Créer l'index Elasticsearch : npx nx run back:reindex-all-bsds-bulk -- -f. Puis vérifier qu'un index a bien été crée : curl localhost:9200/_cat/indices (ou via elasticvue)

  2. Créer un utilisateur Mongo :

mongosh
> use admin
> db.createUser({user: "trackdechets" ,pwd: "password", roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})
  1. Démarrer le back et le front :
npx nx run-many -t serve

ou pour démarrer dans des consoles différentes:

npx nx run api:serve # API
npx nx run front:serve # Frontend
npx nx run-many --parallel=4 -t serve --projects=tag:backend:background
  1. (Optionnel) Démarrer Storybook
npx nx run front:storybook

Conventions

  • Formatage/analyse du code avec prettier et eslint.
  • Typage du code avec les fichiers générées par GraphQL Codegen
    • back/src/generated/graphql/types.ts pour le back
    • libs/front/codegen-ui/src/generated/graphql/types.ts pour le front

Tests unitaires

La commande pour faire tourner tous les tests unitaires est la suivante :

docker compose -f docker-compose.test.yml up

Il est également possible de faire tourner les tests unitaires sur l'environnement de dev en se connectant à chacun des containers. Par exemple :

  1. Démarrer les différents services
    docker compose up -d
    
  2. Faire tourner les tests back
    npx nx run back:test # run all the tests
    npx nx run back:test --testFile src/path/to/my-function.test.ts # run only one test
  3. Faire tourner les tests front
    npx nx run front:test

Tests d'intégration

Ce sont tous les tests ayant l'extension .integration.ts et nécessitant le setup d'une base de données de test. Ils nécessitent de démarrer les containers Docker (ou d'avoir un setup local), puis de lancer les queues.

npm run bg:integration # Démarrage des queues en background, nécessaires aux tests
npx nx run back:test:integration # Lancement des tests d'intégration

Il est également possible de faire tourner chaque test de façon indépendante:

npx nx run back:test:integration --testFile workflow.integration.ts

Tests end-to-end (e2e)

Les tests e2e utilisent Playwright (documentation officielle ici).

Local

Installation

Commencez par:

npm i

Puis il faut installer chromium pour playwright:

npx playwright install chromium --with-deps

Variables d'environnement

Vu que les tests e2e fonctionnent comme les tests d'intégration, à savoir qu'ils repartent d'une base vierge à chaque fois, vous pouvez utiliser les .env.integration (back & front) pour les tests e2e.

Lancer les tests e2e en local

  1. Lancer la DB, ES etc.
  2. Démarrer les services TD avec: npx nx run-many -t serve --configuration=integration --projects=api,front,tag:backend:background --parallel=6
  3. Lancer les tests:
# Console seulement
npx nx run e2e:cli --configuration=integration

# Avec l'UI
npx nx run e2e:ui --configuration=integration

Pour tester un seul fichier:

# Console seulement
npx nx run e2e:cli --file companies.spec.ts --configuration=integration

# Avec l'UI
npx nx run e2e:ui --file companies.spec.ts --configuration=integration

Il est aussi possible de débugguer pas à pas, avec l'UI:

npx nx run e2e:debug --file companies.spec.ts --configuration=integration

Recorder

Playwright vous permet de jouer votre cahier de recette dans un navigateur et d'enregistrer vos actions. Plusieurs outils sont disponibles pour par exemple faire des assertions sur les pages.

Pour lancer le recorder:

npx playwright codegen trackdechets.local --viewport-size=1920,1080

Le code généré apparaît dans une fenêtre à part. Vous pouvez le copier et le coller dans des fichiers de specs.

CI

Débugguer visuellement

Pour prendre un screenshot de la page qui pose problème, modifier playwright.config.ts pour changer le mode headless:

headless: false

Puis placer dans le code, à l'endroit problématique:

const buffer = await page.screenshot();
console.log(buffer.toString('base64'));

// Ou alors, méthode toute faite dans debug.ts
await logScreenshot(page);

Puis utiliser un site comme celui-ci pour transformer le log en base64 en image.

Débugguer le network

Pour observer les requêtes, vous pouvez utiliser (doc ici):

page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));

// Ou alors, méthode toute faite dans debug.ts pour capturer uniquement les calls d'API
debugApiCalls(page);

Créer une PR

  1. Créer une nouvelle branche à partir et à destination de la branche dev.
  2. Implémenter vos changements et penser à mettre à jour la documentation et le changelog (en ajoutant un bloc "Next release" si il n'existe pas encore).
  3. Une fois la PR complète, passer la branche de "draft" à "ready to review" et demander la revue à au moins 1 autre développeur de l'équipe (idéalement 2). Si possible faire un rebase (éviter le merge autant que possible) de la branche dev pour être bien à jour et la CI au vert avant la revue.
  4. Une fois que la PR est approuvée et que les changements demandées ont été apportées, l'auteur de la PR peut la merger dans dev.

Note : l'équipe n'a pas de conventions strictes concernant le nom des branches et les messages de commit mais compte sur le bon sens de chacun.

Déploiement

Le déploiement est géré par Scalingo à l'aide des fichiers de configuration Procfile et .buildpacks placés dans le front et l'api. Chaque update de la branche dev déclenche un déploiement sur l'environnement de recette. Chaque update de la branche master déclenche un déploiement sur les environnements sandbox et prod. Le déroulement dans le détails d'une mise en production est le suivant:

  1. Faire le cahier de recette pour vérifier qu'il n'y a pas eu de régression sur les fonctionnalités critiques de l'application (login, signup, rattachement établissement, invitation collaborateur, création BSD)
  2. Balayer le tableau Favro "Recette du xx/xx/xx" pour vérifier que l'étiquette "Recette OK --> EN PROD" a bien été ajoutée sur toutes les cartes.
  3. Mettre à jour le Changelog.md avec un nouveau numéro de version (versionnage calendaire)
  4. Créer une PR dev -> master
  5. Au besoin résoudre les conflits entre master et dev en fusionnant master dans dev (Éviter de Squash & Merge)
  6. Faire une relecture des différents changements apportés aux modèles de données et scripts de migration.
  7. Si possible faire tourner les migrations sur une copie de la base de prod en local.
  8. S'assurer que les nouvelles variables d'environnement (Cf .env.model) ont bien été ajoutée sur Scalingo dans les environnements sandbox et prod respectivement pour les applications front et api
  9. Merger la PR (Éviter de Squash & Merge) et suivre l'avancement de la CI github.
  10. Suivre l'avancement du déploiement sur Scalingo respectivement pour le front, l'api et la doc.

Migrations

Modèle de données

Les migrations de modèle sont gérées avec Prisma migrate.

Le workflow est le suivant:

  • modification du schéma de la base de donnée, dans le fichier libs/back/prisma/src/schema.prisma
  • génération de la migration correspondante en jouant npx prisma migrate dev. Le CLI demandera de nommer sa migration. Les migrations peuvent être retrouvées dans libs/back/prisma/src/migrations
  • si on souhaite modifier le SQL généré par Prisma avant qu'il soit appliqué, il est possible de jouer npx prisma migrate dev --create-only. C'est notamment utile lorsque l'on souhaite utiliser des fonctionnalitées non supportées par Prisma (ex: index partiel)

Pour plus d'informations sur l'utilisation de Prisma migrate, allez consulter leur documentation.

Scripts de migration

Les scripts sont gérés par le projet libs/back/scripts.

Pour générer un script, on utilise npx nx run @td/scripts:generate. Le CLI demandera alors à nommer le script, et un boilerplate d'écriture de script sera généré. Les fichiers sont générés dans le dossier libs/back/scripts/src/scripts.

Pour jouer les scripts, on utilise npx nx run @td/scripts:migrate. Les scripts exécutés avec succès sont sauvegardés en base de données pour s'assurer qu'ils ne sont joués qu'une seule fois.

Réindexation Elasticsearch des BSDs

Depuis un one-off container de taille XL

  • Réindexation globale sans downtime en utilisant les workers d'indexation La réindexation ne sera déclenchée que si la version du mapping ES a changé

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-all-bsds-bulk -- --useQueue

  • Réindexation globale sans downtime depuis la console (le travail ne sera pas parallélisé) La réindexation ne sera déclenchée que si la version du mapping ES a changé

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-all-bsds-bulk

  • Réindexation globale sans downtime en utilisant les workers d'indexation Le paramètre -f permet de forcer la réindexation même si le mapping n'a pas changé

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-all-bsds-bulk -- --useQueue -f

  • Réindexation globale sans downtime depuis la console (le travail ne sera pas parallélisé) Le paramètre -f permet de forcer la réindexation même si le mapping n'a pas changé

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-all-bsds-bulk -- -f

  • Réindexation de tous les bordereaux d'un certain type (en place)

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-partial-in-place BSFF

  • Réindexation de tous les bordereaux d'un certain type (en supprimant tous les bordereaux de ce type avant)

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-partial-in-place -- -f BSFF

  • Réindexation de tous les bordereaux depuis une certaine date (en place)

FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-partial-in-place -- --since 2023-03-01

Réindexation complète sans downtime lors d'une mise en production

  • Se rendre sur Scalingo pour ajouter 1 worker bulkindexqueuemaster (en charge d'ajouter les chunks) en 2XL et plusieurs workers bulkindexqueue (en charge de process les chunks).
  • On peut retenir la configuration suivante pour les workers bulkindexqueue :
    • 4 workers de taille 2XL
    • BULK_INDEX_BATCH_SIZE=1000
    • BULK_INDEX_JOB_CONCURRENCY=1
  • Se connecter à la prod avec un one-off container de taille XL
  • Lancer la commande FORCE_LOGGER_CONSOLE=true npx nx run back:reindex-all-bsds-bulk -- --useQueue -f (si la version de l'index a été bump, on peut omettre le -f)
  • Suivre l'évolution des jobs d'indexation sur le dashboard bull, l'URL est visible dans le fichier `src/queue/bull-board.ts``. Il est nécessaire de se connecter à l'UI Trackdéchets avec un compte admin pour y avoir accès.
  • Relancer au besoin les "indexChunk" jobs qui ont failed (c'est possible si ES se retrouve momentanément surchargé).
  • Si les workers d'indexation crashent avec une erreur mémoire, ce sera visible dans les logs Scalingo. Il est possible alors que la taille des chunks soient trop importante. Diminuer alors la valeur BULK_INDEX_BATCH_SIZE, cleaner tous les jobs de la queue avant de relancer une réindexation complète. Il peut être opportun de de diminuer la taille des chunks.
  • Si ES est surchargé, il peut être opportun de diminuer le nombre de workers.
  • À la fin de la réindexation, set le nombre de workers bulkindexqueuemaster et bulkindexqueue à 0.

Rattrapage SIRENE

Si les données de raison sociale et d'adresses enregistrés sur les bordereaux sont erronnées suite à un dysfonctionnement de l'index SIRENE, un rattrapage peut être effectué à postériori grâce au script suivante. Tous les bordereaux qui ont été crées ou modifiés entre ces deux dates seront mis à jour.

npx nx run back:sirenify-bulk --since 2024-04-01 --before 2024-04-03

Les jobs de "sirenification" sont dépilés par le worker bulkindexqueue qui doit donc être démarré sur Scalingo avant de lancer le script.

Génération de modèles de bsds vierges

Une commande permet de générer et téléverser sur un bucket S3 les modèles vierges des bsds BSDD, BSDA, BSVHU et BSFF.

Les variables d'environnement S3_BSD_TEMPLATES_* doivent être renseignées.

Lancer la commande

npx nx run back:generate-bsds-templates

Guides

Mettre à jour le changelog

Le changelog est basé sur Keep a Changelog, et le projet suit un schéma de versionning inspiré de Calendar Versioning.

Il est possible de documenter les changements à venir en ajoutant une section "Next release".

Mettre à jour la documentation

Les nouvelles fonctionnalités impactant l'API doivent être documentées dans la documentation technique ./doc en même temps que leur développement. Si possible faire également un post sur le forum technique.

Utiliser un backup de base de donnée

Il est possible d'importer un backup d'une base de donnée d'un environnement afin de le tester en local. La procédure qui suit aura pour effet de remplacer vos données en local par les données du backup.

Procédure automatique de restauration d'une base de donnée de production avec Docker

Un script d'automatisation a été mis en place. Il permet de restaurer soit un backup local, soit le dernier backup de la base de donnée distante choisie. Pour les backups distants, assurez vous d'avoir correctement configuré les variables d'environnement suivantes dans votre fichier .env local:

  • SCALINGO_TOKEN - clé d'API Scalingo
$ pwd
~/dev/trackdechets
$ cd scripts
$ sudo chmod +x restore-db.sh # Si le fichier n'est pas exécutable
$ ./restore-db.sh
# Laissez vous guider...
# La première question détermine si vous souhaitez utiliser un backup distant ou local

Procédure automatique de restauration partielle d'une base de donnée de production

Un script permettant de faire un dump partiel d'une DB a été créé. Il part d'un BSD spécifique qui doit être testé, et traverse récursivement la db pour trouver tous les objets qui y sont reliés, de façon à avoir un environnement de test complet pour reproduire un problème.

Etapes préliminaires:

  • créer une nouvelle DB vide et la mettre dans la variable DATABASE_URL du fichier .env
  • appliquer npx prisma migrate dev
  • ouvrir un tunnel SSH vers la db à dumper en utilisant le client scalingo
    • scalingo login (nécéssite d'avoir une clé SSH renseignée dans Scalingo)
    • scalingo -a <id de la db scalingo> db-tunnel SCALINGO_POSTGRESQL_URL
  • ajouter l'url de la DB tunnelée dans TUNNELED_DB dans le fichier .env. Utiliser un utilisateur read-only pour l'accès, voir avec l'équipe pour en créer un ou obtenir ses credentials.

Utilisation du script:

$ npx nx run partial-backup:run

Le script vous demandera l'id du BSD de départ (utiliser le readableId "BSD-..." pour les BSDD/Form), puis se chargera de charger tous les objets en relation. Une fois le chargement fait, vous aurez un aperçu des données sous cette forme :

What will be copied :
{
  Bsdasri: 3,
  Company: 297,
  AnonymousCompany: 3,
  User: 78,
  TransporterReceipt: 65,
  CompanyAssociation: 408,
  MembershipRequest: 65,
  VhuAgrement: 54,
  BrokerReceipt: 12,
  AccessToken: 77,
  Grant: 12,
  Application: 3,
  WorkerCertification: 35,
  SignatureAutomation: 40,
  TraderReceipt: 11,
  UserActivationHash: 3,
  UserResetPasswordHash: 11,
  FeatureFlag: 1
}

Si les informations semblent raisonnables, vous pouvez accepter d'écrire dans votre DB de destination en tapant "Y".

Si une erreur survient lors du processus d'écriture, il est possible que ce soit dû à:

  • le schema utilisé en local ne correspond pas à celui de la db source
  • le schema Prisma ne correspond pas au schema de la db source
  • la DB de destination n'est pas vide
  • le schema de la DB de destination n'a pas été créé (npx prisma migrate dev)

Si tout se passe correctement, il ne vous reste plus qu'à reconstruire l'index elastic avec les données chargées en appliquant npx nx run back:reindex-all-bsds-bulk -- -f.

Procédure manuelle

  1. Télécharger un backup de la base de donnée nommée prisma que vous souhaitez restaurer
  2. Démarrer le conteneur postgres
    docker compose -f docker-compose.dev.yml up --build postgres
    
  3. Copier le fichier de backup à l'intérieur du conteneur
    # docker cp <fichier backup> <nom du container postgres>:<chemin où copier>
    # exemple :
    docker cp backup trackdechets_postgres_1:/var/backups
    
  4. Accéder au conteneur postgres
    docker exec -it $(docker ps -aqf "name=trackdechets_postgres") bash
    
  5. Restaurer le backup depus le conteneur postgres
    dropdb -U trackdechets prisma
    createdb -U trackdechets prisma
    psql -U trackdechets prisma
      psql (13.3)
      Type "help" for help.
      prisma=# create schema default$default
    # quit psql CTRL-D
    pg_restore -U trackdechets -d prisma --clean /var/backups/backup
    
  6. Quitter le shell du conteneur postgres
  7. Appliquer les migrations présentent dans votre branche actuelle du code source

Créer un tampon de signature pour la génération PDF

Il est possible de créer de nouveaux tampons à partir du fichier stamp.drawio.png. C'est un fichier PNG valide que l'on peut éditer directement dans Visual Code avec l'extension Draw.io VS Code Integration

Nourrir la base de donnée avec des données par défaut

Il peut être assez fastidieux de devoir recréer des comptes de tests régulièrement en local. Pour palier à ce problème, il est possible de nourrir la base de donnée Prisma avec des données par défaut.

  1. Créer le fichier back/prisma/seed.dev.ts en se basant sur le modèle back/prisma/seed.model.ts.
  2. Démarrer les containers postgres et td-api
  3. (Optionnel) Reset de la base de données 3.1 Dans le container postgres : psql -U trackdechets -d prisma -c "DROP SCHEMA \"default\$default\" CASCADE;" pour supprimer les données existantes 3.2 Dans le container td-api: npx prisma db push --preview-feature pour recréer les tables
  4. Dans le container td-api: npx prisma db seed --preview-feature pour nourrir la base de données.

Ajouter un objet spécifique dans la base de données

Au cas où il serait nécessaire d'ajouter un objet à la base de données, vous pouvez utiliser le script "object-creator". Pour celà, modifiez le fichier libs/back/object-creator/src/objects.ts en ajoutant des objets en respectant le format démontré en exemple.

Vous pouvez ensuite utiliser npx nx run object-creator:run et si tout se passe bien, les objets seront créés dans la base de donnée spécifiée dans la variable d'environnement "DATABASE_URL".

Ajouter une nouvelle icône

Les icônes utilisées dans l'application front viennent de https://streamlineicons.com/. Nous détenons une license qui nous permet d'utiliser jusqu'à 100 icônes (cf Streamline Icons Premium License).

Voilà la procédure pour ajouter une icône au fichier Icons.tsx :

  1. Se connecter sur streamlineicons.
  2. Copier le SVG de l'icône concerné.
  3. Convertir le SVG en JSX et l'ajouter au fichier (adapter le code selon les exemples existants : props, remplacer width/height et "currentColor").

Pour s'y retrouver plus facilement, suivre la convention de nommage en place et utiliser le nom donné par streamlineicons.

Clefs de signature token OpenID

Une clef de signature RSA est nécessaire pour signer les tokens d'identité d'Openid.

   openssl genrsa -out keypair.pem 2048
   openssl rsa -in keypair.pem -pubout -out publickey.crt
   openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

Le contenu de pkcs8.key va dans la vairable d'env OIDC_PRIVATE_KEY. Le contenu de publickey.crt est destiné aux applications clientes d'OpenId connect.

Reindexer un bordereau individuel

   npx nx run back:reindex-bsd BSD-XYZ123

Réindexer un type de bordereau

   npx nx run back:reindex-partial-in-place -- bsdasri -f

Dépannage

La base de donnée ne se crée pas

Si la commande pour créer la base de données ne fonctionne pas (npx prisma db push), il est possible que le symbole $ dans le nom de la base (default$default) pose problème. Deux solutions:

  • Encapsulez l'URI de la base avec des guillements simple, ie: DATABASE_URL='postgresql://username:password@postgres:5432/prisma?schema=default$default'
  • Enlevez complètement le paramètre schema: DATABASE_URL=postgresql://username:password@postgres:5432/prisma

Je n'arrive pas à (ré)indexer les BSDs sur Elastic Search

Vous pouvez vérifier vos indexes Eslastic Search avec la commande suivante:

curl -X GET "localhost:9200/_cat/indices"

Si les indexes sont incomplets ou si la commande a échoué, vous pouvez vous connecter au container de l'API et passer en mode verbose avant de relancer l'indexation:

# Pour se connecter au container de l'API
docker exec -it $(docker ps -qf "name=td-api") bash

export FORCE_LOGGER_CONSOLE=true

npx nx run back:reindex-all-bsds-bulk -- -f

Si le problème remonté est un "Segmentation fault", il est probable que la mémoire allouée au container soit insuffisante. Vous pouvez contourner le problème en limitant la taille des batches (dans votre .env):

BULK_INDEX_BATCH_SIZE=100

Vous pouvez également augmenter la taille mémoire allouée au container Docker, dans un fichier docker-compose.override.yml placé à la racine du répo:

[...]
  postgres:
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 128M
[...]
  elasticsearch:
    environment:
      - "ES_JAVA_OPTS=-Xms1G -Xmx1G"
[...]

Si l'indexation ne fonctionne pas et que vous voyez des erreurs de type:

[TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block]

dans les logs elastic, il est probable que votre disque dur soit plein à plus de 95% et que elastic passe donc en read&delete only. Pour résoudre le problème, en admettant qu'il reste quand même un peu de place pour créer l'index (quelques Go max), il faut ajouter cette ligne:

cluster.routing.allocation.disk.threshold_enabled: false

au fichier elasticsearch.yml qui se trouve généralement dans elasticsearch/config/.


Si une erreur NGINX "413 Request Entity too large" interromp le process

{"meta":{"body":"<html>\r\n<head><title>413 Request Entity Too Large</title></head>\r\n<body>\r\n<center><h1>413 Request Entity Too Large</h1></center>\r\n<hr><center>nginx/1.25.5</center>
[...]

et que vous utilisez Elastic derrière le proxy NGINX (es.trackdechets.local), il faut augmenter la taille max de body acceptée par NGINX. Pour celà ajouter cette ligne:

client_max_body_size 100M;

dans le fichier nginx.conf à l'intérieur du bloc "http" ou "server" (qui se trouve généralement dans le dossier nginx/ ou nginx/conf/). redémarrez ensuite nginx pour appliquer la config.

Documentation du code

Process de validation avec Zod

La validation et le parsing des données entrantes est gérée en interne par la librairie Zod.

Déclaration du schéma

La déclaration d'un schéma Zod pour un type de bordereau donnée se fait en composant plusieurs étapes :

  • Déclaration d'un schéma de validation "statique" permettant de définir les types de chaque champ et des règles de validation simples (ex: un email doit ressembler à un email, un N°SIRET doit faire 14 caractères, il peut y avoir au maximum 2 plaques d'immatriculations) ainsi que des valeurs par défaut.
  • Déclaration de règles de validation plus complexes (via la méthode superRefine) faisant intervenir plusieurs champs ou des appels asynchrones à la base de données (ex: la raison du refus doit être renseignée si le déchet est refusé, la date de l'opération doit être postérieure à la date de l'acceptation, les identifiants des bordereaux à regrouper doivent correspondre à des bordereaux en attente de regroupement, etc).
  • Déclaration de transformers permettant de modifier les données (ex: auto-compléter les récépissés transporteurs à partir de la base de données, auto-compléter le nom et l'adresse à partir de la base SIRENE, etc).

Inférence du type

Le schéma ainsi obtenu permet de centraliser tout le process de validation et de transformation des données et Zod nous permet d'inférer deux types :

  • le type attendu en entrée du parsing Zod.
  • le type obtenu en sortie du parsing Zod.

Ces types nous servent de "pivots" entre les données entrantes provenant de la couche GraphQL et le format de données de la couche Prisma.

Utilisation des les mutations create et update

Deux méthodes permettant respectivement de convertir les données GraphQL ou les données Prisma vers le format Zod :

  • graphQlInputToZodBsd(input: GraphQLBsdInput): ZodBsd : permet de convertir les données d'input GraphQL vers le format Zod.
  • prismaToZodBsda(bsd: PrismaBsd): ZodBsd : permet de convertir les données

Dans le cas d'une mutation de création, une version simplifiée du process pourra ressembler à :

function createBsdResolver(_, { input }: MutationCreateBsdArgs, context: GraphQLContext) {
  const user = checkIsAuthenticated(context);
  await checkCanCreate(user, input);
  const zodBsd = await graphQlInputToZodBsd(input);
  const bsd = await parseBsdAsync(
    { ...zodBsd, isDraft },
    {
      user,
      //
      currentSignatureType: !isDraft ? "EMISSION" : undefined
    }
  );
  const bsdData: Prisma.CreateBsdInput = {...
   // calcule ici le payload prisma à partir des données
   // obtenues en sortie de parsing, il faut notament gérer
   // la création / connexion / déconnexion d'objets liés (ex: transporteurs, packagings, etc).
  }
  const created = await repository.create({data: bsdData})


    // [...]
}

Dans le cas d'une mutation d'update, on ne peut pas simplement valider les données entrantes, il faut aussi vérifier que le bordereau obtenu suite à l'update sera toujours valide. En effet beaucoup de règles de validation s'appliquent sur plusieurs champs, si je modifie un des champ je veux m'assurer que sa valeur est toujours cohérente avec les données persistées en base. Je dois par ailleurs vérifier qu'on n'est pas en train de modifier un champ qui a été verrouillée par signature tout en permettant de renvoyer les mêmes données. On passe alors par une méthode dont la signature est la suivante :

type Output = {
  parsedBsd: ParsedZodBsd;
  updatedFields: string[];
};

function mergeInputAndParseBsdAsync(persisted: PrismaBsd, input: GraphQLBsdInput, context: BsdValidationContext): Output {
  const zodPersisted = prismaToZodBsd(persisted);
  const zodInput = await graphQlInputToZodBsd(input);

  // On voit ici l'utilité du schéma Zod comme type pivot entre les données
  // entrantes de la couche GraphQL et les données de la couche prisma
  const bsd: ZodBsff = {
    ...zodPersisted,
    ...zodInput
  };

  // Calcule la signature courante à partir des données si elle n'est
  // pas fourni via le contexte
  const currentSignatureType = context.currentSignatureType ?? getCurrentSignatureType(zodPersisted);

  const contextWithSignature = {
    ...context,
    currentSignatureType
  };

  // Vérifie que l'on n'est pas en train de modifier des données
  // vérrouillées par signature.
  const updatedFields = await checkBsdSealedFields(
    zodPersisted
    bsd,
    contextWithSignature
  );

  const parsedBsd = await parseBsdAsync(bsd, contextWithSignature);

  return { parsedBsff, updatedFields };
}

Le workflow simplifié dans la mutation d'update ressemble alors à ça :

function updateBsdResolver(_, { id, input }: MutationUpdateBsdArgs, context: GraphQLContext) {

   const user = checkIsAuthenticated(context);
   await checkCanUpdate(user, input);

   const persisted = await getBsdOrNotFound({id})

   const { parsedBsd, updatedFields } = mergeInputAndParseBsdAsync(persisted, input, {})

   if (updatedFields.length === 0){
      // évite de faire un update "à blanc" si l'input ne modifie rien
      return expandBsdFromDb(persisted)
   }

  const bsdData: Prisma.CreateBsdInput = {...
   // calcule ici le payload prisma à partir des données
   // obtenues en sortie de parsing, il faut notament gérer
   // la création / connexion / déconnexion d'objets liés (ex: transporteurs, packagings, etc).
  }
  const updated = await repository.update({ data: bsdData })

  // [...]
}

Utilisation dans les mutations sign

Une modification de signature ne modifie pas les données mais nécessite quand même de réaliser le parsing car on va vérifier que les données sont toujours cohérentes avec le type de signature apposée, pour vérifier par exemple que les champs requis à cette étape sont bien présents. La signature courant est alors passée explicitement via le contexte de validation.

function signBsdResolver(_, { id, input }: MutationUpdateBsdArgs, context: GraphQLContext) {
  const user = checkIsAuthenticated(context);
  await checkCanSign(user, input);

  const persisted = await getBsdOrNotFound({ id: input.id });

  const signatureType = getBsdSignatureType(input.type);

  const zodBsd = prismaToZodBsd(persisted);

  // Check that all necessary fields are filled
  await parseBsdAsync(zodBsd, {
    user,
    currentSignatureType: signatureType
  });

  const signed = await repository.update({ data: { ...input, signatureDate: new Date() } });
}

Définition des règles de champs requis et de verrouillage des champs

Le cycle de vie du bordereau implique que le remplissage des champs se fasse au fur et à mesure et que des signatures viennent "verrouiller" certains champs. Les règles métier relatives aux champs requis et verrouillés sont définies dans des tableurs (ex pour le BSFF : BSFF - Informations requises et scellées).

La sémantique de définition pour chaque champ requis / verrouillée est très similaire : ¨un champ est requis / scellé à partir de telle signature si telle condition est remplie sur le bordereau¨. D'où l'idée de créer un fichier de définition commun rules permettant de regrouper la définition des champs verrouillés / requis. Exemple pour le BSDA

export const bsdaEditionRules: BsdaEditionRules = {
   // [...]
  emitterCompanySiret: {
    readableFieldName: "le SIRET de l'entreprise émettrice",
    sealed: { from: "EMISSION" },
    required: {
      from: "EMISSION",
      when: bsda => !bsda.emitterIsPrivateIndividual
    }
  }
  /// [...]

La vérification sur les champs requis se fait dans un refinement synchrone checkRequiredRules tandis que la vérification sur les champs scellés se fait via la méthode checkSealedFields dans la fonction mergeInputAndParseBsdAsync.

Schema recap (ex avec le BSDA)

Validation BSDA