owned this note
owned this note
Published
Linked with GitHub
# Projet OKLM
## 16/12/2021 : Kickoff OKLM dev
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
### Pourquoi ?
Marc:
avoir des ressources, pour ne pas se prendre la tête au démarrage, pour me concentrer sur le métier
se fait souvent un tuto quand il apprend quelque chose
d'autres personnes font sans doute ça, on peut le mettre en commun
centre de ressources partagé
pas forcément quelque chose de figé comme un boilerplate (chaque aspect est intéressant pris indépendament, sans forcément être coincé dans des choix)
noms de personnes dans une startup d'état qui utilisent cette techno
Romain:
souvent des problématiques où je m'y connais pas trop, je recherche l'état de l'art (geocoding, ...)
"je ne sais pas ce que je ne sais pas" => "je sais ce que je ne sais pas": étape qui est très difficile (carto ?)
ensuite aussi les ressources pour en apprendre plus sur ces sujets
garder sa liberté de penser (comprendre pour choisir)
comment amener les gens dans ce "bon niveau" ni trop haut ni trop bas
### Quoi ?
énumérer les grands sujets qui peuvent nous concerner ?
"désamorcer la complexité en avance"
Faire une sorte d'annuaire qui annonce quelles startups et éventuellement quels devs ont bossé sur tel et tel sujets ?
code.gouv.fr
https://beta.gouv.fr/startups/
Pour les lang de prog, facile, libs utilisées faisables
mais pour les thèmes d'archi ? domaines métier (géolocalisation, adresse, ...)
de la curation à la main ?
Guide: comment être un bon dev ?
=> déjà plus ou moins géré par la doc betagouv
=> plus simple d'apporter de la valeur sur un sujet particulier (ex: Authentification)
ex: un repo avec une configuration bateau de Keycloak avec le DS de l'état (+ l'adaptateur FranceConnect ?)
permet de se faire reconnaitre sur ce sujet, les devs posent des questions, on répond et améliore l'exemple en continu
constat: il y a plein de services top mais on ne les connait pas
### boilerplate
idée: proposer un repo avec une application déjà bien lancée (avec une vraie auth, un vrai exemple de formulaire + interface admin)
pourrait être une sorte de Démarches (moins) Simplifiées (le pendant full-code de DS qui est no-code, pour adresser le marché des SE qui sont trop complexes pour DS)
- comment savoir qu'on peut utiliser ca ?
- à l'embarquement betagouv
- série de tutos qui t'amènent au boilerplate ?
- quand on crée un tuto, on utilise le boilerplate comme exemple (on rajoute un exemple dans le boilerplate)
- quand on utilise le boilerplate, on a le tuto qui explique
- il y a parfois des choix à faire qui seront incompatibles (ie on ne peut pas faire plusieurs options sur le même boilerplate)
- utiliser des forks ?
- faire des choix, opinionnés, mais expliquer pourquoi
- pas d'accord avec le choix ?
- fait une PR/fork et propose
- faire le boilerplate à plusieurs
- d'apprendre des autres contributeurs
- débattre de chaque choix
- garder une trace des discussions
- ajouter des fichiers md dans le repo
On serait d'accord sur le langage: typescript
aller vers des choix très communs
quand la communication est difficile entre dev et non-dev: faire intervenir un dev externe, qui peut aider le dev à expliquer ses difficultés, à mieux prioriser, découper ses tâches
=> "médiateur tech" ?
Ouverture d'un pad pour faire une synthèse de nos échanges:
https://hackmd.io/j6F14DDpTMG9-rEFCgc3tw?both
Next up: mob programming d'un repo (le 3 janvier 2022)
outils pour le mob programming:
codewithme
gitpod
vscode.dev + LiveShare
## 05/01/2022 : Première session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
OKLM: "Arriver rapidement à l'alpha d'un produit lambda de beta" :grin:
On discute et teste gitpod.io, un service de conteneur avec une interface graphique à base de visual studio code.
Retex rapide:
- un conteneur spawn à partir d'un lien github (marche aussi avec gitlab), en quelques secondes
- la vue par défaut du conteneur est une instance vscode dans le browser
- on peut rajouter des extensions à ce vscode, elles s'executent dans le conteneur, c'est donc isolé de notre env local
- on a accès au terminal, ça s'execute sur le conteneur
- il est possible de partager un conteneur, via une url
- chaque utilisateur a accès au conteneur comme si on était tous connectés en ssh
- l'état du conteneur est le même pour tous (fichiers, git status, ...)
- un seul utilisateur, ce qui fait qu'un commit sera fait en tant que la personne qui a spawn le conteneur (peut être surprenant en mob, même pas déconnant sachant qu'en bossant ensemble, on prend la responsabilité du code ensemble)
- chacun a son instance de vscode
- chacun ouvre ses onglets
- on ne voit pas ce que les autres font
- mais il y a quand même un peu de synchro
- le terminal est parfois partagé (on n'a pas compris pourquoi c'est pas toujours)
- on voit en direct ce que l'autre tape et le résultat
- super pratique pour avoir une vision partagée d'un test runner en mode watch
- sur la version web de vscode, on peut modifier en même temps un même fichier et voir ce que l'autre tape, presque en live (et sans forcément sauvegarder le fichier)
- si on modifie en même temps, vscode nous prévient que le fichier a été modifié entre temps, on peut voir un diff ou bien écraser les modifs de l'autre
- en pair/mob, il faudra quand même se coordoner, se "passer la main"
- il est possible d'ouvrir le conteneur avec un vscode local
- permet d'avoir ses extensions locales (aide à la saisie, snippets, etc.) en plus des extensions qui tournent sur le serveur
- le terminal est aussi connecté en ssh sur la machine
- il est possible de forwarder des ports entre la machine locale et le conteneur directement dans vscode, c'est même automatique quand on lance une application (genre `npx http-server`)
- nous avons eu du mal à accéder à un web serveur qui tournait sur le gitpod
- à investiguer
- il est possible de "forker" un gitpod via un snapshot: l'état complet du pod et de vscode sont conservés
- On peut vraiment préparer un gitpod dans un état très précis pour le partager
- Le forks sont indépendants, la modification de leur état n'influe pas sur les autres
- on peut tester un piste et la jeter
- Parait vraiment intéressant pour debugger en équipe
- pour l'instant, on a testé sur vscode mais les IDE de Jetbrains sont en cours d'intégration (soit "beta" soit "soon")
Ca pourrait devenir un environnement de developpement principal, qu'on peut accéder de partout, même sur une machine pas puissante (qui a ou pas vscode en local, au moins un navigateur). Pour bosser sur des données sensibles qu'il ne faut pas avoir sur sa machine ? Pour lancer un stagiaire/junior qui n'a pas encore une machine configurée ?
Intéressant pour le pair/mob programming, parce qu'on partage non seulement le code (qu'on pourrait le faire via git), mais aussi un environnement complet et même un terminal (pour les tests en mode watch par exemple).
Qu'est-ce qu'on mets dans notre boilerplate ?
- Clean / hexa architecture
- c'est une évidence pour avoir du code découplé
- l'investissement et courbe d'apprentissage ne sont pas trop élevés
- Event Sourcing
- est-ce que c'est accessible pour quelqu'un de novice ?
- pas forcément, c'est un nouveau paradigme
- mais proche d'autres paradigmes réactifs comme Redux, RxJS, etc. qui sont communs
- est-ce qu'on fait certains use-case en ES et d'autres non ?
- a priori oui
- mais ça peut complexifier la conception parce que ça veut dire que pour chaque nouvelle feature, il faudra décider (comment?) s'il faut le faire en mode ES ou pas. Avoir plusieurs modes en simultané dans l'application pourrait embrouiller.
- on part sur de l'event-sourcing partout et on mise sur le tooling pour adoucir la pente:
- README.md béton, expliquer les concepts sans faire du name-dropping (il y a de nombreux articles négatifs sur l'event sourcing, quand on voit passer les titres sans vraiment rentrer dans le sujet, on peut avoir un a priori vraiment négatif, ce qui est dommage)
- boilerplate assez poussé pour permettre de voir comment est implémenté des vraies fonctionnalités, copier ('polluer les gens avec des bonnes pratiques')
- outil pour visualiser
- ...
- hypothèse à valider: on arrive à rendre les choses digestes
- Monorepo/Multirepo/Repo simple
- Multirepo
- principe: chaque brique indépendante est dans son repo dédié
- les dépendances sont traitées comme des libs
- un CI par répo / service
- possibilité d'isoler en fonction de l'utilisateur (sécurité)
- pratique quand on a plusieurs équipes (pas d'interférence)
- inconvénient: si une feature modifie plusieurs libs, plusieurs PR nécessaires
- inconvénient: incompatibilité possible de la "dernière" version de chaque lib entre elles (versionning fort requis)
- Monorepo:
- principe: chaque brique est dans son dossier dans un repo partagé
- pour la gestion des builds/dépendances, on passe par un orchestrateur comme lerna ou turborepo
- permet de faire une PR par feature
- force l'alignement des applications (pas d'incompatibilité)
- inconvénient: complexité ajoutée par l'orchestrateur, à configurer
- Repo simple:
- principe: on a une application suffisamment simple pour avoir un seul dossier `src` et un seul `package.json`, parce qu'il n'y a qu'une seule chose à déployer
- solution la plus simple, à condition d'avoir un besoin simple (pas de dépendances entre des briques qui doivent être buildées séparément)
- dépend donc du choix suivant MPA/SPA
- MPA / SPA
- SPA: un frontend distinct du backend
- permet plus de libertés en terme d'interaction
- changer de 'page' en gardant un contexte global
- ex: avoir une vidéo en lecture tout en se baladant
- première page plus longue à afficher mais les suivantes rapides
- coûts en terme de complexité de dev:
- gérer les aller-retours avec le serveur (état loading, état erreur)
- gérer le routing
- deux états à gérer: serveur et client
- MPA: le serveur retourne du html, chaque changement de page est un aller-retour avec le serveur
- plus simple: le serveur s'occupe d'aller chercher la donnée via une query, puis passe la donnée à la vue qui retourne du html, qui est retourné au client
- affichage immédiat pour le client dès la première page
- on garde la possibilité de retourner du js pour rendre la page interactive
- un bundle par page pour enrichir les interactions, au lieu d'un bundle global
- si on utilise un framework UI fait pour du SPA (React, Vue, Angular, Svelte,...), on peut coder la logique d'interaction au sein du template
- simple à basculer sur SPA le cas échéant
- une page avec des interactions poussées (routing, appels api) fait passer de MPA à SPA sans bascule particulière
- PWA
- on ajoute une worker pour rendre l'application (ou juste une page) accessible et fonctionnelle offline
- dans l'esprit c'est pour du SPA mais on peut aussi imaginer rendre une seule page fortement interactive PWA
- Framework front
- PAD a fait seulement du React
- MG et RC ont fait principalement du Angular mais se mettent à React, apprécient le coté fonctionnel, vont en faire dans le cadre de leur mission à court terme et sont intéressés de partir là-dessus pour ce boilerplate
- on choisit donc React
- on peut imaginer ensuite de donner des exemples d'utilisation d'autres libs
- c'est aussi un bon exemple d'avantage d'archi clean/hexa: on peut avoir un framework par page, ils cohabitent
- les avantages de ce boilerplate se situent avant tout coté archi (backend)
- ce serait dommage que le choix de React détournent certaines personnes
- Déploiement
- rester agnostique autant que possible pour éviter les contraintes
- Clever cloud, scalingo, aws, terraform, etc.
- on commence avec un, puis on inclut les autres
## 10/01/2022 : Deuxième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
Romain a une proposition de CI à faire.
Marc & PA ont participé à la présentation de Remix par Paul "Tchak" Chavard. Ca nous semble vraiment bien fait. Est-ce que ça peut être intéressant de l'utiliser pour OKLM ?
Implications
- Remix gère la stack complete: le serveur web (reçoit les requêtes http et retourne du html/js/...) et son intégration avec les vues (React)
- Un peu trop spécifique: risque de ne pas parler à un assez grand monde
- Pas assez mature, trop exotique
- On peut le faire dans un second temps, comme POC (en gardant l'architecture globale)
Objectif de ce soir: "Hello world !", embarquant typescript, express, react, webpack, jest
(on fera une passe tooling (prettier, CI, etc.) par la suite)
On se rend vite compte qu'on doit se mettre d'accord sur les termes et concepts employés. Dès le départ, on n'est pas alignés sur le sens du terme "**application**".
- Pour Marc, il s'agit de l'ensemble des règles métier liées aux cas d'usages (le rouge dans le schéma Clean Architecture "Application Business Rules").
<img src="https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg"/>
- Pour Pierre-Antoine, il s'agit de la partie qu'on peut lancer (ie un process) et donc de la concrétisation (ex: serveur api, application client, worker indépendant). Ca correspond à l'adapter primaire de l'archi hexagonale (à gauche sur le schéma).
Hexagonal architecture
<img src="https://1.bp.blogspot.com/-gMjQfR3vFeE/X-3cmlDLobI/AAAAAAAABmw/QD9LT4mZ1SwHmi-1ceAOOdrudKrMygQYQCLcBGAsYHQ/s1112/hexagonale_architecture.jpg"/>
Pierre-Antoine fait un tour rapide sur l'arborescence Potentiel et un décalage se fait sentir.
Ce que Pierre-Antoine appelle "**DTO**", Marc et Romain appellent "**Transfert**" mais leur signification n'est pas tout à fait la même.
- Pour Pierre-Antoine, il s'agit de ce qui sort d'une query et qui est destiné à une vue.
- Pour Marc et Romain, il y a plusieurs interaces où un transfert a lieu:
- entre les DB et le repository (qui fournit aussi les données du read model, contrairement au CQRS où le repository ne charge que le write model)
- entre le repository et l'api
- entre l'api et l'application front (contexte SPA)
- entre l'appli front et les composants qui le composent
- Ces différences s'expliquent par deux grandes différences entre les projets
- MPA vs SPA
- En MPA, le controlleur du backend fournit directement les données à la vue (Controller (back) -> vue (back))
- En SPA, l'application front appelle une api, qui est traité par une controller, qui appelle ensuite une db (presenter/use-case (front) -> controller (back) -> presenter/use-case (front) -> vue (front))
- Il y a donc une interface de plus en SPA vs MPA, ce qui explique
- CQRS vs non-CQRS
- En CQRS, le modèle de lecture et d'écriture sont séparés.
- C'est le rôle d'une _Query_ d'aller chercher les données nécessaires pour afficher une vue, à leur source (base de données)
- Une query retourne un objet mis en forme pour les besoins spécifiques de cette vue (pas de réutilisation des query, elles sont spécifiques et non génériques)
- Une vue = une query
- On peut appeler cette forme _DTO_ ou encore _Props_ puisque c'est la même chose
- Ce DTO est le _modèle de lecture_
- On peut aller plus loin: une vue = une query = une table/vue de la db (mais c'est pas une contrainte, juste une optimisation de découplage)
- Une query est implémentée dans la couche infra et appellée directement par le controller
- il n'y a pas de règle métier dans une query, c'est une lecture simple, même s'il peut y avoir des règles liées aux droits de l'utilisateur
- Le rôle des _Commandes_ est d'appliquer des règles métier qui provoquent une mutation du système en fonction de l'état actuel du celui-ci
- _Use-case_ = commande
- L'état actuel du système nécessaire pour appliquer la règle métier est appelé _modèle d'écriture_
- une règle métier sur le modèle d'écriture s'appelle aussi _invariant_
- le modèle d'écriture n'est pas chargé à partir des mêmes tables/vues que les query, il a sa propre source de vérité (en event-sourcing, c'est l'historique des événements situés dans l'_event store_)
- le modèle d'écriture est encapsulé dans un _Agrégat_
- l'agrégat concerne un état partiel du système (ex: une instance de `Candidat` pour chaque personne physique)
- il peut arriver qu'un agrégat deviennent très gros et qu'on décide ou non de le découper (il n'y a pas de règle stricte sur ses contours)
- l'agrégat a des _commandes_, qui sont les mutations attendues sur le scope de cet agrégat (ex: `Candidat.accepter()`)
- l'agrégat a la connaissance suffisante de l'état du système pour executer ces commandes (ex: Si `accepter` n'est possible que pour les candidats majeurs, `Candidat` aura `candidateAge`)
- c'est le repository qui lui donne ces données au chargement
- l'agrégat n'a pas de dépendances et n'effectue pas d'appel à une source de données tiers
- les données de l'agrégat ne sont en général pas utilisées à l'extérieur de celui-ci
- éventuellement par le use-case mais jamais par une vue
- si état+règle aboutissent à une illégalité (ie l'invariant ne serait pas respecté si la commande aboutissait), alors la commande échoue et retourne une erreur spécifique au problème (ex: `CandidatTropJeuneError`)
- si la commande aboutit, l'agrégat conserve la mutation en interne (en event sourcing, ce serait un ou plusieurs événements rajoutés dans une liste d'attente) et ne retourne rien
- `MyAgregate.doMyAction: (args) => Promise<void>`
- le use-case obtient un agrégat via un _Repository_
- le repository effectue l'appel à la base de données et se sert des données brutes pour initialiser l'agrégat
- il appartient à l'infra et est injecté dans le use-case
- `Repository<MyAggregate>.load: (aggregateId) => Promise<MyAggregate>`
- le use-case appelle des commandes sur l'agrégat
- en cas de succès, le use-case sauvegarde l'agrégat via le repository
- `Repository<MyAggregate>.save: (aggregate: MyAggregate) => Promise<void>`
- le repository va chercher les mutations internes à l'agrégat et les sauvegarde en db
- en event-sourcing, le repository récupère les événements présents dans la liste d'attente de l'agrégat et les publie sur l'event store.
- En non-CQRS, il n'y a pas de distinction entre lecture et écriture
- Ce sont les mêmes données qui servent à appliquer les règles métier que pour afficher
- Un repository est chargé de récupérer les données en base et aussi d'effectuer des changements sur ces données
- Des mappers prennent les données brutes qui correspondent au résultat d'une requête en base auquel est associé un model de donnée et construisents des objets métiers en affectant les paramettres nécessaires pour la créations des entités dans core. D'autres mappers permettent de passer de produire des objets de transfert adaptés aux besoins de l'application (vues, features) à partir du modèle métier défini dans core.
Si on part sur du CQRS, il faudra garder à l'esprit la place de chaque concept dans la distinction entre Commande (écriture) et Query (lecture)
- Lecture: query, vues, dto/props, projection (table dédiée aux vues)
- Ecriture: commande, use-cases, core, règles métier, agrégat, repository
On essaye ensuite de concrétiser une arborescence
Arborescence:
- src
- "oklm" => instance concrète (ie lancer un serveur http à partir des dépendances: variables d'environnement, modules métiers, adapters d'infra, ...)
- index.ts
- config/bootstrap/init (lieu d'injection des dépendances, pas forcément nécessaire si on fait l'injection lors de l'appel)
- infra
- gestion de la data (queries, projections)
- un dossier par context borné
- accès à des api tiers
- UI / Views
- layouts, composants, ... (react)
- templates de mail
- modules/domains/contexts/areas/features => bounded contexts
- "my bounded context"
- use-cases (front et back)
- core
- events
- DTOs (types avec la forme des objets )
- infra
- gestion de la donnée
- UI ?
Rq: on peut découper par bounded context ou par couche archi (couleur clean archi)
Pour l'infra, soit c'est un dossier haut-niveau avec un sous-dossier par BC, soit c'est un sous-dossier de chaque BC.
On peut approcher le dev de l'UI par le métier (use-case, presenter, composants) ou par l'UX (ce que je vois, ce que je veux faire, visuel puis extraction des logiques métier).
Injection de dépendances:
Soit lors du lancement de l'application (comme sur Potentiel dans config).
Soit lors de l'appel au controller (on injecte au moment de l'appel, pas besoin de dossier/fichier config)
Si on le temps d'ici la prochaine fois, on peut déjà avancer un peu sur le boilerplate: tsconfig.json, express, react, prettier, jest, ...
Histoire d'avoir un "Hello world" à la prochaine séance OKLM.
Pour une traçabilité des changements, on peut faire une PR et la merger tout de suite.
Rdv lundi prochain à la même heure !
Remarque Pierre-Antoine: _J'ai été autant embrouillé et inspiré par cette conversation. Une intuition que quelque chose clochait dans nos discussion m'a forcé à faire le point sur pas mal de concepts. J'ai amendé ce rapport pour tenter d'expliquer nos différences de points de vue (cf MPA/SPA, CQRS/non-CSQRS) et pour ce qui est des propositions de convergence/clarification, je poursuis dans une issue sur le repo github._
## 20/01/2022 : Troisième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
RETEX Remix
https://remix.run/blog/remix-vs-next
Pour chaque route de query/page:
- handler express (router.get('/machin', () => { ... }))
```ts
import { getMachin } from 'infra'
import { MachinPage } from 'views'
router.get('/machin', (request, response) => {
if(!request.user){
response.status(403).send()
return
}
const data = await getMachin()
response.json(data).send()
// or
response.send(renderToHtml(MachinPage(data)))
})
export const MachinPage = (data: MachinDTO) => {
// possibilité A: mon template est entièrement défini dans le fichier
return (<div>Coucou {data.hello}</div>)
}
router.post('/machin', (request, response) => {
})
// machin.stories.tsx
import { MachinPage } from '/machin'
// ne marcherait pas parce que le module /machin importe du code back-only
```
- règles d'accès: middleware express (enrichi l'objet Request)
- query (sur la db): getMachin()
- DTO: MachinDTO / MachinProps
- MachinPage.tsx: (props: MachinDTO) => JSX.Element
Sur Remix ça donne:
```ts
// app/machin.tsx => /machin
import { db } from 'infra'
type MachinDTO = {
hello: string
}
export const getMachin = (db) => () => {
return await db.query({ where: { }})
}
export const action = ({ request, formData }) => {
const choix = formData.get('choix')
// const { choix } = request.body
// validation
return await useCase(choix)
}
export const loader = (request) => {
const data: MachinDTO = getMachin(db)()
return data
}
export default MachinPage = () => {
//
const data = useLoaderData<MachinDTO>()
// submit va appeler la méthode action()
return (<form><input type="text" name="choix" /><button type="submit"></form>)
// possibilité A: mon template est entièrement défini dans le fichier
return (<div>Coucou {data.hello}</div>)
// possibilité B: j'appelle un composant visuel
return <MonComposant data={data}/>
}
// machin.integration.ts
import { getMachin } from 'machin.tsx'
describe('', () => {
})
```
```
// posts.tsx
export const loader = () => {
return 5
}
export default PostsLayout = () => {
const postCount = useLoaderData()
return (
<div>
Il y a {postCount} posts.
<Outlet/>
</div>
)
}
```
- app
- routes
- projectListPage (exemple de query)
- ProjectListPage.tsx
- composant visuel (props: ProjectListPageProps) => JSX
- exporte la définition du type ProjectListPageProps
- export const projectListPageRoute = '/projets'
- components/
- sous-composants de ProjectListPage.tsx
- getProjectListPageProps.ts
- query (db -> dto)
- (args) => Promise<ProjectListPageProps/>
- getProjectListPageProps.integration.ts
- test d'integration de la query
- projectListPage.handler.ts
- express handler
- appelle getProjectListPageProps()
- build la vue ProjectListPage en lui passant les données reçus
- retourne le html ou le json
- addProject (exemple de command)
- TODO
- infra ("libs" ?)
- api tiers (mail, authentification externe, ...)
- middlewares
- table de données (projections)
- et projecteurs (update de projections)
- eventBus/eventStore
- UI
- composants partagés par plusieurs pages
- les sous-composants des pages se situent dans le dossier components/ de la page
- utils
- helpers de l'application
- domain/modules/core
- use-cases
- événements
- agrégats
- TODO
```ts
// NavBar.tsx
// Si c'est colocalisé
// NB: probablement auto-importé par l'IDE
import { projectListPageRoute } from '@routes/projectListPage'
import { otherPageRoute } from '@routes/otherPage'
// Si c'est centralisé
import { projectListPageRoute, otherPageRoute } from 'routes.ts'
export const NavBar = () => {
return (
<ul>
<li>
<a href={projectListPageRoute}>Liste des Projets</a>
</li>
<li>
<a href={otherPageRoute}>Autre</a>
</li>
</ul>
)
}
```
## 24/01/2022 : Quatrième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
SpeedOps:
- Extension du TDD, en incluant la delivery
- Déployer dès le day-1 et conserver des cycles de déploiement de l'ordre de l'heure
- Maitriser le déploiement:
- le code
- les dépendances infra
- l'état (ex: données dans la base démo)
- Empower le reste de l'équipe
- faire une séance de pair avec quelqu'un du métier
- lui montrer en live le changement
- l'équipe a confiance dans la capacité à livrer très régulièrement
- bonne communication sur le besoins métier
- le fait que le code ressemble au métier aide énormément (ubiquitous language)
Comment convaincre les autres devs ?
- Baisser la charge cognitive plutot que de dire de lire des bouquins
- termes spécifiques à archi
- Outils
- boilerplate
- exemples
DRY nous incite à la généricité (partager un morceau de code pour plusieurs besoins métiers).
- mal compris => devrait s'appliquer à un invariant métier et pas juste à de la répétition de lignes de code
- ça a un coût, puisque ça crée un couplage
- YAGNI + TDD => à contre-courant de DRY, nous incite à faire le moins possible pour que le test passe
- on rejette l'optimisation prématurée
- le refactoring vient en 2nd
Est-ce qu'on intègre d'autres devs dans nos discussions ?
- Plus on est nombreux, plus de points de vue,
- meilleure solution ?
- pas de convergence (bike-shedding) ?
- design by commitee
- Faire une première version à 3 puis intégrer les autres
- ils pourront challenger nos choix
- mais ils le feront sans doute sur les choix les plus importants et pas les trucs futiles
*C'est quoi OKLM ?*
Romain: "Création d'un boilerplate pour un projet betagouv, similaire à ce qu'on aurait pu faire avec Démarche Simplifiées, en utilisant une archi sympa et en respectant l'état de l'art des bonnes pratiques.
Respecter certains principes (SpeedOps/DevOps/GitOps/DeployOps).
Aider les devs qui n'ont pas de facilité pour discuter avec leur PO/équipe."
Marc: "Viser l'adoption par la communauté betagouv. Ca doit être une alternative positive et accessible pour les développeurs, même s'ils ne sont pas au courant des bonnes pratiques, qu'ils vont adopter malgré eux. Proposer quelque chose qui fonctionne très rapidement et en se conformant à ce projet, on prend des bonnes pratiques."
PA: "Un exemple réaliste d'application typescript mettant en oeuvre une serie de pratiques visant à développer sereinement un produit durable (robuste et maintenable)."
*Pour quoi adopter OKLM ?*
- Livrer vite et souvent
- Pouvoir être responsable de la prod sans perdre sa sérennité
- ownership
- Créer du code avec la transmission en tête
- écrire ton code et la doc comme si c'était une conversation avec un maintainer futur
- readme : section "Dear Maintainer"
- autre point de vue: "Les suivants pourront reprendre le projet sans mal"
- Un outil pour toute l'équipe
- l'application répond aux besoins métier et en épouse les contours
- le développement comme une conversation entre toutes les parties prenantes
*Quels principes / pratiques ?*
*Comment fait OKLM ?*
- Découplage métier / infra
- limiter sa dépendance à des libs tiers (API, dbs, ...)
- isoler le métier pour mieux le concevoir
- "Je change de lib sans toucher au code métier"
- Découplage domaines métier
- séparer les responsabilités indépendantes pour mieux les concevoir
- "Je change les règles d'un contexte métier sans toucher aux autres"
- Découpler modèle de données et données
- être capable de modifier
- "Je pousse un nouveau schéma de BDD en prod, sans m'embêter avec la migration des données"
- Ne pas être dépendant à un framework
- l'orchestration de l'application se fait par convention, librement, et non via une API
- Des outils pour améliorer la DX
- analyse (types, linters, autoformattage, visualisation, ...)
- génération de code (si un pattern nécessite du boilerplate)
*Comment est-ce que je m'en servirais ?*
- Comme starter pour mon nouveau projet typescript/node
- Pour avoir des exemples fonctionnels de certains pratiques, d'usage de certaines libs
- Comme support de discussion entre devs
- partir d'un fork pour proposer un changement, une intégration, etc. et lancer la discussion !
## 31/01/2022 : Cinquième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
Comment gérer l'aspect humain au démarrage d'un projet ? Il n'y a pas encore de confiance entre le client et le dev et il n'y a pas grand chose à livrer à court terme.
Marc démarre souvent par la UI, même si elle n'est branchée à rien. Ca rend les choses concrètes et visibles.
Ca ne marche que si tu es capable de faire une UI à moindre coût...
Laisser le temps faire son travail pour la confiance.
Marc: DSFR très bien construit, du beau sass. Va dans le détail pour les composants mais il manquerait quelques utilitaires (pour l'alignement, flex). Ca peut être intéressant d'ajouter quelques utilitaires perso ou bien tiré d'une lib (genre tailwind).
Mob oklm:
- Typescript
- Express
- React
- Hello world !
- Prettier
- Eslint
- Jest
Reste encore eslint qui ne s'interface pas correctement avec l'IDE.
Next up:
- Cas d'usage réaliste pour aborder l'archi par la pratique
- Idées d'application un peu complexe (style startup betagouv avec des notions de gestion poussées)
- Déployons sur un PaaS !
autre manager de paquet qui à l'air cool ? : https://pnpm.io/
Autre lien : https://docs.renovatebot.com/
PAD: à discuter la prochaine fois aussi: qu'est-ce qu'on cherche en terme d'impact d'oklm (pour nous? pour les autres?) ?
Nous faisons des choix (d'archi, de technos, outils, etc.) et ce serait intéressant d'utiliser la mesure d'impact pour nous guider.
Jérome qui vend : https://turborepo.org à étudier ?
## 07/02/2022 : Sixième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
Jérôme Burkard
Lancement des instances de demo/devs/test liée aux branches features.
Données de prod ? Dump la prod lors de la CD, création de db et restore avec la dump.
Event sourcing: les seeds sont sous la forme d'un historique d'événements, qui lors de la CD, sont injectés dans la table de vérité et puis on lance de rebuild pour avoir les données dans les projections.
On peut imaginer que sur une feature branche, on rajoute un fichier qui porte le nom de la branche et que la CD cherche un fichier qui porte ce nom pour le charger.
Il serait possible d'exporter un historique d'événements en faisant le scénario en local.
Découper les aspects techniques pour éviter la surcharge mentale des dévelopeurs qui découvrent oklm.
- Des choix par défaut qui sont assez simples et courants pour éviter la friction lors de l'arivée
- Proposer d'aller plus loin sur tel et tel sujet
Est-ce qu'on doit proposer la version complète d'eslint par défaut ou bien la version simple (et proposer d'activer la version complète en opt-in) ?
- eslintrc.js (simple) et eslintrc.advanced.js (complet), et si on veut en activer l'une ou l'autre config, il suffit de renommer le fichier qu'on désire
Comment trouver la bonne limite entre l'usage d'une lib externe et refaire soi-même ?
Commençons par une config complète et retirons les règles qui nous causent des frictions (ie font chier).
Idées d'application:
- Assez simple pour être compréhensible
- Assez complexe pour illustrer les concepts d'archi
- Ce serait dommage qu'on se dise "j'aurais fait ça plus vite en CRUD"
- Bonus: ça sert vraiment à la communauté
Idées:
- Stack overflow pour betagouv
- Annuaire sur les domaines d'expertises des devs beta
- Annuaire des domaines techniques traités par la communauté
- On en a parlé sur domaine-dev mais c'est perdu par la suite
- Bot mattermost qui fait le lien entre des discussions et une base persistente ?
- Très peu de choses finissent dans la documentation betagouv
- scanner les repos pour voir qui est concerné par quels technos/libs
Pitch:
Le stackoverflow de la communauté betagouv
Point de départ:
- Pouvoir poser une question
- Pouvoir répondre à une question
- Pouvoir lister les questions
- Pouvoir faire une recherche dans les questions
Anglais ou français ? Le sujet se prête à un traduction facile en anglais.
Langage:
- Topic
Alternative: Démarches (moins) Simplifiées (viser 90% des besoins projets betagouv)
- Authentification/Autorisation
- avec roles
- Un formulaire pour les usagers
- Avec des champs riches
- geo
- entreprise
- Une interface admin pour voir les dépots des usagers
- et faire une action dessus ?
- accepter/refuser
- poser une question (pièce complémentaire)
- télécharger un fichier csv d'export des données
- émettre une attestation pdf
- ...
- Envoi de mails de notification lors du traitement
- Carte géographique des dépots
- Statistiques
L'idée n'est pas de réinventer des implémentations pour toutes ces briques mais plutot de donner un exemple de mise en oeuvre de libs/services tiers (potentiellement plusieurs choix alternatifs).
Libs/services tiers à intégrer :
- Mail
- Authentification
- Export excel/zip
Fonctionnalités :
- Formulaire dépôt
- Espace d'administration
Objectifs pour la prochaine:
- mob pour coder
- "En tant qu'usager, je peux me connecter"
- on vise le vrai parcours visuel/fonctionnel
- fake auth
- "En tant qu'usager, je peux remplir mon formulaire de dépôt"
- simple POST, émission d'un événement
Pour la prochaine fois, si envie et dispo:
- CI/CD
- docker / Adapter de db
A appliquer: règle des 5 minutes
Monad ou pas ? Ou pas.
(super utile quand on sait s'en servir mais retire de la lisibilité et fait peur à ceux qui ne connaissent et/ou ne veulent pas connaitre)
## 21/02/2022 : Septième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
Jérôme Burkard
Questions soulevées:
- transactions et role du répo
- PA:
- pessimistic concurrency: repo.transaction(aggregateId, callback)
- optimistic concurrency: repo.load(aggregateId), puis repo.save(aggregate)
- le save() fail si jamais il y a eu un changement depuis le load
- Jérôme:
- repo.load(aggregateId, transaction) + repo.save(aggregate, transaction)
- on a un objet transaction qui permet de bloquer
- permet d'éviter l'écriture transaction + callback et d'avoir un truc plus impératif
- Evenements: maintenir un type union ?
- Jérôme déclare tous ses événements au même endroit et dans une union
- permet d'avoir le type du payload juste avec le topic(type) de l'événement
- PA préfère déclarer ses événements dans leur module respectif
- évite de devoir maintenir un type union avec tous les événements
- utilise un généric pour les subscribe, afin d'avoir le type du payload
Nous avons créé un diagramme pour expliquer la fonctionnement d'un système CQRS/ES.
## 28/02/2022 : Huitième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
Jérôme Burkard
Nous avons continué la description du diagramme CQRS/ES
La commande vient du client et contient l'id de l'objet à créer / modifier dans son payload. Cet id ne vient pas de la base de données comme on peut le voir.
- Est-ce qu'il y a un risque de collision (utilisation d'un id deja affecté) ?
- Non, parce qu'on va charger un agrégat en utilisant cet id, alors on saura s'il est déjà existant
- Ca résout au passage la problème de la soumission multiple accidentelle d'un formulaire (un seul réussira, ce qui rend la requête idempotente)
- Est-ce qu'il y a un risque de triche (utilisation volontaire d'un id d'objet existant) ?
- Pas de différence avec un pattern "classique", c'est à la couche d'authZ de faire des vérifications
- Quels avantages ?
- Mode offline
- On peut préparer plusieurs commandes (création, mise à jour, liaison, ...) sur des entités sans avoir à passer par le serveur
- exemple: je crée le projet avec id "ABC", je peux ajouter un titre à "ABC" puis le supprimer sans avoir à attendre un id du serveur, chaque commande connait déjà l'id de l'entité, avant même qu'elle n'existe coté serveur.
- Mode distribué
- Plusieurs services peuvent écouter les commandes et ses effets, et agir sans attendre les autres
- ex: J'invite un utilisateur à accéder à mon projet, un événement UtilisaterInvitéSurProjet est émis par la commande, avec dans son payload (projectId, userId=email)
- Mon contexte Users reçoit cet événement et vérifie si l'utilisateur existe
- s'il n'existe pas, il est créé et une invitation est envoyée
- Mon contexte Authorization rajoute un ACL pour le couple (projectId, email)
- NB: il n'a pas eu besoin d'un id d'utilisateur de la part de mon contexte Users, l'email sert d'id naturel de mon utilisateur
- Ces contextes peuvent s'executer en parallèle, vu que le payload contient l'id de l'utilisateur
- ils sont découplés
- dans un schéma classique, j'aurais d'abord créé un utilisateur en lui affectant un id unique, puis j'aurais rajouté un ACL, créant ainsi un couplage.
Stratégies pour parer à l'eventual consistency:
- coté client, faire un redirection/refresh au bout d'un certain temps
- pour laisser le temps aux projections de se mettre à jour
- PA: ce qu'on fait, c'est qu'on affiche une page genre "Votre demande a bien été acceptée" avec un lien "Retourner à ma demande", ce qui fait gagner un peu de temps.
- coté client, faire de l'optimistic update
- mettre à jour la page sans passer par une requête au serveur
- on "devine" à quoi la page ressemblera si la commande passe
- si la commande passe, ce qui est affiché est déjà à jour, rien à changer
- si la commande fail, rollback l'affichage et afficher un message d'erreur
- particulièrement efficace pour les commandes qui sont quasi-sures de passer
- coté serveur, executer les mises à jour de projection en synchrone
- quand la requête revient avec success, les projections sont déjà à jour, les query peuvent être appelées dans la foulée
- marche bien pour les mises à jour de projections courtes (genre ecriture en base)
- que faire en cas de succès de la commande (l'événement est dans l'event store) mais une mise à jour de projection fail ?
- on considère que la requête avec quand même successful parce que la source de vérité est à jour
- mais il faut un retry sur la projection
- bonne idée pour un projet en démarrage
- fonctionne avec l'implémentation in-memory conçue par Jérôme
Pour les stratégies de retry, Redis Streams offre une solution intéressante:
- chaque projecteur / event handler est dans un "consumer group" Redis
- chaque "consumer group" doit ACK explicitement le traitement de chaque événement
- il est donc possible de savoir quels événements ont été bien traités et par quels handlers
- on peut rajouter un orchestrateur qui rejoue les handlers sur les événements en échec, voire qui les mets dans une queue séparée si échecs répétés (Dead letter queue)
- c'est résilient au crash du serveur d'application
- le serveur relancé peut reprendre le travail où il a été arrêté
- rajoute une dépendance infra mais particulièrement intéressant pour les applications
Il est possible de switcher d'un mode (ex: event bus in memory -> Redis Streams) juste en changeant l'infra. Le reste de la logique (events, handlers) et l'historique ne sont pas affectés.
A gérer en fonction des besoins du projet.
Les projections peuvent évoluer et être reconstruite à partir de l'historique.
Mais que faire si un événement évolue ?
- On ne peut pas changer la forme du payload des événements a posteriori, parce qu'il y aurait des événements non-conformes dans l'historique
- attention, il est parfois trop simple de modifier le type d'un payload dans le code
- un linter pourrait peut-être alerter de certaines modifications hasardeuses (genre renommer une propriété, rajouter une propriété sans mentionner qu'elle peut être absente du payload, ...)
- On peut rajouter une propriété avec un type optionnel
- On peut rajouter une "version" sur le payload, pour que les handlers vérifient la version du payload avant d'utiliser le contenu
- On peut créer un nouvel événement qui vient "annuler et remplacer" un autre événement
- on crée un nouveau type d'événement
- on peut utiliser une date dans le passé pour le nouvel événement (ex: la même date que l'ancien événement)
- on crée un script de migration qui, pour chaque ancien événement, va émettre un nouvel événement
- l'historique conservera les anciens événements mais on peut les ignorer
- on mets à jour les handlers et les agrégats concernés
- pour écouter le nouvel événement
- pour ignore les anciens événements
- on marque l'ancien événement comme deprecated
Ce découplage écriture/lecture nous permets d'ajuster la complexité/charge coté lecture ou écriture et donc l'adapter aux besoins du projet.
Ex: pour un item qui est lourd à lire (ex: générer un export de plusieurs Go), on peut imaginer le générer/mettre à jour à chaque événement. La charge sera à l'écriture et non à la lecture (le fichier est pré-généré)
Ex2: pour des éléments qui changent très souvent et qui dépendent fortement les uns des autres, on va éviter de dénormaliser à l'écriture, privilégier des écriture simples dans plusieurs tables, et faire une jointure à la query. Une dénormalisation demande un nombre quadratique de mises à jour de projection. A réserver pour les cas simples.
NEXT : Projection en vrai avec postgres
EN bonus : projection en csv/html ?
## 07/03/2022 : Neuvième session de mob
Pierre-Antoine Duchateau
Marc Gavanier
Objectif: projections avec postgres
(On peut garder un event store in memory pour le moment, même si c'est un peu illogique de persister les projections mais pas l'historique)
Pour le rebuild, vu qu'on n'aura aucun événement dans l'historique au démarrage (il est in-memory), on peut lancer un script de seeds qui injecte une série d'événements dans l'historique avant de lancer le script qui construit les projections.
Les mises à jour de projections et les query seront plus simples que dans un projet non-CQRS, essayons de faire du vanilla sql pour éviter un ORM.
Nous avançons bien sur la partie postgres avec
- Une notion de projection avec une API
- create: chargé de faire la mise en place coté infra (en postgres: le CREATE TABLE...)
- buildFromEvent:
- appelé pour chaque événement de l'historique et pour chaque projection
- chargé d'envoyer l'événement aux projecteurs compétents
- initEventBus:
- appelé lors du lancement de l'application pour appeler "eventBus.subscribe" pour chaque projecteur
- Un script migrate.ts qui peut être lancé via `yarn migrate` pour lancer les seeds et ensuite construire les projections, en appelant d'abord `create` puis chargé l'historique et appeller `buildFromEvent`.
On a mis à jour les pages DemandePage et DemandeListPage pour avoir un parcours fonctionnel.
On a testé la mise à jour de la projection `demandes` via `yarn migrate` à chaque fois qu'on a changé son schéma. Ca marche au poil.
Pour l'instant, c'est une migration "bourrin", qui drop la table, en crée une nouvelle et la construit from scratch. Plus tard, on voudra sans doute vérifier que le schéma de la table a changé avant de la drop.
On a eu des soucis avec l'event store in-memory. Forcément, ça le remets à zéro à chaque lancement d'application et il est donc vide, alors que les projections, elles, ont été construite sur une infra persistente lors du `yarn migrate`.
La prochaine étape naturelle est donc d'implémenter un event store sur base de postgres.
Ce n'est pas évident, parce qu'il faudra implémenter la logique de transaction coté postgres plutot que coté nodejs.
## 11/03/2022
Pierre-Antoine Duchateau
J'ai implémenté un event-store en postgres et adapté le processus de migration/seed pour nos besoins immédiat (les seeds s'executent à chaque migrate, ce qui ne serait pas pertinent en prod evidemment). On pourra séparer les seeds de la migration plus tard (ou alors ne pas runner les seeds si c'est la prod en se basant sur les variables d'environnement).
Il y a en réalité quatre étapes:
- Création du schéma postgres de l'event store
- uniquement s'il n'est pas existant
- on ne drop jamais cette table
- Insertion d'événements de seeds
- dans l'eventstore
- sans publication sur l'event bus
- on ne veut pas déclencher les effets, juste mettre à jour l'historique
- Recréation des projections
- si nécessaire
- c'est la projection elle-même qui détermine si c'est nécessaire
- en général, drop la table et la récrée
- on pourrait imaginer créer une table séparée le temps de la builder et drop l'ancienne et renommer la nouvelle à la dernière minute pour ne pas disrupter l'application.
- Rejouer les événements de l'historique
- uniquement pour les projections reconstruites
J'ai mis en place un event dispatcher, qui fait l'intermédiaire entre l'event bus et les projecteurs (handlers qui mettent à jour la projection).
C'est l'event dispatcher qui joue le role de chef de gare pour envoyer les événements du bon type au bon handler. Celui-ci est branché à l'eventbus via `subscribeAll`.
Next up:
- Refaire une passe tous ensemble sur le fonctionnement des projections
- et l'implémentation
- Poursuivre dans les use-cases métier pour voir naitre les besoins d'archi
## 14/03/2022 Dixième session de mob
Marc Gavanier
Romain Cambonie
Pierre-Antoine Duchâteau
EventStore:
Que faire si un événement est écrit en base mais la publication fail ?
S'il y a plusieurs événements et certains ont été publiés avant un échec, on ne peut pas rollback (les effets sont déjà là).
L'event bus doit avoir sa propre logique de retry: 'publish' ne peut pas fail. Soit un événement est effectivement émis, soit il sera émis plus tard.
Outbox pattern: les événements à emettre sont insérés dans une table spéciale "outbox", dans la même transaction que l'historique. C'est possible parce que l'outbox est dans la même base de données que l'historique.
L'eventbus lui-même vient consommer ces événements dans l'outbox. En cas d'échec d'émission, on peut mettre en place une politique de retry (ou d'abandon) de ces événements.
L'eventbus actuel est in-memory et crée donc un couplage bas-niveau entre l'appel à publish et l'execution des effets des handlers.
Si un effet est de mettre à jour une projection, ça passe.
Le jour où on a un event handler qui execute un use-case qui émet des événements, on va avoir un blocage parce qu'on a un lock sur la table events.
Une solution serait d'implémenter un event bus découplé, pour lequel les effets sont déclenchés dans un autre contexte (ex: event emitter, redis).
What's next ?
Nous sommes partant pour attaquer les couches Authentification et Autorisation: ce sont des sujets complexes et que toutes les applications doivent adresser.
Cas métier:
- Pour déposer une demande, il faut être connecté en tant que conducteur
- Une demande a un type (ex: récupération de points de permis de conduire: réclamation ou inscription à un stage)
- Chaque instructeur est affecté à un type (il ne peut répondre qu'à celles-ci)
- Un instructeur peut refuser une demande ou l'envoyer pour acceptation par un administrateur
- Un administrateur est chargé de valider l'acceptation des demandes instruites par les instructeurs (ou les refuser).
- Les conducteurs peuvent créer leur compte
- Les administrateurs qui créent les comptes des instructeurs
- ceux-ci reçoivent un mail d'invitation
Roles: Conducteur, Instructeur (type), Administrateur
- Pour être connecté en tant qu'usager je peux rentrer un login email et mot de passe
- Pour être connecté en tant qu'usager je peux rentrer mon numéro de téléphone avec un identifiant de l'appareil
- France connect :) ?
- Les instructeurs/administrateurs doivent utiliser un 2FA
Découpler
- Authentification
- Autorisation
- Profil
Bien prendre en compte les cas où
- un usager s'inscrit
- un instructeur est invité à gérer des demandes par un administrateur
## 25/03/2022 Onzième session de mob
Jérôme Burkard
Romain Cambonie
Pierre-Antoine Duchâteau
Nous avons attaqué la partie authentification.
- Installation de keycloak (dans un conteneur docker)
- Configuration de keycloak : création d'un client 'oklm-web' pour permettre un login
- Middleware express pour vérifer l'identité
- redirige sur la page de login en cas d'absence de session
- session in memory pour le moment
Next up:
- thème DSFR keycloak
- couche d'autorisation (dans le use-case ? dans le controleur ? via des types ?)
## 28/03/2022 Douzième session
Jérôme Burkard
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
express-session: comment faire pour scaler une application ? (est-ce qu'un store de session persisté suffirait à pouvoir balancer la charge ?)
Comment savoir quand un utilisateur arrive pour la première fois ?
- absence de role dans la session qui nous indique que c'est un utilisateur
- initier la création de compte dans l'application, puis diriger vers keycloak pour la création de mot de passe, et utiliser le callback (url qui contient un token dont le payload contient les informations de l'utilisateur à créer, notamment son oklmUserId) pour lier le keycloakId à l'utilisateur qui vient d'être créé.
On a deux notions:
- l'utilisateur (au sens local d'oklm)
- ses identitiés
- peuvent être local
- auquel cas, c'est l'application elle-même qui se charge de vérifier l'identité, par exemple via un login/mot de passe
- peuvent être déléguées à un identity provider tiers
- ex: Keycloak
- chaque tiers a sa méthode de vérification de l'identité et son identifiant (ex: keycloakId, email, etc.)
Dans un application bateau, on n'a pas besoin de distinguer ces notions, parce qu'elles sont fusionnées dans l'application, qui joue le role d'identity provider et de client (consommateur d'identité).
Quand on délégue l'authentification à keycloak, on doit faire attention de ne pas tomber dans l'amalgame et bien comprendre le role de chacun.
Quand un utilisateur a une session ouverte sur keycloak, sa session ne contient que des identifiants sous la responsabilité de keycloak (id unique de keycloak et si c'est l'email, c'est un email dont keycloak s'est chargé de vérifier la validité).
Dans notre application, nous aurons notre propre notion d'utilisateur, liées à des ressources propres (par exemple: ses démarches). Pour cela, il conviendra de lui affecter un userId interne.
Il faudra ensuite lier l'identifiant de l'identity provider à notre identifiant interne.
Ca pourrait être l'adresse email, si c'est un identifiant du provider mais ça suppose qu'un utilisateur ne peut pas changer son email.
Le mieux est de gérer la liaison entre les deux via un table qui fait la correspondance entre userId et, par exemple, un keycloakId.
Comme nous sommes en event sourcing, nous n'attacherons pas beaucoup d'importance à la table elle-même (dont le schéma pourra changer), mais à l'évenement. Par exemple: CompteUsagerCréé (userId, nom, email, keycloakId).
Cet événement pourra mettre à jour une projection "usagers" si nous trouvons cela pratique.
Style DSFR keycloak
- On part d'un thème de base
- Nos utilisateurs voudront personnaliser leur thème pour avoir leur entête
- est-ce que c'est possible de leur proposer de partir de notre base dsfr et de ne modifier qu'un seul fichier ? (ex: header.ftl)
- l'objectif serait que l'utilisateur n'ait que les quelques fichiers qu'il a personnalisé dans son repo
- Pour distribuer:
- Repo github pour héberger le theme DSFR (qui servira de "base")
- Ajouter des étapes dans keycloak/Dockerfile pour puller ce theme
- créer un thème dans le repo avec un fichier theme.properties qui désigne dsfr comme base
- contient uniquement un fichier header.ftl à surcharger par l'utilisateur
Next up:
- Créer un repo oklmdev/keycloak-DSFR
- Mettre à jour le Dockerfile pour puller le code de oklmdev/keycloak-DSFR
- Faire un flux complet
## 08/03/2022 Treizième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Nous avons avancé sur la partie applicative de l'authN
- enrichissement de request avec l'utilisateur correspondant au keycloakId
- utiliser la session pour éviter de devoir faire la recherche d'utilisateurs pour chaque requête
- placement de toute la logique keycloak dans son propre dossier d'infra
- création d'un événement UtilisateurInscritViaIdentityProvider
- choix de l'id d'agrégat (provider+clé)
On a besoin d'un agrégat pour s'assurer qu'il n'y a pas plusieurs créations d'utilisateur pour le même provider+clé.
Next :
- définir agrégat identitée
- action inscrireUtilisateur
- vérifie qu'il n'y a pas d'autre événements UtilisateurInscritViaIdentityProvider pour ce provider+clé
- émet UtilisateurInscritViaIdentityProvider s'il n'y en a pas
- register...(), ouvrir transaction sur Identité et appeler l'action inscrireUtilisateur
## 11/03/2022 Quatorzième session
Romain Camboniex
Pierre-Antoine Duchateau
Marc Gavanier
Quel flux d'inscription ?
On garde le flux d'inscription via Keycloak, on pourra plus tard utiliser l'api coté front pour faire la même chose sans utiliser la page keycloak.
On a eu un problème de dépendance circulaire dans le dossier keycloak. On l'a résolu en faisant un import précis plutot que d'utiliser le fichier index.ts
Nous avons donc terminé la partie gestion de l'authentification avec Keycloak.
next :
Gestion de roles / right ?
(Eg 2 Demandeurs voient leur demandes respectives privés) => Ressource base auth
Accepter / refuser une demande nécessite les droits administrateurs
Seuls les demandeurs peuvent déposer des demandes
Plus tard: introduire la notion d'instructeur, qui voit un seul type de demandes.
## 02/05/2022 Quinzième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
On digresse totalement: on parle de transmission de mémoire familiale. PA quitte sa mission Potentiel pour bosser sur le sujet.
## 13/05/2022 Seizième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Nous sommes partis sur la création d'un formulaire pour saisir une nouvelle demande.
Nous l'avons intégré directement dans la page de listing des demandes, avec un post sans `action` ce qui fait que c'est le controller de la route en cours qui reçoit le payload.
Comme nous n'avons pas envie d'utiliser l'application en mode hot reload pour tester la composition de l'interface graphique, nous avons installé Storybook.
Remarque: storybook dit qu'il ne voit pas de stories et suggère de vérifier que le glob est correct, même quand le problème vient du fait que le fichier `.stories.tsx` ne contient juste pas ce qui est attendu (erreur de config affiché, erreur de syntaxe en cause).
Storybook est vraiment cool pour tester les UI sans avoir à l'intégrer dans l'app elle-même et faire des scénarii de tests manuels. C'est du VDD.
Dans un premier temps, on fait simplement un `publish` d'un événement `DemandeDéposée` dans le controlleur post. Mais on se rend compte qu'en cas de refresh de la page, on resoumets la même demande et elle est donc présente en doublon.
Nous décidons d'utiliser un agrégat pour gérer ce problème. Pour que ça fonctionne, il faut ouvrir une transaction sur un identifiant unique, qui sera le même à chaque soumission du formulaire.
Pour cela, l'identifiant doit provenir du formulaire lui-même, dans un champ `hidden`.
Ainsi, le controleur reçoit le corps du formulaire (y compris la `demandeId`), ouvre une transaction sur l'agrégat `Demande` qui a `demandeId` comme `agregateId`, et execute la nouvelle action `déposer`, qui vérifie que le status est bien à "nouvelle".
Nous nous arrêtons là, et gardons la partie AuthZ pour lundi.
Next step:
- `Demande.deposer` doit accepter un type fort pour `déposéePar` pour nous assurer qu'il s'agit bien d'un Demandeur.
- La vérification se fera dans le controleur qui appelle l'action (et non dans l'action elle-même qui n'a pas accès au domaine AuthZ)
- => implémenter un type guard qui fera la vérification et renforcera le type de `request.user` avant de le passer à `déposer`
- Les types en sortie de `request.body` sont par défaut `any`
- ça peut être utile de rajouter une validation (type yup) en début de controleur
## 16/05/2022 Dix-septième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Nous travaillons sur les type guards:
- typer en entrée d'action pour forcer un test des arguments en amont (dans le controleur)
- validation de corps de formulaires
- utilisation de zod
- création de validateurs custom zod (typés)
- ex: chaines de caractères non-vide
- number => Epoch (type fort pour s'assurer que ça sort d'une méthode qui utilise Date.getTime())
- authZ: vérification du role de l'utilisateur
Zod ou yup ?
L'inférence de type est similaire.
Reste à voir : la lisibilité des messages d'erreur (PAD avait opté pour yup sur Potentiel pour avoir une meilleure maitrise des messages d'erreur en Français).
Next up:
- Envoyer un formulaire avec des valeurs erronées
- retourner les messages d'erreur à l'utilisateur
- via page dédiée ?
- à coté du formulaire ?
- Update resolveUserFromKeycloak pour récupérer le role de l'utilisateur et le mettre dans la session
- DemandeurInscrit (role implicite)
- Keycloak: écoute DemandeurInscrit et affecte le role à l'utilisateur
- resolveUserFormKeycloak récupère le role de keycloak
- (si on avait besoin de lister les utilisateurs, on utiliserait une projection dédiée)
## 20/05/2022 Dix-huitième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Comment développer/financer/maintenir des projets transverses open-source au sein de betagouv ?
Développeur transverse, c'est un CDD et n'est pas remote.
https://github.com/betagouv/awesome-betagouv
https://github.com/romain-cambonie/serenity-workflow
Ca ne peut prendre que s'il y a un proto viable, celui-ci est forcément initialement auto-financé par les devs ?
L'état peut financer le déploiement et la maintenance, éventuellement des évolutions.
Si l'état ne finance pas, on peut imaginer de financer dans le privé (si l'outil est aussi pertinent dans un context plus générique).
Mature dev: les devs ne sont pas de bons clients, ne cherchez pas à construire des produits à leur intention.
Oui mais il est possible de vendre aux décisionnaires de la boite, vu que les produits ont aussi de la valeur pour eux.
Les startups d'état ne peuvent pas acheter des jours par ci par là, du fait du mode de facturation.
Il est possible de vendre des licences pour du code qui est disponible (open core).
Aussi de vendre l'outillage.
L'idéal serait que betagouv soit le client. Ils achetent bien des services en commun (Sentry, Mattermost, Scalingo, ...) pour les startups d'état.
## 30/05/2022 Dix-neuvième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Retour du séminaire pour Romain:
- hyper ouvert sur la mutualisation (Julien, Vincent, ...)
- "Toutes les SE commencent avec un senior dev, donc ils savent faire"
- "Faut tout faire en no-code"
- Beaucoup couplé à des stacks techniques (Django, etc.)
- On est très en avance sur les concepts d'archi (même parmi les seniors)
Role: stocké sur keycloak ?
- Si c'est sur keycloak
- En cas de changement de provider, il faudrait qu'il puisse gérer les roles (pas terrible)
- L'information circule mal de keycloak vers l'application
- Il faut attendre que l'utilisateur se connecte et arrive sur l'application, on peut regarder le contenu de son token
- On essaye de garder le contrat avec keycloak le plus simple possible
- On peut déléguer le domaine d'authentification
- Mais déléguer le domaine générique autorisation ?
- Si c'est juste pour stocker les roles, keycloak est un adaptateur secondaire, une base de données
- Ca prés_ente un certain intérêt de pouvoir gérer les roles des utilisateurs via leur interface
- Mais avec un coût d'intégration
- Puissant mais complexe à mettre en oeuvre (montée en compétences lourde)
- et potentiellement limité (si la SE a des besoins spécifiques)
Parfois les règles métier peuvent avoir besoin de "claim", plus précis qu'un role et surtout réutilisable (ex: peut afficher ceci, peut faire cela).
Le role peut être vu comme un groupe de claims (sa définition est une série de claims).
Un utilisateur peut avoir des claims sans role.
Pas forcément besoin d'avoir toutes les claims. La brique qui a besoin de vérifier les claims peut les réclamer dans son interface.
Possible de passer par un objet typé (les clés sont des propriétés de l'objet) ou par un enum.
```typescript=
// Dans module/authZ
type PossibleClaims = 'peut_afficher_nombre_demandes' | 'peut_afficher_liste_demandes';
type HasClaims<Claims extends PossibleClaims> = {
[Key in Claims]: boolean;
};
// Dans routes/DemandeListPage
export type DemandeListPageProps = {
claims: HasClaims<'peut_afficher_nombre_demandes' | 'peut_afficher_liste_demandes'>;
demandes: { id: string; type: string; déposéeLe: number }[];
};
export const DemandeListPage = ({ claims, demandes }: DemandeListPageProps) => {
return (
<div>
<h1>Demandes</h1>
{claims.peut_afficher_nombre_demandes && (
<div style={{ marginTop: 20 }}>Il y a {demandes.length} demandes en base</div>
)}
//...
```
```typescript=
// Dans routes/DemandeListPage
enum Claims {
peut_afficher_nombre_demandes = 'peut_afficher_nombre_demandes',
peut_afficher_liste_demandes = 'peut_afficher_liste_demandes',
}
export type DemandeListPageProps = {
claims: Claims[];
demandes: { id: string; type: string; déposéeLe: number }[];
};
export const DemandeListPage = ({ claims, demandes }: DemandeListPageProps) => {
return (
<div>
<h1>Demandes</h1>
{claims.includes(Claims.peut_afficher_nombre_demandes) && (
<div style={{ marginTop: 20 }}>Il y a {demandes.length} demandes en base</div>
)}
//...
```
L'inconvénient d'un enum est qu'il a une réalité dans javascript et dans typescript (ce qui peut embrouiller ceux qui ont du mal à comprendre la distinction).
Les claims sont déjà un besoin avancé. Ils permettent de réutiliser une règle métier (ex: role === 'admin' && project.createdBy === userId => user_can_delete_item).
Il s'agit d'une technique de DRY, de découplage mais qui va introduire un niveau d'indirection (et donc un coût cognitif et technique).
Parcours d'inscription:
- Page publique d'inscription => demandeur
- Le compte admin technique est seedé
- identifiants dans les vars d'env
- au lancement, s'il n'y a pas d'admin, on crée le compte admin
- c'est le seul à pouvoir affecter les roles admins de plus haut niveau
- L'admin peut affecter des roles spécifiques via l'interface
Est-ce qu'on préscrit sur la définition des roles ? Avec des contraintes fortes ? Ou bien juste exposer les best-practice via la documentation ?
On garde la notion d'admin technique qui a son compte créé lors du lancement.
Chaque projet gère ses propres roles.
TODO Next:
- Lors du lancement, créer le compte admin technique
- Vérifier s'il n'existe pas déjà
- Ses identifiants/mot de passe sont dans les variables d'environnement
- Emettre AdminTechniqueCréé (userId, email)
- keycloak: créer ce compte via l'api backend, l'activer immédiatement (pas de validation), mot de passe (à récupérer dans var env) temporaire à changer
- Emettre AdminTechniqueInscritViaKeycloak
- mettre à jour utilisateur_keycloak
- update role_utilisateur
- nouvelle projection à créer (userId, role)
- resolveUserFromKeycloak
- aller chercher le role dans role_utilisateur
- query à créer: getRoleUtilisateur(userId): Role
- Page d'inscription
- demander un email et un mot de passe
- faire un appel à l'api keycloak depuis le front pour créer le compte utilisateur
- validation par mail
- click sur le mail: redirection vers page de login (chez nous)
- Page de login
- saisir email et mot de passe
- faire un appel l'api keycloak
- on récupère le jwt et redirige ???
- A explorer dans le SDK Keycloak
- si nouvel utilisateur
- émettre DemandeurInscritViaKeycloak
- mettre à jour utilisateur_keycloak (userId, keycloakId)
- mettre à jour role_utilisateur
## 13/06/2022 Vingtième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Constat : c'est trop compliqué de rentrer dans le code
Est-ce que c'est possible de simplifier l'orga du code ?
Problèmes:
- Indirection
- Tu peux voir facilement qu'est-ce qui émets un événement
- Mais pas ses effets (commun à toute pub/sub)
Solutions:
- Essayer de colocaliser un maximum les fichiers
- point d'entrée dev-friendly : actions/pages plutot que découpage selon abstractions d'archi
- possible de faire un découpage clean archi qui ressemble à un mode classique ?
- simplifier les agrégats
- retirer le concept de repo ?
- un agrégat par action ?
-
- Exemple d'appli complète
- Documentation
- Outil de visualisation (cf https://stately.ai)
- Tuto step by step pour apprendre par l'usage
- Exemple side-by-side de requête "classique" (post + get) et "event sourcing"
- "Hell story de dev": cas de figure où une demande métier plutot simple rend la vie du dev impossible
- Question chiante pour le dev: "Qu'est-ce que ça fait si je fais ça ?" => force à trouver la réponse dans le code, et prendre la responsabilité de la véracité de ton analyse
## 14/06/2022 Session spéciale
Pierre-Antoine Duchateau
Clément Charles
Nous faisons quelques changements pour améliorer la compréhension:
- Déplacer les items de "page" (ex: DemandeListPage) dans un dossier /pages
- Nous pouvons retirer "Page" du nom parce que c'est dans un dossier /pages
- Le controller de route express reçoit un suffixe .route.ts (ex: demandeListe.route.ts)
- La query reçoit un suffixe .query.ts (ex: getDemandeList.query.ts)
- On crée un helper responseAsHtml(response, Composant(props)) pour remplacer les appels à response.send(ReactDOMServer.renderToString(Composant(props)))
- Déplacer les items d'actions (ou commandes, ex: accepterDemande) dans un dossier /actions
- idem les controllers prennent un suffixe .route.ts
- A trouver/améliorer: comment rediriger/réutiliser vers une page après avoir effectué une action
- ex de solution: demandeDetails.route.ts exporte un méthode returnDemandePage
- effectue la query
- passe les props au composant
- retourne du html via response.send()
- importé par accepterDemande qui l'appelle à la fin
- inconvénient: un refresh de la page refait un POST
- autre possibilité: response.redirect
- le contexte (message d'erreur, succès) doit être sérialisé dans l'url
- à vérifier: peut-on faire un response.flash() avant un redirect() ?
- Remarque: en séparant /pages et /actions, on suit les contours du CQRS
- Les projections sont déplacées dans un nouveau dossier /tables
- au même niveau que /pages et /actions
- tables est plus parlant que projection
- le type reste Projection
- on crée un helper makeProjectionTable()
- EventDispatcher n'est plus visible
- Suppression de la notion de repository
- On fait directement une transaction sur l'eventStore, et on construit manuellement l'agrégat
- AVANT: demandeRepo.transaction(demandeId, (demande) => {...})
- APRES: transaction(demandeId, (events) => {
const demande = makeDemande(events)
demande.accepter(...)
return demande.getPendingEvents()
})
- Plus verbeux
- Plus explicite, moins abstrait
- Plus générique, moins contraint
- Il sera toujours possible pour les projets qui le veulent de créer un helper pour encapsuler cette logique
- Fusionner Projection et EventDispatcher => ProjectionTable
- La notion d'event dispatcher est un détail d'implémentation
- Renommer /modules en /domain
- Déclarer les erreurs (qui sont spécifiques à une action en général), directement avec l'action
- import * as actions from './actions' => import { accepter, déposer } from './actions'
- plus de travail à brancher une nouvelle action
- moins "astucieux", plus explicite
- le type retourné par Demande est plus lisible
A voir pour la suite:
- Mettre en avant les événements ?
- actuellement situés dans src/domain/demande/events
- serait-ce pertinent d'essayer de les placer plutot dans src/events/demande par exemple ?
- dans ce cas, où placer les agrégats et les actions?
- la correspondance événement-action n'est pas totalement évidente
- il peut y avoir des événements sans action (ie attachés à aucun agrégat)
- certains événements correspondent à une action
- toute les actions émettent au moins un événement
- un événement peut être émis par plusieurs actions
- |action|0..n --- n|event|
- Mieux nommer / expliquer les agrégats
- C'est avant tout un objet transactionnel, or quand on lit agrégat, on ne perçoit pas cet aspect
- placer les agrégats dans des sous-dossiers /transactions ?
- Améliorer le cas de figure courant: créer une nouvelle entité avec un id unique qui vient de l'extérieur
- cf IdentitéKeycloak
- Il y a actuellement pas mal de boilerplate (action, State, buildState, ...) juste pour s'assurer que l'id n'est pas utilisé
- Est-ce qu'il peut y avoir plusieurs événements à l'origine d'une entité ?
- A priori oui
- IdentitéKeycloak s'assure de l'unicité de keycloakId, pas de userId
- En même temps, userId ne vient pas de l'extérieur ici
- Est-ce que c'est nécessaire de s'assurer de l'unicité de la clé si l'id est déjà une colonne marquée "unique" dans la bdd ?
- Oui, on ne veut pas dépendre d'une vérification qui vient de l'infra
- Notre historique serait incohérent
-
## 20/06/2022 Vingt-et-unième session
Romain Cambonie
Pierre-Antoine Duchateau
Marc Gavanier
Clément Charles
https://doc.incubateur.net/communaute/travailler-a-beta-gouv/actions-transverses/sengager-dans-une-action-transverse
=> Utiliser 10% de mon temps pour une action tranverse
=> Possible de le faire a posteriori ?
Retour de Jeremy Buget sur Keycloak: ça ne scale pas sur les PaaS.
=> Mais c'est quoi le nombre d'utilisateurs chez Pix ?
Ils utilisent https://docs.gravitee.io mais c'est un peu léger.
Nous simplifions encore l'arborescence des fichiers et nous assurons que l'application se lance après ces changements (il y a des ajustements à faire).
## 04/07/2022 Vingt-deuxième session
Romain Cambonie
Marc Gavanier
=> Thème keycloak en react sans le moteur de template de keycloack par défaut : [website](https://www.keycloakify.dev/) / [github](https://github.com/InseeFrLab/keycloakify)
Mise en place de [Cypress](https://www.cypress.io/)
- Lecture de la [documentation](https://docs.cypress.io/guides/overview/why-cypress)
- Test E2E de la page d'accueil + liens vers la page de connexion
Discussion au sujet des paramétrages possibles depuis l'interface de GitHub
- Signature de commits (empêche les rebases automatiques depuis l'interface de GitHub)
- Actions obligatoires
- Résolutions des conversations obligatoires
- Validation de revue obligatoire
- Configurer les branches qui peuvent être reversées dans main
- Releases
## 18/07/2022 Vingt-troisième session
Romain Cambonie
Marc Gavanier
Après étude de l'état du projet il est décider de commencer par mettre une intégration continue.
Au clone du project nous rencontrons plusieurs soucis pour faire tourner la solution en local.
Nous faisons une PR de fix pour le local, parmi les problèmes restant nous n'avons pas comprit ou réussit à executer les tests.
NEXT :
- Mettre en place l'intégration continue
- Démarrer un tutoriel pas à pas à mettre dans BIENVENUE_GITPOD.md pour expliquer la marche à suivre pour ajouter une nouvelle fonctionnalité de bout en bout.
## 28/07/2022 Vingt-quatrième session
Romain Cambonie
Marc Gavanier
- Début de la mise en place du tutoriel (implémentation de la rétractation d'une demande)
- Ajout d'un lien vers la page des demandes depuis la page d'accueil quand le contrevenant est connecté
- probème : on ne peut pas récupérer le contexte depuis une page
```typescript
ReactDOMServer.renderToString(
React.createElement(SessionContext.Provider, { value: { isLoggedIn: !!request.session.user } }, element)
)
```
Dans `\src\libs\responseAsHtml.ts` on injecte le contexte dans l'élément AccueilPage mais nous n'avons pas trouvé comment le récuperer directement car 'ça casse les règle des hooks'
```
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
```
Pour régler le soucis nous devons passer par un composant enfant => `AccueilLinks`
## 29/08/2022 Vingt-cinquième session
Romain Cambonie
Pierre-Antoine Duchâteau
Marc Gavanier
Nous reprenons le travail fait par Romain et Marc sur le tutoriel.
On essaye de revenir sur un cas d'usage techniquement plus simple
- L'utilisateur peut envoyer un message au support
- c'est plus simple car pas de règles métier
- on peut commencer par quelque chose d'ultra simple: créer une nouvelle page/vue (aussi une occasion de montrer comment on peut utiliser storybook)
- puis avancer dans la complexité: persister les données, afficher les données persistées, faire de la validation, ...
L'idée est d'accompagner le développeur sur des baby steps
Techniquement: on s'est rendus compte qu'on pouvait afficher le tutoriel dans gitpod
- ouvrir BIENVENUE_GITPOD.md en mode preview (grace a une astuce dans .vscode/settings.json)
- faire un prebuild via .gitpod.yml (TODO) pour accélerer le chargement/build des assets et donc l'affichage de la page web
## 12/09/2022
Décision du NEXT step:
- Mettre Netlify
- Avoir un système qui switche entre la page d'accueil 'space' et la landing page netlify (au travers d'un commit).