@nodefony/drizzle 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 +162 -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/index.js +105 -0
- package/dist/nodefony/command/migrateShared.js +247 -0
- package/dist/nodefony/command/orm-generate.js +356 -0
- package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
- package/dist/nodefony/command/orm-migrate-repair.js +114 -0
- package/dist/nodefony/command/orm-migrate-status.js +67 -0
- package/dist/nodefony/command/orm-migrate.js +141 -0
- package/dist/nodefony/command/orm-reset.js +166 -0
- package/dist/nodefony/config/config.js +107 -0
- package/dist/nodefony/config/defineModuleConfig.js +63 -0
- package/dist/nodefony/entity/auditEventEntity.js +93 -0
- package/dist/nodefony/entity/colKit.js +260 -0
- package/dist/nodefony/entity/idempotencyEntity.js +74 -0
- package/dist/nodefony/entity/sessionEntity.js +75 -0
- package/dist/nodefony/entity/tokenEntity.js +198 -0
- package/dist/nodefony/entity/totpSecretEntity.js +98 -0
- package/dist/nodefony/entity/userTable.js +141 -0
- package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
- package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
- package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/migrations-schema/mysql.js +48 -0
- package/dist/nodefony/migrations-schema/postgres.js +48 -0
- package/dist/nodefony/migrations-schema/sqlite.js +48 -0
- package/dist/nodefony/registerStores.js +218 -0
- package/dist/nodefony/service/DrizzleService.js +282 -0
- package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
- package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
- package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
- package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
- package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
- package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
- package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
- package/dist/nodefony/src/SessionStorage.js +259 -0
- package/dist/nodefony/src/connectorTarget.js +59 -0
- package/dist/nodefony/src/likeSql.js +50 -0
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
- package/dist/nodefony/src/migrator/adopt.js +553 -0
- package/dist/nodefony/src/migrator/appSchema.js +414 -0
- package/dist/nodefony/src/migrator/catalog.js +76 -0
- package/dist/nodefony/src/migrator/destructive.js +213 -0
- package/dist/nodefony/src/migrator/divergence.js +84 -0
- package/dist/nodefony/src/migrator/drivers/index.js +39 -0
- package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
- package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
- package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
- package/dist/nodefony/src/migrator/explain.js +565 -0
- package/dist/nodefony/src/migrator/hash.js +47 -0
- package/dist/nodefony/src/migrator/history.js +219 -0
- package/dist/nodefony/src/migrator/index.js +16 -0
- package/dist/nodefony/src/migrator/kit.js +296 -0
- package/dist/nodefony/src/migrator/name.js +68 -0
- package/dist/nodefony/src/migrator/paths.js +88 -0
- package/dist/nodefony/src/migrator/refusals.js +143 -0
- package/dist/nodefony/src/migrator/resolve.js +281 -0
- package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
- package/dist/nodefony/src/migrator/sources.js +419 -0
- package/dist/nodefony/src/migrator/status.js +231 -0
- package/dist/nodefony/src/migrator/types.js +91 -0
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
- package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
- package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
- package/dist/nodefony/src/orm-core/index.js +4 -0
- package/dist/nodefony/src/queryKit.js +318 -0
- package/dist/nodefony/src/safeTarget.js +55 -0
- package/dist/types/index.d.ts +76 -0
- package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
- package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
- package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
- package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
- package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
- package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
- package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
- package/dist/types/nodefony/config/config.d.ts +110 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
- package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
- package/dist/types/nodefony/entity/colKit.d.ts +130 -0
- package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
- package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
- package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
- package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
- package/dist/types/nodefony/entity/userTable.d.ts +65 -0
- package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
- package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
- package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
- package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
- package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
- package/dist/types/nodefony/registerStores.d.ts +52 -0
- package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
- package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
- package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
- package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
- package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
- package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
- package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
- package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
- package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
- package/dist/types/nodefony/src/likeSql.d.ts +29 -0
- package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
- package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
- package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
- package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
- package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
- package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
- package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
- package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
- package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
- package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
- package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
- package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
- package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
- package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
- package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
- package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
- package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
- package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
- package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
- package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
- package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
- package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
- package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
- package/dist/types/nodefony/src/queryKit.d.ts +136 -0
- package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
- package/docs/index.md +954 -0
- package/docs/migrations.md +691 -0
- package/migrations/mysql/0000_framework_init.sql +137 -0
- package/migrations/mysql/meta/_journal.json +13 -0
- package/migrations/postgres/0000_framework_init.sql +128 -0
- package/migrations/postgres/meta/_journal.json +13 -0
- package/migrations/sqlite/0000_framework_init.sql +127 -0
- package/migrations/sqlite/meta/_journal.json +13 -0
- package/package.json +126 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Migrations de schéma — de la base de dev au déploiement sans interruption"
|
|
3
|
+
navTitle: Migrations de schéma
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/drizzle"
|
|
6
|
+
topic: drizzle
|
|
7
|
+
section: "Persistance"
|
|
8
|
+
audience: [developer, devops]
|
|
9
|
+
tags:
|
|
10
|
+
[
|
|
11
|
+
migrations,
|
|
12
|
+
schema,
|
|
13
|
+
ddl,
|
|
14
|
+
deploiement,
|
|
15
|
+
kubernetes,
|
|
16
|
+
production,
|
|
17
|
+
sqlite,
|
|
18
|
+
postgresql,
|
|
19
|
+
mysql,
|
|
20
|
+
]
|
|
21
|
+
version: "doc"
|
|
22
|
+
status: stable
|
|
23
|
+
updated: 2026-08-30
|
|
24
|
+
source: "src/packages/@nodefony/drizzle/nodefony/src/migrator/"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
📍 [Documentation](../../../../../docs/index.md) › [Drizzle — ORM SQL](index.md) › **Migrations de schéma**
|
|
28
|
+
|
|
29
|
+
> Une base de données ne se met pas à jour toute seule, et elle ne se remplace pas non plus. Une
|
|
30
|
+
> **migration** est un fichier de SQL versionné qui la fait passer d'une version du schéma à la
|
|
31
|
+
> suivante, en gardant la trace de son passage. Cette page dit comment Nodefony les produit, les
|
|
32
|
+
> applique et les surveille — et surtout comment déployer sans interrompre le service, ce qui est la
|
|
33
|
+
> seule question difficile du sujet.
|
|
34
|
+
>
|
|
35
|
+
> Deux lecteurs, deux moitiés. **En développement**, presque rien à faire : le schéma se répare tout
|
|
36
|
+
> seul, et la seule commande à retenir est `orm:reset`. **En exploitation**, tout se joue avant le
|
|
37
|
+
> déploiement : un travail d'orchestrateur applique les migrations pendant que l'ancienne version
|
|
38
|
+
> sert encore, avec un compte qui n'est pas celui du trafic.
|
|
39
|
+
|
|
40
|
+
## 📖 Lexique
|
|
41
|
+
|
|
42
|
+
| Terme | Ce que ça veut dire |
|
|
43
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| **migration** | un fichier de SQL versionné qui fait passer la base d'une version du schéma à la suivante, et qui garde trace de son passage |
|
|
45
|
+
| **schéma** | la forme de la base : ses tables, ses colonnes, leurs types et leurs contraintes — pas les données qu'elle contient |
|
|
46
|
+
| **DDL** | _Data Definition Language_ — la part du SQL qui crée et modifie le schéma (`CREATE TABLE`, `ALTER TABLE`), par opposition à celle qui lit et écrit les données |
|
|
47
|
+
| **source de migrations** | un dossier de migrations livré par quelqu'un : le framework en a une, l'application une autre, un module tiers peut en apporter une |
|
|
48
|
+
| **historique** | la table `nodefony_migrations` où l'applicateur écrit ce qu'il a posé, quand, et avec quelle empreinte |
|
|
49
|
+
| **empreinte** | la somme de contrôle du contenu d'un fichier de migration — c'est elle qui détecte qu'un fichier déjà appliqué a été modifié après coup |
|
|
50
|
+
| **adoption (_baseline_)** | déclarer qu'une base existante correspond déjà à certaines migrations, sans les exécuter |
|
|
51
|
+
| **dérive** | un fichier de migration modifié APRÈS avoir été appliqué : l'historique et le disque ne disent plus la même chose |
|
|
52
|
+
| **divergence** | la base ne correspond plus au code, alors que l'historique est complet et que rien n'est en attente |
|
|
53
|
+
| **expansion / contraction** | la façon de changer un schéma en deux déploiements, pour qu'à aucun moment le code en service et la base ne soient incompatibles |
|
|
54
|
+
|
|
55
|
+
## Qu'est-ce qu'une migration, et pourquoi le développement n'en a pas besoin
|
|
56
|
+
|
|
57
|
+
En développement, Nodefony **dérive** le schéma du code : les entités que vous déclarez deviennent
|
|
58
|
+
des `CREATE TABLE IF NOT EXISTS` au démarrage. C'est immédiat et sans cérémonie — mais `IF NOT
|
|
59
|
+
EXISTS` ne fait évoluer aucune table qui existe déjà. Ajoutez un champ à une entité, et la table
|
|
60
|
+
gardera la forme qu'elle avait.
|
|
61
|
+
|
|
62
|
+
En production, cette dérivation serait pire qu'inutile : elle poserait des tables dont l'historique
|
|
63
|
+
ne garderait aucune trace, et personne ne saurait plus d'où elles viennent — exactement la
|
|
64
|
+
divergence que les migrations existent pour empêcher.
|
|
65
|
+
|
|
66
|
+
D'où **trois modes**, un par connecteur, réglés par la clé `ddl` :
|
|
67
|
+
|
|
68
|
+
| Mode | Qui fabrique le schéma | Où c'est le défaut |
|
|
69
|
+
| --------- | ------------------------------------------------------------ | ---------------------------------- |
|
|
70
|
+
| `auto` | dérivé du code au démarrage, et **rattrapé** (voir plus bas) | développement, test |
|
|
71
|
+
| `migrate` | les migrations, appliquées au démarrage sous verrou | nulle part — jamais un défaut |
|
|
72
|
+
| `none` | personne ici : un travail extérieur s'en charge | tout le reste, production comprise |
|
|
73
|
+
|
|
74
|
+
> [!IMPORTANT]
|
|
75
|
+
> `migrate` n'est le défaut d'aucun environnement, et c'est délibéré. Faire migrer le schéma par le
|
|
76
|
+
> processus qui sert le trafic, c'est accepter que N exemplaires démarrant ensemble se disputent la
|
|
77
|
+
> base. Le verrou les sérialise, mais le patron sain reste le travail d'orchestrateur décrit plus
|
|
78
|
+
> bas. `migrate` existe pour les déploiements sans orchestrateur — un serveur unique, une machine
|
|
79
|
+
> virtuelle.
|
|
80
|
+
|
|
81
|
+
## 🚀 Démarrage rapide
|
|
82
|
+
|
|
83
|
+
Cinq verbes, et un seul à retenir pour le quotidien du développement.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Ce que la base a reçu, ce qui reste, et ce qu'il faut taper. N'écrit rien.
|
|
87
|
+
nodefony orm:migrate:status
|
|
88
|
+
|
|
89
|
+
# Applique ce qui est en attente (framework d'abord, application ensuite).
|
|
90
|
+
nodefony orm:migrate
|
|
91
|
+
|
|
92
|
+
# Écrire la migration qui aligne la base sur VOS entités.
|
|
93
|
+
nodefony orm:generate --name ajout_du_titre
|
|
94
|
+
|
|
95
|
+
# Voir le SQL sans l'appliquer — la même validation que la vraie.
|
|
96
|
+
nodefony orm:migrate --dry-run
|
|
97
|
+
|
|
98
|
+
# Adopter une base existante : marquer des migrations comme appliquées, sans les exécuter.
|
|
99
|
+
nodefony orm:migrate:baseline
|
|
100
|
+
|
|
101
|
+
# Reprendre une base qui existait AVANT toute migration : la référence est LUE sur la base.
|
|
102
|
+
nodefony orm:migrate:baseline --from-database
|
|
103
|
+
|
|
104
|
+
# Effacer les marqueurs d'échec, APRÈS avoir regardé ce qui s'est passé.
|
|
105
|
+
nodefony orm:migrate:repair
|
|
106
|
+
|
|
107
|
+
# Développement seulement : supprime et recrée la base du connecteur.
|
|
108
|
+
nodefony orm:reset
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Toutes acceptent `--connector <nom>` (défaut : `default`) et `--json`. Le flux `--json` est **pur** :
|
|
112
|
+
`nodefony orm:migrate:status --json | jq` ne casse sur aucune ligne de journal.
|
|
113
|
+
|
|
114
|
+
Les migrations du **framework** sont livrées dans le paquet : vous n'avez pas à les produire. Celles
|
|
115
|
+
de votre **application**, vous les écrivez avec `orm:generate` — voir la section suivante.
|
|
116
|
+
|
|
117
|
+
Côté configuration, il n'y a rien à écrire pour le cas courant : le mode se résout par
|
|
118
|
+
environnement. Ne le déclarer que pour s'en écarter — un serveur unique qui migre au démarrage :
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { defineConfig, use } from "nodefony";
|
|
122
|
+
|
|
123
|
+
export default defineConfig(() => ({
|
|
124
|
+
modules: [
|
|
125
|
+
use("@nodefony/drizzle", {
|
|
126
|
+
connectors: {
|
|
127
|
+
// Le démarrage applique les migrations, sous verrou. À réserver aux
|
|
128
|
+
// déploiements SANS orchestrateur — un serveur unique, une machine
|
|
129
|
+
// virtuelle : ailleurs, c'est un travail dédié qui migre, et les
|
|
130
|
+
// exemplaires restent en "none".
|
|
131
|
+
default: { ddl: "migrate" },
|
|
132
|
+
},
|
|
133
|
+
migrations: {
|
|
134
|
+
// Retenir la mise en service tant que le schéma est en retard (défaut),
|
|
135
|
+
// et faire de la divergence une barrière plutôt qu'une observation.
|
|
136
|
+
check: "fail",
|
|
137
|
+
divergence: "fail",
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
],
|
|
141
|
+
}));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Écrire les migrations de votre application — `orm:generate`
|
|
145
|
+
|
|
146
|
+
Vous modifiez une entité, vous tapez un verbe, vous relisez le fichier produit :
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
nodefony orm:generate --name ajout_du_titre
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Il n'y a **rien à installer ni à configurer**. La commande trouve vos entités là où le générateur
|
|
153
|
+
les écrit — `nodefony/entity/*.ts`, dans l'application et dans chacun de ses modules —, produit la
|
|
154
|
+
migration dans `migrations/<dialecte>/`, et vous dit ce qu'elle a écrit. L'outil qui calcule la
|
|
155
|
+
différence est piloté à l'intérieur ; vous n'avez ni sa configuration à tenir, ni son dossier de
|
|
156
|
+
sortie à connaître, ni son journal à comprendre.
|
|
157
|
+
|
|
158
|
+
Le dialecte est celui de votre connecteur : vos entités sont du Drizzle **natif**, donc écrites pour
|
|
159
|
+
un moteur. C'est ce qui vous laisse toute la puissance du moteur dans une entité — et ce qui fait
|
|
160
|
+
qu'une migration vaut pour lui seul.
|
|
161
|
+
|
|
162
|
+
### Ce que la commande refuse, et pourquoi c'est une bonne nouvelle
|
|
163
|
+
|
|
164
|
+
**Une migration est immuable dès qu'une base l'a reçue.** Une migration à laquelle il manque une
|
|
165
|
+
table ne se corrige donc pas : elle se remplace par une suivante, sur toutes les bases qui ont déjà
|
|
166
|
+
appliqué la première. C'est pour cela que trois situations arrêtent la commande avant qu'elle
|
|
167
|
+
n'écrive quoi que ce soit — chacune nomme ce qui cloche :
|
|
168
|
+
|
|
169
|
+
| Ce qui est refusé | Ce que ça veut dire |
|
|
170
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
171
|
+
| une entité enregistrée qu'aucun fichier ne fournit | son fichier a été déplacé, renommé, ou ne s'importe pas seul — la migration serait écrite **sans sa table** |
|
|
172
|
+
| un fichier de l'application qui exporte une table du framework | la migration porterait un second `CREATE TABLE` pour cette table : elle passerait sur une base vierge, et échouerait sur toute base **déjà migrée** |
|
|
173
|
+
| une migration qui **supprime** des données | les fichiers **sont écrits** et conservés : ce sont eux qu'il faut lire. C'est leur mise en service qui est retenue, et elle a sa propre garde |
|
|
174
|
+
|
|
175
|
+
Le troisième cas mérite deux précisions.
|
|
176
|
+
|
|
177
|
+
D'abord, quand une colonne disparaît et qu'une autre apparaît, aucun outil ne peut deviner s'il
|
|
178
|
+
s'agit d'un **renommage** — les données suivent — ou d'une suppression suivie d'un ajout — les
|
|
179
|
+
données sont perdues. C'est une intention, pas une différence de schéma. Rejouez alors la commande
|
|
180
|
+
dans un terminal, et répondez à la question posée.
|
|
181
|
+
|
|
182
|
+
Ensuite, **il n'y a rien à regénérer pour « assumer »** : au moment où ce refus paraît, les
|
|
183
|
+
fichiers sont déjà écrits et inscrits au journal. Relancer la génération ne produirait plus rien —
|
|
184
|
+
elle répondrait « le schéma n'a pas bougé ». Les deux issues réelles sont donc celles que le refus
|
|
185
|
+
propose : annuler les fichiers avec votre outil de gestion de versions, ou les appliquer. C'est
|
|
186
|
+
`orm:migrate` qui met en service, et c'est lui qui porte la garde `--allow-destructive` hors du
|
|
187
|
+
développement.
|
|
188
|
+
|
|
189
|
+
Un quatrième refus n'a rien à voir avec votre schéma : **l'outil qui écrit les migrations n'est pas
|
|
190
|
+
installé** (`NF_GENERATE_TOOL_MISSING`). C'est une dépendance de DÉVELOPPEMENT — votre application
|
|
191
|
+
la déclare, et un `npm install` la pose. Elle manque quand l'installation s'est faite sans les
|
|
192
|
+
dépendances de développement (`--omit=dev`, une image de production). Le refus le dit et donne le
|
|
193
|
+
geste ; il ne parle pas de votre base, qui n'y est pour rien — appliquer des migrations, lui, ne
|
|
194
|
+
réclame aucun outil tiers.
|
|
195
|
+
|
|
196
|
+
### Une entité qui pointe vers une table du framework
|
|
197
|
+
|
|
198
|
+
Déclarer une référence vers une table du framework (`session`, `audit_event`…) est légitime : ce
|
|
199
|
+
qui est refusé, c'est de **ré-exporter** cette table depuis vos fichiers. Les tables du framework
|
|
200
|
+
sont exclues du plan de votre application — elles ont leurs propres migrations, appliquées avant les
|
|
201
|
+
vôtres. Pour une vraie clé étrangère SQL, écrivez une migration libre.
|
|
202
|
+
|
|
203
|
+
> [!IMPORTANT]
|
|
204
|
+
> **`User` n'en fait PAS partie.** L'identité est du domaine : la table `User` appartient à votre
|
|
205
|
+
> application, qui la décrit et en porte les migrations. Le framework ne la livre plus. C'est ce
|
|
206
|
+
> qui vous permet d'y ajouter vos propres champs — ce qu'aucune table du framework n'autorise.
|
|
207
|
+
|
|
208
|
+
### Ce qu'aucun schéma ne peut déduire — la migration libre
|
|
209
|
+
|
|
210
|
+
Une vue, un déclencheur, un index particulier, un remplissage de données ne se déduisent d'aucune
|
|
211
|
+
entité :
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
nodefony orm:generate --custom --name vue_des_ventes
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Vous obtenez un fichier **vide**, déjà inscrit au journal, que vous écrivez à la main. Il est
|
|
218
|
+
appliqué comme les autres : une seule fois, dans l'ordre, et son empreinte est gravée.
|
|
219
|
+
|
|
220
|
+
### Reprendre une base qui existait AVANT toute migration
|
|
221
|
+
|
|
222
|
+
C'est l'état d'une application qui passe du développement à la production : la base porte ses
|
|
223
|
+
tables **et** ses données, et le dossier `migrations/` est vide — le mode de développement les a
|
|
224
|
+
créées au démarrage, sans jamais écrire de fichier.
|
|
225
|
+
|
|
226
|
+
Dans cet état, `orm:generate` **refuse** (`NF_GENERATE_DATABASE_NOT_ADOPTED`). Ce n'est pas une
|
|
227
|
+
précaution : la première migration décrirait la _création_ de tables qui existent, elle ne
|
|
228
|
+
s'appliquerait jamais, et l'adopter graverait dans l'historique un schéma que la base n'a pas —
|
|
229
|
+
l'état dont plus aucune commande ne sort.
|
|
230
|
+
|
|
231
|
+
Le refus nomme le geste, et **seulement celui qui produira quelque chose**. Si la base porte
|
|
232
|
+
_toutes_ les tables que le code déclare — le cas quand rien n'a changé depuis qu'elle a été
|
|
233
|
+
faite — il n'y a **qu'une** commande, et elle EST votre première migration :
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
nodefony orm:migrate:baseline --from-database
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Regénérer derrière ne donnerait rien : le code n'a pas bougé, il n'y a aucun écart à écrire.
|
|
240
|
+
La suite redevient ordinaire — le jour où vous ajoutez un champ :
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
nodefony orm:generate --name ajout_du_slug # produit un ALTER, plus un CREATE
|
|
244
|
+
nodefony orm:migrate
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Si la base n'en porte qu'une _partie_ — des entités ont été ajoutées depuis —, le refus propose
|
|
248
|
+
les deux gestes : l'adoption d'abord, la génération ensuite, qui écrira l'écart restant.
|
|
249
|
+
|
|
250
|
+
La commande **lit** le schéma de la base, en écrit la migration de référence sous
|
|
251
|
+
`migrations/<dialecte>/0000_<nom>.sql`, et l'inscrit comme appliquée. **Aucune instruction n'est
|
|
252
|
+
exécutée sur la base** : elle décrit un état déjà atteint. Son corps est laissé _exécutable_, pour
|
|
253
|
+
qu'un environnement neuf puisse être monté depuis ces mêmes fichiers.
|
|
254
|
+
|
|
255
|
+
Deux faits qu'elle publie, et qu'il faut lire :
|
|
256
|
+
|
|
257
|
+
| Ce qu'elle dit | Ce que ça veut dire |
|
|
258
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
259
|
+
| des tables **lues sans être déclarées** | la base est partagée. L'outil de lecture ne sait pas restreindre son champ ; la génération suivante proposera de les **supprimer**. Relisez le fichier. |
|
|
260
|
+
| un corps **resté en commentaire** | la référence ne recréerait rien sur une base neuve — l'outil a changé sa mise en forme. |
|
|
261
|
+
|
|
262
|
+
Deux nettoyages ont lieu sans que vous ayez à y penser, et il vaut mieux savoir qu'ils existent :
|
|
263
|
+
|
|
264
|
+
- **Les objets d'une table exclue ne sont pas repris.** L'exclusion porte sur les tables, jamais sur
|
|
265
|
+
ce qui gravite autour : la séquence de la table d'historique entrait dans la référence, qui
|
|
266
|
+
échouait alors sur un environnement neuf — la séquence y est déjà posée par les migrations du
|
|
267
|
+
framework — et dont la génération suivante proposait la suppression, que la base refuse.
|
|
268
|
+
- **PostgreSQL : le schéma visé est celui de votre connexion, et il est retiré de la référence.**
|
|
269
|
+
Si votre application vit ailleurs que dans `public` (le montage habituel d'une base mutualisée),
|
|
270
|
+
la lecture se fait bien dans VOTRE schéma — sans quoi elle décrirait les tables de quelqu'un
|
|
271
|
+
d'autre. Mais la référence, elle, est écrite sans le nommer : vos entités ne le nomment pas non
|
|
272
|
+
plus, et une référence qualifiée ferait poser une question de renommage au premier champ ajouté.
|
|
273
|
+
|
|
274
|
+
> ⚠️ **Sur MariaDB, cette commande refuse — et elle dit pourquoi.** MariaDB n'a pas de type JSON
|
|
275
|
+
> natif : il l'écrit en `longtext` assorti d'un `CHECK (json_valid(…))`. L'outil qui relit les
|
|
276
|
+
> schémas ne sait pas lire ces contraintes, et il lit la base **entière** avant de filtrer : les
|
|
277
|
+
> tables du framework suffisent donc à le bloquer. Le repli tient en quatre gestes, que le message
|
|
278
|
+
> d'erreur rappelle — relever le schéma (`SHOW CREATE TABLE`), un `orm:generate --custom`, y coller
|
|
279
|
+
> le schéma, puis `orm:migrate:baseline`.
|
|
280
|
+
>
|
|
281
|
+
> **Cela ne concerne que cette commande de reprise.** Créer les tables, appliquer les migrations et
|
|
282
|
+
> faire tourner l'application sont inchangés sur MariaDB.
|
|
283
|
+
|
|
284
|
+
## En développement — le schéma se répare tout seul
|
|
285
|
+
|
|
286
|
+
C'est le scénario de tous les jours d'une équipe : le back ajoute un champ à une entité, le front
|
|
287
|
+
tire la branche, et sa base locale date d'avant.
|
|
288
|
+
|
|
289
|
+
Au démarrage, en mode `auto`, la connexion se fait en **trois temps** — et l'ordre est ce qui compte :
|
|
290
|
+
|
|
291
|
+
1. les tables manquantes sont créées (`CREATE TABLE IF NOT EXISTS`) ;
|
|
292
|
+
2. le schéma déclaré est **comparé** à celui de la base, table par table
|
|
293
|
+
(`compareSchema()`, `schemaDiff.ts:122`) ;
|
|
294
|
+
3. les index sont posés.
|
|
295
|
+
|
|
296
|
+
Entre les deux, ce qui se rattrape est rattrapé :
|
|
297
|
+
|
|
298
|
+
- **colonne manquante qui accepte le vide** → elle est ajoutée (`additiveSql()`,
|
|
299
|
+
`schemaDiff.ts:181`) et journalisée en clair. Le front n'a rien à taper : il tire, le serveur
|
|
300
|
+
redémarre, ça marche.
|
|
301
|
+
- **colonne manquante et obligatoire** → jamais posée. La créer exigerait d'inventer une valeur pour
|
|
302
|
+
les lignes déjà présentes, ce qui est une décision métier, pas une décision d'outil. L'écart est
|
|
303
|
+
publié, journalisé, avec le geste exact.
|
|
304
|
+
- **colonne en trop dans la base** → ignorée, toujours. Une application qui écrit des migrations
|
|
305
|
+
libres (une vue, un déclencheur, une colonne ajoutée à une table d'entité) a une base
|
|
306
|
+
légitimement différente du schéma déclaré, en permanence.
|
|
307
|
+
|
|
308
|
+
> [!NOTE]
|
|
309
|
+
> Ce troisième temps n'est pas cosmétique. Un index porte sur des colonnes : le poser sur une table
|
|
310
|
+
> à laquelle il en manque une échoue, et cet échec-là **tuait le démarrage** — le développeur
|
|
311
|
+
> recevait un `no such column` du pilote, sans nom de connecteur, sans geste, et sans serveur pour
|
|
312
|
+
> aller voir. Les index d'une table encore en écart sont donc sautés : l'écart, lui, a déjà été dit.
|
|
313
|
+
|
|
314
|
+
Quand le rattrapage ne suffit pas, une seule commande à retenir :
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
nodefony orm:reset --connector default
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Elle **supprime et recrée** la base, et refuse dès que l'environnement n'est pas `development` —
|
|
321
|
+
liste blanche, pas liste noire : `staging` et tout environnement inconnu refusent aussi.
|
|
322
|
+
|
|
323
|
+
## En production — le patron de déploiement
|
|
324
|
+
|
|
325
|
+
### Le travail d'orchestrateur, AVANT le déploiement des pods
|
|
326
|
+
|
|
327
|
+
Le consensus cloud-native est solide, et Nodefony ne cherche pas à le remplacer : **les migrations
|
|
328
|
+
s'appliquent dans un travail dédié, qui se termine avant que le premier nouvel exemplaire ne
|
|
329
|
+
démarre**. Les pods, eux, tournent en `ddl: "none"`.
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
# Kubernetes — le patron de référence. Le déploiement attend la fin du travail.
|
|
333
|
+
apiVersion: batch/v1
|
|
334
|
+
kind: Job
|
|
335
|
+
metadata:
|
|
336
|
+
name: nodefony-migrate
|
|
337
|
+
spec:
|
|
338
|
+
backoffLimit: 2
|
|
339
|
+
template:
|
|
340
|
+
spec:
|
|
341
|
+
restartPolicy: Never
|
|
342
|
+
containers:
|
|
343
|
+
- name: migrate
|
|
344
|
+
image: mon-application:1.4.0
|
|
345
|
+
command: ["npx", "nodefony", "orm:migrate", "--json"]
|
|
346
|
+
env:
|
|
347
|
+
- name: NF_MIGRATE_DATABASE_URL
|
|
348
|
+
valueFrom:
|
|
349
|
+
secretKeyRef: { name: db-migrator, key: url }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Trois propriétés en découlent, et elles valent d'être nommées :
|
|
353
|
+
|
|
354
|
+
- **Le verrou rend les variantes sûres.** Si deux exemplaires du travail partent ensemble, le second
|
|
355
|
+
attend : le verrou est natif au moteur — `lock()` (`postgresDriver.ts:178`) demande un
|
|
356
|
+
`pg_advisory_lock` —, donc il s'auto-libère à la mort de la connexion. Une table de verrou maison laisserait un zombie à
|
|
357
|
+
déverrouiller à la main.
|
|
358
|
+
- **Rien n'est appliqué deux fois.** L'historique (`nodefony_migrations`, `types.ts:23`) est écrit
|
|
359
|
+
dans la même transaction que le DDL, là où le moteur le permet.
|
|
360
|
+
- **Un pod en retard ne reçoit pas de trafic.** En `none` comme en `migrate`, l'état du schéma est
|
|
361
|
+
publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:345`) : `/readyz`
|
|
362
|
+
répond 503, l'orchestrateur sort l'exemplaire du répartiteur de charge, et l'ancien continue de
|
|
363
|
+
servir. `/livez` n'est jamais touché — un schéma en retard n'est pas un processus malade, et le
|
|
364
|
+
redémarrer ne réparerait rien. La vérification est rejouée toutes les 15 secondes : dès que le
|
|
365
|
+
schéma est à jour, l'exemplaire redevient disponible **tout seul**, sans redéploiement.
|
|
366
|
+
|
|
367
|
+
### Les droits du compte qui migre ne sont pas ceux du trafic
|
|
368
|
+
|
|
369
|
+
Le compte qui applique une migration a besoin de modifier le schéma. Celui qui sert les requêtes,
|
|
370
|
+
non — et lui laisser ce pouvoir, c'est offrir un `DROP TABLE` à la première injection réussie.
|
|
371
|
+
|
|
372
|
+
`orm:migrate` lit donc `NF_MIGRATE_DATABASE_URL` **en priorité sur l'URL du connecteur**, et cette
|
|
373
|
+
variable n'est lue par personne d'autre : c'est le véhicule du moindre privilège.
|
|
374
|
+
|
|
375
|
+
```sql
|
|
376
|
+
-- PostgreSQL : le compte du TRAFIC ne peut que lire et écrire des données.
|
|
377
|
+
GRANT CONNECT ON DATABASE app TO app_runtime;
|
|
378
|
+
GRANT USAGE ON SCHEMA public TO app_runtime;
|
|
379
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_runtime;
|
|
380
|
+
|
|
381
|
+
-- Le compte qui MIGRE possède le schéma, et ne sert jamais de requête applicative.
|
|
382
|
+
GRANT CREATE ON SCHEMA public TO app_migrator;
|
|
383
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public
|
|
384
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_runtime;
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
> [!WARNING]
|
|
388
|
+
> `orm:reset` **n'est jamais** concerné : il ne lit pas `NF_MIGRATE_DATABASE_URL`. Une variable qui
|
|
389
|
+
> porte des droits de schéma ne doit pas pouvoir désigner la cible d'un effacement.
|
|
390
|
+
|
|
391
|
+
### Déployer sans interruption — expansion, puis contraction
|
|
392
|
+
|
|
393
|
+
Pendant un déploiement progressif, deux versions du code parlent à **une seule** base. La règle qui
|
|
394
|
+
en découle n'a rien d'optionnel : **toute migration doit être compatible avec la version encore en
|
|
395
|
+
service**. Un changement destructif se fait donc en deux déploiements.
|
|
396
|
+
|
|
397
|
+
Renommer une colonne `email` en `contact_email`, sans une seconde d'interruption :
|
|
398
|
+
|
|
399
|
+
| Étape | Migration | Code déployé |
|
|
400
|
+
| --------------- | --------------------------------- | ------------------------------------ |
|
|
401
|
+
| 1 — expansion | ajouter `contact_email`, nullable | écrit les DEUX colonnes, lit `email` |
|
|
402
|
+
| 2 | recopier les données existantes | inchangé |
|
|
403
|
+
| 3 | — | lit `contact_email`, écrit les deux |
|
|
404
|
+
| 4 — contraction | supprimer `email` | ne connaît plus `email` |
|
|
405
|
+
|
|
406
|
+
Chaque étape est déployable seule, et à aucun moment la base n'est incompatible avec ce qui tourne.
|
|
407
|
+
C'est plus long que le `ALTER TABLE … RENAME` d'un seul coup — et c'est la seule façon connue de ne
|
|
408
|
+
pas couper le service.
|
|
409
|
+
|
|
410
|
+
## Pourquoi il n'y a PAS de sauvegarde automatique
|
|
411
|
+
|
|
412
|
+
Aucune commande de Nodefony ne sauvegarde votre base avant d'agir. C'est une décision, pas un
|
|
413
|
+
manque, et **aucun outil de migration sérieux ne le fait** non plus.
|
|
414
|
+
|
|
415
|
+
Une sauvegarde automatique donnerait une assurance qui n'existe pas. Sur une base de production de
|
|
416
|
+
plusieurs centaines de gigaoctets, la prendre depuis le processus qui migre prendrait des heures,
|
|
417
|
+
saturerait le disque de l'exemplaire, et échouerait au pire moment. Sur une base répliquée, elle
|
|
418
|
+
ignorerait les réplicas. Et surtout : **une restauration est une décision d'exploitation**, avec sa
|
|
419
|
+
fenêtre d'indisponibilité, sa perte de données assumée entre deux points de reprise, et quelqu'un
|
|
420
|
+
qui la prend. Un outil qui prétendrait la préparer tout seul inviterait à ne pas y penser.
|
|
421
|
+
|
|
422
|
+
Ce que l'outil fait à la place, et qui vaut mieux :
|
|
423
|
+
|
|
424
|
+
- **il refuse d'appliquer sans que vous sachiez.** Le SQL en attente est examiné avant la moindre
|
|
425
|
+
écriture (`scanDestructive()`, `destructive.ts:184`) : une suppression de données est refusée hors
|
|
426
|
+
développement, en nommant l'instruction et ce qui disparaît. Il faut `--allow-destructive` pour
|
|
427
|
+
passer outre, et **au démarrage il n'y a aucun drapeau pour lever le refus** — un exemplaire qui
|
|
428
|
+
redémarre ne supprime jamais de colonne de lui-même, parce que personne ne regarde à ce
|
|
429
|
+
moment-là ;
|
|
430
|
+
- **il vous apprend l'expansion/contraction au moment où elle sert**, c'est-à-dire quand vous lisez
|
|
431
|
+
le refus.
|
|
432
|
+
|
|
433
|
+
La protection réelle contre la perte de données, c'est le patron ci-dessus et votre politique de
|
|
434
|
+
sauvegarde — pas un fichier `.bak` pris par un outil qui ne sait rien de votre infrastructure.
|
|
435
|
+
|
|
436
|
+
## La troisième source — le verdict `divergent`
|
|
437
|
+
|
|
438
|
+
Les outils de migration connaissent **deux** choses : les fichiers, et l'historique. Ils en
|
|
439
|
+
concluent « tout est appliqué ». Ils ne regardent jamais la base.
|
|
440
|
+
|
|
441
|
+
Nodefony croise une **troisième** source (`describeDivergence()`, `divergence.ts:127`), et rend un
|
|
442
|
+
constat qu'aucun outil ne produit en continu :
|
|
443
|
+
|
|
444
|
+
> l'historique est complet, aucune migration n'est en attente — **et pourtant la base ne correspond
|
|
445
|
+
> pas au code**.
|
|
446
|
+
|
|
447
|
+
Quelqu'un a passé un `ALTER` à la main un soir d'astreinte, un correctif d'urgence n'a jamais été
|
|
448
|
+
reporté, deux environnements ont divergé. Le verdict s'appelle `divergent`, il s'affiche dans
|
|
449
|
+
`orm:migrate:status`, il est publié par la sonde, et il obéit à trois règles :
|
|
450
|
+
|
|
451
|
+
- **il ne se paie que lorsque les deux autres sources n'ont plus rien à dire.** Tant qu'une
|
|
452
|
+
migration attend, le verdict est déjà décidé : interroger la base coûterait une requête par table
|
|
453
|
+
sans rien apprendre ;
|
|
454
|
+
- **il signale ce qui MANQUE, jamais ce qu'il trouve en trop.** Sans cette règle, toute application
|
|
455
|
+
à migrations libres l'aurait allumé à vie — donc appris comme du bruit, donc mort ;
|
|
456
|
+
- **il ne fait pas tomber un déploiement — sauf quand une TABLE d'entité manque.** Le code de
|
|
457
|
+
sortie reste `0` pour une colonne en écart : superviser n'est pas bloquer, et une application à
|
|
458
|
+
migrations libres en a une en permanence. Mais aucune main légitime ne fait _disparaître_ une
|
|
459
|
+
table que le code déclare comme entité : quand elle manque, le schéma applicatif n'a jamais été
|
|
460
|
+
posé, l'application rendra 500 sur chacune de ses routes, et le processus **retient sa mise en
|
|
461
|
+
service** (`/readyz` répond 503). Le seuil se règle :
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
use("@nodefony/drizzle", {
|
|
465
|
+
// "report" (défaut) — affiche tout écart, ne retient QUE sur une table absente
|
|
466
|
+
// "fail" — tout écart retient la mise en service
|
|
467
|
+
// "off" — rien n'est comparé, rien n'est publié
|
|
468
|
+
migrations: { divergence: "fail" },
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**La graduation est unique** (`divergenceIsBlocking()`, `explain.ts`) : la commande et la sonde de
|
|
473
|
+
disponibilité lisent la même règle, donc votre passe d'intégration continue et votre orchestrateur
|
|
474
|
+
ne peuvent pas se contredire.
|
|
475
|
+
|
|
476
|
+
### Il dit CE QUI diverge, pas seulement QU'IL diverge
|
|
477
|
+
|
|
478
|
+
Un verdict qui annonce un écart sans le nommer envoie ouvrir un client SQL et comparer table par
|
|
479
|
+
table, sur une base de production, au pire moment. La sortie porte donc les tables et les colonnes,
|
|
480
|
+
**séparées selon qu'elles se rattrapent ou non** — une colonne qui accepte le vide s'ajoute sans
|
|
481
|
+
rien inventer, une colonne obligatoire exige une décision métier :
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
nodefony orm:migrate:status --json | jq '.divergence'
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"missingTables": ["webhook_endpoint"],
|
|
490
|
+
"blocking": [
|
|
491
|
+
{ "table": "User", "column": "tenantId", "type": "text", "nullable": false }
|
|
492
|
+
],
|
|
493
|
+
"additive": [
|
|
494
|
+
{
|
|
495
|
+
"table": "audit_event",
|
|
496
|
+
"column": "metadata",
|
|
497
|
+
"type": "jsonb",
|
|
498
|
+
"nullable": true
|
|
499
|
+
}
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
La clé vit au premier niveau, dans le cœur neutre — pas sous `driver` : un second ORM remplira la
|
|
505
|
+
même structure, et un `jq` écrit aujourd'hui ne doit pas graver le nom d'un pilote. **Sur une base
|
|
506
|
+
conforme, la clé est ABSENTE** (jamais un objet vide) : `.divergence == null` suffit à tester.
|
|
507
|
+
|
|
508
|
+
L'écran lisible en dit autant : le résumé nomme les trois premières entrées de chaque famille, et la
|
|
509
|
+
liste complète ne se déroule que lorsqu'elle ne tient plus dans la phrase.
|
|
510
|
+
|
|
511
|
+
Les gestes proposés suivent **l'environnement** : `orm:reset` efface, elle n'est acceptée qu'en
|
|
512
|
+
développement, et elle n'est donc proposée que là. Ailleurs, la sortie renvoie vers l'écriture d'une
|
|
513
|
+
migration correctrice (`orm:generate --custom`) puis son application.
|
|
514
|
+
|
|
515
|
+
Ils suivent aussi **ce qui manque**. Une table d'entité absente se rattrape par le générateur — il
|
|
516
|
+
sait la produire, puisque le code la déclare — et c'est `orm:generate --name …` qui est proposé, pas
|
|
517
|
+
`--custom` : envoyer écrire à la main ce que la commande d'à côté écrit seule serait un geste juste
|
|
518
|
+
pour une colonne et absurde pour une table.
|
|
519
|
+
|
|
520
|
+
## Quand l'historique MENT — `repair --forget`
|
|
521
|
+
|
|
522
|
+
Il existe un état que ni les fichiers ni l'historique ne peuvent signaler seuls : **une migration
|
|
523
|
+
inscrite comme réussie que personne n'a jamais exécutée.** L'historique est complet, rien n'est en
|
|
524
|
+
attente, aucun marqueur d'échec — et pourtant la base ne porte pas les tables.
|
|
525
|
+
|
|
526
|
+
Deux chemins y mènent, tous deux réels :
|
|
527
|
+
|
|
528
|
+
- une **adoption bornée au mauvais endroit** : `orm:migrate:baseline --up-to <tag>` inscrit tout ce
|
|
529
|
+
qui précède le tag, et si l'on s'est trompé de borne, ce qui suit a été gravé sans être appliqué ;
|
|
530
|
+
- une base **héritée** d'une version antérieure aux gardes actuelles.
|
|
531
|
+
|
|
532
|
+
Le verdict `divergent` le voit et NOMME ce qui manque. Mais aucune commande ne « rejoue » une
|
|
533
|
+
migration que l'historique donne pour appliquée : la réparation ordinaire ne connaît que les
|
|
534
|
+
marqueurs d'ÉCHEC, et répond « rien à réparer ». C'est une impasse, et une impasse fait faire des
|
|
535
|
+
gestes qu'on regrette — au banc de découvrabilité, le seul chemin restant était d'effacer la base.
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
nodefony orm:migrate:status --json # NOMME les tables qui manquent
|
|
539
|
+
nodefony orm:migrate:repair --forget app/0003_facture # désinscrit CETTE migration
|
|
540
|
+
nodefony orm:migrate # elle est rejouée
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Trois propriétés, et elles sont volontaires :
|
|
544
|
+
|
|
545
|
+
| Propriété | Pourquoi |
|
|
546
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
547
|
+
| **il faut la NOMMER** | `<source>/<tag>` exactement. Ni motif, ni lot, ni « toutes celles de cette source » : un oubli en masse est le geste qu'on ne veut pas rendre facile. |
|
|
548
|
+
| **la base n'est PAS touchée** | seule la ligne d'historique disparaît. Si la migration avait bien été appliquée, son rejeu échouera — bruyamment, et c'est le comportement voulu. |
|
|
549
|
+
| **c'est le produit qui le fait** | l'interdit de toucher à la table d'historique vise la main dans un client SQL, sans trace. Le geste existe donc ici, borné et journalisé, plutôt qu'ailleurs. |
|
|
550
|
+
|
|
551
|
+
> ⚠️ N'utilisez cette option que sur un état CONSTATÉ. Le point de départ n'est pas ce refus, c'est
|
|
552
|
+
> `orm:migrate:status` : c'est lui qui dit quelles tables manquent, donc quelle migration n'a
|
|
553
|
+
> jamais tourné.
|
|
554
|
+
|
|
555
|
+
## Codes de sortie et sortie `--json`
|
|
556
|
+
|
|
557
|
+
La grille est **figée, et ne sera jamais réaffectée** — des passes d'intégration continue s'y
|
|
558
|
+
adossent, et en changer le sens casserait des tests que nous ne voyons pas.
|
|
559
|
+
|
|
560
|
+
| Code | Ce que ça veut dire |
|
|
561
|
+
| ---- | --------------------------------------------------------------------------------------------------------------- |
|
|
562
|
+
| `0` | à jour, ou appliqué avec succès |
|
|
563
|
+
| `1` | une action humaine est requise : migrations en attente, dérive, échec, refus destructif, table d'entité absente |
|
|
564
|
+
| `2` | la commande n'a pas pu travailler : base injoignable, verrou indisponible, usage invalide |
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# Barrière d'intégration continue : la passe s'arrête si le schéma n'est pas à jour.
|
|
568
|
+
nodefony orm:migrate:status --json || exit 1
|
|
569
|
+
|
|
570
|
+
# Ce qu'un agent lit — jamais une phrase française.
|
|
571
|
+
nodefony orm:migrate:status --json | jq -r '.verdict, .nextActions[0].command'
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Chaque sortie `--json` porte `formatVersion: 1` au premier niveau. Ajouter un champ est une version
|
|
575
|
+
mineure ; en retirer ou en renommer un est interdit sur la série majeure.
|
|
576
|
+
|
|
577
|
+
## Le même état, dans la console d'administration
|
|
578
|
+
|
|
579
|
+
Ce que la ligne de commande rend, la console le montre — page **Migrations**
|
|
580
|
+
(`/nodefony/migrate`) : le verdict, l'identité du connecteur, les gestes à copier, et une ligne par
|
|
581
|
+
migration avec sa date, sa durée, son auteur, son déploiement et le motif de son échec.
|
|
582
|
+
|
|
583
|
+
Trois points du plan d'administration la servent, tous derrière le rôle d'administration :
|
|
584
|
+
|
|
585
|
+
| Point | Ce qu'il rend |
|
|
586
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------- |
|
|
587
|
+
| `GET /nodefony/orm/api/migrations?connector=` | **exactement** la charge utile de `orm:migrate:status --json` |
|
|
588
|
+
| `GET /nodefony/orm/api/migrations/plan?connector=` | ce qui S'APPLIQUERAIT, avec le SQL de chaque migration en attente |
|
|
589
|
+
| `POST /nodefony/orm/api/migrations/apply?connector=` | applique — **refusé hors développement**, en disant par quoi passer |
|
|
590
|
+
|
|
591
|
+
Trois choses valent d'être dites, parce qu'elles décident de ce que vous pouvez croire à l'écran :
|
|
592
|
+
|
|
593
|
+
- **L'écran ne calcule rien.** Il affiche l'objet que le plan lui rend, et cet objet est celui de la
|
|
594
|
+
commande — vérifié par égalité. Deux calculs de la même question finiraient par se contredire, et
|
|
595
|
+
c'est le jour d'un incident qu'on s'en apercevrait.
|
|
596
|
+
- **Appliquer depuis la console n'existe qu'en développement**, et le refus vient du produit, pas de
|
|
597
|
+
l'interface : un appel direct au plan est refusé de la même façon. En production, les migrations
|
|
598
|
+
passent par un travail d'orchestrateur qui se termine AVANT que le premier nouvel exemplaire ne
|
|
599
|
+
démarre.
|
|
600
|
+
- **Un connecteur qui ne porte pas de migrations reçoit une réponse qui le NOMME** (`501`), jamais
|
|
601
|
+
une page vide. Un écran qui se tait quand la donnée manque ressemble à « tout va bien ».
|
|
602
|
+
|
|
603
|
+
## ⚠️ Pièges
|
|
604
|
+
|
|
605
|
+
- **Un fichier de migration déjà appliqué ne se modifie pas.** L'empreinte le détecte et le verdict
|
|
606
|
+
devient `drift`. Écrivez une migration correctrice — c'est plus long, et c'est ce qui garde
|
|
607
|
+
l'historique honnête.
|
|
608
|
+
- **MySQL ne sait pas annuler un `CREATE TABLE`.** Son DDL valide implicitement : après un échec à
|
|
609
|
+
mi-course, la reprise aveugle est interdite, et c'est `orm:migrate:repair` — après inspection
|
|
610
|
+
humaine — qui tranche.
|
|
611
|
+
- **Une colonne obligatoire SANS valeur par défaut ne se comporte pas pareil selon le moteur.** Sur
|
|
612
|
+
une table qui porte déjà des lignes, sqlite refuse (« Cannot add a NOT NULL column with default
|
|
613
|
+
value NULL ») et PostgreSQL refuse (« contains null values ») — ils ne peuvent pas inventer la
|
|
614
|
+
valeur des lignes existantes. **MySQL/MariaDB accepte** et les remplit de chaînes vides, mode
|
|
615
|
+
strict compris : le champ est déclaré obligatoire et ne contient que du vide, sans un
|
|
616
|
+
avertissement. Donnez toujours un défaut, ou déclarez le champ facultatif ; si les deux sont
|
|
617
|
+
nécessaires, c'est trois migrations — ajouter avec défaut, remplir (`--custom`), retirer le
|
|
618
|
+
défaut. Mesuré sur les trois moteurs :
|
|
619
|
+
`src/packages/@nodefony/drizzle/tests/integration/user-migrations.e2e.test.ts:1`.
|
|
620
|
+
- **`orm:migrate:repair --update-hashes` réécrit les empreintes.** Il fait taire une dérive au lieu
|
|
621
|
+
de la corriger : les autres bases ont reçu l'ancienne version du fichier et ne recevront jamais la
|
|
622
|
+
nouvelle. C'est pour cela que le refus propose d'abord de RÉTABLIR le fichier, et ce
|
|
623
|
+
ré-alignement seulement en second — ne l'utilisez qu'en sachant que la modification était sans
|
|
624
|
+
effet (une reformulation, un commentaire).
|
|
625
|
+
- **`orm:migrate --dry-run` n'écrit rien du tout** — pas même la table d'historique, et il ne prend
|
|
626
|
+
pas le verrou. Un compte en lecture seule suffit donc à voir le plan, et la base reste
|
|
627
|
+
bit-à-bit celle d'avant : c'est ce qui rend l'essai vérifiable.
|
|
628
|
+
- **Une migration en attente qui se range AVANT la dernière appliquée est refusée** (deux branches
|
|
629
|
+
fusionnées). `--out-of-order` l'assume, et il faut l'assumer sciemment : l'ordre d'application ne
|
|
630
|
+
sera plus celui du journal.
|
|
631
|
+
- **En développement, une colonne obligatoire ajoutée à une entité ne se rattrape pas.** C'est le
|
|
632
|
+
cas le plus fréquent après le cas nullable, et la seule issue est `orm:reset` — ou une vraie
|
|
633
|
+
migration si la base porte des données auxquelles vous tenez.
|
|
634
|
+
- **Le rattrapage automatique n'existe qu'en mode `auto`.** En `migrate` et `none`, la comparaison
|
|
635
|
+
constate et ne répare jamais.
|
|
636
|
+
- **Un tag et un nom de source sont SENSIBLES À LA CASSE.** `--up-to 0003_Audit` et `--source App`
|
|
637
|
+
sont refusés, en nommant la bonne graphie. Ce n'est pas du zèle : sans point d'arrêt reconnu,
|
|
638
|
+
l'adoption déclarerait à niveau **tout** l'historique — et une base ne reçoit jamais une migration
|
|
639
|
+
qu'elle croit déjà avoir. Le refus sur `--source` évite l'autre moitié du piège : filtrer sur un
|
|
640
|
+
nom inconnu ne touche rien et rend pourtant « rien à réparer ».
|
|
641
|
+
- **Un `.sql` peut être écrit avec une marque d'ordre des octets** (les éditeurs Windows et
|
|
642
|
+
PowerShell la posent). Elle est retirée à la lecture, et ne compte ni dans le marqueur de format
|
|
643
|
+
ni dans l'empreinte : un dépôt relu sous Windows ne fait donc pas diverger les empreintes posées
|
|
644
|
+
par l'image Linux qui a migré la base d'équipe.
|
|
645
|
+
- **Une ligne qui commence par deux tirets À L'INTÉRIEUR d'une chaîne littérale reste de la
|
|
646
|
+
donnée.** C'est le cas d'un remplissage textuel multi-ligne écrit avec `orm:generate --custom` :
|
|
647
|
+
la retirer changerait silencieusement ce qui est inséré.
|
|
648
|
+
- **Le séparateur d'instructions écrit DANS un commentaire n'en est pas un.** `--> statement-breakpoint`
|
|
649
|
+
commence lui-même par deux tirets : rien ne le distingue d'un commentaire à l'œil du moteur. Une
|
|
650
|
+
ligne de commentaire qui le CITE — ce que fait le gabarit d'une migration libre — ne coupe donc
|
|
651
|
+
rien.
|
|
652
|
+
|
|
653
|
+
## 🧪 Tests & couverture
|
|
654
|
+
|
|
655
|
+
| Ce qui est prouvé | Où |
|
|
656
|
+
| ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
657
|
+
| Applicateur : identité ensembliste, dérive, ordre, échec puis réparation, adoption, idempotence | `tests/integration/migrator-sqlite.test.ts` |
|
|
658
|
+
| Verrou entre process, absence de zombie, DDL non transactionnel | `migrator-postgres.e2e.test.ts`, `migrator-mysql.e2e.test.ts` |
|
|
659
|
+
| Les cinq verbes sur un **boot réel**, dans les trois modes | `migrate-cli.e2e.test.ts` (`NF_RUN_CLI_BOOT=1`) |
|
|
660
|
+
| Rattrapage additif, refus d'inventer, colonne en trop ignorée | `schema-reconcile.test.ts` |
|
|
661
|
+
| Rattrapage sur **serveurs réels** (types, catalogue, index) | `schema-reconcile-dialects.e2e.test.ts` |
|
|
662
|
+
| Verdict `divergent`, son absence quand un geste est déjà dû, et le DÉTAIL qu'il nomme | `migrate-divergence.test.ts` |
|
|
663
|
+
| Refus destructif : ce qui perd des données, ce qui n'en perd pas | `migrate-destructive.test.ts` |
|
|
664
|
+
| Parité entre le schéma migré et le schéma dérivé, sur les 3 dialectes | `migrations-parity-*.test.ts` |
|
|
665
|
+
| **Chaque réglage** sur son couple (refus sans lui, travail avec lui), 3 dialectes | `migrate-reglages.test.ts` |
|
|
666
|
+
| Lecture et empreinte d'un fichier : marque d'ordre des octets, CRLF, chaînes littérales | `tests/unit/migrationFichiers.test.ts` |
|
|
667
|
+
| Le nom d'une migration, et l'invariant « une suggestion est toujours acceptable » | `tests/unit/migrationName.test.ts` |
|
|
668
|
+
| Le cycle complet **dans une application générée** : génération, barrière de déploiement, `/readyz` 503, divergence provoquée | gabarit `tests/migrations.e2e.test.ts` de toute app à ORM |
|
|
669
|
+
| Le découpage en instructions : séparateur dans un commentaire, séparateur collé en fin de ligne | `tests/unit/migratorContracts.test.ts` |
|
|
670
|
+
|
|
671
|
+
Les bancs PostgreSQL et MySQL/MariaDB exigent leurs serveurs :
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
docker compose -f docker/docker-compose.yml --profile postgres up -d postgres
|
|
675
|
+
docker compose -f docker/docker-compose.yml --profile mariadb up -d mariadb
|
|
676
|
+
NF_PG_URL=postgres://nodefony:nodefony-dev@127.0.0.1:5432/nodefony \
|
|
677
|
+
NF_MYSQL_URL=mysql://nodefony:nodefony-dev@127.0.0.1:3306/nodefony npm test
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
> [!CAUTION]
|
|
681
|
+
> Sans ces variables, les bancs se **skippent** — et un test skippé compte comme vert. La suite du
|
|
682
|
+
> module affiche en fin de passe ce qu'elle n'a PAS exercé : lisez ce bloc avant de conclure.
|
|
683
|
+
|
|
684
|
+
## 🔗 Pour aller plus loin
|
|
685
|
+
|
|
686
|
+
- ⬆️ **Retour au hub** : [@nodefony/drizzle](index.md)
|
|
687
|
+
- [Configuration du module](index.md#configuration) — la clé `ddl` et le bloc `migrations`
|
|
688
|
+
- [Dialectes](index.md#dialectes--une-base-par-déploiement-un-seul-code) — porter ses entités sur
|
|
689
|
+
PostgreSQL et MySQL
|
|
690
|
+
- [Les huit stores du framework](index.md#les-huit-stores-du-framework--la-persistance-clé-en-main) —
|
|
691
|
+
ce que le paquet livre déjà, migrations comprises
|