@nodefony/devkit 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-migrate-schema
|
|
3
|
+
description: >
|
|
4
|
+
Fait évoluer le schéma d'une base Nodefony et le porte en production, par les commandes
|
|
5
|
+
`orm:generate` et `orm:migrate` du framework — jamais par un `ALTER` écrit à la main ni par la
|
|
6
|
+
suppression d'une base. Porte la lecture de l'état (que l'application tourne ou non), le plan
|
|
7
|
+
avant le geste, les codes de refus et le geste que chacun appelle, les trois interdits qui
|
|
8
|
+
cassent un historique, et le patron de déploiement où les migrations passent AVANT les
|
|
9
|
+
exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
|
|
10
|
+
changé.
|
|
11
|
+
Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
|
|
12
|
+
"modifier une table existante", "migration", "migrer le schéma", "orm:migrate", "orm:generate",
|
|
13
|
+
"la base est en retard", "appliquer les migrations", "déployer un changement de schéma",
|
|
14
|
+
"comment passer ce modèle en production", "ma base ne correspond plus au code",
|
|
15
|
+
"no such column", "column does not exist", "erreur SQL après avoir changé une entité",
|
|
16
|
+
"adopter une base existante", "réparer une migration en échec", "le pod ne devient pas prêt",
|
|
17
|
+
"comment tester ma migration", "éprouver une migration", "vérifier qu'une migration marche",
|
|
18
|
+
"prouver que ma migration s'applique", "essayer sans casser ma base", "base d'essai",
|
|
19
|
+
"rejouer les migrations depuis zéro", "repartir d'une base propre".
|
|
20
|
+
metadata:
|
|
21
|
+
version: 2
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Faire évoluer un schéma, et le porter en production
|
|
25
|
+
|
|
26
|
+
## 1. La seule chose à savoir avant tout le reste
|
|
27
|
+
|
|
28
|
+
**En développement, il n'y a rien à faire.** La base suit le code : la table naît au démarrage, et
|
|
29
|
+
un champ ajouté **qui accepte le vide** est posé au démarrage suivant.
|
|
30
|
+
|
|
31
|
+
**Deux cas seulement sortent de là**, et ce sont eux qui amènent ici :
|
|
32
|
+
|
|
33
|
+
- un champ **obligatoire** ajouté à une table qui existe déjà — il n'est jamais rattrapé ;
|
|
34
|
+
- **la production**, où le démarrage ne fabrique JAMAIS le schéma.
|
|
35
|
+
|
|
36
|
+
Si l'application est en développement et que le champ ajouté accepte le vide, il suffit de
|
|
37
|
+
redémarrer. Ne produis pas une migration pour ça.
|
|
38
|
+
|
|
39
|
+
## 2. Lire l'état — deux voies, selon que l'application tourne
|
|
40
|
+
|
|
41
|
+
L'état est **le même objet** dans les deux cas : ne le recompose jamais à partir d'autre chose.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx nodefony orm:migrate:status --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Codes de sortie, et ils ne changeront pas :
|
|
48
|
+
|
|
49
|
+
| Code | Ce que ça veut dire |
|
|
50
|
+
| ---- | --------------------------------------------------------------------------------- |
|
|
51
|
+
| `0` | à jour |
|
|
52
|
+
| `1` | une action humaine est requise (en attente, dérive, échec, base en écart) |
|
|
53
|
+
| `2` | la commande n'a pas pu travailler (base injoignable, verrou tenu, usage invalide) |
|
|
54
|
+
|
|
55
|
+
Quand l'application **tourne**, le même état se lit par son plan d'administration, sous le rôle
|
|
56
|
+
d'administration : `GET /nodefony/orm/api/migrations?connector=<nom>`. Une porte MCP le catalogue
|
|
57
|
+
sous le domaine `orm`, chemin `migrations` — il n'y a **aucun outil dédié** à chercher.
|
|
58
|
+
|
|
59
|
+
**Lis `verdict` et `nextActions[0].command`, jamais la phrase française.** La phrase est un rendu ;
|
|
60
|
+
le verdict est la source.
|
|
61
|
+
|
|
62
|
+
## 3. Faire évoluer un schéma — trois gestes, dans cet ordre
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx nodefony orm:generate --name ajout_slug
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Écrit le fichier de migration qui manque, déduit de la différence entre les entités et la dernière
|
|
69
|
+
migration. Le nom entre dans une identité **immuable une fois publiée** : minuscules et `_`.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx nodefony orm:migrate --dry-run --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Le plan AVANT le geste.** Rend ce qui s'appliquerait, dans l'ordre, sans rien écrire. C'est ce
|
|
76
|
+
qu'on montre à un humain avant d'agir, et c'est ce qu'on relit soi-même avant de continuer.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx nodefony orm:migrate
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Applique sous verrou, écrit l'historique dans la même transaction que le schéma là où le moteur le
|
|
83
|
+
permet. **Rejouer n'applique rien** et sort `0` : les trois verbes sont idempotents, on peut donc
|
|
84
|
+
reprendre après une coupure sans lire d'état préalable.
|
|
85
|
+
|
|
86
|
+
> Ce que `orm:generate` ne peut pas déduire — une vue, un déclencheur, un remplissage de données —
|
|
87
|
+
> s'écrit dans une migration libre : `npx nodefony orm:generate --custom --name backfill_slug`
|
|
88
|
+
> dépose un fichier vide et son entrée de journal. Le gabarit déposé explique comment séparer les
|
|
89
|
+
> instructions ; suis-le à la lettre.
|
|
90
|
+
|
|
91
|
+
### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
|
|
92
|
+
|
|
93
|
+
Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
|
|
94
|
+
pas le même effet selon le serveur. Mesuré sur les trois, table peuplée :
|
|
95
|
+
|
|
96
|
+
| Moteur | Ce qui se passe |
|
|
97
|
+
| --------------- | ------------------------------------------------------------------------------------------- |
|
|
98
|
+
| sqlite | **refus** — `Cannot add a NOT NULL column with default value NULL` |
|
|
99
|
+
| PostgreSQL | **refus** — `column "x" of relation "y" contains null values` |
|
|
100
|
+
| MySQL / MariaDB | **accepté** — les lignes existantes reçoivent une valeur VIDE (`''`), sans un avertissement |
|
|
101
|
+
|
|
102
|
+
Les deux premiers t'arrêtent parce qu'ils ne peuvent pas inventer la valeur des lignes déjà là. Le
|
|
103
|
+
troisième l'invente : le champ est déclaré obligatoire et ne contient que du vide, ce qui passe tous
|
|
104
|
+
les contrôles et ne se voit qu'au moment où quelqu'un lit ces comptes. **Le mode strict n'y change
|
|
105
|
+
rien** — c'est le comportement de `ALTER TABLE … ADD COLUMN`, pas celui des insertions.
|
|
106
|
+
|
|
107
|
+
Donc, toujours, quel que soit ton moteur : **un champ obligatoire s'ajoute avec une valeur par
|
|
108
|
+
défaut** (`role:string=membre`), ou **se déclare facultatif** (`department:string?`). Si tu as
|
|
109
|
+
besoin des deux — obligatoire, et sans défaut à terme — c'est trois migrations : ajouter avec
|
|
110
|
+
défaut, remplir (`--custom`), puis retirer le défaut.
|
|
111
|
+
|
|
112
|
+
#### 🔴 …et si le champ est UNIQUE, le conseil ci-dessus se retourne contre toi
|
|
113
|
+
|
|
114
|
+
Une valeur par défaut est la **même pour toutes les lignes**. Sur un champ unique, elle ne répare
|
|
115
|
+
donc rien : elle garantit la collision dès la deuxième ligne déjà présente
|
|
116
|
+
(`UNIQUE constraint failed`). Et le générateur écrit l'ajout de colonne **et** son index unique
|
|
117
|
+
dans la MÊME migration — un enchaînement qui ne réussit que sur une table vide.
|
|
118
|
+
|
|
119
|
+
Le geste est en trois temps, et l'ordre ne s'inverse pas :
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 1. le champ, FACULTATIF et sans unicité, déclaré dans l'entité — puis :
|
|
123
|
+
npx nodefony orm:generate --name ajout_slug
|
|
124
|
+
npx nodefony orm:migrate
|
|
125
|
+
|
|
126
|
+
# 2. remplir chaque ligne d'une valeur DISTINCTE (SQL libre : le générateur ne
|
|
127
|
+
# peut pas inventer la valeur métier de lignes qu'il ne connaît pas)
|
|
128
|
+
npx nodefony orm:generate --custom --name remplir_slug
|
|
129
|
+
# → écrire l'UPDATE dans le fichier déposé, puis :
|
|
130
|
+
npx nodefony orm:migrate
|
|
131
|
+
|
|
132
|
+
# 3. le champ passe unique (et obligatoire si besoin) dans l'entité — puis :
|
|
133
|
+
npx nodefony orm:generate --name slug_unique
|
|
134
|
+
npx nodefony orm:migrate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`orm:generate` **te le dira** : il relit le SQL qu'il vient d'écrire et signale
|
|
138
|
+
`add-not-null-sans-defaut` et `colonne-neuve-puis-index-unique` sous « À REGARDER avant
|
|
139
|
+
d'appliquer ». Il ne refuse pas — il ne lit pas la base et ignore si ta table porte des lignes —,
|
|
140
|
+
mais s'il le signale et que ta table n'est pas vide, la migration échouera.
|
|
141
|
+
|
|
142
|
+
**À l'étape 3, sur sqlite, attends-toi à un refus `NF_GENERATE_DESTRUCTIVE`** — mesuré sur une
|
|
143
|
+
table de deux lignes. Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur
|
|
144
|
+
n'en a pas, alors l'outil RECONSTRUIT la table (`CREATE __new_billets` → `INSERT … SELECT` →
|
|
145
|
+
`DROP TABLE` → `RENAME`). Le `DROP TABLE` est reconnu comme destructeur, et il l'est en général —
|
|
146
|
+
ici il porte sur une table déjà recopiée, une ligne plus haut, dans la même migration. **Relis le
|
|
147
|
+
fichier avant de décider** : si tu y vois l'`INSERT INTO __new_… SELECT … FROM …` juste avant le
|
|
148
|
+
`DROP`, la reconstruction conserve les lignes, et `orm:migrate` l'applique sans broncher (les
|
|
149
|
+
fichiers sont écrits, c'est leur mise en service qui était refusée). Éprouvé de bout en bout :
|
|
150
|
+
deux lignes semées, trois étapes, deux lignes intactes et l'index unique en place.
|
|
151
|
+
|
|
152
|
+
> **Ne jamais** répondre à un échec de migration en refaisant la base. Une migration qui n'est pas
|
|
153
|
+
> passée n'a **rien** changé — sqlite et PostgreSQL l'annulent entière. C'est le fichier qu'il faut
|
|
154
|
+
> découper, pas les données qu'il faut sacrifier. Et si tu dois t'y reprendre à plusieurs fois,
|
|
155
|
+
> `NF_MIGRATE_DATABASE_URL` détourne la commande vers une base d'ESSAI et laisse la tienne intacte.
|
|
156
|
+
|
|
157
|
+
### La base existait AVANT toute migration — un geste de plus, une seule fois
|
|
158
|
+
|
|
159
|
+
Une application passée du mode développement à la production a ses tables **et** un dossier
|
|
160
|
+
`migrations/` vide. Dans cet état, `orm:generate` refuse — `NF_GENERATE_DATABASE_NOT_ADOPTED` :
|
|
161
|
+
la première migration décrirait la création de tables qui existent déjà, avec leurs données, et
|
|
162
|
+
l'adopter graverait dans l'historique un schéma que la base n'a pas.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx nodefony orm:migrate:baseline --from-database # la référence est LUE sur la base
|
|
166
|
+
npx nodefony orm:generate --name ajout_du_slug # produit un ALTER, plus un CREATE
|
|
167
|
+
npx nodefony orm:migrate
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`--from-database` lit le schéma de la base, en écrit la migration de référence et l'inscrit comme
|
|
171
|
+
appliquée. **Aucune instruction n'est exécutée sur la base.** À faire une fois, avant tout le reste.
|
|
172
|
+
|
|
173
|
+
Deux choses qu'il rapporte et qu'il faut lire :
|
|
174
|
+
|
|
175
|
+
- **des tables lues sans être déclarées** — la base est partagée avec autre chose. L'outil de
|
|
176
|
+
lecture ne sait pas restreindre son champ ; ces tables entrent dans la référence, et la
|
|
177
|
+
génération suivante proposera de les SUPPRIMER. Relis le fichier avant de continuer.
|
|
178
|
+
- **un corps resté en commentaire** — la référence ne recréerait rien sur une base neuve.
|
|
179
|
+
|
|
180
|
+
> ⚠️ **Sur MariaDB, `--from-database` ne fonctionne pas**, et il le dit au lieu de mourir. MariaDB
|
|
181
|
+
> écrit le type JSON en `longtext` + `CHECK (json_valid(…))`, que l'outil de lecture ne sait pas
|
|
182
|
+
> relire — et il lit la base ENTIÈRE avant de filtrer, donc les tables du framework suffisent à le
|
|
183
|
+
> bloquer. Le repli, sur ce serveur : relever le schéma (`SHOW CREATE TABLE`), le coller dans un
|
|
184
|
+
> `orm:generate --custom --name base_existante`, puis `orm:migrate:baseline`.
|
|
185
|
+
> Cela ne concerne QUE cette commande de reprise : la création des tables, leur migration et le
|
|
186
|
+
> fonctionnement de l'application sont inchangés sur MariaDB.
|
|
187
|
+
|
|
188
|
+
## 4. Éprouver une migration — sur une base d'ESSAI, jamais sur la tienne
|
|
189
|
+
|
|
190
|
+
Quand il faut **prouver** qu'une migration fait ce qu'elle annonce, la réponse n'est jamais de
|
|
191
|
+
détruire la base pour repartir de zéro : c'est de migrer **ailleurs**.
|
|
192
|
+
|
|
193
|
+
`NF_MIGRATE_DATABASE_URL` sert exactement à ça. Elle remplace la connexion pour les quatre
|
|
194
|
+
commandes de migration — `orm:migrate`, `orm:migrate:status`, `orm:migrate:baseline`,
|
|
195
|
+
`orm:migrate:repair` — et **pour elles seules** : ni le démarrage de l'application, ni `orm:reset`,
|
|
196
|
+
ni un store ne la lisent. Ta base de développement n'est pas ouverte pendant l'essai ; elle n'est
|
|
197
|
+
même pas touchée.
|
|
198
|
+
|
|
199
|
+
**Deux décors, deux questions différentes. Choisis selon ce que tu dois prouver.**
|
|
200
|
+
|
|
201
|
+
### a. Une base NEUVE — « la suite s'applique-t-elle depuis zéro ? »
|
|
202
|
+
|
|
203
|
+
C'est le décor d'une installation propre, et celui d'un nouvel environnement.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# sqlite : un fichier qui n'existe pas encore suffit — le pilote le crée.
|
|
207
|
+
# PowerShell : $env:NF_MIGRATE_DATABASE_URL = "sqlite:./var/databases/essai.sqlite"
|
|
208
|
+
export NF_MIGRATE_DATABASE_URL="sqlite:./var/databases/essai.sqlite"
|
|
209
|
+
|
|
210
|
+
npx nodefony orm:migrate --dry-run --json # le plan : ce qui s'appliquerait, rien d'écrit
|
|
211
|
+
npx nodefony orm:migrate --json # applique — sur la base d'essai
|
|
212
|
+
npx nodefony orm:migrate:status --json # doit rendre `up-to-date`, code 0
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Sur PostgreSQL ou MySQL, la base d'essai se crée à côté (`CREATE DATABASE app_essai;`) et l'URL la
|
|
216
|
+
désigne. Le dialecte doit être le MÊME que celui du connecteur : viser une base d'un autre dialecte
|
|
217
|
+
est refusé — `NF_MIGRATE_URL_MISMATCH`, rien n'est appliqué. Ce refus est une protection, pas un
|
|
218
|
+
obstacle à contourner.
|
|
219
|
+
|
|
220
|
+
### b. Une COPIE de ta base — « s'applique-t-elle sur mes données ? »
|
|
221
|
+
|
|
222
|
+
C'est le décor qui compte pour une migration qui touche des lignes existantes : un champ
|
|
223
|
+
obligatoire ajouté à une table déjà remplie, un remplissage, une contrainte resserrée. Une base
|
|
224
|
+
neuve ne prouve RIEN de tout ça — elle est vide.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# sqlite : une copie du fichier. Le chemin par défaut du connecteur `default` est
|
|
228
|
+
# `var/databases/nodefony-drizzle.db` — `orm:migrate:status --json` le confirme.
|
|
229
|
+
cp var/databases/nodefony-drizzle.db /tmp/essai.sqlite
|
|
230
|
+
# PostgreSQL : CREATE DATABASE app_essai TEMPLATE app; (ou une restauration de sauvegarde)
|
|
231
|
+
|
|
232
|
+
export NF_MIGRATE_DATABASE_URL="sqlite:/tmp/essai.sqlite"
|
|
233
|
+
npx nodefony orm:migrate --json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Ce qui fait la PREUVE
|
|
237
|
+
|
|
238
|
+
Trois choses, et elles se montrent :
|
|
239
|
+
|
|
240
|
+
1. **Le verdict** : `orm:migrate:status --json` rend `up-to-date` et le code `0` sur la base
|
|
241
|
+
d'essai.
|
|
242
|
+
2. **Ce que la base porte vraiment** — les tables et les colonnes attendues sont là. Un verdict
|
|
243
|
+
`up-to-date` dit que l'historique est complet, pas que le schéma te convient.
|
|
244
|
+
3. **Que ta base n'a pas bougé.** Montre-le au lieu de l'affirmer : une empreinte avant et après
|
|
245
|
+
(`shasum -a 256 var/databases/nodefony-drizzle.db`) doit être **identique**.
|
|
246
|
+
|
|
247
|
+
> Et si l'essai échoue, il échoue sur la base d'essai. C'est tout l'intérêt : on jette le fichier,
|
|
248
|
+
> on corrige la migration, on recommence. Rien à réparer, rien à réexpliquer.
|
|
249
|
+
|
|
250
|
+
🔴 **Quand l'essai est fini, RETIRE la variable** — `unset NF_MIGRATE_DATABASE_URL` (PowerShell :
|
|
251
|
+
`Remove-Item Env:NF_MIGRATE_DATABASE_URL`). Oubliée dans le terminal, elle détourne
|
|
252
|
+
silencieusement chaque commande de migration suivante vers la base d'essai : `orm:migrate` rend
|
|
253
|
+
« appliqué » et le code du succès, pendant que ta vraie base ne reçoit rien. Le seul symptôme
|
|
254
|
+
arrive plus tard, quand l'application démarre sur un schéma qui n'a pas bougé.
|
|
255
|
+
|
|
256
|
+
Tu n'as rien à interroger pour savoir où tu tapes : **chaque commande de migration annonce la base
|
|
257
|
+
qu'elle vise**. Quand une variable la détourne, l'en-tête de l'état le dit en toutes lettres —
|
|
258
|
+
« ⚠ NF_MIGRATE_DATABASE_URL détourne ce connecteur vers … » — et la charge utile `--json` porte le
|
|
259
|
+
même fait (`driver.target`). C'est le même chemin pour l'écran et pour la machine : les deux ne
|
|
260
|
+
peuvent pas diverger.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
nodefony orm:migrate:status
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
> ⚠️ **`orm:migrate:baseline` n'est pas un outil d'essai.** Il sert à ADOPTER une base qui porte
|
|
267
|
+
> déjà les tables sans historique — une fois, à la reprise d'un existant. S'en servir pour se
|
|
268
|
+
> fabriquer un décor de départ écrit un historique faux dans la base visée : les migrations
|
|
269
|
+
> adoptées y sont marquées appliquées sans l'avoir été. Pour un décor de départ, c'est le §4 —
|
|
270
|
+
> une base d'essai, et rien d'autre.
|
|
271
|
+
|
|
272
|
+
## 5. Les refus, et le geste que chacun appelle
|
|
273
|
+
|
|
274
|
+
Un refus n'est pas une panne : c'est le produit qui s'arrête devant une décision qui t'appartient.
|
|
275
|
+
Le `code` est stable — **lis-le, il désigne le geste**.
|
|
276
|
+
|
|
277
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
278
|
+
| ---------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
279
|
+
| `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
|
|
280
|
+
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
|
|
281
|
+
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
|
|
282
|
+
| `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
|
|
283
|
+
| `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 |
|
|
284
|
+
| `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 |
|
|
285
|
+
| `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
|
|
286
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, 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` |
|
|
287
|
+
| `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 |
|
|
288
|
+
| `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
|
|
289
|
+
|
|
290
|
+
Les codes exhaustifs, avec un exemple de charge utile pour chacun :
|
|
291
|
+
[`references/verdicts.md`](references/verdicts.md).
|
|
292
|
+
|
|
293
|
+
## 6. Les trois interdits
|
|
294
|
+
|
|
295
|
+
Chacun casse l'historique de façon irrattrapable, et aucun ne produit d'erreur au moment où on le
|
|
296
|
+
commet.
|
|
297
|
+
|
|
298
|
+
1. **Ne jamais modifier un fichier `.sql` déjà appliqué.** Son empreinte est enregistrée : le
|
|
299
|
+
modifier fait basculer le verdict en dérive sur toutes les bases où il est passé. Une correction
|
|
300
|
+
s'écrit dans une migration NEUVE.
|
|
301
|
+
2. **Ne jamais toucher à la table d'historique à la main.** Elle est le seul témoin de ce qui a été
|
|
302
|
+
appliqué ; une ligne ajoutée ou retirée à la main fait mentir tous les verdicts suivants.
|
|
303
|
+
L'interdit porte sur le client SQL, pas sur le produit : quand l'historique affirme une migration
|
|
304
|
+
que la base n'a jamais reçue, le geste existe et il est borné —
|
|
305
|
+
`orm:migrate:repair --forget <source>/<tag>` désinscrit UNE entrée nommée, pour qu'elle soit
|
|
306
|
+
rejouée. Il ne touche pas la base ; si la migration avait bien été appliquée, son rejeu échouera.
|
|
307
|
+
3. **Ne jamais renuméroter ni renommer une migration publiée.** L'identité voyage : elle est
|
|
308
|
+
enregistrée dans chaque base où la migration est passée.
|
|
309
|
+
|
|
310
|
+
Et un quatrième, qui n'est pas un interdit d'historique mais de méthode : **ne supprime pas une
|
|
311
|
+
base pour « repartir propre »**, et n'efface pas non plus son dossier de données. La commande qui
|
|
312
|
+
le fait (`orm:reset`) existe, refuse partout sauf en développement, et n'est jamais la réponse à
|
|
313
|
+
une migration qui refuse.
|
|
314
|
+
|
|
315
|
+
**Ce qu'il faut faire à la place** : migrer une base d'ESSAI — c'est le §4, et il couvre les deux
|
|
316
|
+
besoins qui poussent à détruire. « Je veux vérifier que ma migration part d'une base propre » →
|
|
317
|
+
décor (a), une base neuve. « Je veux la voir passer sur des données » → décor (b), une copie. Dans
|
|
318
|
+
les deux cas tu obtiens la même preuve, en gardant ta base ET son historique.
|
|
319
|
+
|
|
320
|
+
## 7. En production — les migrations passent AVANT les exemplaires
|
|
321
|
+
|
|
322
|
+
Le patron, et il n'a pas d'alternative raisonnable : **un travail dédié applique les migrations, et
|
|
323
|
+
se termine avant que le premier nouvel exemplaire ne démarre**. Les exemplaires, eux, ne fabriquent
|
|
324
|
+
jamais de schéma.
|
|
325
|
+
|
|
326
|
+
Une application générée avec une base SQL porte déjà cette recette dans `deploy/migrate-job.yaml`,
|
|
327
|
+
rendue à son nom. Ne la réécris pas : lis son en-tête, il porte le mode d'emploi.
|
|
328
|
+
|
|
329
|
+
Trois faits qui évitent trois faux diagnostics :
|
|
330
|
+
|
|
331
|
+
- **Un exemplaire dont la base est en retard répond `503` sur `/readyz`** (jamais sur `/livez`) et
|
|
332
|
+
reste hors du répartiteur de charge. Ce n'est pas une panne : c'est la protection. Applique les
|
|
333
|
+
migrations, les exemplaires se mettent en service **seuls**.
|
|
334
|
+
- **Le compte qui migre n'est pas celui qui sert.** `NF_MIGRATE_DATABASE_URL` remplace la connexion
|
|
335
|
+
pour la commande de migration seulement — c'est le véhicule du moindre privilège, et elle doit
|
|
336
|
+
désigner une connexion **directe** (un répartiteur de connexions en mode transaction casse le
|
|
337
|
+
verrou).
|
|
338
|
+
- **Pendant un remplacement progressif, l'ancien et le nouveau code coexistent.** Une migration
|
|
339
|
+
doit donc rester compatible avec la version précédente : on AJOUTE d'abord (colonne facultative,
|
|
340
|
+
table, index), on retire dans une version ULTÉRIEURE.
|
|
341
|
+
|
|
342
|
+
## 8. Ce que tu n'as pas le droit de faire, et pourquoi ce n'est pas une consigne
|
|
343
|
+
|
|
344
|
+
En production, appliquer des migrations depuis un serveur qui sert le trafic est **refusé par le
|
|
345
|
+
produit**, pas déconseillé : le point d'application du plan d'administration refuse hors
|
|
346
|
+
développement, et le compte de base de données d'un exemplaire n'a pas le droit de modifier un
|
|
347
|
+
schéma. Un refus du moteur est bruyant ; ne cherche pas à le contourner, c'est le travail de
|
|
348
|
+
déploiement qui porte ce droit.
|
|
349
|
+
|
|
350
|
+
En développement, à l'inverse, appliquer est normal — c'est là que le cycle complet se joue.
|
|
351
|
+
|
|
352
|
+
## 9. Quand passer la main
|
|
353
|
+
|
|
354
|
+
| Le besoin | Où aller |
|
|
355
|
+
| ------------------------------------------------------------- | --------------------------------------------------- |
|
|
356
|
+
| Créer une entité, un service CRUD, un controller de ressource | skill `nodefony-add-crud` |
|
|
357
|
+
| Comprendre la grammaire de champs et les index | skill `nodefony-add-crud` |
|
|
358
|
+
| Le détail des codes de verdict, avec un exemple par code | `references/verdicts.md` |
|
|
359
|
+
| Ce que le module publie sur les migrations | `node_modules/@nodefony/drizzle/docs/migrations.md` |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Les verdicts de migration, en entier
|
|
2
|
+
|
|
3
|
+
> Chargé à la demande. Le `SKILL.md` porte les cinq refus courants et leur geste ; cette page les
|
|
4
|
+
> donne tous, avec la charge utile que la commande rend en `--json`.
|
|
5
|
+
|
|
6
|
+
## Ce que rend une lecture d'état
|
|
7
|
+
|
|
8
|
+
`orm:migrate:status --json` rend **un seul objet**. Son cœur est NEUTRE — un second moteur de base
|
|
9
|
+
de données remplira la même structure — et tout ce qui est propre au pilote SQL vit sous `driver`.
|
|
10
|
+
N'écris jamais un chemin de lecture qui passe par le nom d'un pilote.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"formatVersion": 1,
|
|
15
|
+
"connector": "default",
|
|
16
|
+
"verdict": "pending",
|
|
17
|
+
"exitCode": 1,
|
|
18
|
+
"summary": "1 migration en attente sur « default ».",
|
|
19
|
+
"nextActions": [
|
|
20
|
+
{
|
|
21
|
+
"command": "nodefony orm:migrate --dry-run",
|
|
22
|
+
"args": ["orm:migrate", "--dry-run"]
|
|
23
|
+
},
|
|
24
|
+
{ "command": "nodefony orm:migrate", "args": ["orm:migrate"] }
|
|
25
|
+
],
|
|
26
|
+
"sources": [
|
|
27
|
+
{
|
|
28
|
+
"name": "app",
|
|
29
|
+
"applied": 2,
|
|
30
|
+
"pending": 1,
|
|
31
|
+
"failed": 0,
|
|
32
|
+
"pendingTags": ["0003_ajout_slug"],
|
|
33
|
+
"drifted": [],
|
|
34
|
+
"missing": [],
|
|
35
|
+
"entries": [
|
|
36
|
+
{
|
|
37
|
+
"tag": "0001_init",
|
|
38
|
+
"status": "applied",
|
|
39
|
+
"appliedAt": 1756400000000,
|
|
40
|
+
"durationMs": 42,
|
|
41
|
+
"appliedBy": "poste-de-dev",
|
|
42
|
+
"runId": "b0e2…"
|
|
43
|
+
},
|
|
44
|
+
{ "tag": "0003_ajout_slug", "status": "pending" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"driver": {
|
|
49
|
+
"kind": "sql",
|
|
50
|
+
"dialect": "postgres",
|
|
51
|
+
"ddl": "none",
|
|
52
|
+
"historyTable": "nodefony_migrations"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Les six verdicts**, dans l'ordre de gravité — le premier qui s'applique gagne, et cet ordre dit
|
|
58
|
+
quel geste vient EN PREMIER :
|
|
59
|
+
|
|
60
|
+
| `verdict` | Ce que ça dit | `exitCode` |
|
|
61
|
+
| ------------ | -------------------------------------------------------------------- | ---------- |
|
|
62
|
+
| `failed` | une migration a échoué : rien d'autre ne se discute avant réparation | `1` |
|
|
63
|
+
| `drift` | un fichier appliqué a changé depuis son application | `1` |
|
|
64
|
+
| `adopt` | la base porte les tables sans historique — elle est antérieure | `1` |
|
|
65
|
+
| `divergent` | la base ne correspond pas au schéma déclaré | `0` ou `1` |
|
|
66
|
+
| `pending` | des migrations restent à appliquer | `1` |
|
|
67
|
+
| `up-to-date` | rien à faire | `0` |
|
|
68
|
+
|
|
69
|
+
> `divergent` est le seul dont le code de sortie DÉPEND d'un réglage : selon la conduite choisie,
|
|
70
|
+
> il informe (`0`) ou bloque (`1`). Superviser ne doit pas faire tomber un déploiement par défaut.
|
|
71
|
+
|
|
72
|
+
## Ce que rend un refus
|
|
73
|
+
|
|
74
|
+
Une sortie qui porte `error` est un ARRÊT ; une sortie qui porte `verdict` est un état lu. Aucune
|
|
75
|
+
n'a jamais les deux — c'est le discriminant à tester.
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"formatVersion": 1,
|
|
80
|
+
"connector": "default",
|
|
81
|
+
"exitCode": 1,
|
|
82
|
+
"error": {
|
|
83
|
+
"code": "NF_MIGRATE_BASELINE_REQUIRED",
|
|
84
|
+
"summary": "Cette base porte déjà les tables du schéma mais n'a aucun historique de migration.",
|
|
85
|
+
"meaning": "",
|
|
86
|
+
"nextActions": [
|
|
87
|
+
{
|
|
88
|
+
"command": "nodefony orm:migrate:baseline --connector default",
|
|
89
|
+
"args": ["orm:migrate:baseline", "--connector", "default"]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tous les codes
|
|
97
|
+
|
|
98
|
+
### Refus de l'applicateur — l'état de la base ou des fichiers
|
|
99
|
+
|
|
100
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
101
|
+
| ---------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `NF_MIGRATE_BASELINE_REQUIRED` | tables présentes, historique vide | `orm:migrate:baseline` — adopte explicitement, n'exécute aucun SQL |
|
|
103
|
+
| `NF_MIGRATE_BASELINE_AMBIGUOUS` | la base s'écarte du schéma déclaré : adopter graverait un faux | `--up-to <tag>` pour borner, ou `--from-database` si aucune migration n'existe |
|
|
104
|
+
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé, mais des migrations existent déjà | `orm:migrate:baseline` sans option — l'historique des fichiers fait foi |
|
|
105
|
+
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration écrite, et la base porte déjà ces tables | `orm:migrate:baseline --from-database` — elle EST la première migration. Regénérer ensuite SEULEMENT si le refus le propose (couverture partielle) : sinon il n'y a aucun écart à écrire |
|
|
106
|
+
| `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | lire l'erreur enregistrée, constater la base, puis `orm:migrate:repair` |
|
|
107
|
+
| `NF_MIGRATE_HASH_MISMATCH` | le fichier d'une migration appliquée a changé | RÉTABLIR le fichier (premier geste proposé) ; `repair --update-hashes` ensuite, et seulement si la modification était sans effet |
|
|
108
|
+
| `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière appliquée | renommer la nouvelle pour qu'elle suive la dernière appliquée |
|
|
109
|
+
| `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
|
|
110
|
+
| `NF_MIGRATE_UNKNOWN_FORMAT` | un fichier n'est pas au format que cet applicateur lit | vérifier le journal de la source ; ne pas éditer à la main |
|
|
111
|
+
| `NF_MIGRATE_LOCK_TIMEOUT` | le verrou est tenu par un autre travail | attendre, puis rejouer — le verbe est idempotent. Sort en **2** : ce n'est pas une panne, c'est le déploiement d'à côté |
|
|
112
|
+
| `NF_MIGRATE_JOURNAL_MISMATCH` | le journal annonce un fichier que le dossier ne contient pas | rétablir le fichier, ou régénérer la source |
|
|
113
|
+
|
|
114
|
+
### Refus d'usage — la demande elle-même
|
|
115
|
+
|
|
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_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
|
|
124
|
+
| `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
|
|
125
|
+
| `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
|
|
126
|
+
| `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
|
|
127
|
+
| `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
|
|
128
|
+
| `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
|
|
129
|
+
| `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
|
|
130
|
+
| `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
|
|
131
|
+
| `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 |
|
|
132
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
|
|
133
|
+
| `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
|
|
134
|
+
|
|
135
|
+
## Un contrat qui ne bougera pas
|
|
136
|
+
|
|
137
|
+
`formatVersion` vaut `1` au premier niveau de chaque sortie. Ajouter un champ est une évolution
|
|
138
|
+
mineure ; en retirer ou en renommer un est interdit sur la série majeure. Les codes de sortie
|
|
139
|
+
`0` / `1` / `2` sont figés : des passes d'intégration continue s'y adossent.
|