@nodefony/drizzle 10.0.0-alpha.6 → 10.0.0-alpha.7
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/README.md +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/src/migrator/destructive.js +78 -19
- package/dist/nodefony/src/migrator/kit.js +5 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +36 -0
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Adapter [Drizzle ORM](https://orm.drizzle.team/) pour Nodefony, branché sur
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install @nodefony/drizzle drizzle-orm better-sqlite3
|
|
14
|
+
npm install @nodefony/drizzle@alpha drizzle-orm better-sqlite3
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Utilisation comme module Nodefony (bootable)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -36,8 +36,8 @@ import OrmMigrateStatus from "./nodefony/command/orm-migrate-status.js";
|
|
|
36
36
|
import OrmMigrateBaseline from "./nodefony/command/orm-migrate-baseline.js";
|
|
37
37
|
import OrmMigrateRepair from "./nodefony/command/orm-migrate-repair.js";
|
|
38
38
|
import OrmReset from "./nodefony/command/orm-reset.js";
|
|
39
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
40
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
39
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
40
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
41
41
|
import SessionStorage from "./nodefony/src/SessionStorage.js";
|
|
42
42
|
import "./nodefony/src/migrator/index.js";
|
|
43
43
|
import { DrizzleUserRepository } from "./nodefony/src/DrizzleUserRepository.js";
|
|
@@ -64,23 +64,82 @@ const PATTERNS = [
|
|
|
64
64
|
}
|
|
65
65
|
];
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `x`, renommer. Ce `DROP TABLE` est **structurel**, les données ont été
|
|
72
|
-
* recopiées juste avant ; le signaler comme une perte ferait crier au loup à
|
|
73
|
-
* chaque changement de colonne, et le garde serait désarmé au bout de trois
|
|
74
|
-
* fois.
|
|
75
|
-
*
|
|
76
|
-
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne
|
|
77
|
-
* n'apparaît pas dans le `INSERT … SELECT`, ses données disparaissent bel et
|
|
78
|
-
* bien. C'est pourquoi la recréation reste SIGNALÉE, avec le geste — lire le
|
|
79
|
-
* SQL —, et seulement déclassée en avertissement.
|
|
67
|
+
* Le SQL d'une migration, qu'on le reçoive découpé ou d'un bloc.
|
|
68
|
+
*
|
|
69
|
+
* Les deux formes existent pour de bon : l'audit d'APPLICATION travaille sur
|
|
70
|
+
* les instructions déjà séparées, celui de GÉNÉRATION sur le fichier entier.
|
|
80
71
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
function asSql(sql) {
|
|
73
|
+
return typeof sql === "string" ? sql : sql.join("\n");
|
|
74
|
+
}
|
|
75
|
+
/** Un nom de table tel qu'il s'écrit en SQL, guillemets facultatifs. */
|
|
76
|
+
const IDENT = "[`\"']?(\\w+)[`\"']?";
|
|
77
|
+
/**
|
|
78
|
+
* Les tables qu'une migration RECONSTRUIT, par la ronde que SQLite impose.
|
|
79
|
+
*
|
|
80
|
+
* 🔴 **La convention de nommage n'est PAS la règle.** La version précédente
|
|
81
|
+
* cherchait le préfixe `__new_` que produit l'outil de génération : elle ne
|
|
82
|
+
* reconnaissait donc une reconstruction que si c'est LUI qui l'avait écrite.
|
|
83
|
+
* Une migration reprise à la main — cas courant, et mesuré sur un agent réel
|
|
84
|
+
* qui avait nommé sa table d'étape `articles_new` — retombait sur
|
|
85
|
+
* « supprime une table et TOUTES ses lignes », sur un SQL qui ne perd rien.
|
|
86
|
+
*
|
|
87
|
+
* Ce qui fait une reconstruction, c'est la RONDE, quels que soient les noms :
|
|
88
|
+
* une table d'étape est créée, les lignes de la table visée y sont recopiées,
|
|
89
|
+
* la visée est supprimée, et l'étape prend sa place. Les quatre doivent être
|
|
90
|
+
* présentes dans le même fichier, et se RÉPONDRE — c'est le renommage final
|
|
91
|
+
* qui dit quelle table d'étape appartient à quelle table visée.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne n'apparaît
|
|
94
|
+
* pas dans le `INSERT … SELECT`, ses données disparaissent bel et bien. La
|
|
95
|
+
* reconstruction reste donc SIGNALÉE, avec le geste — lire le SQL —, et
|
|
96
|
+
* seulement déclassée en avertissement.
|
|
97
|
+
*
|
|
98
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
99
|
+
* @returns les tables reconstruites, en minuscules ; vide s'il n'y en a aucune.
|
|
100
|
+
*/
|
|
101
|
+
function rebuiltTables(sql) {
|
|
102
|
+
const joint = asSql(sql);
|
|
103
|
+
const out = /* @__PURE__ */ new Set();
|
|
104
|
+
const renames = joint.matchAll(new RegExp(`\\bALTER\\s+TABLE\\s+${IDENT}\\s+RENAME\\s+TO\\s+${IDENT}`, "gi"));
|
|
105
|
+
for (const rename of renames) {
|
|
106
|
+
const staging = rename[1];
|
|
107
|
+
const target = rename[2];
|
|
108
|
+
if (staging === void 0 || target === void 0) continue;
|
|
109
|
+
const created = new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"']?${staging}[\`"']?\\b`, "i");
|
|
110
|
+
const copied = new RegExp(`\\bINSERT\\s+INTO\\s+[\`"']?${staging}[\`"']?[\\s\\S]{0,4000}?\\bFROM\\s+[\`"']?${target}[\`"']?\\b`, "i");
|
|
111
|
+
const dropped = new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?[\`"']?${target}[\`"']?\\b`, "i");
|
|
112
|
+
if (created.test(joint) && copied.test(joint) && dropped.test(joint)) out.add(target.toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Les tables qu'un SQL SUPPRIME, dans l'ordre où il les nomme.
|
|
118
|
+
*
|
|
119
|
+
* Exportée pour que l'audit de génération et celui d'application lisent la
|
|
120
|
+
* MÊME chose : deux extractions écrites séparément divergeraient, et l'une des
|
|
121
|
+
* deux surfaces se mettrait à refuser ce que l'autre laisse passer.
|
|
122
|
+
*
|
|
123
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
124
|
+
* @returns les noms supprimés, en minuscules.
|
|
125
|
+
*/
|
|
126
|
+
function droppedTables(sql) {
|
|
127
|
+
return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].map((m) => m[1]).filter((n) => n !== void 0).map((n) => n.toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* L'instruction fait-elle partie de la mécanique d'une reconstruction ?
|
|
131
|
+
*
|
|
132
|
+
* Le verdict porte sur la table que CETTE instruction touche, jamais sur le
|
|
133
|
+
* fichier entier : une migration qui reconstruit `articles` et supprime
|
|
134
|
+
* `brouillons` doit continuer de signaler la seconde.
|
|
135
|
+
*/
|
|
136
|
+
function isRebuildMechanics(statement, kind, rebuilt) {
|
|
137
|
+
if (kind === "drop-table") return droppedTables(statement).some((name) => rebuilt.has(name));
|
|
138
|
+
if (kind === "rename") {
|
|
139
|
+
const target = new RegExp(`\\bRENAME\\s+TO\\s+${IDENT}`, "i").exec(statement)?.[1];
|
|
140
|
+
return target !== void 0 && rebuilt.has(target.toLowerCase());
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
84
143
|
}
|
|
85
144
|
/** Borne une instruction pour l'affichage, sans jamais la déformer. */
|
|
86
145
|
function truncate(statement) {
|
|
@@ -96,10 +155,10 @@ function truncate(statement) {
|
|
|
96
155
|
function scanDestructive(files) {
|
|
97
156
|
const out = [];
|
|
98
157
|
for (const file of files) {
|
|
99
|
-
const
|
|
158
|
+
const rebuilt = rebuiltTables(file.statements);
|
|
100
159
|
for (const statement of file.statements) for (const p of PATTERNS) {
|
|
101
160
|
if (!p.re.test(statement)) continue;
|
|
102
|
-
const structurel =
|
|
161
|
+
const structurel = isRebuildMechanics(statement, p.kind, rebuilt);
|
|
103
162
|
out.push({
|
|
104
163
|
source: file.source,
|
|
105
164
|
tag: file.tag,
|
|
@@ -210,4 +269,4 @@ function checkDataAdvice(connector, target) {
|
|
|
210
269
|
return `Ces migrations ont modifié des tables qui portaient déjà des lignes. Pour VÉRIFIER que les données ont suivi, ne repars pas d'une base vide : ce sont ces lignes-là qui sont la réponse, les effacer efface la question. Deux moyens. Compter et regarder sur place — « SELECT COUNT(*) » sur les tables touchées, et les valeurs des colonnes nouvellement remplies. Ou rejouer le même lot sur une COPIE, sans toucher à celle-ci : ${copy}, puis désigne-la par NF_MIGRATE_DATABASE_URL :\n NF_MIGRATE_DATABASE_URL=${url} nodefony orm:migrate${suffixe}\n (PowerShell : $env:NF_MIGRATE_DATABASE_URL = "${url}" ; nodefony orm:migrate${suffixe})`;
|
|
211
270
|
}
|
|
212
271
|
//#endregion
|
|
213
|
-
export { checkDataAdvice, dataLoss, destructiveActions, renderDestructive, scanDestructive, summarizeDestructive, touchesExistingRows };
|
|
272
|
+
export { checkDataAdvice, dataLoss, destructiveActions, droppedTables, rebuiltTables, renderDestructive, scanDestructive, summarizeDestructive, touchesExistingRows };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FORMAT_MARKER } from "./types.js";
|
|
2
2
|
import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed } from "./refusals.js";
|
|
3
|
+
import { droppedTables, rebuiltTables } from "./destructive.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -149,6 +150,10 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
149
150
|
{
|
|
150
151
|
id: "drop-table",
|
|
151
152
|
pattern: /\bDROP\s+TABLE\b/i,
|
|
153
|
+
detect: (code) => {
|
|
154
|
+
const rebuilt = rebuiltTables(code);
|
|
155
|
+
return droppedTables(code).some((table) => !rebuilt.has(table));
|
|
156
|
+
},
|
|
152
157
|
what: "supprime une table ET toutes ses lignes",
|
|
153
158
|
todo: "sauvegarder, puis appliquer en deux temps (cf expand/contract)"
|
|
154
159
|
},
|
|
@@ -62,6 +62,42 @@ export interface IDestructiveFinding {
|
|
|
62
62
|
/** L'instruction elle-même, bornée pour rester lisible. */
|
|
63
63
|
statement: string;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Les tables qu'une migration RECONSTRUIT, par la ronde que SQLite impose.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 **La convention de nommage n'est PAS la règle.** La version précédente
|
|
69
|
+
* cherchait le préfixe `__new_` que produit l'outil de génération : elle ne
|
|
70
|
+
* reconnaissait donc une reconstruction que si c'est LUI qui l'avait écrite.
|
|
71
|
+
* Une migration reprise à la main — cas courant, et mesuré sur un agent réel
|
|
72
|
+
* qui avait nommé sa table d'étape `articles_new` — retombait sur
|
|
73
|
+
* « supprime une table et TOUTES ses lignes », sur un SQL qui ne perd rien.
|
|
74
|
+
*
|
|
75
|
+
* Ce qui fait une reconstruction, c'est la RONDE, quels que soient les noms :
|
|
76
|
+
* une table d'étape est créée, les lignes de la table visée y sont recopiées,
|
|
77
|
+
* la visée est supprimée, et l'étape prend sa place. Les quatre doivent être
|
|
78
|
+
* présentes dans le même fichier, et se RÉPONDRE — c'est le renommage final
|
|
79
|
+
* qui dit quelle table d'étape appartient à quelle table visée.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne n'apparaît
|
|
82
|
+
* pas dans le `INSERT … SELECT`, ses données disparaissent bel et bien. La
|
|
83
|
+
* reconstruction reste donc SIGNALÉE, avec le geste — lire le SQL —, et
|
|
84
|
+
* seulement déclassée en avertissement.
|
|
85
|
+
*
|
|
86
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
87
|
+
* @returns les tables reconstruites, en minuscules ; vide s'il n'y en a aucune.
|
|
88
|
+
*/
|
|
89
|
+
export declare function rebuiltTables(sql: readonly string[] | string): ReadonlySet<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Les tables qu'un SQL SUPPRIME, dans l'ordre où il les nomme.
|
|
92
|
+
*
|
|
93
|
+
* Exportée pour que l'audit de génération et celui d'application lisent la
|
|
94
|
+
* MÊME chose : deux extractions écrites séparément divergeraient, et l'une des
|
|
95
|
+
* deux surfaces se mettrait à refuser ce que l'autre laisse passer.
|
|
96
|
+
*
|
|
97
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
98
|
+
* @returns les noms supprimés, en minuscules.
|
|
99
|
+
*/
|
|
100
|
+
export declare function droppedTables(sql: readonly string[] | string): string[];
|
|
65
101
|
/**
|
|
66
102
|
* Cherche, dans les migrations en attente, ce qui détruit ou casse.
|
|
67
103
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/drizzle",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.7",
|
|
4
4
|
"description": "Moteur SQL de Nodefony sur Drizzle ORM (SQLite, PostgreSQL, MySQL) : dépôts typés, migrations de schéma et adoption d'une base existante",
|
|
5
5
|
"nodefony": {
|
|
6
6
|
"storeKind": "durable",
|
|
@@ -57,33 +57,33 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"drizzle-orm": "0.45.2",
|
|
59
59
|
"tslib": "2.8.1",
|
|
60
|
-
"zod": "^4.6.
|
|
60
|
+
"zod": "^4.6.5"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
64
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
65
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
66
|
-
"@nodefony/security": "^10.0.0-alpha.
|
|
67
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
63
|
+
"@nodefony/framework": "^10.0.0-alpha.7",
|
|
64
|
+
"@nodefony/http": "^10.0.0-alpha.7",
|
|
65
|
+
"@nodefony/orm-core": "^10.0.0-alpha.7",
|
|
66
|
+
"@nodefony/security": "^10.0.0-alpha.7",
|
|
67
|
+
"@nodefony/user": "^10.0.0-alpha.7",
|
|
68
68
|
"@types/better-sqlite3": "9.6.0",
|
|
69
|
-
"@types/node": "26.
|
|
69
|
+
"@types/node": "26.6.1",
|
|
70
70
|
"@types/pg": "8.23.1",
|
|
71
71
|
"better-sqlite3": "13.0.3",
|
|
72
72
|
"drizzle-kit": "0.31.10",
|
|
73
73
|
"mysql2": "3.24.4",
|
|
74
|
-
"nodefony": "^10.0.0-alpha.
|
|
74
|
+
"nodefony": "^10.0.0-alpha.7",
|
|
75
75
|
"pg": "8.23.0",
|
|
76
76
|
"rimraf": "6.1.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
80
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
81
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
82
|
-
"@nodefony/security": "^10.0.0-alpha.
|
|
83
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
79
|
+
"@nodefony/framework": "^10.0.0-alpha.7",
|
|
80
|
+
"@nodefony/http": "^10.0.0-alpha.7",
|
|
81
|
+
"@nodefony/orm-core": "^10.0.0-alpha.7",
|
|
82
|
+
"@nodefony/security": "^10.0.0-alpha.7",
|
|
83
|
+
"@nodefony/user": "^10.0.0-alpha.7",
|
|
84
84
|
"better-sqlite3": "^13.0.0",
|
|
85
85
|
"mysql2": "^3.24.0",
|
|
86
|
-
"nodefony": "^10.0.0-alpha.
|
|
86
|
+
"nodefony": "^10.0.0-alpha.7",
|
|
87
87
|
"pg": "^8.23.0"
|
|
88
88
|
},
|
|
89
89
|
"repository": {
|