@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,565 @@
|
|
|
1
|
+
import { HISTORY_TABLE } from "./types.js";
|
|
2
|
+
//#region nodefony/src/migrator/explain.ts
|
|
3
|
+
/**
|
|
4
|
+
* Le RENDU des migrations : un seul producteur, quatre destinataires.
|
|
5
|
+
*
|
|
6
|
+
* La ligne de commande, la sortie `--json`, le plan d'administration et
|
|
7
|
+
* l'assistance affichée dans un corps d'erreur disent tous la même chose. Écrire
|
|
8
|
+
* la phrase pour l'humain d'un côté et l'objet pour la machine de l'autre
|
|
9
|
+
* ferait deux implémentations d'une même règle — et elles divergeraient, comme
|
|
10
|
+
* toutes les copies.
|
|
11
|
+
*
|
|
12
|
+
* ## Ce que ce fichier garantit, et qui n'est pas cosmétique
|
|
13
|
+
*
|
|
14
|
+
* **Aucune sortie ne laisse l'utilisateur sans geste suivant.** Chaque situation
|
|
15
|
+
* — succès, attente, refus, panne — produit trois choses :
|
|
16
|
+
*
|
|
17
|
+
* 1. **le FAIT**, en français, sans terme d'art (« le fichier `0002_x.sql` a
|
|
18
|
+
* changé après avoir été appliqué ») ;
|
|
19
|
+
* 2. **ce que ça veut dire**, c'est-à-dire la cause la plus probable ;
|
|
20
|
+
* 3. **la commande exacte à copier**, jamais une allusion à une option qu'il
|
|
21
|
+
* faudrait deviner.
|
|
22
|
+
*
|
|
23
|
+
* Un agent lit `nextActions[0].command` et sait quoi faire sans comprendre le
|
|
24
|
+
* français ; un humain lit les mêmes mots sous une forme lisible. C'est la même
|
|
25
|
+
* donnée.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Version de la charge utile `--json` **et** du plan d'administration.
|
|
29
|
+
*
|
|
30
|
+
* Contrat public : ajouter un champ est une évolution mineure, en retirer ou en
|
|
31
|
+
* renommer un est interdit sur toute la série majeure. Un `jq` écrit par un
|
|
32
|
+
* utilisateur ne doit jamais casser sur une mise à jour de correctif.
|
|
33
|
+
*/
|
|
34
|
+
const MIGRATION_FORMAT_VERSION = 1;
|
|
35
|
+
/** Grille des codes de sortie — **figée, jamais réaffectée**. */
|
|
36
|
+
const EXIT = {
|
|
37
|
+
/** À jour, ou appliqué avec succès. */
|
|
38
|
+
ok: 0,
|
|
39
|
+
/** Une action humaine est requise sur la base ou sur les fichiers. */
|
|
40
|
+
actionRequired: 1,
|
|
41
|
+
/** La commande n'a pas pu faire son travail (verrou, connexion, usage). */
|
|
42
|
+
error: 2
|
|
43
|
+
};
|
|
44
|
+
/** Fabrique une action affichable et exécutable. */
|
|
45
|
+
function action(command) {
|
|
46
|
+
return {
|
|
47
|
+
command,
|
|
48
|
+
args: command.split(" ").slice(1)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Situation d'ensemble d'un plan, dans l'ordre de gravité.
|
|
53
|
+
*
|
|
54
|
+
* L'ordre n'est pas esthétique : il dit quel geste vient EN PREMIER. Une
|
|
55
|
+
* migration en échec doit être réparée avant qu'on parle de ce qui reste à
|
|
56
|
+
* appliquer, sinon l'utilisateur lance une commande qui va refuser.
|
|
57
|
+
*
|
|
58
|
+
* @param plan - plan calculé en lecture seule.
|
|
59
|
+
* @param divergent - la base a-t-elle divergé du schéma déclaré ?
|
|
60
|
+
* @returns le verdict, énumération gelée.
|
|
61
|
+
*/
|
|
62
|
+
function verdictOf(plan, divergent = false) {
|
|
63
|
+
if (plan.failed.length > 0) return "failed";
|
|
64
|
+
if (plan.drifted.length > 0 || plan.missing.length > 0) return "drift";
|
|
65
|
+
if (plan.baselineRequired) return "adopt";
|
|
66
|
+
if (plan.pending.length > 0) return "pending";
|
|
67
|
+
return divergent ? "divergent" : "up-to-date";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* L'historique porte-t-il des migrations que CE code ne connaît pas, et rien
|
|
71
|
+
* d'autre ne cloche-t-il ?
|
|
72
|
+
*
|
|
73
|
+
* C'est l'état NORMAL de deux moments qu'on ne peut pas éviter : une mise à jour
|
|
74
|
+
* progressive, où les anciens exemplaires servent encore pendant que le travail
|
|
75
|
+
* de migration a déjà appliqué la suite ; et un retour arrière, où le code
|
|
76
|
+
* revient en arrière et la base reste en avance. Dans les deux cas, l'exemplaire
|
|
77
|
+
* n'a rien à appliquer, et tout ce qu'il connaît concorde.
|
|
78
|
+
*
|
|
79
|
+
* **Le verdict, lui, reste `drift` — et c'est juste** : ce qui est écrit dans
|
|
80
|
+
* l'historique ne se retrouve pas sur le disque. L'énumération des verdicts est
|
|
81
|
+
* GELÉE avec {@link MIGRATION_FORMAT_VERSION}, et un huitième mot casserait tout
|
|
82
|
+
* consommateur qui les traite exhaustivement. Ce qui était faux n'était pas le
|
|
83
|
+
* constat, c'était ce que la sonde de disponibilité en DÉDUISAIT : retenir le
|
|
84
|
+
* trafic sortait du service tous les anciens exemplaires dès la fin du travail
|
|
85
|
+
* de migration, avant que le premier nouveau soit prêt — une coupure totale sur
|
|
86
|
+
* un déploiement nominal, et l'impossibilité de revenir en arrière.
|
|
87
|
+
*
|
|
88
|
+
* Ce que cette fonction ne couvre PAS, et qui doit continuer de retenir : une
|
|
89
|
+
* empreinte qui a changé (`drifted`), une migration en attente ou en échec, une
|
|
90
|
+
* adoption requise. Une base en avance n'a rien de commun avec un fichier
|
|
91
|
+
* réécrit après coup.
|
|
92
|
+
*
|
|
93
|
+
* @param plan - plan calculé par le migrateur.
|
|
94
|
+
* @returns vrai si le seul écart est un historique en avance sur ce code.
|
|
95
|
+
*/
|
|
96
|
+
function isAheadOnly(plan) {
|
|
97
|
+
return plan.missing.length > 0 && plan.drifted.length === 0 && plan.pending.length === 0 && plan.failed.length === 0 && !plan.baselineRequired;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Une divergence RETIENT-elle un déploiement ?
|
|
101
|
+
*
|
|
102
|
+
* ## Pourquoi ce n'est pas un simple « le mode vaut-il `fail` »
|
|
103
|
+
*
|
|
104
|
+
* Le défaut est l'observation, et pour une bonne raison : une application qui
|
|
105
|
+
* écrit des migrations libres — vues, déclencheurs, colonnes ajoutées à la main
|
|
106
|
+
* — a une base légitimement différente du schéma déclaré, en permanence. Faire
|
|
107
|
+
* tomber ses déploiements là-dessus rendrait le constat inutilisable, et la
|
|
108
|
+
* première chose qu'on ferait serait de l'éteindre.
|
|
109
|
+
*
|
|
110
|
+
* **Mais ce raisonnement ne couvre pas une TABLE d'entité absente.** Aucune
|
|
111
|
+
* migration libre ne fait disparaître une table que le code déclare comme
|
|
112
|
+
* entité ; quand elle manque, c'est que le schéma applicatif n'a jamais été
|
|
113
|
+
* posé — la migration n'a pas été générée, pas commitée, ou pas appliquée. Ce
|
|
114
|
+
* n'est pas une base « différente », c'est une base sur laquelle l'application
|
|
115
|
+
* ne peut RIEN faire : chacune de ses routes rendra 500, et le pod se serait
|
|
116
|
+
* déclaré prêt.
|
|
117
|
+
*
|
|
118
|
+
* C'est exactement le constat qui a ouvert ce chantier : les onze tables du
|
|
119
|
+
* framework posées, zéro table applicative, et toutes les routes d'entités en
|
|
120
|
+
* erreur — sans qu'aucun verdict ne le dise assez fort pour arrêter quoi que
|
|
121
|
+
* ce soit.
|
|
122
|
+
*
|
|
123
|
+
* La graduation tient donc en trois lignes, et chaque lecteur y trouve son
|
|
124
|
+
* seuil : `off` ne retient jamais, `fail` retient tout écart, `report` — le
|
|
125
|
+
* défaut — retient ce qu'aucune main légitime ne produit.
|
|
126
|
+
*
|
|
127
|
+
* @param divergence - les écarts nommés, ou `null` quand il n'y en a pas.
|
|
128
|
+
* @param mode - `migrations.divergence`, tel que la configuration le déclare.
|
|
129
|
+
* @returns `true` si la situation doit retenir la mise en service.
|
|
130
|
+
*/
|
|
131
|
+
function divergenceIsBlocking(divergence, mode = "report") {
|
|
132
|
+
if (!divergence || mode === "off") return false;
|
|
133
|
+
return mode === "fail" || divergence.missingTables.length > 0;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Le code de sortie d'un verdict.
|
|
137
|
+
*
|
|
138
|
+
* **`divergent` ne fait pas tomber un déploiement** quand la configuration le
|
|
139
|
+
* laisse en observation : superviser n'est pas bloquer. C'est ce qui rend le
|
|
140
|
+
* constat utilisable — une application qui écrit des migrations libres a une
|
|
141
|
+
* base légitimement différente du schéma déclaré, en permanence.
|
|
142
|
+
*
|
|
143
|
+
* @param verdict - situation d'ensemble.
|
|
144
|
+
* @param divergenceBlocks - `true` quand la divergence doit retenir la mise en
|
|
145
|
+
* service, tel que {@link divergenceIsBlocking} en décide.
|
|
146
|
+
* @returns `0`, `1` ou `2`.
|
|
147
|
+
*/
|
|
148
|
+
function exitCodeOf(verdict, divergenceBlocks = false) {
|
|
149
|
+
if (verdict === "up-to-date") return EXIT.ok;
|
|
150
|
+
if (verdict === "divergent") return divergenceBlocks ? EXIT.actionRequired : EXIT.ok;
|
|
151
|
+
return EXIT.actionRequired;
|
|
152
|
+
}
|
|
153
|
+
/** Regroupe les entrées d'un plan par source, en gardant l'ordre. */
|
|
154
|
+
function groupSources(plan) {
|
|
155
|
+
const byName = /* @__PURE__ */ new Map();
|
|
156
|
+
const ensure = (name) => {
|
|
157
|
+
let entry = byName.get(name);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
entry = {
|
|
160
|
+
name,
|
|
161
|
+
applied: 0,
|
|
162
|
+
pending: 0,
|
|
163
|
+
failed: 0,
|
|
164
|
+
pendingTags: [],
|
|
165
|
+
drifted: [],
|
|
166
|
+
missing: [],
|
|
167
|
+
entries: []
|
|
168
|
+
};
|
|
169
|
+
byName.set(name, entry);
|
|
170
|
+
}
|
|
171
|
+
return entry;
|
|
172
|
+
};
|
|
173
|
+
const detail = (row) => ({
|
|
174
|
+
tag: row.tag,
|
|
175
|
+
status: row.success ? "applied" : "failed",
|
|
176
|
+
appliedAt: row.startedAt,
|
|
177
|
+
...row.executionMs !== null ? { durationMs: row.executionMs } : {},
|
|
178
|
+
...row.appliedBy !== null ? { appliedBy: row.appliedBy } : {},
|
|
179
|
+
runId: row.runId,
|
|
180
|
+
...row.error !== null ? { error: row.error } : {}
|
|
181
|
+
});
|
|
182
|
+
for (const row of plan.applied) {
|
|
183
|
+
const entry = ensure(row.source);
|
|
184
|
+
entry.applied += 1;
|
|
185
|
+
entry.entries.push(detail(row));
|
|
186
|
+
}
|
|
187
|
+
for (const file of plan.pending) {
|
|
188
|
+
const entry = ensure(file.source);
|
|
189
|
+
entry.pending += 1;
|
|
190
|
+
entry.pendingTags.push(file.tag);
|
|
191
|
+
entry.entries.push({
|
|
192
|
+
tag: file.tag,
|
|
193
|
+
status: "pending"
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const row of plan.failed) {
|
|
197
|
+
const entry = ensure(row.source);
|
|
198
|
+
entry.failed += 1;
|
|
199
|
+
entry.entries.push(detail(row));
|
|
200
|
+
}
|
|
201
|
+
for (const d of plan.drifted) {
|
|
202
|
+
const entry = ensure(d.source);
|
|
203
|
+
entry.drifted.push({
|
|
204
|
+
tag: d.tag,
|
|
205
|
+
expected: d.expected,
|
|
206
|
+
actual: d.actual
|
|
207
|
+
});
|
|
208
|
+
const already = entry.entries.find((e) => e.tag === d.tag);
|
|
209
|
+
if (already) already.status = "drifted";
|
|
210
|
+
else entry.entries.push({
|
|
211
|
+
tag: d.tag,
|
|
212
|
+
status: "drifted"
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const m of plan.missing) {
|
|
216
|
+
const entry = ensure(m.source);
|
|
217
|
+
entry.missing.push(m.tag);
|
|
218
|
+
entry.entries.push({
|
|
219
|
+
tag: m.tag,
|
|
220
|
+
status: "missing"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return [...byName.values()];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Compose la charge utile complète d'un état de migration.
|
|
227
|
+
*
|
|
228
|
+
* @param plan - plan calculé en lecture seule.
|
|
229
|
+
* @param ctx - mode de schéma et conduite face à la divergence.
|
|
230
|
+
* @returns la charge utile, identique pour la ligne de commande, `--json`, le
|
|
231
|
+
* plan d'administration et la sonde.
|
|
232
|
+
*/
|
|
233
|
+
function buildReport(plan, ctx) {
|
|
234
|
+
const divergence = ctx.divergence ?? null;
|
|
235
|
+
const verdict = verdictOf(plan, divergence !== null);
|
|
236
|
+
const sources = groupSources(plan);
|
|
237
|
+
return {
|
|
238
|
+
formatVersion: 1,
|
|
239
|
+
connector: plan.connector,
|
|
240
|
+
verdict,
|
|
241
|
+
exitCode: exitCodeOf(verdict, divergenceIsBlocking(divergence, ctx.divergenceMode)),
|
|
242
|
+
summary: summaryOf(plan, verdict, divergence),
|
|
243
|
+
nextActions: actionsOf(plan, verdict, ctx.canReset === true && ctx.ddl === "auto", divergence),
|
|
244
|
+
sources,
|
|
245
|
+
...divergence ? { divergence } : {},
|
|
246
|
+
driver: {
|
|
247
|
+
kind: "sql",
|
|
248
|
+
dialect: plan.dialect,
|
|
249
|
+
ddl: ctx.ddl,
|
|
250
|
+
historyTable: HISTORY_TABLE,
|
|
251
|
+
...ctx.target === void 0 ? {} : { target: ctx.target },
|
|
252
|
+
...ctx.fromMigrateUrl === void 0 ? {} : { fromMigrateUrl: ctx.fromMigrateUrl }
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Combien d'écarts par famille la PHRASE nomme avant de dire « et N de plus ». */
|
|
257
|
+
const SUMMARY_GAPS = 3;
|
|
258
|
+
/**
|
|
259
|
+
* La phrase a-t-elle nommé TOUS les écarts, ou en a-t-elle tronqué ?
|
|
260
|
+
*
|
|
261
|
+
* Sert au seul rendu à l'écran : quand la phrase dit déjà tout, dérouler la
|
|
262
|
+
* même chose juste au-dessus est du bruit — et le bruit est ce qui fait
|
|
263
|
+
* arrêter de lire une sortie d'incident.
|
|
264
|
+
*
|
|
265
|
+
* @param d - les écarts.
|
|
266
|
+
* @returns `true` si aucune famille n'a été tronquée.
|
|
267
|
+
*/
|
|
268
|
+
function namesEverything(d) {
|
|
269
|
+
return d.missingTables.length <= SUMMARY_GAPS && d.blocking.length <= SUMMARY_GAPS && d.additive.length <= SUMMARY_GAPS;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Ce qui diverge, EN TOUTES LETTRES — tables et colonnes nommées.
|
|
273
|
+
*
|
|
274
|
+
* C'est la moitié utile du verdict `divergent`. Sans elle, « la base ne
|
|
275
|
+
* correspond pas au schéma déclaré » envoie ouvrir un client SQL et comparer
|
|
276
|
+
* table par table, sur une base de production, au pire moment — pour une
|
|
277
|
+
* réponse que le produit avait déjà calculée.
|
|
278
|
+
*
|
|
279
|
+
* **Bornée à trois entrées par famille**, dans l'ordre de gravité : une phrase
|
|
280
|
+
* qui déroule quarante colonnes n'est plus lue. Le compte total reste dit, et
|
|
281
|
+
* `IMigrationReport.divergence` porte la liste ENTIÈRE pour qui la veut.
|
|
282
|
+
*
|
|
283
|
+
* @param d - les écarts, tels que la comparaison les a séparés.
|
|
284
|
+
* @returns la phrase, sans point final ni majuscule initiale.
|
|
285
|
+
*/
|
|
286
|
+
function nameGaps(d) {
|
|
287
|
+
const parts = [];
|
|
288
|
+
const pushBounded = (names, singular, plural) => {
|
|
289
|
+
if (names.length === 0) return;
|
|
290
|
+
const remainder = names.length > SUMMARY_GAPS ? `, et ${names.length - SUMMARY_GAPS} de plus` : "";
|
|
291
|
+
parts.push(`${names.length > 1 ? plural : singular} ${names.slice(0, SUMMARY_GAPS).join(", ")}${remainder}`);
|
|
292
|
+
};
|
|
293
|
+
const col = (g) => `« ${g.table}.${g.column} »`;
|
|
294
|
+
pushBounded(d.missingTables.map((t) => `« ${t} »`), "table absente :", "tables absentes :");
|
|
295
|
+
pushBounded(d.blocking.map(col), "colonne manquante et OBLIGATOIRE :", "colonnes manquantes et OBLIGATOIRES :");
|
|
296
|
+
pushBounded(d.additive.map(col), "colonne manquante :", "colonnes manquantes :");
|
|
297
|
+
return parts.join(" ; ");
|
|
298
|
+
}
|
|
299
|
+
/** Le FAIT, en une phrase, sans terme d'art. */
|
|
300
|
+
function summaryOf(plan, verdict, divergence = null) {
|
|
301
|
+
const c = plan.connector;
|
|
302
|
+
switch (verdict) {
|
|
303
|
+
case "up-to-date": return `Le connecteur « ${c} » est à jour : tout ce qui est enregistré a été appliqué, et les fichiers n'ont pas bougé depuis.`;
|
|
304
|
+
case "pending": {
|
|
305
|
+
const n = plan.pending.length;
|
|
306
|
+
const names = plan.pending.slice(0, 3).map((f) => `${f.source}/${f.tag}`).join(", ");
|
|
307
|
+
const remainder = n > 3 ? `, et ${n - 3} de plus` : "";
|
|
308
|
+
return `Le connecteur « ${c} » a ${n} migration${n > 1 ? "s" : ""} à appliquer : ${names}${remainder}.`;
|
|
309
|
+
}
|
|
310
|
+
case "drift": {
|
|
311
|
+
const parts = [];
|
|
312
|
+
if (plan.drifted.length > 0) {
|
|
313
|
+
const d = plan.drifted[0];
|
|
314
|
+
parts.push(`le fichier « ${d.source}/${d.tag} » a été modifié APRÈS avoir été appliqué (son empreinte a changé)`);
|
|
315
|
+
}
|
|
316
|
+
if (plan.missing.length > 0) {
|
|
317
|
+
const m = plan.missing[0];
|
|
318
|
+
parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
|
|
319
|
+
}
|
|
320
|
+
return `Le connecteur « ${c} » ne concorde plus avec son historique : ${parts.join(" ; ")}.`;
|
|
321
|
+
}
|
|
322
|
+
case "failed": {
|
|
323
|
+
const f = plan.failed[0];
|
|
324
|
+
const when = f.startedAt ? new Date(f.startedAt).toISOString() : "à une date inconnue";
|
|
325
|
+
const why = f.error ? ` (erreur : ${f.error})` : "";
|
|
326
|
+
return `Le connecteur « ${c} » porte ${plan.failed.length} migration${plan.failed.length > 1 ? "s" : ""} qui n'a pas abouti : « ${f.source}/${f.tag} », commencée le ${when}${why}.`;
|
|
327
|
+
}
|
|
328
|
+
case "adopt": return `La base du connecteur « ${c} » contient déjà des tables, mais aucune migration n'y est enregistrée. Nodefony ne devine pas : appliquer les migrations sur une base déjà peuplée écraserait peut-être une base qui n'est pas la bonne.`;
|
|
329
|
+
case "divergent": return `Le connecteur « ${c} » a son historique complet et rien en attente, et la base ne correspond pourtant pas au schéma déclaré dans le code${divergence ? ` — ${nameGaps(divergence)}` : ""}. ${divergence && divergence.missingTables.length > 0 ? `L'application ne peut PAS servir ces entités : leur migration n'a jamais été générée, commitée ou appliquée.` : `Quelqu'un a modifié la base directement, ou un correctif d'urgence n'a pas été reporté.`}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Ce qu'il faut TAPER, du plus direct au plus assumé.
|
|
334
|
+
*
|
|
335
|
+
* @param plan - plan calculé en lecture seule.
|
|
336
|
+
* @param verdict - situation d'ensemble.
|
|
337
|
+
* @param canReset - `orm:reset` est-elle acceptée dans cet environnement ?
|
|
338
|
+
* @param divergence - les écarts nommés : le geste n'est pas le même selon
|
|
339
|
+
* qu'il manque une colonne ou une table entière.
|
|
340
|
+
* @returns les gestes, dans l'ordre où les tenter.
|
|
341
|
+
*/
|
|
342
|
+
function actionsOf(plan, verdict, canReset = false, divergence = null) {
|
|
343
|
+
const c = plan.connector;
|
|
344
|
+
const suffixe = c === "default" ? "" : ` --connector ${c}`;
|
|
345
|
+
switch (verdict) {
|
|
346
|
+
case "up-to-date": return [];
|
|
347
|
+
case "pending": return [action(`nodefony orm:migrate${suffixe}`), action(`nodefony orm:migrate${suffixe} --dry-run`)];
|
|
348
|
+
case "drift": {
|
|
349
|
+
const out = [];
|
|
350
|
+
if (plan.drifted.length > 0) {
|
|
351
|
+
out.push(action(`git checkout -- migrations/`));
|
|
352
|
+
out.push(action(`nodefony orm:migrate:repair${suffixe} --update-hashes`));
|
|
353
|
+
}
|
|
354
|
+
if (plan.missing.length > 0) out.push(action(`nodefony orm:migrate${suffixe} --ignore-missing`));
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
case "failed": return [
|
|
358
|
+
action(`nodefony orm:migrate:status${suffixe} --json`),
|
|
359
|
+
action(`nodefony orm:migrate:repair${suffixe}`),
|
|
360
|
+
action(`nodefony orm:migrate${suffixe}`)
|
|
361
|
+
];
|
|
362
|
+
case "adopt": return [action(`nodefony orm:migrate:baseline${suffixe}`), action(`nodefony orm:migrate:status${suffixe}`)];
|
|
363
|
+
case "divergent":
|
|
364
|
+
if (divergence && divergence.missingTables.length > 0) return [
|
|
365
|
+
action(`nodefony orm:generate${suffixe} --name rattrapage_schema`),
|
|
366
|
+
action(`nodefony orm:migrate${suffixe}`),
|
|
367
|
+
action(`nodefony orm:migrate:status${suffixe} --json`)
|
|
368
|
+
];
|
|
369
|
+
return canReset ? [action(`nodefony orm:migrate:status${suffixe} --json`), action(`nodefony orm:reset${suffixe}`)] : [
|
|
370
|
+
action(`nodefony orm:migrate:status${suffixe} --json`),
|
|
371
|
+
action(`nodefony orm:generate${suffixe} --custom --name rattrapage_schema`),
|
|
372
|
+
action(`nodefony orm:migrate${suffixe}`)
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Ce que le verdict veut dire — la CAUSE, entre le fait et le geste.
|
|
378
|
+
*
|
|
379
|
+
* C'est le bloc qu'on omet d'habitude, et c'est celui qui évite l'appel au
|
|
380
|
+
* collègue : l'utilisateur sait ce qui s'est passé, donc il sait si le geste
|
|
381
|
+
* proposé lui convient.
|
|
382
|
+
*
|
|
383
|
+
* @param verdict - situation d'ensemble.
|
|
384
|
+
* @returns une ou deux phrases, ou une chaîne vide si le fait se suffit.
|
|
385
|
+
*/
|
|
386
|
+
function meaningOf(verdict) {
|
|
387
|
+
switch (verdict) {
|
|
388
|
+
case "up-to-date": return "";
|
|
389
|
+
case "pending": return "C'est la situation normale après avoir tiré du code qui change le schéma, ou avant un déploiement. Rien n'a encore été modifié dans la base.";
|
|
390
|
+
case "drift": return "Un fichier de migration ne se modifie jamais après avoir été appliqué : d'autres bases ont reçu l'ancienne version, et elles ne recevront jamais la nouvelle. Le geste normal est de restaurer le fichier et d'écrire une NOUVELLE migration. Ré-aligner les empreintes ne se fait que si l'on sait que la modification était sans effet.";
|
|
391
|
+
case "failed": return "La migration s'est arrêtée en cours de route. Selon la base, elle a pu laisser un état partiel : MySQL valide chaque instruction de schéma sans possibilité de retour, PostgreSQL et SQLite annulent la migration fautive entière. Regarde l'état réel de la base AVANT de lever le marqueur — c'est ce que « réparer » veut dire ici, et rien d'autre.";
|
|
392
|
+
case "adopt": return "Cela arrive quand on branche Nodefony sur une base qui existait déjà, ou quand on a créé les tables autrement (schéma dérivé du code en développement). Déclarer la base à niveau enregistre les migrations comme appliquées SANS les exécuter. Vérifie d'abord que c'est bien la base attendue.";
|
|
393
|
+
case "divergent": return "Aucun outil de migration ne regarde la base elle-même : ils comparent les fichiers à l'historique et concluent. Nodefony compare aussi au schéma déclaré dans le code, ce qui rend ce cas visible. Il n'est PAS bloquant par défaut, parce qu'une application qui écrit des migrations libres a légitimement une base différente du schéma déclaré.";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Construit le styliste : couleurs en terminal, texte nu ailleurs.
|
|
398
|
+
*
|
|
399
|
+
* @param tty - la sortie est-elle un terminal ?
|
|
400
|
+
* @returns les fonctions de mise en forme.
|
|
401
|
+
*/
|
|
402
|
+
function styleFor(tty) {
|
|
403
|
+
const wrap = (code) => (s) => tty ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
404
|
+
return {
|
|
405
|
+
bold: wrap("1"),
|
|
406
|
+
dim: wrap("2"),
|
|
407
|
+
green: wrap("32"),
|
|
408
|
+
yellow: wrap("33"),
|
|
409
|
+
red: wrap("31")
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** Rend les trois blocs — le fait, ce que ça veut dire, ce qu'il faut taper. */
|
|
413
|
+
function renderBlocks(style, summary, meaning, actions, actionTitle = "À faire") {
|
|
414
|
+
let out = `${summary}\n`;
|
|
415
|
+
if (meaning) out += `\n${style.dim(meaning)}\n`;
|
|
416
|
+
if (actions.length > 0) {
|
|
417
|
+
out += `\n${style.bold(`${actionTitle} :`)}\n`;
|
|
418
|
+
for (const a of actions) out += ` ${style.green(a.command)}\n`;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Rendu humain d'un état de migration — l'écran que voit celui qui tape
|
|
424
|
+
* `orm:migrate:status`.
|
|
425
|
+
*
|
|
426
|
+
* @param report - charge utile complète.
|
|
427
|
+
* @param style - mise en forme (couleurs ou texte nu).
|
|
428
|
+
* @returns le texte prêt à écrire sur la sortie standard.
|
|
429
|
+
*/
|
|
430
|
+
function renderStatus(report, style) {
|
|
431
|
+
const badge = {
|
|
432
|
+
"up-to-date": style.green("✓ à jour"),
|
|
433
|
+
pending: style.yellow("→ en attente"),
|
|
434
|
+
drift: style.red("⚠ ne concorde plus"),
|
|
435
|
+
failed: style.red("✗ migration interrompue"),
|
|
436
|
+
adopt: style.yellow("? base à déclarer"),
|
|
437
|
+
divergent: style.yellow("≠ base différente du code")
|
|
438
|
+
};
|
|
439
|
+
let out = `${style.bold(`Connecteur ${report.connector}`)} ${style.dim(`(${report.driver.dialect}, schéma : ${report.driver.ddl}, historique : ${report.driver.historyTable})`)}\n`;
|
|
440
|
+
if (report.driver.fromMigrateUrl === true) out += ` ${style.yellow(`⚠ NF_MIGRATE_DATABASE_URL détourne ce connecteur vers ${report.driver.target ?? "une autre base"}`)}\n`;
|
|
441
|
+
out += ` ${badge[report.verdict]}\n\n`;
|
|
442
|
+
for (const s of report.sources) {
|
|
443
|
+
const bits = [`${s.applied} appliquée${s.applied > 1 ? "s" : ""}`, `${s.pending} en attente`];
|
|
444
|
+
if (s.failed > 0) bits.push(style.red(`${s.failed} interrompue(s)`));
|
|
445
|
+
if (s.drifted.length > 0) bits.push(style.red(`${s.drifted.length} modifiée(s) après coup`));
|
|
446
|
+
if (s.missing.length > 0) bits.push(style.red(`${s.missing.length} fichier(s) disparu(s)`));
|
|
447
|
+
out += ` ${style.bold(s.name.padEnd(12))} ${bits.join(" · ")}\n`;
|
|
448
|
+
if (s.pendingTags.length > 0) out += ` ${" ".repeat(12)} ${style.dim(`→ ${s.pendingTags.join(", ")}`)}\n`;
|
|
449
|
+
}
|
|
450
|
+
if (report.sources.length === 0) out += ` ${style.dim("aucune source de migrations")}\n`;
|
|
451
|
+
if (report.divergence && !namesEverything(report.divergence)) out += renderDivergence(report.divergence, style);
|
|
452
|
+
out += `\n${renderBlocks(style, report.summary, meaningOf(report.verdict), report.nextActions)}`;
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
/** Combien d'écarts on déroule à l'écran avant de renvoyer au `--json`. */
|
|
456
|
+
const DIVERGENCE_LINES = 10;
|
|
457
|
+
/**
|
|
458
|
+
* La LISTE des écarts, à l'écran — ce que le résumé n'a pas la place de dire.
|
|
459
|
+
*
|
|
460
|
+
* Le résumé nomme les trois premiers de chaque famille, parce qu'une phrase
|
|
461
|
+
* doit rester lisible ; ici on déroule, parce que c'est précisément la liste
|
|
462
|
+
* que l'exploitant serait allé chercher à la main dans un client SQL. Au-delà
|
|
463
|
+
* de {@link DIVERGENCE_LINES} entrées, on s'arrête et on dit où est le reste :
|
|
464
|
+
* un écran de quarante lignes ne se lit pas davantage qu'une phrase de
|
|
465
|
+
* quarante noms.
|
|
466
|
+
*
|
|
467
|
+
* @param d - les écarts, séparés selon qu'ils se rattrapent ou non.
|
|
468
|
+
* @param style - mise en forme.
|
|
469
|
+
* @returns le bloc, prêt à concaténer.
|
|
470
|
+
*/
|
|
471
|
+
function renderDivergence(d, style) {
|
|
472
|
+
const lines = [
|
|
473
|
+
...d.missingTables.map((t) => `${"table".padEnd(9)} ${t}`),
|
|
474
|
+
...d.blocking.map((g) => `${"colonne".padEnd(9)} ${g.table}.${g.column} ` + style.red(`(${g.type}, OBLIGATOIRE — ne se rattrape pas)`)),
|
|
475
|
+
...d.additive.map((g) => `${"colonne".padEnd(9)} ${g.table}.${g.column} ` + style.dim(`(${g.type}, se rattrape)`))
|
|
476
|
+
];
|
|
477
|
+
let out = `\n ${style.bold("Ce qui manque dans la base :")}\n`;
|
|
478
|
+
for (const l of lines.slice(0, DIVERGENCE_LINES)) out += ` ${l}\n`;
|
|
479
|
+
if (lines.length > DIVERGENCE_LINES) out += ` ${style.dim(`… et ${lines.length - DIVERGENCE_LINES} autre(s) — la liste entière est dans « --json », sous « divergence »`)}\n`;
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Rendu humain de ce que la DÉCOUVERTE des entités a vu.
|
|
484
|
+
*
|
|
485
|
+
* Bloc court, posé sous les refus dont la cause peut être un schéma déclaré
|
|
486
|
+
* amputé : un fichier illisible, une table écrite pour un autre moteur, un
|
|
487
|
+
* dossier d'entités qui ne rend rien. L'outil de diff ne distingue pas une
|
|
488
|
+
* table absente d'une table SUPPRIMÉE — sans ces trois nombres, la correction
|
|
489
|
+
* naturelle porte sur la base, qui n'y est pour rien.
|
|
490
|
+
*
|
|
491
|
+
* @param facts - ce que la découverte a relevé.
|
|
492
|
+
* @param style - mise en forme.
|
|
493
|
+
* @returns le bloc, prêt à concaténer ; vide si rien n'appelle l'attention.
|
|
494
|
+
*/
|
|
495
|
+
function describeDiscovery(facts, style) {
|
|
496
|
+
let out = `\n${style.bold("Ce que la découverte a vu")} ${style.dim(`(${facts.filesScanned} fichier(s) d'entités examiné(s))`)} :\n • ${facts.tables.length} table(s) de l'application retenue(s)${facts.tables.length > 0 ? ` : ${facts.tables.join(", ")}` : ""}\n`;
|
|
497
|
+
if (facts.otherDialect.length > 0) out += ` • ${facts.otherDialect.length} écartée(s), écrite(s) pour un autre moteur :\n` + facts.otherDialect.map((o) => ` ${o.table} (${o.dialect}) — ${o.file}\n`).join("");
|
|
498
|
+
if (facts.unreadable.length > 0) out += ` • ${facts.unreadable.length} fichier(s) que la découverte n'a PAS su lire :\n` + facts.unreadable.map((u) => ` ${u.file} — ${u.cause}\n`).join("");
|
|
499
|
+
if (facts.unreadable.length > 0 || facts.otherDialect.length > 0 || facts.tables.length === 0) out += `\n${style.dim("Une table qui n'entre pas dans cette découverte se présente à l'outil de diff comme une table SUPPRIMÉE. Avant de toucher à la base, vérifier que ces fichiers fournissent bien les tables attendues pour CE moteur : le défaut est alors dans le dossier d'entités, et la base n'y est pour rien.")}\n`;
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Rendu humain d'un REFUS de l'applicateur.
|
|
504
|
+
*
|
|
505
|
+
* Un refus est un contrat, pas un message : il énonce le fait, ce qu'il
|
|
506
|
+
* signifie, et donne la commande exacte à copier. L'utilisateur ne doit jamais
|
|
507
|
+
* avoir eu connaissance d'une option à l'avance.
|
|
508
|
+
*
|
|
509
|
+
* @param verdict - verdict structuré porté par le refus.
|
|
510
|
+
* @param message - phrase française déjà composée par l'applicateur.
|
|
511
|
+
* @param style - mise en forme.
|
|
512
|
+
* @param ddl - mode de schéma effectif, quand il change la cause (cf {@link refusalInMode}).
|
|
513
|
+
* @returns le texte prêt à écrire sur la sortie d'erreur.
|
|
514
|
+
*/
|
|
515
|
+
function renderRefusal(verdict, message, style, ddl) {
|
|
516
|
+
const enMode = ddl ? refusalInMode(verdict.code, ddl, verdict.connector) : null;
|
|
517
|
+
const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code] ?? "";
|
|
518
|
+
const actions = enMode?.actions ?? verdict.nextActions;
|
|
519
|
+
return `${style.red(style.bold("Migration refusée"))} ${style.dim(`[${verdict.code}]`)}\n\n` + renderBlocks(style, message, meaning, actions);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Ce qu'un refus veut dire QUAND ON CONNAÎT LE MODE DE SCHÉMA.
|
|
523
|
+
*
|
|
524
|
+
* Le même fait mécanique n'a pas la même cause selon le mode, et le geste
|
|
525
|
+
* change avec la cause. Constaté en exécutant la commande pour de vrai : sur une
|
|
526
|
+
* base parfaitement NEUVE, en développement, `orm:migrate` refuse en disant que
|
|
527
|
+
* la base « porte déjà les tables ». C'est exact — et c'est le DÉMARRAGE
|
|
528
|
+
* lui-même qui vient de les créer, quelques millisecondes plus tôt, parce que le
|
|
529
|
+
* mode `auto` dérive le schéma du code. Sans cette précision, l'utilisateur
|
|
530
|
+
* cherche une base ancienne qui n'existe pas.
|
|
531
|
+
*
|
|
532
|
+
* @param code - code du refus.
|
|
533
|
+
* @param ddl - mode de schéma effectif du connecteur.
|
|
534
|
+
* @param connector - connecteur concerné, pour composer les gestes.
|
|
535
|
+
* @returns l'explication et les gestes, ou `null` si le refus se suffit.
|
|
536
|
+
*/
|
|
537
|
+
function refusalInMode(code, ddl, connector) {
|
|
538
|
+
if (code !== "NF_MIGRATE_BASELINE_REQUIRED" || ddl !== "auto") return null;
|
|
539
|
+
const suffixe = connector === "default" ? "" : ` --connector ${connector}`;
|
|
540
|
+
return {
|
|
541
|
+
meaning: "Ce connecteur est en mode `auto` : c'est le DÉMARRAGE qui fabrique le schéma à partir du code, et il vient de le faire — les tables existent donc déjà, même sur une base créée à l'instant. Ne cherche pas une vieille base : les deux façons de fabriquer un schéma se croisent, c'est tout. Trois issues, selon ce que tu veux vraiment. Éprouver les migrations comme en production : repars d'une base vide avec le mode `none` — c'est un réglage du connecteur, pas une variable posée devant la commande. Repartir de zéro en développement : vide la base. Garder cette base et la déclarer à niveau : adopte-la (aucun SQL ne sera exécuté).",
|
|
542
|
+
actions: [action(`nodefony orm:reset${suffixe}`), action(`nodefony orm:migrate:baseline${suffixe}`)]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Ce que chaque refus de l'applicateur veut dire, en clair.
|
|
547
|
+
*
|
|
548
|
+
* Table complète et exhaustive par construction : le type est indexé par
|
|
549
|
+
* l'union des codes, donc en ajouter un sans écrire sa phrase ne compile pas.
|
|
550
|
+
* C'est le seul moyen d'avoir la garantie qu'aucun refus ne sortira nu.
|
|
551
|
+
*/
|
|
552
|
+
const REFUSAL_MEANING = {
|
|
553
|
+
NF_MIGRATE_BASELINE_REQUIRED: "La base contient déjà des tables alors qu'aucune migration n'y est enregistrée. Appliquer les migrations maintenant exécuterait des créations de tables qui existent — souvent parce qu'on s'est trompé de base. Déclarer la base à niveau enregistre les migrations comme appliquées sans les exécuter ; à ne faire qu'après avoir vérifié que c'est la bonne base.",
|
|
554
|
+
NF_MIGRATE_FAILED_MARKER: "Une migration précédente s'est arrêtée en cours de route. Reprendre à l'aveugle par-dessus un état partiel est le meilleur moyen d'aggraver la situation. Regarde l'état réel de la base, corrige-le si besoin, puis lève le marqueur.",
|
|
555
|
+
NF_MIGRATE_HASH_MISMATCH: "Un fichier déjà appliqué a été modifié depuis. Les autres bases ont reçu l'ancienne version et ne recevront jamais la nouvelle : le fichier et la réalité ont divergé. Le geste normal est de restaurer le fichier et d'écrire une nouvelle migration.",
|
|
556
|
+
NF_MIGRATE_OUT_OF_ORDER: "Une migration en attente se range avant la dernière appliquée de sa source — c'est la trace d'une fusion de branches. L'appliquer quand même est souvent juste, mais c'est une décision : deux bases pourraient ne pas avoir reçu les mêmes changements dans le même ordre.",
|
|
557
|
+
NF_MIGRATE_MISSING_FILE: "Une migration enregistrée comme appliquée n'a plus de fichier, alors que sa source est bien présente. Soit le fichier a été supprimé par erreur, soit la base a connu une version du code que ce dépôt ne contient pas.",
|
|
558
|
+
NF_MIGRATE_UNKNOWN_FORMAT: "Un fichier ne porte pas le format que cet applicateur sait lire. Lire au mieux un fichier d'un format inconnu, c'est exécuter du SQL découpé au hasard — donc jamais.",
|
|
559
|
+
NF_MIGRATE_LOCK_TIMEOUT: "Un autre processus applique des migrations sur cette base, ou en a laissé le verrou pris. Le verrou est tenu par une connexion : il se libère tout seul quand la connexion meurt. Vérifie qu'aucun autre travail de migration ne tourne, puis relance.",
|
|
560
|
+
NF_MIGRATE_UNKNOWN_TAG: "Le tag demandé ne désigne aucune migration connue — une faute de frappe, ou une casse qui diffère. Le refus est là pour une raison précise : sans point d'arrêt, l'adoption déclare à niveau TOUTES les migrations, y compris celles que la base n'a jamais reçues. Elle ne les recevrait alors plus jamais.",
|
|
561
|
+
NF_MIGRATE_JOURNAL_MISMATCH: "Le journal d'une source annonce une migration dont le fichier n'est pas dans le dossier. La source est incohérente avec elle-même : une copie incomplète, un fichier ignoré par le gestionnaire de versions, ou un paquet publié sans ses migrations. Rien n'a été appliqué — l'applicateur ne devine jamais le contenu d'un fichier absent.",
|
|
562
|
+
NF_MIGRATE_UNKNOWN_SOURCE: "La source demandée n'est pas déclarée par cette application. Filtrer sur un nom inconnu ne touche aucune ligne et rend pourtant « rien à réparer » : le marqueur d'échec resterait en place, et la migration suivante échouerait pour la même raison."
|
|
563
|
+
};
|
|
564
|
+
//#endregion
|
|
565
|
+
export { EXIT, MIGRATION_FORMAT_VERSION, action, buildReport, describeDiscovery, divergenceIsBlocking, exitCodeOf, isAheadOnly, meaningOf, refusalInMode, renderRefusal, renderStatus, styleFor, verdictOf };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region nodefony/src/migrator/hash.ts
|
|
3
|
+
/**
|
|
4
|
+
* Empreinte d'un fichier de migration : **normalisée** et **auto-descriptive**.
|
|
5
|
+
*
|
|
6
|
+
* Deux gestes, deux raisons distinctes.
|
|
7
|
+
*
|
|
8
|
+
* **Normalisée** (CRLF → LF) : Windows est un impératif produit, et un checkout
|
|
9
|
+
* sous `core.autocrlf` réécrit les `.sql`. Hacher les octets bruts ferait
|
|
10
|
+
* diverger toutes les empreintes de celles posées par l'image Linux qui a migré
|
|
11
|
+
* la base d'équipe — arrêt sur dérive permanent, pour un non-changement. Le
|
|
12
|
+
* garde-fou reste entier : toute modification RÉELLE du SQL déclenche l'arrêt,
|
|
13
|
+
* seule la représentation des fins de ligne cesse de compter.
|
|
14
|
+
*
|
|
15
|
+
* **Préfixée de son algorithme** (`sha256:<hex>`) : c'est la seule porte de
|
|
16
|
+
* sortie pour introduire un jour un autre algorithme en RECONNAISSANT les
|
|
17
|
+
* lignes anciennes, sans réécrire une seule base de production.
|
|
18
|
+
*
|
|
19
|
+
* @param content - contenu du fichier `.sql`, tel que lu sur le disque.
|
|
20
|
+
* @returns l'empreinte préfixée, telle qu'elle est stockée en base.
|
|
21
|
+
*/
|
|
22
|
+
function migrationHash(content) {
|
|
23
|
+
return `sha256:${createHash("sha256").update(normalizeSql(content), "utf8").digest("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalise un contenu SQL : marque d'ordre des octets retirée, fins de ligne en LF.
|
|
27
|
+
*
|
|
28
|
+
* Les deux gestes répondent au même fait — **le fichier a voyagé** — et doivent
|
|
29
|
+
* donc vivre au même endroit, sous peine de ne corriger qu'une moitié du
|
|
30
|
+
* problème.
|
|
31
|
+
*
|
|
32
|
+
* **La marque d'ordre des octets** (`U+FEFF`) est posée en tête par les
|
|
33
|
+
* éditeurs Windows et par PowerShell (`>` et `Out-File` l'écrivent par défaut).
|
|
34
|
+
* `fs.readFile(…, "utf8")` ne la retire pas : elle reste le premier caractère du
|
|
35
|
+
* contenu. Sans ce nettoyage, la première ligne cesse d'être reconnue comme le
|
|
36
|
+
* marqueur de format, et le refus affiche deux chaînes **visuellement
|
|
37
|
+
* identiques** — « attendu ceci, lu cela », avec ceci et cela à l'œil pareils.
|
|
38
|
+
* C'est le pire message d'erreur possible : celui qui n'apprend rien.
|
|
39
|
+
*
|
|
40
|
+
* @param content - contenu brut, tel que lu sur le disque.
|
|
41
|
+
* @returns le même contenu, sans marque d'ordre des octets et en LF.
|
|
42
|
+
*/
|
|
43
|
+
function normalizeSql(content) {
|
|
44
|
+
return (content.charCodeAt(0) === 65279 ? content.slice(1) : content).replace(/\r\n/g, "\n");
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { migrationHash, normalizeSql };
|