@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,219 @@
|
|
|
1
|
+
import { HISTORY_TABLE } from "./types.js";
|
|
2
|
+
//#region nodefony/src/migrator/history.ts
|
|
3
|
+
/**
|
|
4
|
+
* Étapes d'amorçage postérieures au format d'origine.
|
|
5
|
+
*
|
|
6
|
+
* **Vide dans cette version** — l'étape 0 est le `CREATE` lui-même. Ce tableau
|
|
7
|
+
* est le point d'extension : ajouter une colonne à l'historique, aujourd'hui ou
|
|
8
|
+
* dans cinq versions, se fait ICI et nulle part ailleurs.
|
|
9
|
+
*/
|
|
10
|
+
const HISTORY_STEPS = [];
|
|
11
|
+
/** Colonnes du format d'origine, dans l'ordre du `CREATE`. */
|
|
12
|
+
const BASE_COLUMNS = [
|
|
13
|
+
"source",
|
|
14
|
+
"tag",
|
|
15
|
+
"hash",
|
|
16
|
+
"run_id",
|
|
17
|
+
"started_at",
|
|
18
|
+
"finished_at",
|
|
19
|
+
"execution_ms",
|
|
20
|
+
"success",
|
|
21
|
+
"error",
|
|
22
|
+
"applied_by"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Le moteur se plaint-il d'une COLONNE d'historique qu'il ne trouve pas ?
|
|
26
|
+
*
|
|
27
|
+
* Reconnaît la table d'historique d'une AUTRE provenance : la table porte le
|
|
28
|
+
* bon nom, elle n'a pas les bonnes colonnes. `CREATE TABLE IF NOT EXISTS` ne
|
|
29
|
+
* la répare pas — elle existe —, et la première lecture échoue sur un message
|
|
30
|
+
* de moteur brut, que le fourre-tout des pannes habille alors de deux causes
|
|
31
|
+
* FAUSSES : « la base n'a pas répondu » et « les droits manquent ». Mesuré au
|
|
32
|
+
* banc : c'est ce message qui a renvoyé un agent détruire une base de
|
|
33
|
+
* production après qu'il eut pourtant suivi le conseil de travailler sur une
|
|
34
|
+
* copie — copie qu'il avait dû fabriquer à la main, avec un historique inventé.
|
|
35
|
+
*
|
|
36
|
+
* PURE, et une grammaire par moteur : les trois formulent la même panne dans
|
|
37
|
+
* trois langues, et un motif écrit pour l'une est muet pour les deux autres.
|
|
38
|
+
* Bornée aux colonnes que ce fichier déclare : une colonne APPLICATIVE
|
|
39
|
+
* manquante est un tout autre incident, qui a déjà sa voie.
|
|
40
|
+
*
|
|
41
|
+
* @param message - message d'erreur rendu par le pilote.
|
|
42
|
+
* @returns la colonne d'historique introuvable, ou `null`.
|
|
43
|
+
*/
|
|
44
|
+
function missingHistoryColumn(message) {
|
|
45
|
+
for (const pattern of [
|
|
46
|
+
/no such column:\s*(?:[\w."`]*\.)?[`"']?(\w+)[`"']?/i,
|
|
47
|
+
/column\s+[`"']?(?:[\w.]*\.)?[`"']?(\w+)[`"']?\s+does not exist/i,
|
|
48
|
+
/unknown column\s+[`"']?(?:[\w.]*\.)?[`"']?(\w+)[`"']?/i
|
|
49
|
+
]) {
|
|
50
|
+
const name = pattern.exec(message)?.[1]?.toLowerCase();
|
|
51
|
+
if (name !== void 0 && BASE_COLUMNS.includes(name)) return name;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** DDL de création par dialecte, au format d'origine. */
|
|
56
|
+
const CREATE_SQL = {
|
|
57
|
+
sqlite: `CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source TEXT NOT NULL,\n tag TEXT NOT NULL,\n hash TEXT NOT NULL,\n run_id TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n finished_at INTEGER,\n execution_ms INTEGER,\n success INTEGER NOT NULL DEFAULT 0,\n error TEXT,\n applied_by TEXT,\n UNIQUE (source, tag)\n)`,
|
|
58
|
+
postgres: `CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (\n id BIGSERIAL PRIMARY KEY,\n source TEXT NOT NULL,\n tag TEXT NOT NULL,\n hash TEXT NOT NULL,\n run_id TEXT NOT NULL,\n started_at BIGINT NOT NULL,\n finished_at BIGINT,\n execution_ms INTEGER,\n success BOOLEAN NOT NULL DEFAULT FALSE,\n error TEXT,\n applied_by TEXT,\n UNIQUE (source, tag)\n)`,
|
|
59
|
+
mysql: `CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (\n id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n source VARCHAR(190) NOT NULL,\n tag VARCHAR(190) NOT NULL,\n hash VARCHAR(255) NOT NULL,\n run_id VARCHAR(64) NOT NULL,\n started_at BIGINT NOT NULL,\n finished_at BIGINT NULL,\n execution_ms INT NULL,\n success TINYINT(1) NOT NULL DEFAULT 0,\n error TEXT NULL,\n applied_by VARCHAR(255) NULL,\n UNIQUE KEY uq_${HISTORY_TABLE} (source, tag)\n)`
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Crée la table d'historique si besoin, puis l'amène au format courant.
|
|
63
|
+
*
|
|
64
|
+
* À appeler **juste après le verrou et AVANT la moindre lecture** : c'est
|
|
65
|
+
* l'ordre qui rend l'évolution de la table possible sans outil de conversion
|
|
66
|
+
* chez l'utilisateur.
|
|
67
|
+
*
|
|
68
|
+
* @param driver - pilote à connexion unique, verrou déjà tenu.
|
|
69
|
+
* @returns les colonnes ajoutées par l'amorçage (vide dans le cas courant).
|
|
70
|
+
*/
|
|
71
|
+
async function ensureHistorySchema(driver) {
|
|
72
|
+
await driver.exec(CREATE_SQL[driver.dialect]);
|
|
73
|
+
const present = new Set((await driver.columnsOf(HISTORY_TABLE)).map((c) => c.toLowerCase()));
|
|
74
|
+
const added = [];
|
|
75
|
+
for (const step of HISTORY_STEPS) {
|
|
76
|
+
if (present.has(step.column.toLowerCase())) continue;
|
|
77
|
+
await driver.exec(step.ddl[driver.dialect]);
|
|
78
|
+
added.push(step.column);
|
|
79
|
+
}
|
|
80
|
+
return added;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Lit l'historique complet, **colonnes nommées une par une**.
|
|
84
|
+
*
|
|
85
|
+
* Jamais `SELECT *` : c'est ce qui rend l'ajout d'une colonne inoffensif pour
|
|
86
|
+
* un applicateur plus ancien, qui continue de lire exactement ce qu'il connaît.
|
|
87
|
+
*
|
|
88
|
+
* @param driver - pilote à connexion unique.
|
|
89
|
+
* @returns les lignes, dans leur ordre d'insertion.
|
|
90
|
+
*/
|
|
91
|
+
async function readHistory(driver) {
|
|
92
|
+
return (await driver.query(`SELECT ${BASE_COLUMNS.join(", ")} FROM ${HISTORY_TABLE} ORDER BY id`)).map((row) => ({
|
|
93
|
+
source: String(row.source),
|
|
94
|
+
tag: String(row.tag),
|
|
95
|
+
hash: String(row.hash),
|
|
96
|
+
runId: String(row.run_id),
|
|
97
|
+
startedAt: Number(row.started_at),
|
|
98
|
+
finishedAt: row.finished_at === null ? null : Number(row.finished_at),
|
|
99
|
+
executionMs: row.execution_ms === null ? null : Number(row.execution_ms),
|
|
100
|
+
success: toBoolean(row.success),
|
|
101
|
+
error: row.error === null ? null : String(row.error),
|
|
102
|
+
appliedBy: row.applied_by === null ? null : String(row.applied_by)
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `INSERT` d'une ligne d'historique — **jamais positionnel**.
|
|
107
|
+
*
|
|
108
|
+
* @param driver - pilote à connexion unique.
|
|
109
|
+
* @param row - ligne à écrire.
|
|
110
|
+
*/
|
|
111
|
+
async function insertHistory(driver, row) {
|
|
112
|
+
await driver.query(`INSERT INTO ${HISTORY_TABLE} (${BASE_COLUMNS.join(", ")}) VALUES (${BASE_COLUMNS.map(() => "?").join(", ")})`, [
|
|
113
|
+
row.source,
|
|
114
|
+
row.tag,
|
|
115
|
+
row.hash,
|
|
116
|
+
row.runId,
|
|
117
|
+
row.startedAt,
|
|
118
|
+
row.finishedAt,
|
|
119
|
+
row.executionMs,
|
|
120
|
+
fromBoolean(row.success, driver.dialect),
|
|
121
|
+
row.error,
|
|
122
|
+
row.appliedBy
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Marque une ligne d'historique terminée (chemin MySQL, DDL non transactionnel).
|
|
127
|
+
*
|
|
128
|
+
* @param driver - pilote à connexion unique.
|
|
129
|
+
* @param row - ligne dont le résultat est connu.
|
|
130
|
+
*/
|
|
131
|
+
async function finishHistory(driver, row) {
|
|
132
|
+
await driver.query(`UPDATE ${HISTORY_TABLE} SET finished_at = ?, execution_ms = ?, success = ?, error = ?, hash = ? WHERE source = ? AND tag = ?`, [
|
|
133
|
+
row.finishedAt,
|
|
134
|
+
row.executionMs,
|
|
135
|
+
fromBoolean(row.success, driver.dialect),
|
|
136
|
+
row.error,
|
|
137
|
+
row.hash,
|
|
138
|
+
row.source,
|
|
139
|
+
row.tag
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Supprime les marqueurs d'échec d'une source, ou de toutes.
|
|
144
|
+
*
|
|
145
|
+
* @param driver - pilote à connexion unique.
|
|
146
|
+
* @param source - source à réparer ; toutes si omise.
|
|
147
|
+
* @returns les migrations dont le marqueur a été levé.
|
|
148
|
+
*/
|
|
149
|
+
async function deleteFailed(driver, source) {
|
|
150
|
+
const failed = (await readHistory(driver)).filter((row) => (!row.success || row.finishedAt === null) && (source === void 0 || row.source === source));
|
|
151
|
+
for (const row of failed) await driver.query(`DELETE FROM ${HISTORY_TABLE} WHERE source = ? AND tag = ?`, [row.source, row.tag]);
|
|
152
|
+
return failed.map(({ source: s, tag }) => ({
|
|
153
|
+
source: s,
|
|
154
|
+
tag
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Désinscrit UNE entrée nommée de l'historique, quel que soit son état.
|
|
159
|
+
*
|
|
160
|
+
* ## Pourquoi ce geste existe, alors qu'un interdit dit de ne pas y toucher
|
|
161
|
+
*
|
|
162
|
+
* L'interdit porte sur la modification À LA MAIN, dans un client SQL, sans
|
|
163
|
+
* trace. Il existait pourtant un état dont AUCUNE commande ne sortait : une
|
|
164
|
+
* migration inscrite `success` que personne n'a jamais exécutée — une adoption
|
|
165
|
+
* mal bornée, une base héritée d'une version antérieure aux gardes. Le
|
|
166
|
+
* générateur disait « c'est l'historique qu'il faut reprendre » et renvoyait
|
|
167
|
+
* vers la réparation, qui ne sait lever que des marqueurs d'ÉCHEC : elle
|
|
168
|
+
* répondait « rien à réparer », et l'on revenait au point de départ. Trois
|
|
169
|
+
* messages vrais, aucun geste — et le seul chemin restant était de détruire la
|
|
170
|
+
* base.
|
|
171
|
+
*
|
|
172
|
+
* Le geste est donc rendu au produit, où il laisse une trace et où il est
|
|
173
|
+
* BORNÉ : une entrée précisément nommée, jamais un lot, jamais un motif.
|
|
174
|
+
*
|
|
175
|
+
* ⚠️ Ne touche pas la base : après cet oubli, la migration sera REJOUÉE au
|
|
176
|
+
* prochain passage. Si elle avait réellement été appliquée, ce rejeu échouera
|
|
177
|
+
* — bruyamment, ce qui est le comportement voulu.
|
|
178
|
+
*
|
|
179
|
+
* @param driver - pilote sous verrou.
|
|
180
|
+
* @param entries - entrées à désinscrire, chacune nommée `source` et `tag`.
|
|
181
|
+
* @returns celles qui existaient et ont été retirées.
|
|
182
|
+
*/
|
|
183
|
+
async function forgetEntries(driver, entries) {
|
|
184
|
+
const existing = await readHistory(driver);
|
|
185
|
+
const removed = [];
|
|
186
|
+
for (const target of entries) {
|
|
187
|
+
if (!existing.some((row) => row.source === target.source && row.tag === target.tag)) continue;
|
|
188
|
+
await driver.query(`DELETE FROM ${HISTORY_TABLE} WHERE source = ? AND tag = ?`, [target.source, target.tag]);
|
|
189
|
+
removed.push({
|
|
190
|
+
source: target.source,
|
|
191
|
+
tag: target.tag
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return removed;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Normalise un booléen tel que le rend la base.
|
|
198
|
+
*
|
|
199
|
+
* PostgreSQL rend `true`, SQLite et MySQL rendent `1` — et un `1` textuel
|
|
200
|
+
* traverse certains pilotes. Les trois se lisent ici, une fois.
|
|
201
|
+
*
|
|
202
|
+
* @param value - valeur brute lue en base.
|
|
203
|
+
* @returns le booléen correspondant.
|
|
204
|
+
*/
|
|
205
|
+
function toBoolean(value) {
|
|
206
|
+
return value === true || value === 1 || value === "1";
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Encode un booléen pour la base.
|
|
210
|
+
*
|
|
211
|
+
* @param value - booléen applicatif.
|
|
212
|
+
* @param dialect - dialecte cible.
|
|
213
|
+
* @returns la valeur à binder.
|
|
214
|
+
*/
|
|
215
|
+
function fromBoolean(value, dialect) {
|
|
216
|
+
return dialect === "postgres" ? value : value ? 1 : 0;
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
export { HISTORY_STEPS, deleteFailed, ensureHistorySchema, finishHistory, forgetEntries, insertHistory, missingHistoryColumn, readHistory };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FORMAT_MARKER, HISTORY_TABLE, MigrationVerdictError, STATEMENT_BREAKPOINT } from "./types.js";
|
|
2
|
+
import { sameColumnName, schemaReader } from "./catalog.js";
|
|
3
|
+
import { MYSQL_LOCK_NAME_SQL, MYSQL_LOCK_PREFIX, MysqlMigrationDriver } from "./drivers/mysqlDriver.js";
|
|
4
|
+
import { PG_LOCK_KEY, PostgresMigrationDriver } from "./drivers/postgresDriver.js";
|
|
5
|
+
import { SqliteMigrationDriver } from "./drivers/sqliteDriver.js";
|
|
6
|
+
import { openMigrationDriver } from "./drivers/index.js";
|
|
7
|
+
import { HISTORY_STEPS, ensureHistorySchema, readHistory } from "./history.js";
|
|
8
|
+
import { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, defaultMigrationSources, frameworkMigrationsDir } from "./paths.js";
|
|
9
|
+
import { migrationHash, normalizeSql } from "./hash.js";
|
|
10
|
+
import { SUPPORTED_JOURNAL_VERSIONS, createdTables, frameworkTables, loadSources, orderSources, splitStatements } from "./sources.js";
|
|
11
|
+
import { DEFAULT_LOCK_TIMEOUT_MS, DrizzleMigrator } from "./DrizzleMigrator.js";
|
|
12
|
+
import { additiveSql, compareSchema, hasGap } from "./schemaDiff.js";
|
|
13
|
+
import { comparisonAgainstDeclared, describeDivergence, gapAgainstDeclared } from "./divergence.js";
|
|
14
|
+
import { MIGRATION_NAME_MAX, checkMigrationName, suggestMigrationName } from "./name.js";
|
|
15
|
+
import { adoptFromDatabase, introspectionUrl, readJournal, snapshotTables, tablesPresentIn, uncommentIntrospection } from "./adopt.js";
|
|
16
|
+
export { APP_RANK, APP_SOURCE, DEFAULT_LOCK_TIMEOUT_MS, DrizzleMigrator, FORMAT_MARKER, FRAMEWORK_RANK, FRAMEWORK_SOURCE, HISTORY_STEPS, HISTORY_TABLE, MIGRATION_NAME_MAX, MYSQL_LOCK_NAME_SQL, MYSQL_LOCK_PREFIX, MigrationVerdictError, MysqlMigrationDriver, PG_LOCK_KEY, PostgresMigrationDriver, STATEMENT_BREAKPOINT, SUPPORTED_JOURNAL_VERSIONS, SqliteMigrationDriver, additiveSql, adoptFromDatabase, checkMigrationName, compareSchema, comparisonAgainstDeclared, createdTables, defaultMigrationSources, describeDivergence, ensureHistorySchema, frameworkMigrationsDir, frameworkTables, gapAgainstDeclared, hasGap, introspectionUrl, loadSources, migrationHash, normalizeSql, openMigrationDriver, orderSources, readHistory, readJournal, sameColumnName, schemaReader, snapshotTables, splitStatements, suggestMigrationName, tablesPresentIn, uncommentIntrospection };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { FORMAT_MARKER } from "./types.js";
|
|
2
|
+
import { MigrationToolError, generationToolMissing } from "./refusals.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
//#region nodefony/src/migrator/kit.ts
|
|
7
|
+
/**
|
|
8
|
+
* Plomberie `drizzle-kit` — la partie qui ne connaît NI le framework NI
|
|
9
|
+
* l'application, et que les deux exécutent donc à l'identique.
|
|
10
|
+
*
|
|
11
|
+
* Elle vivait dans `scripts/`, qui n'est pas publié (`files` ne porte que
|
|
12
|
+
* `dist`, `docs` et `migrations`). La commande `orm:generate`, elle, tourne chez
|
|
13
|
+
* l'utilisateur : lui laisser dépendre de `scripts/` aurait été livrer un verbe
|
|
14
|
+
* dont la moitié manque au paquet. Le socle a donc rejoint le code publié, et
|
|
15
|
+
* `scripts/` le CONSOMME — une seule implémentation, celle qu'exécutent le dépôt
|
|
16
|
+
* du framework et toute application.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **Le piège qui justifie ce fichier** : `drizzle-kit` **rend le code 0 quand
|
|
19
|
+
* il échoue**. Une exception non rattrapée part sur la sortie d'erreur et le
|
|
20
|
+
* process sort quand même à zéro. Tout appelant doit donc exiger une **preuve
|
|
21
|
+
* positive** que la génération a eu lieu — c'est le rôle de {@link runGenerate}.
|
|
22
|
+
* Un second piège en découle : le dossier de sortie ne peut pas être ABSOLU
|
|
23
|
+
* (l'outil le préfixe par `./`, fabriquant `.//Users/…`), et l'échec de lecture
|
|
24
|
+
* qui s'ensuit se présente lui aussi comme un succès.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Résout le binaire de `drizzle-kit` sans passer par un lanceur de shell.
|
|
28
|
+
*
|
|
29
|
+
* `npx` est un `.cmd` sous Windows, inexécutable sans `shell: true` — qui
|
|
30
|
+
* rouvrirait une injection par le nom de migration. Le paquet n'exporte pas son
|
|
31
|
+
* binaire (`exports` ne couvre que `.` et `./api`), donc on remonte les dossiers
|
|
32
|
+
* `node_modules` comme le ferait Node.
|
|
33
|
+
*
|
|
34
|
+
* @param from - dossier de départ de la remontée (racine du paquet qui génère,
|
|
35
|
+
* ou racine de l'application).
|
|
36
|
+
* @returns chemin absolu de `bin.cjs`.
|
|
37
|
+
* @throws Error si `drizzle-kit` n'est pas installé au-dessus de `from`.
|
|
38
|
+
*/
|
|
39
|
+
function resolveDrizzleKitBin(from) {
|
|
40
|
+
let dir = from;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const candidate = path.join(dir, "node_modules", "drizzle-kit", "bin.cjs");
|
|
43
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir) throw new MigrationToolError(generationToolMissing());
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Lance `drizzle-kit generate` et EXIGE la preuve qu'il a tourné.
|
|
51
|
+
*
|
|
52
|
+
* @param options - `cwd` (dossier depuis lequel l'outil est lancé — les chemins
|
|
53
|
+
* de la configuration lui sont relatifs), `configRel` (configuration, chemin
|
|
54
|
+
* relatif à `cwd`), `name` (nom imposé de la migration), `label` (ce qui est
|
|
55
|
+
* cité dans l'erreur).
|
|
56
|
+
* @returns la sortie complète de l'outil (sortie standard puis sortie d'erreur).
|
|
57
|
+
* @throws Error si le code est non nul, ou si rien ne prouve que la génération a
|
|
58
|
+
* eu lieu — l'absence de preuve n'est JAMAIS lue comme « rien à faire ».
|
|
59
|
+
*/
|
|
60
|
+
function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
|
|
61
|
+
const result = spawnSync(process.execPath, [
|
|
62
|
+
resolveDrizzleKitBin(cwd),
|
|
63
|
+
"generate",
|
|
64
|
+
`--config=${configRel}`,
|
|
65
|
+
`--name=${name}`
|
|
66
|
+
], {
|
|
67
|
+
cwd,
|
|
68
|
+
encoding: "utf8"
|
|
69
|
+
});
|
|
70
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
71
|
+
if (result.status !== 0 || !generationHappened(output)) {
|
|
72
|
+
if (isInteractivePromptFailure(output)) {
|
|
73
|
+
const replay = regenerateCommand ?? `nodefony orm:generate --name ${name}`;
|
|
74
|
+
throw new Error(`Un RENOMMAGE probable a été détecté sur ${label}, et il faut trancher.\n\n drizzle-kit ne peut pas deviner votre intention : une colonne qui\n disparaît et une autre qui apparaît, c'est soit un renommage — les\n données SUIVENT —, soit une suppression puis un ajout — les données\n sont PERDUES. Il pose donc la question, et il n'y a pas de terminal\n ici pour y répondre.\n\n Rejouer la commande dans un terminal interactif :\n ${replay}\n\n ⚠️ Après avoir répondu « renamed », RELIRE le fichier produit : quand\n une colonne est renommée ET que son type change, l'outil n'écrit que\n le renommage et OUBLIE le changement de type (drizzle-orm#3826).`);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`La génération n'a pas eu lieu sur ${label} (code ${result.status}). Ne rien conclure de ce silence : l'outil rend 0 même en échec.\n` + output.trim());
|
|
77
|
+
}
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Lance `drizzle-kit introspect` et EXIGE la preuve qu'il a travaillé.
|
|
82
|
+
*
|
|
83
|
+
* C'est la seule commande de la chaîne qui LIT la base pour en tirer des
|
|
84
|
+
* fichiers. Elle sert l'adoption d'une base qui existait avant les migrations :
|
|
85
|
+
* l'instantané qu'elle dépose décrit l'état RÉEL, celui à partir duquel la
|
|
86
|
+
* génération suivante produira un `ALTER` au lieu d'un `CREATE TABLE`.
|
|
87
|
+
*
|
|
88
|
+
* La preuve n'est pas cherchée dans le texte de l'outil mais dans ce qu'il
|
|
89
|
+
* LAISSE : l'appelant relit le journal des fichiers. Ici on ne garde que le
|
|
90
|
+
* refus le plus grossier — un code de sortie non nul —, parce que l'outil rend
|
|
91
|
+
* `0` même en échec et qu'un marqueur de texte a déjà menti une fois (il change
|
|
92
|
+
* avec la couleur du terminal).
|
|
93
|
+
*
|
|
94
|
+
* @param options - `cwd` (dossier depuis lequel l'outil est lancé), `configRel`
|
|
95
|
+
* (configuration, chemin relatif à `cwd`), `label` (ce qui est cité en cas
|
|
96
|
+
* d'échec).
|
|
97
|
+
* @returns la sortie complète de l'outil.
|
|
98
|
+
* @throws Error si le code est non nul.
|
|
99
|
+
*/
|
|
100
|
+
function runIntrospect({ cwd, configRel, label }) {
|
|
101
|
+
const result = spawnSync(process.execPath, [
|
|
102
|
+
resolveDrizzleKitBin(cwd),
|
|
103
|
+
"introspect",
|
|
104
|
+
`--config=${configRel}`
|
|
105
|
+
], {
|
|
106
|
+
cwd,
|
|
107
|
+
encoding: "utf8"
|
|
108
|
+
});
|
|
109
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
110
|
+
if (result.status !== 0) throw new Error(`La lecture du schéma de ${label} a échoué (code ${result.status}).\n` + output.trim());
|
|
111
|
+
return output;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* La génération a-t-elle EU LIEU ? — lue sur une sortie DÉCOLORÉE.
|
|
115
|
+
*
|
|
116
|
+
* L'outil ne rend pas de code d'échec (il sort 0 même quand il rate) : la seule
|
|
117
|
+
* preuve disponible est un marqueur dans son texte. Encore faut-il le chercher
|
|
118
|
+
* dans le texte, et non dans sa mise en forme.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 **Le piège, payé en intégration continue** : la forge pose `FORCE_COLOR`,
|
|
121
|
+
* l'outil colore alors sa coche — `[`, une séquence d'échappement, `✓`, une
|
|
122
|
+
* autre séquence, `]` — et `"[✓]"` n'est plus une sous-chaîne. La génération
|
|
123
|
+
* réussissait, le fichier était écrit, et l'appelant annonçait qu'elle n'avait
|
|
124
|
+
* pas eu lieu. Vert sur un poste sans terminal, rouge à la forge : la sonde
|
|
125
|
+
* mesurait la présentation.
|
|
126
|
+
*
|
|
127
|
+
* Fonction PURE, pour qu'elle s'éprouve sans lancer un process — une règle qui
|
|
128
|
+
* exige un sous-processus pour être vue rouge n'est jamais vue rouge.
|
|
129
|
+
*
|
|
130
|
+
* @param output - sortie complète de l'outil, telle qu'elle a été capturée.
|
|
131
|
+
* @returns `true` si l'outil dit avoir écrit, ou n'avoir rien eu à écrire.
|
|
132
|
+
*/
|
|
133
|
+
function generationHappened(output) {
|
|
134
|
+
const plain = output.replace(/\u001B\[[0-9;]*m/g, "");
|
|
135
|
+
return plain.includes("No schema changes") || plain.includes("[✓]");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Instructions qui DÉTRUISENT des données, par dialecte.
|
|
139
|
+
*
|
|
140
|
+
* Sources : la documentation PostgreSQL sur les verrous d'`ALTER TABLE`, le
|
|
141
|
+
* comportement constaté de `drizzle-kit`, et le fait — vérifié — que l'outil se
|
|
142
|
+
* décrit lui-même comme une aide à la productivité, pas comme un dispositif de
|
|
143
|
+
* sûreté de déploiement. Aucun générateur de diff ne peut distinguer seul un
|
|
144
|
+
* renommage (les données suivent) d'une suppression suivie d'un ajout (les
|
|
145
|
+
* données disparaissent) : c'est une intention, pas une différence de schéma.
|
|
146
|
+
*
|
|
147
|
+
* Chaque entrée porte le motif, ce qui se passe, et ce qu'il faut faire — le
|
|
148
|
+
* message d'un refus doit dire quoi faire, pas seulement ce qui est refusé.
|
|
149
|
+
*/
|
|
150
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
151
|
+
{
|
|
152
|
+
id: "drop-table",
|
|
153
|
+
pattern: /\bDROP\s+TABLE\b/i,
|
|
154
|
+
what: "supprime une table ET toutes ses lignes",
|
|
155
|
+
todo: "sauvegarder, puis appliquer en deux temps (cf expand/contract)"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "drop-column",
|
|
159
|
+
pattern: /\bDROP\s+COLUMN\b/i,
|
|
160
|
+
what: "supprime une colonne ET son contenu, sans retour possible",
|
|
161
|
+
todo: "s'il s'agissait d'un RENOMMAGE, regénérer dans un terminal interactif et répondre « renamed » : l'outil produit alors un RENAME, qui conserve les données"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "alter-column-type",
|
|
165
|
+
pattern: /\bALTER\s+(?:COLUMN\s+)?[`"\w]+\s+(?:SET\s+DATA\s+)?TYPE\b/i,
|
|
166
|
+
what: "convertit une colonne : les valeurs qui n'entrent pas dans le nouveau type sont perdues ou font échouer la migration à mi-parcours",
|
|
167
|
+
todo: "vérifier la conversion sur une copie des données de production avant d'appliquer"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "modify-column",
|
|
171
|
+
pattern: /\bMODIFY\s+COLUMN\b/i,
|
|
172
|
+
what: "réécrit une colonne MySQL (type, nullabilité) — mêmes risques",
|
|
173
|
+
todo: "vérifier la conversion sur une copie des données avant d'appliquer"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "truncate",
|
|
177
|
+
pattern: /\bTRUNCATE\b/i,
|
|
178
|
+
what: "vide une table entière",
|
|
179
|
+
todo: "ne jamais laisser une instruction de ce genre dans une migration"
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
/**
|
|
183
|
+
* Ce qui ne détruit rien, mais doit être VU avant d'appliquer.
|
|
184
|
+
*
|
|
185
|
+
* Deux natures, et elles ne justifient ni l'une ni l'autre un refus — le
|
|
186
|
+
* générateur ne lit pas la base, il ne peut donc pas trancher à la place de
|
|
187
|
+
* celui qui la connaît.
|
|
188
|
+
*
|
|
189
|
+
* **Ce qui VERROUILLE.** Le scénario type, largement documenté : un
|
|
190
|
+
* `ALTER TABLE` prend un verrou exclusif, se met en file derrière une requête
|
|
191
|
+
* longue, et toutes les requêtes suivantes s'empilent derrière lui ; le parc de
|
|
192
|
+
* connexions se vide en quelques dizaines de secondes, et l'application rend des
|
|
193
|
+
* 503 alors que la migration, elle, n'a rien de lent.
|
|
194
|
+
*
|
|
195
|
+
* **Ce qui ÉCHOUE sur une table peuplée.** L'inapplicabilité est une propriété
|
|
196
|
+
* du SQL écrit, pas de la donnée : une colonne obligatoire sans défaut, un index
|
|
197
|
+
* unique posé sur une colonne qu'on vient d'ajouter. Elle se voit donc sans se
|
|
198
|
+
* connecter — et ne pas la dire a déjà conduit un agent à supprimer une base
|
|
199
|
+
* pour sortir de l'impasse (tâche 33 du banc de découvrabilité).
|
|
200
|
+
*/
|
|
201
|
+
const BLOCKING_PATTERNS = [
|
|
202
|
+
{
|
|
203
|
+
id: "create-index-not-concurrent",
|
|
204
|
+
dialects: ["postgres"],
|
|
205
|
+
pattern: /\bCREATE\s+(?:UNIQUE\s+)?INDEX\b(?!\s+CONCURRENTLY)/i,
|
|
206
|
+
what: "bloque les écritures de la table pendant toute la construction de l'index (minutes sur une grande table)",
|
|
207
|
+
todo: "sur une table déjà volumineuse en production, appliquer l'index à part, en `CREATE INDEX CONCURRENTLY` (hors transaction)"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "set-not-null",
|
|
211
|
+
dialects: ["postgres"],
|
|
212
|
+
pattern: /\bSET\s+NOT\s+NULL\b/i,
|
|
213
|
+
what: "scanne la table entière sous verrou exclusif pour valider chaque ligne",
|
|
214
|
+
todo: "ajouter d'abord une contrainte `CHECK … NOT VALID`, la valider à part, puis poser le `NOT NULL`"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "add-not-null-sans-defaut",
|
|
218
|
+
pattern: /\bADD\s+(?:COLUMN\s+)?[`"']?\w+[`"']?[^;\n]*?\bNOT\s+NULL\b(?![^;\n]*\bDEFAULT\b)/i,
|
|
219
|
+
what: "ajoute une colonne OBLIGATOIRE sans valeur par défaut : sur une table qui porte déjà des lignes, sqlite et PostgreSQL REFUSENT la migration, et MySQL/MariaDB la remplit de chaînes vides sans un avertissement",
|
|
220
|
+
todo: "donner un défaut à la déclaration du champ (`role:string=membre`), ou le déclarer facultatif (`department:string?`) ; s'il faut les deux, c'est en trois temps — ajouter avec défaut, remplir (`--custom`), retirer le défaut"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "colonne-neuve-puis-index-unique",
|
|
224
|
+
pattern: /\bCREATE\s+UNIQUE\s+INDEX\b/i,
|
|
225
|
+
detect: (code) => {
|
|
226
|
+
const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].map((m) => m[1].toLowerCase());
|
|
227
|
+
if (added.length === 0) return false;
|
|
228
|
+
return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some((m) => m[1].split(",").map((c) => c.trim().replace(/[`"']/g, "").toLowerCase()).some((c) => added.includes(c)));
|
|
229
|
+
},
|
|
230
|
+
what: "ajoute une colonne ET pose son index UNIQUE dans la même migration : toutes les lignes déjà présentes reçoivent la même valeur, et l'index échoue sur la deuxième — cet enchaînement ne réussit que sur une table vide",
|
|
231
|
+
todo: "séparer : ajouter la colonne sans contrainte d'unicité, remplir chaque ligne d'une valeur DISTINCTE (`nodefony orm:generate --custom`), puis poser l'index unique"
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
/**
|
|
235
|
+
* Analyse le SQL d'une migration et rend ce qui mérite un refus ou un regard.
|
|
236
|
+
*
|
|
237
|
+
* Volontairement **textuelle** : il ne s'agit pas d'analyser du SQL, mais de
|
|
238
|
+
* refuser de laisser passer sans un mot ce qui détruit des données. Un motif de
|
|
239
|
+
* trop fait poser une question ; un motif de moins fait perdre une table.
|
|
240
|
+
*
|
|
241
|
+
* @param sql - contenu d'un fichier de migration.
|
|
242
|
+
* @param dialect - dialecte concerné (certains risques lui sont propres).
|
|
243
|
+
* @returns `{ destructive, blocking }`, chacun décrivant ce qui a été reconnu.
|
|
244
|
+
*/
|
|
245
|
+
function auditMigrationSql(sql, dialect) {
|
|
246
|
+
const code = sql.split("\n").filter((line) => !line.trimStart().startsWith("--")).join("\n");
|
|
247
|
+
const match = (list) => list.filter((rule) => !rule.dialects || rule.dialects.includes(dialect)).filter((rule) => rule.pattern.test(code)).filter((rule) => rule.detect === void 0 || rule.detect(code)).map(({ id, what, todo }) => ({
|
|
248
|
+
id,
|
|
249
|
+
what,
|
|
250
|
+
todo
|
|
251
|
+
}));
|
|
252
|
+
return {
|
|
253
|
+
destructive: match(DESTRUCTIVE_PATTERNS),
|
|
254
|
+
blocking: match(BLOCKING_PATTERNS)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Reconnaît l'échec de `drizzle-kit` faute de terminal interactif.
|
|
259
|
+
*
|
|
260
|
+
* L'outil pose une question — « cette colonne a-t-elle été renommée, ou
|
|
261
|
+
* supprimée puis ajoutée ? » — à laquelle lui seul ne peut pas répondre. Sans
|
|
262
|
+
* terminal, il échoue, et **rend 0**. Sans reconnaissance explicite, l'utilisateur
|
|
263
|
+
* reçoit une pile d'appels de l'outil au lieu de la seule chose qui compte : la
|
|
264
|
+
* question qu'on lui pose, et où y répondre.
|
|
265
|
+
*
|
|
266
|
+
* @param output - sortie complète de l'outil.
|
|
267
|
+
* @returns `true` si l'échec vient d'une question restée sans terminal.
|
|
268
|
+
*/
|
|
269
|
+
function isInteractivePromptFailure(output) {
|
|
270
|
+
return /Interactive prompts require a TTY/i.test(output);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
|
|
274
|
+
*
|
|
275
|
+
* Écrit en fins de ligne `\n` quel que soit le système : le dépôt et le gabarit
|
|
276
|
+
* d'application déclarent `* text=auto eol=lf`, sans quoi une copie de travail
|
|
277
|
+
* Windows produirait une fausse dérive à chaque lecture.
|
|
278
|
+
*
|
|
279
|
+
* @param dir - dossier `<sortie>/<dialecte>` dont on marque les fichiers.
|
|
280
|
+
* @returns le nombre de fichiers marqués.
|
|
281
|
+
*/
|
|
282
|
+
function stampFormatMarker(dir) {
|
|
283
|
+
if (!fs.existsSync(dir)) return 0;
|
|
284
|
+
let stamped = 0;
|
|
285
|
+
for (const name of fs.readdirSync(dir)) {
|
|
286
|
+
if (!name.endsWith(".sql")) continue;
|
|
287
|
+
const file = path.join(dir, name);
|
|
288
|
+
const body = fs.readFileSync(file, "utf8");
|
|
289
|
+
if (body.startsWith("-- nodefony:migration format=1")) continue;
|
|
290
|
+
fs.writeFileSync(file, `${FORMAT_MARKER}\n${body.replace(/\r\n/g, "\n")}`);
|
|
291
|
+
stamped++;
|
|
292
|
+
}
|
|
293
|
+
return stamped;
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region nodefony/src/migrator/name.ts
|
|
2
|
+
/**
|
|
3
|
+
* Ce qu'un nom de migration a le droit d'être — et ce qu'on propose sinon.
|
|
4
|
+
*
|
|
5
|
+
* **Pourquoi une fonction pure, à part de la commande** : ce nom entre dans le
|
|
6
|
+
* tag du fichier, et un tag ne se renomme plus une fois la migration appliquée
|
|
7
|
+
* quelque part — c'est lui qui dit à chaque base ce qu'elle a déjà reçu. La
|
|
8
|
+
* règle qui le garde mérite donc d'être exerçable sans démarrer une
|
|
9
|
+
* application : un contrôle qui coûte trois minutes est un contrôle qu'on
|
|
10
|
+
* saute.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Longueur maximale d'un nom de migration.
|
|
14
|
+
*
|
|
15
|
+
* Le tag complet vaut `NNNN_<nom>` et le fichier `<tag>.sql` : à 120
|
|
16
|
+
* caractères, on reste très en deçà des 255 octets qu'un système de fichiers
|
|
17
|
+
* accepte pour un nom, et loin des 260 caractères qu'un chemin Windows tolère
|
|
18
|
+
* par défaut. La borne n'est pas là pour économiser des octets — elle est là
|
|
19
|
+
* pour qu'un nom trop long échoue AVANT d'avoir écrit un fichier, avec une
|
|
20
|
+
* phrase, plutôt qu'au moment de l'écriture avec un code d'erreur système.
|
|
21
|
+
*/
|
|
22
|
+
const MIGRATION_NAME_MAX = 120;
|
|
23
|
+
/** Ce qu'un nom peut contenir : minuscules, chiffres et le trait bas. */
|
|
24
|
+
const FORME = /^[a-z0-9_]+$/;
|
|
25
|
+
/** Un nom doit porter au moins une lettre ou un chiffre — `___` n'en est pas un. */
|
|
26
|
+
const SUBSTANCE = /[a-z0-9]/;
|
|
27
|
+
/**
|
|
28
|
+
* Dérive un nom acceptable d'une saisie qui ne l'est pas.
|
|
29
|
+
*
|
|
30
|
+
* @param input - ce que l'utilisateur a tapé.
|
|
31
|
+
* @returns un nom conforme, ou `undefined` s'il n'en reste rien de sensé.
|
|
32
|
+
*/
|
|
33
|
+
function suggestMigrationName(input) {
|
|
34
|
+
const derive = input.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 120);
|
|
35
|
+
return SUBSTANCE.test(derive) ? derive : void 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Vérifie un nom de migration, et dit ce qu'il faudrait taper à la place.
|
|
39
|
+
*
|
|
40
|
+
* @param input - nom reçu de la ligne de commande, éventuellement absent.
|
|
41
|
+
* @returns le verdict, avec sa raison et sa suggestion.
|
|
42
|
+
*/
|
|
43
|
+
function checkMigrationName(input) {
|
|
44
|
+
if (!input) return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: "Il manque le nom de la migration."
|
|
47
|
+
};
|
|
48
|
+
if (input.length > 120) return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: `Le nom fait ${input.length} caractères, la limite est 120 : il devient un nom de fichier, et un chemin trop long échoue à l'écriture sur certains systèmes.`,
|
|
51
|
+
suggestion: suggestMigrationName(input)
|
|
52
|
+
};
|
|
53
|
+
if (!FORME.test(input)) return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: `Le nom « ${input} » ne convient pas : minuscules, chiffres et « _ » seulement.`,
|
|
56
|
+
suggestion: suggestMigrationName(input)
|
|
57
|
+
};
|
|
58
|
+
if (!SUBSTANCE.test(input)) return {
|
|
59
|
+
ok: false,
|
|
60
|
+
reason: `Le nom « ${input} » ne porte aucune lettre ni chiffre : il produirait un tag que personne ne saura relire.`
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
name: input
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { MIGRATION_NAME_MAX, checkMigrationName, suggestMigrationName };
|