@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 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.149.0/helpers/esm/decorate.js
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.149.0/helpers/esm/decorateMetadata.js
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.149.0/helpers/esm/decorateMetadata.js";
40
- import __decorate from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
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
- * Reconnaît la recréation de table de SQLite, et l'exclut de la perte de données.
68
- *
69
- * SQLite ne sait pas modifier une colonne : l'outil de génération produit alors
70
- * la ronde connue créer `__new_x`, y recopier les lignes de `x`, supprimer
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 isTableRebuild(statements) {
82
- const joint = statements.join("\n");
83
- return /\bCREATE\s+TABLE\s+[`"']?__new_/i.test(joint) && /\bINSERT\s+INTO\s+[`"']?__new_/i.test(joint);
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 rebuild = isTableRebuild(file.statements);
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 = rebuild && (p.kind === "drop-table" || p.kind === "rename");
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.6",
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.1"
60
+ "zod": "^4.6.5"
61
61
  },
62
62
  "devDependencies": {
63
- "@nodefony/framework": "^10.0.0-alpha.6",
64
- "@nodefony/http": "^10.0.0-alpha.6",
65
- "@nodefony/orm-core": "^10.0.0-alpha.6",
66
- "@nodefony/security": "^10.0.0-alpha.6",
67
- "@nodefony/user": "^10.0.0-alpha.6",
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.5.1",
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.6",
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.6",
80
- "@nodefony/http": "^10.0.0-alpha.6",
81
- "@nodefony/orm-core": "^10.0.0-alpha.6",
82
- "@nodefony/security": "^10.0.0-alpha.6",
83
- "@nodefony/user": "^10.0.0-alpha.6",
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.6",
86
+ "nodefony": "^10.0.0-alpha.7",
87
87
  "pg": "^8.23.0"
88
88
  },
89
89
  "repository": {