@nodefony/devkit 10.0.0-beta.4 → 10.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -3
- package/dist/nodefony/controllers/DevkitController.js +1 -1
- package/dist/nodefony/controllers/McpController.js +2 -2
- package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
- package/dist/nodefony/mcp/profileReport.js +1 -1
- package/dist/nodefony/mcp/profileRoute.js +28 -9
- package/dist/nodefony/service/DevkitService.js +4 -4
- package/docs/index.md +1 -1
- package/package.json +12 -12
- package/skills/nodefony-add-crud/SKILL.md +43 -24
- package/skills/nodefony-add-realtime-channel/SKILL.md +376 -111
- package/skills/nodefony-add-realtime-channel/references/binaire.md +191 -0
- package/skills/nodefony-add-realtime-channel/references/production.md +121 -0
- package/skills/nodefony-browser/SKILL.md +27 -2
- package/skills/nodefony-browser/references/sondes.md +13 -1
- package/skills/nodefony-browser/scripts/audit.mjs +12 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +94 -88
- package/skills/nodefony-browser/scripts/lib/actions.mjs +113 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +50 -11
- package/skills/nodefony-browser/scripts/lib/probes.mjs +56 -0
- package/skills/nodefony-browser/scripts/watch.mjs +27 -11
- package/skills/nodefony-dev/SKILL.md +11 -0
- package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
- package/skills/nodefony-devops/SKILL.md +28 -21
- package/skills/nodefony-migrate-schema/SKILL.md +110 -20
- package/skills/nodefony-migrate-schema/references/verdicts.md +22 -20
|
@@ -15,16 +15,25 @@
|
|
|
15
15
|
* `@env` NF_BROWSER_UNTIL expression JS évaluée DANS la page ; l'observation s'arrête dès qu'elle est vraie (point d'arrêt applicatif)
|
|
16
16
|
* `@env` NF_BROWSER_CUT coupure réseau `<début>:<durée>` en ms (`4000:3000`), comptée depuis l'ouverture de la page — éprouve la REPRISE d'un flux ; chaque socket dit alors quand il s'est fermé et rouvert
|
|
17
17
|
* `@env` NF_BROWSER_MAXFRAMES nombre de frames WebSocket conservées par sens (défaut 12)
|
|
18
|
+
* `@env` NF_BROWSER_ACTIONS séquence d'interactions jouée UNE FOIS la page ouverte, PENDANT l'observation — même grammaire que `inspect.mjs` (`clic`, `saisir:Nom=x`, `attendre`…). C'est ce qui fait voir la trame qu'un GESTE envoie : un bouton qui écrit par la socket, un formulaire soumis
|
|
18
19
|
* `@requires` conteneur du profil `browser` démarré · serveur Nodefony joignable
|
|
19
20
|
* `@output` JSON : sockets et leurs frames (horodatées), requêtes non-2xx, erreurs console, verdict de la condition
|
|
20
21
|
*/
|
|
21
22
|
import { open, goTo } from "./lib/browser.mjs";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
parseActions,
|
|
25
|
+
parseCut,
|
|
26
|
+
pageExpression,
|
|
27
|
+
sendableCloseCode,
|
|
28
|
+
signsInMidway,
|
|
29
|
+
} from "./lib/probes.mjs";
|
|
30
|
+
import { playActions } from "./lib/actions.mjs";
|
|
23
31
|
|
|
24
32
|
const PAGE = process.argv[2] ?? process.env.NF_BROWSER_PAGE ?? "/";
|
|
25
33
|
const DURATION = Number(process.argv[3] ?? 8000);
|
|
26
34
|
const UNTIL = process.env.NF_BROWSER_UNTIL ?? "";
|
|
27
35
|
const MAX = Number(process.env.NF_BROWSER_MAXFRAMES ?? 12);
|
|
36
|
+
const ACTIONS = parseActions(process.env.NF_BROWSER_ACTIONS);
|
|
28
37
|
const { cut: CUT, invalid: invalidCut } = parseCut(process.env.NF_BROWSER_CUT);
|
|
29
38
|
if (invalidCut) {
|
|
30
39
|
console.error(
|
|
@@ -35,7 +44,8 @@ if (invalidCut) {
|
|
|
35
44
|
// Sockets que la coupure FERME (cf « La coupure réseau » plus bas).
|
|
36
45
|
const isRouted = (url) => !new URL(url).pathname.startsWith("/_vite/");
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const ANONYMOUS = signsInMidway(ACTIONS);
|
|
48
|
+
const { browser, ctx, page, reuse } = await open({ anonymous: ANONYMOUS });
|
|
39
49
|
|
|
40
50
|
const t0 = Date.now();
|
|
41
51
|
const at = () => Date.now() - t0;
|
|
@@ -144,7 +154,7 @@ if (CUT) {
|
|
|
144
154
|
});
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
await goTo(page, ctx, PAGE, reuse);
|
|
157
|
+
await goTo(page, ctx, PAGE, reuse, { anonymous: ANONYMOUS });
|
|
148
158
|
|
|
149
159
|
if (CUT) {
|
|
150
160
|
cutTimers.push(
|
|
@@ -168,6 +178,17 @@ if (CUT) {
|
|
|
168
178
|
);
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
// ── Les gestes : joués PENDANT l'observation ───────────────────────────────
|
|
182
|
+
// Les écouteurs sont déjà posés : la trame qu'un clic envoie est enregistrée
|
|
183
|
+
// comme les autres. Une cible introuvable arrête la sonde (code 65), comme
|
|
184
|
+
// `inspect.mjs` — observer un geste qui n'a pas eu lieu ne prouve rien.
|
|
185
|
+
const failed = await playActions(page, ACTIONS);
|
|
186
|
+
if (failed !== null) {
|
|
187
|
+
console.error(failed.message);
|
|
188
|
+
await browser.close();
|
|
189
|
+
process.exit(failed.code);
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
// ── Le « point d'arrêt » : une CONDITION, pas une ligne de code ──────────────
|
|
172
193
|
// `waitForFunction` réévalue l'expression dans la page à chaque animation frame.
|
|
173
194
|
// C'est ce qui remplace utilement un breakpoint dans un pilotage automatisé :
|
|
@@ -176,14 +197,9 @@ if (CUT) {
|
|
|
176
197
|
// observe simplement pendant la durée demandée.
|
|
177
198
|
let verdict = "durée écoulée";
|
|
178
199
|
if (UNTIL) {
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
// condition impossible. Faux vert vécu, découvert seulement en éprouvant le
|
|
183
|
-
// sens négatif. On invoque donc explicitement les formes fonction.
|
|
184
|
-
const expr = /^\s*(\(|function\b|async\b)/.test(UNTIL)
|
|
185
|
-
? `(${UNTIL})()`
|
|
186
|
-
: UNTIL;
|
|
200
|
+
// Une forme fonction est APPELÉE (cf `pageExpression`) : passée telle quelle,
|
|
201
|
+
// l'attente réussirait TOUJOURS, condition impossible comprise.
|
|
202
|
+
const expr = pageExpression(UNTIL);
|
|
187
203
|
try {
|
|
188
204
|
await page.waitForFunction(expr, null, { timeout: DURATION });
|
|
189
205
|
verdict = `condition VRAIE après ${at()} ms`;
|
|
@@ -456,6 +456,13 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
|
|
|
456
456
|
sur les scripts, et le navigateur refuse un `<script>` en ligne qui n'en porte pas (« Refused to
|
|
457
457
|
execute inline script »). Un `curl` ne le voit JAMAIS, il ne lit que le corps. Signe le script
|
|
458
458
|
(valeur `this.context?.cspNonce`) ou sors-le dans un fichier servi ; ne desserre PAS la politique.
|
|
459
|
+
- **Une image, une police ou une API tierce est bloquée** (« violates the following Content
|
|
460
|
+
Security Policy directive ») : AJOUTE l'origine exacte par `headers.cspSources` de
|
|
461
|
+
`@nodefony/security` (`{ cdn: { "img-src": ["https://cdn.example.com"] } }`), ou sur la seule
|
|
462
|
+
route qui en a besoin par `@Csp` — ne réécris pas `headers.csp`. Une socket servie depuis une
|
|
463
|
+
AUTRE origine se déclare par `realtime.publicOrigin`. Ce que les navigateurs ont bloqué se lit,
|
|
464
|
+
en administrateur, sur `GET /nodefony/security/api/csp-reports` (`headers.cspReport`) — Brave
|
|
465
|
+
n'envoie aucun rapport, éprouve sous Chrome ou Firefox.
|
|
459
466
|
- **En production, la modification front n'apparaît jamais** : hors développement il n'y a PAS de
|
|
460
467
|
rechargement à chaud, et le manifeste est lu AU BOOT. `npm run build`, puis **redémarre le
|
|
461
468
|
serveur**, puis rechargement forcé.
|
|
@@ -474,6 +481,10 @@ npm run verify # types, style, mise en forme, .env.example, tests, build,
|
|
|
474
481
|
npm run test:e2e # boot réel + HTTP/WS — le gate LENT, hors `verify`
|
|
475
482
|
```
|
|
476
483
|
|
|
484
|
+
Ton diff change ce que le build produit (taille, dépendance, build plus lent) ? Chaque build
|
|
485
|
+
écrit un rapport : lis [`references/rapport-de-build.md`](references/rapport-de-build.md) avant de
|
|
486
|
+
recalculer quoi que ce soit à la main.
|
|
487
|
+
|
|
477
488
|
Ton diff ajoute une route, une requête ou touche la sécurité ? Relis-le AVANT de commiter :
|
|
478
489
|
skill `nodefony-security-review` — un script y nomme, ligne par ligne, ce qu'un œil laisse passer.
|
|
479
490
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Le rapport de build — ce que pèse ce que tu bâtis
|
|
2
|
+
|
|
3
|
+
> Chargé depuis `nodefony-dev` §10, quand un changement touche ce que le build produit.
|
|
4
|
+
|
|
5
|
+
Chaque build (`npm run build` : serveur ET front) écrit, sans rien demander, un rapport sous
|
|
6
|
+
`node_modules/.rolldown/report/<dossier de sortie>/` — le chemin relatif au projet, chaque `..`
|
|
7
|
+
devenu `__` (le front bâti dans `public/dist` → `node_modules/.rolldown/report/public/dist/`).
|
|
8
|
+
|
|
9
|
+
- **Quand le lire** : une taille qui change, une dépendance qui entre, un paquet présent en deux
|
|
10
|
+
versions, un build qui ralentit. Pas pour savoir si le build passe : son code de sortie le dit.
|
|
11
|
+
- **Comment** : `entry.md` d'abord (une vingtaine de lignes, il nomme le fichier à ouvrir
|
|
12
|
+
ensuite). Pour juger TON changement, épingle la base AVANT de modifier
|
|
13
|
+
(`cp .state.json baseline.json` dans ce dossier), rebâtis, lis `delta.md`. Un chiffre :
|
|
14
|
+
`jq '.metrics["output.total_bytes"]' metrics.json` — ne lis pas les `.json` en entier.
|
|
15
|
+
- **Ce qu'il ne dit pas** : ses octets sont comptés AVANT minification et compression, jamais
|
|
16
|
+
un poids réseau ; il ne vérifie aucun type (`npm run typecheck`) ; le mode watch et le serveur
|
|
17
|
+
de développement n'en écrivent pas ; une installation propre l'efface, base comprise.
|
|
18
|
+
- **Le couper** (côté serveur) : `defineNodefonyRolldownConfig({ buildReport: false })` dans
|
|
19
|
+
`rolldown.config.ts`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-devops
|
|
3
3
|
metadata:
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
description: >
|
|
6
6
|
Exploite CETTE application Nodefony en conteneur : image, compose, frontal (nginx, HAProxy, h2c,
|
|
7
7
|
Ingress), secrets, variables, Kubernetes, Podman, nombre de pods. N'enseigne pas Docker — il
|
|
@@ -80,6 +80,12 @@ Ce que l'application ne peut pas deviner. Chacun de ces oublis produit une dégr
|
|
|
80
80
|
clair »), le cookie de session perd son préfixe `__Host-`, et l'adresse cliente devient celle du
|
|
81
81
|
frontal — donc une vague de reconnexions WebSocket épuise **un seul** quota. Sans le moindre
|
|
82
82
|
message. Détail et preuve → [`references/frontal.md`](references/frontal.md).
|
|
83
|
+
- **Une seule CSP, et c'est l'application qui la pose.** Elle porte un nonce neuf par page, que le
|
|
84
|
+
frontal ne peut pas connaître. N'ajoute pas de `Content-Security-Policy` dans nginx, haproxy ou
|
|
85
|
+
une annotation d'ingress : le navigateur appliquerait les deux, et les scripts signés seraient
|
|
86
|
+
bloqués — page blanche. La poser au frontal quand même : `headers: { csp: "" }` dans la config
|
|
87
|
+
de `@nodefony/security`, puis une politique SANS nonce au frontal →
|
|
88
|
+
`node_modules/@nodefony/security/docs/headers.md` § « Poser la CSP au proxy ».
|
|
83
89
|
- **Une inactivité de frontal ≥ 300 s.** Plus courte que le battement, elle coupe des WebSocket et
|
|
84
90
|
des flux SSE **vivants**. `proxy:generate` la dérive ; un contrôleur d'entrée Kubernetes, non.
|
|
85
91
|
- **Un bus temps réel partagé dès deux exemplaires** : `NF_REALTIME_DRIVER=redis`. Sans lui, un
|
|
@@ -116,26 +122,27 @@ Chaque ligne a la commande qui la **prouve**. Une case cochée sans commande est
|
|
|
116
122
|
|
|
117
123
|
Le tableau qu'on lit en urgence.
|
|
118
124
|
|
|
119
|
-
| Symptôme
|
|
120
|
-
|
|
|
121
|
-
| L'exemplaire redémarre en boucle, journaux muets
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
| Le
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
| Depuis l'activation de h2c,
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
| La
|
|
137
|
-
|
|
|
138
|
-
|
|
|
125
|
+
| Symptôme | Cause | Correction |
|
|
126
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| L'exemplaire redémarre en boucle, journaux muets | La sonde de vivacité tombe avant la fin du démarrage | Sonde de démarrage sur `/readyz`, dimensionnée sur le **pire** cas |
|
|
128
|
+
| Page blanche derrière le frontal, console « Refused to execute… blocked by CSP » | Deux CSP : celle de l'application et une ajoutée au frontal | Retirer celle du frontal, ou `headers.csp: ""` dans l'application |
|
|
129
|
+
| Le conteneur sort en **137** à l'arrêt | Il a été tué : la fenêtre d'arrêt est ≤ au drain | Fenêtre ≥ 30 s (Podman : `--stop-timeout 20`, son défaut est **10 s**) |
|
|
130
|
+
| Des `5xx` **pendant** chaque déploiement | Le répartiteur envoie encore du trafic quand le drain a commencé | Un délai avant drain, et un court arrêt avant terminaison |
|
|
131
|
+
| Le cookie de session n'a pas le préfixe `__Host-` en production | Schéma constaté `http` derrière le frontal | `NF__HTTP__TRUSTPROXY` |
|
|
132
|
+
| La WebSocket se ferme seule au bout d'une minute | L'inactivité du frontal est plus courte que le battement | Régénérer le frontal ; sur un contrôleur d'entrée, `proxy-read-timeout` ≥ 300 s → [`references/frontal.md`](references/frontal.md) |
|
|
133
|
+
| Un message temps réel n'arrive qu'à une partie des clients | Plusieurs exemplaires, bus local : chaque pod ne diffuse qu'à ses propres clients | `NF_REALTIME_DRIVER=redis` + `NF_REDIS_URL` → [`references/dimensionnement.md`](references/dimensionnement.md) |
|
|
134
|
+
| Depuis l'activation de h2c, le pod n'est jamais prêt | La sonde vise le port h2c (5153), qui refuse HTTP/1.1 | Sonder le port HTTP (5151) |
|
|
135
|
+
| Depuis l'activation de h2c, les WebSocket ne s'ouvrent plus | Le frontal les envoie au port h2c | Router `Upgrade: websocket` vers le port HTTP → [`references/frontal.md`](references/frontal.md) §8 |
|
|
136
|
+
| `DB_SQLITE_FALLBACK` en production alors que la base est posée | Le nom de la variable est faux d'un caractère, ou le Secret n'est pas monté dans CE conteneur | `npx nodefony doctor --env production` nomme la variable lue (`NF_DATABASE_URL` ou `NF_DATABASE_URL_FILE`) |
|
|
137
|
+
| Le pod est tué (`OOMKilled`) avec peu de connexions | La contre-pression : des clients lents retiennent jusqu'à 4 Mio chacun | Baisser `maxBackpressure` sur `websocket` **et** `websocketSecure` → [`references/dimensionnement.md`](references/dimensionnement.md) |
|
|
138
|
+
| Toutes les connexions répondent `403` « doit passer par HTTPS » | Même cause : le mot de passe arrive sur une requête que l'application croit en clair | `NF__HTTP__TRUSTPROXY` — jamais `allowInsecureCredentials` |
|
|
139
|
+
| Sortie **78** au démarrage, une clé ou une variable nommée | Faute de configuration : clé inconnue (ou retirée par une mise à jour), valeur invalide, secret requis absent (`NF_CSRF_SECRET`, `NF_JWT_KEYSET`) | Lire la ligne qui la nomme ; `npx nodefony doctor --env production` annonce AVANT les variables requises (pas `NF_JWT_KEYSET`, exigée seulement si l'app émet des jetons) |
|
|
140
|
+
| Les journaux d'audit montrent tous la **même adresse** | L'adresse du client n'est pas propagée, ou le frontal n'est pas de confiance | `trustProxy` ; en Podman sans privilèges, **c'est irréparable** (§7) |
|
|
141
|
+
| `EACCES` au premier démarrage sur un volume | Le volume est né `root:root` : monté **par-dessus** un dossier dont l'image avait fixé le porteur | Volume nommé (il hérite du porteur) ou groupe de système de fichiers |
|
|
142
|
+
| La base refuse des connexions dès qu'on monte en exemplaires | Une base administrée les compte ; le pool n'est pas borné | Réduire les exemplaires — le plafond n'est pas encore réglable (§6) |
|
|
143
|
+
| La sonde répond `429` | Ce n'est **pas** la sonde : c'est la limitation de débit — donc l'appel n'a pas pris le raccourci | Interroger `/livez` et `/readyz` nus, sans en-tête ni préfixe ajouté |
|
|
144
|
+
| Un secret retiré du `Dockerfile` reste dans l'image | Une couche antérieure le contient encore ; l'effacer plus loin ne l'enlève pas | `npx nodefony image:check`, et reconstruire sans jamais l'avoir copié |
|
|
145
|
+
| La variable posée « ne fait rien » | Le nom est faux d'un caractère — une variable inconnue est **ignorée**, pas refusée | `npx nodefony doctor` → [`references/variables.md`](references/variables.md) |
|
|
139
146
|
|
|
140
147
|
## 6. Kubernetes — ce qui est rendu, et ce qui te reste
|
|
141
148
|
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-migrate-schema
|
|
3
3
|
description: >
|
|
4
|
-
Fait évoluer le schéma d'une base Nodefony et le porte en production, par
|
|
5
|
-
`orm:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
|
|
10
|
-
changé.
|
|
4
|
+
Fait évoluer le schéma d'une base Nodefony et le porte en production, par `orm:generate` et
|
|
5
|
+
`orm:migrate` — jamais par un `ALTER` à la main ni en supprimant la base. Porte la lecture de
|
|
6
|
+
l'état, le plan avant le geste, chaque code de refus et son geste, le renommage déclaré, les trois
|
|
7
|
+
interdits qui cassent un historique, et le déploiement où les migrations passent AVANT les
|
|
8
|
+
exemplaires. À charger AVANT de modifier une entité déjà en base, ou de déployer un schéma changé.
|
|
11
9
|
Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
10
|
+
"migrer le schéma", "orm:migrate", "orm:generate", "déployer un changement de schéma",
|
|
11
|
+
"no such column",
|
|
14
12
|
"column does not exist", "ma base ne correspond plus au code", "adopter une base existante",
|
|
15
13
|
"réparer une migration en échec", "éprouver une migration sans casser ma base",
|
|
16
|
-
"rejouer les migrations depuis zéro"
|
|
14
|
+
"rejouer les migrations depuis zéro", "renommer une colonne ou une table",
|
|
15
|
+
"changer le type ou la longueur d'une colonne", "changer de base", "passer à PostgreSQL ou
|
|
16
|
+
MySQL".
|
|
17
17
|
metadata:
|
|
18
|
-
version:
|
|
18
|
+
version: 4
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
# Faire évoluer un schéma, et le porter en production
|
|
@@ -29,18 +29,27 @@ metadata:
|
|
|
29
29
|
> la ligne exacte :
|
|
30
30
|
> `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`.
|
|
31
31
|
|
|
32
|
-
## 1. La seule chose à savoir avant tout le reste
|
|
32
|
+
## 1. La seule chose à savoir avant tout le reste — QUI fabrique le schéma ici ?
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
La clé `ddl` d'un connecteur répond à une seule question : qui fabrique les tables. Le mode n'est
|
|
35
|
+
presque jamais écrit, il se DÉDUIT — et c'est lui qui décide du geste.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
| Situation | Mode | Qui fabrique les tables |
|
|
38
|
+
| -------------------------------------------------------- | --------- | ----------------------------------------------------------- |
|
|
39
|
+
| `ddl` écrit dans la configuration | lui | ce que dit la configuration — il gagne toujours |
|
|
40
|
+
| développement, l'application versionne **≥ 1** migration | `migrate` | les migrations, appliquées au démarrage |
|
|
41
|
+
| développement, **aucune** migration ; ou `NODE_ENV=test` | `auto` | le démarrage, d'après les entités (création + ajouts vides) |
|
|
42
|
+
| **tout le reste**, production comprise | `none` | personne au démarrage : un job lance `orm:migrate` AVANT |
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
🔴 **Une application créée par `nodefony create app` avec un ORM SQL est en `migrate` dès
|
|
45
|
+
l'installation** : le générateur écrit ET applique sa migration initiale. Le cas courant, en
|
|
46
|
+
développement, est donc : **une entité change → une migration s'écrit** (§ 3). Ne crois pas
|
|
47
|
+
qu'« en développement la base suit le code » sans avoir vérifié qu'aucune migration n'existe
|
|
48
|
+
(`npx nodefony orm:migrate:status --json`).
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
En `auto` seulement, un champ ajouté **qui accepte le vide** est posé au démarrage suivant — rien à
|
|
51
|
+
taper. Tout le reste (renommage, type, longueur, suppression, champ obligatoire) n'y est JAMAIS
|
|
52
|
+
rattrapé : l'ancienne colonne reste en base, la nouvelle naît vide.
|
|
44
53
|
|
|
45
54
|
## 2. Lire l'état — deux voies, selon que l'application tourne
|
|
46
55
|
|
|
@@ -115,6 +124,62 @@ relue et versionnée en amont, et c'est `orm:migrate` seul qui l'applique.
|
|
|
115
124
|
> second échoue sur un objet qui n'a jamais existé. Un `ddl` écrit dans la configuration gagne
|
|
116
125
|
> toujours ; sous `NODE_ENV=test`, rien ne change.
|
|
117
126
|
|
|
127
|
+
### 🔴 Un RENOMMAGE — le DÉCLARER, jamais le laisser deviner
|
|
128
|
+
|
|
129
|
+
Une colonne qui disparaît pendant qu'une autre apparaît, c'est un renommage (les données suivent)
|
|
130
|
+
OU une suppression puis un ajout (les données sont perdues). Aucun diff ne voit une intention. Ne
|
|
131
|
+
lance donc JAMAIS `orm:generate` nu après avoir renommé un champ : il s'arrête
|
|
132
|
+
(`NF_GENERATE_NEEDS_ANSWER`), et **il n'y a pas de question à laquelle répondre** — l'outil tourne
|
|
133
|
+
sans terminal, humain devant l'écran compris. La réponse se DÉCLARE :
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# une colonne : la table sous le nom que porte son entité
|
|
137
|
+
npx nodefony orm:generate --name titre_en_anglais --rename article.titre=title
|
|
138
|
+
# une table, puis une colonne DANS la table renommée (son NOUVEAU nom)
|
|
139
|
+
npx nodefony orm:generate --name lignes --rename invoice_line=invoice_det --rename invoice_det.fk_parent_line=fk_parent
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- La migration porte un `RENAME`, **changement de type compris** (`varchar(8)` → `varchar(16)`
|
|
143
|
+
renommé ET élargi s'écrit en entier).
|
|
144
|
+
- Le rapport `--json` le confirme : `renamed: ["article.titre → title"]`. Relis le fichier quand même.
|
|
145
|
+
- Un nom mal écrit, une ancienne colonne absente du dernier instantané, un nouveau nom qu'aucune
|
|
146
|
+
entité ne déclare : refus `NF_GENERATE_RENAME`, **rien n'est écrit**.
|
|
147
|
+
- Une VRAIE suppression ET un ajout sans rapport dans la même table : **deux générations** — d'abord
|
|
148
|
+
l'entité sans la colonne retirée (la perte est nommée par le garde destructif), puis avec
|
|
149
|
+
l'ajout. Ne déclare pas `--rename` pour faire taire l'outil : ce serait mentir sur les données.
|
|
150
|
+
|
|
151
|
+
Allonger une colonne (`varchar(16)` → `varchar(32)`) ou monter un entier de rang (`integer` →
|
|
152
|
+
`bigint`) n'est PAS destructif : le générateur le constate sur l'état de départ et n'exige rien. Le
|
|
153
|
+
raccourcir, ou rendre la colonne obligatoire, reste refusé sans `--allow-destructive`.
|
|
154
|
+
|
|
155
|
+
### Les pièges d'un schéma qui ÉVOLUE — constatés en faisant migrer un vrai progiciel
|
|
156
|
+
|
|
157
|
+
Chacun a été rencontré en portant un schéma réel d'une version à la suivante, sur SQLite,
|
|
158
|
+
PostgreSQL, MariaDB et MySQL. Le geste est le seul qui ait tenu sur les quatre.
|
|
159
|
+
|
|
160
|
+
1. **Renommer la PROPRIÉTÉ sans renommer la colonne ne demande AUCUNE migration.** Le modificateur
|
|
161
|
+
`column` garde le nom en base : `title: { kind: "string", length: 180, column: "titre" }`. Le
|
|
162
|
+
code lit `title`, la base garde `titre`, `orm:generate` répond « rien à écrire ». Choisis-le
|
|
163
|
+
quand seul le nom vu par le code doit changer — c'est le renommage qui ne coûte rien.
|
|
164
|
+
2. **Une table renommée fait apparaître des `DROP CONSTRAINT` / `DROP INDEX` puis leurs `ADD`** :
|
|
165
|
+
les noms de contraintes que le kit dérive du nom de table changent, la base garde les anciens.
|
|
166
|
+
En SQLite, la table est même RECRÉÉE (`__new_<table>`, recopie, `DROP TABLE`, `RENAME`). C'est
|
|
167
|
+
la mécanique, pas une perte, et le garde ne la signale pas. Ce qui se vérifie : la ligne
|
|
168
|
+
`renamed` du rapport, et **aucun `DROP COLUMN`** dans le fichier.
|
|
169
|
+
3. **Une référence posée sur des lignes DÉJÀ remplies échoue si une ligne pointe vers rien** —
|
|
170
|
+
sur les quatre moteurs : PostgreSQL et MySQL refusent l'`ADD CONSTRAINT`, SQLite refuse la
|
|
171
|
+
validation (« référence(s) orpheline(s) », table et ligne nommées). La migration est annulée et
|
|
172
|
+
un marqueur d'échec posé. Geste : corriger d'ABORD les lignes dans une migration libre
|
|
173
|
+
(`--custom` : `UPDATE … SET fk = NULL WHERE …`), PUIS déclarer la référence ; après un échec,
|
|
174
|
+
`orm:migrate:repair` lève le marqueur une fois la cause traitée.
|
|
175
|
+
4. **Retirer un champ de l'entité, c'est `DROP COLUMN`** : refusé sans `--allow-destructive`.
|
|
176
|
+
Un progiciel qui abandonne une colonne la laisse souvent en base sur les installations
|
|
177
|
+
existantes (le script de passage réel ne la supprime pas). Si ses données comptent encore,
|
|
178
|
+
garde le champ, facultatif, plutôt que de le retirer.
|
|
179
|
+
5. **Une évolution se prouve sur une base PEUPLÉE, jamais vide** (§ 4 b). Une table vide ne
|
|
180
|
+
déclenche ni la recréation SQLite d'une table référencée, ni une référence orpheline : un défaut
|
|
181
|
+
de l'applicateur SQLite n'apparaissait QUE sur des lignes réelles. Une ligne par table suffit.
|
|
182
|
+
|
|
118
183
|
### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
|
|
119
184
|
|
|
120
185
|
Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
|
|
@@ -218,6 +283,29 @@ Deux choses qu'il rapporte et qu'il faut lire :
|
|
|
218
283
|
> Cela ne concerne QUE cette commande de reprise : la création des tables, leur migration et le
|
|
219
284
|
> fonctionnement de l'application sont inchangés sur MariaDB.
|
|
220
285
|
|
|
286
|
+
### Changer de BASE — de SQLite à PostgreSQL, MySQL ou MariaDB
|
|
287
|
+
|
|
288
|
+
Une entité écrite par `create entity` est une description PORTABLE (`defineTable`) : elle suit la
|
|
289
|
+
base du connecteur, **sans être réécrite**. Changer de moteur tient en quatre gestes :
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npm install pg # ou mysql2 — le pilote de la NOUVELLE base (absent : le démarrage le nomme)
|
|
293
|
+
# .env : NF_DATABASE_URL=postgres://user:secret@hote:5432/app (le dialecte se déduit de l'URL)
|
|
294
|
+
npx nodefony orm:generate --name initiale # migration INITIALE de ce dialecte
|
|
295
|
+
npx nodefony orm:migrate
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- **Les migrations se rangent par dialecte** (`migrations/<dialecte>/`) : celles de SQLite ne
|
|
299
|
+
s'appliquent jamais ailleurs, d'où la migration initiale du nouveau moteur.
|
|
300
|
+
- 🔴 **Les DONNÉES ne suivent pas.** Le schéma change de base, les lignes restent dans l'ancienne :
|
|
301
|
+
aucune commande ne les transfère — export puis import, à ta charge.
|
|
302
|
+
- **Une table NATIVE ne suit pas** (`sqliteTable`, ou `create entity --dialect <moteur>`) : pointée
|
|
303
|
+
vers un autre moteur, l'application refuse de démarrer en nommant l'entité
|
|
304
|
+
(`EntityNotPortedError`). Régénère-la sans `--dialect` (`create entity <Nom> … --force`).
|
|
305
|
+
- **Certains écarts appartiennent au moteur**, pas au framework : l'ordre de tri des chaînes et
|
|
306
|
+
l'égalité sans casse suivent la collation de la base, la place des `NULL` dans un tri diffère, le
|
|
307
|
+
type `vector` n'existe qu'en PostgreSQL. Le SQL natif que tu écris reste propre à ton dialecte.
|
|
308
|
+
|
|
221
309
|
## 4. Éprouver une migration — sur une base d'ESSAI, jamais sur la tienne
|
|
222
310
|
|
|
223
311
|
Quand il faut **prouver** qu'une migration fait ce qu'elle annonce, la réponse n'est jamais de
|
|
@@ -320,9 +408,11 @@ Le `code` est stable — **lis-le, il désigne le geste**.
|
|
|
320
408
|
| `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
|
|
321
409
|
| `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
|
|
322
410
|
| `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
|
|
323
|
-
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma
|
|
411
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, RIEN n'attend, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
|
|
324
412
|
| `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
|
|
325
413
|
| `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
|
|
414
|
+
| `NF_GENERATE_NEEDS_ANSWER` | une colonne disparaît et une autre apparaît : renommage ou perte ? | renommage → `--rename table.ancienne=nouvelle` ; vraie suppression + ajout → deux générations |
|
|
415
|
+
| `NF_GENERATE_RENAME` | un `--rename` mal écrit, ou que le dernier instantané ne permet pas | corriger la déclaration — la table sous le nom de son entité ; rien n'a été écrit |
|
|
326
416
|
|
|
327
417
|
Les codes exhaustifs, avec un exemple de charge utile pour chacun :
|
|
328
418
|
[`references/verdicts.md`](references/verdicts.md).
|
|
@@ -113,26 +113,28 @@ n'a jamais les deux — c'est le discriminant à tester.
|
|
|
113
113
|
|
|
114
114
|
### Refus d'usage — la demande elle-même
|
|
115
115
|
|
|
116
|
-
| Code | Ce qui s'est passé
|
|
117
|
-
| -------------------------------- |
|
|
118
|
-
| `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom
|
|
119
|
-
| `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue
|
|
120
|
-
| `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application
|
|
121
|
-
| `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte
|
|
122
|
-
| `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration
|
|
123
|
-
| `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`)
|
|
124
|
-
| `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi
|
|
125
|
-
| `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs
|
|
126
|
-
| `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données
|
|
127
|
-
| `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause
|
|
128
|
-
| `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine
|
|
129
|
-
| `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable
|
|
130
|
-
| `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse
|
|
131
|
-
| `NF_GENERATE_UNREADABLE_ENTITY` | rien d'écrit, et un fichier d'entités ne s'importe pas : sa table n'est vue par personne
|
|
132
|
-
| `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework
|
|
133
|
-
| `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données
|
|
134
|
-
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma
|
|
135
|
-
| `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé
|
|
116
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
117
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
|
|
119
|
+
| `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
|
|
120
|
+
| `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
|
|
121
|
+
| `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
|
|
122
|
+
| `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
|
|
123
|
+
| `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
|
|
124
|
+
| `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
|
|
125
|
+
| `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
|
|
126
|
+
| `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
|
|
127
|
+
| `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
|
|
128
|
+
| `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
|
|
129
|
+
| `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
|
|
130
|
+
| `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
|
|
131
|
+
| `NF_GENERATE_UNREADABLE_ENTITY` | rien d'écrit, et un fichier d'entités ne s'importe pas : sa table n'est vue par personne | corriger la cause nommée (souvent un paquet absent des dépendances), ou sortir le fichier de `nodefony/entity/` |
|
|
132
|
+
| `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
|
|
133
|
+
| `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
|
|
134
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, RIEN n'attend, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
|
|
135
|
+
| `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
|
|
136
|
+
| `NF_GENERATE_NEEDS_ANSWER` | une colonne disparaît et une autre apparaît — l'outil demande « renommage ? », sans terminal possible | renommage → `--rename table.ancienne=nouvelle` ; vraie suppression + ajout → deux générations |
|
|
137
|
+
| `NF_GENERATE_RENAME` | `--rename` mal écrit, ancien nom absent du dernier instantané, ou nouveau nom qu'aucune entité ne porte | corriger la déclaration (la table sous le nom de son entité) — rien n'a été écrit |
|
|
136
138
|
|
|
137
139
|
## Un contrat qui ne bougera pas
|
|
138
140
|
|