@nodefony/drizzle 10.0.0-alpha.9 → 10.0.0-beta.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.
Files changed (39) hide show
  1. package/README.md +19 -19
  2. package/dist/index.js +3 -3
  3. package/dist/nodefony/command/orm-generate.js +3 -3
  4. package/dist/nodefony/config/defineModuleConfig.js +2 -2
  5. package/dist/nodefony/entity/userTable.js +1 -1
  6. package/dist/nodefony/registerStores.js +2 -2
  7. package/dist/nodefony/service/DrizzleService.js +26 -12
  8. package/dist/nodefony/src/DrizzleAuditStore.js +1 -1
  9. package/dist/nodefony/src/connectorTarget.js +1 -1
  10. package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
  11. package/dist/nodefony/src/migrator/adopt.js +1 -1
  12. package/dist/nodefony/src/migrator/destructive.js +2 -3
  13. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +1 -1
  14. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +1 -1
  15. package/dist/nodefony/src/migrator/explain.js +7 -10
  16. package/dist/nodefony/src/migrator/history.js +25 -2
  17. package/dist/nodefony/src/migrator/kit.js +23 -9
  18. package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
  19. package/dist/nodefony/src/migrator/resolve.js +4 -4
  20. package/dist/nodefony/src/migrator/sources.js +13 -8
  21. package/dist/nodefony/src/orm-core/DrizzleOrm.js +49 -29
  22. package/dist/nodefony/src/orm-core/DrizzleRepository.js +38 -26
  23. package/dist/nodefony/src/queryKit.js +5 -5
  24. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
  25. package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
  26. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
  27. package/dist/types/nodefony/src/migrator/destructive.d.ts +1 -1
  28. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +2 -2
  29. package/dist/types/nodefony/src/migrator/explain.d.ts +9 -9
  30. package/dist/types/nodefony/src/migrator/name.d.ts +1 -1
  31. package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
  32. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +3 -3
  33. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +1 -1
  34. package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +2 -2
  35. package/dist/types/nodefony/src/queryKit.d.ts +9 -9
  36. package/dist/types/nodefony/src/safeTarget.d.ts +2 -2
  37. package/docs/index.md +13 -13
  38. package/docs/migrations.md +1 -1
  39. package/package.json +14 -14
package/README.md CHANGED
@@ -16,29 +16,29 @@ npm install @nodefony/drizzle@alpha drizzle-orm better-sqlite3
16
16
 
17
17
  ## Utilisation comme module Nodefony (bootable)
18
18
 
19
- Ajouter `@nodefony/drizzle` à `@modules()` de l'app : le `DrizzleService` connecte
20
- au boot un ORM par connecteur configuré (c'est l'ORM SQL par défaut recommandé).
19
+ Ajouter `@nodefony/drizzle` au manifeste `modules` de `nodefony.config.ts` : le `DrizzleService`
20
+ connecte au boot un ORM par connecteur configuré (c'est l'ORM SQL par défaut recommandé).
21
21
 
22
22
  ```typescript
23
- @modules([
24
- "@nodefony/drizzle", // connecte au boot, ferme au shutdown
25
- "@nodefony/http",
26
- // ...
27
- ])
28
- class App extends Module {}
23
+ // nodefony.config.ts
24
+ import { defineConfig, use } from "nodefony";
25
+
26
+ export default defineConfig({
27
+ modules: [
28
+ "@nodefony/http",
29
+ // connecte au boot, ferme au shutdown ; sans `filename`, la base vit sous var/databases/
30
+ use("@nodefony/drizzle", {
31
+ connectors: { default: { dialect: "sqlite" } },
32
+ }),
33
+ ],
34
+ });
29
35
  ```
30
36
 
31
- Config par défaut (`nodefony/config/config.ts`) — surchargeable côté app via
32
- `nodefony/config/modules/drizzle-config.ts` :
33
-
34
- ```typescript
35
- export default {
36
- connectors: {
37
- default: { filename: "<root>/var/databases/nodefony-drizzle.db" },
38
- // ":memory:" ou absent → base éphémère
39
- },
40
- };
41
- ```
37
+ Le schéma (`nodefony/config/config.ts`) porte les défauts ; l'application les surcharge par
38
+ `use("@nodefony/drizzle", { … })`, par `NF_DATABASE_URL` (dialecte déduit du schéma d'URL) ou par
39
+ l'override générique `NF__DRIZZLE__<CHEMIN>`. Sans `filename`, un connecteur SQLite écrit dans
40
+ `<app>/var/databases/nodefony-drizzle.db` (`nodefony-<connecteur>.db` pour les autres) ;
41
+ `filename: ":memory:"` donne une base éphémère, pour les tests.
42
42
 
43
43
  Au runtime, l'ORM est récupérable via le registre :
44
44
 
package/dist/index.js CHANGED
@@ -77,12 +77,12 @@ let Drizzle = class Drizzle extends Module {
77
77
  * (convention Zod, alignée sur `@nodefony/mongoose` — audit config ORM 2026-06).
78
78
  */
79
79
  async onKernelRegister() {
80
- const validated = defineDrizzleConfig(this.options ?? {}, this.appOptions ?? {});
80
+ const validated = defineDrizzleConfig(this.options, this.appOptions ?? {});
81
81
  this.options = validated;
82
82
  registerUserStore("drizzle");
83
83
  const frameworkHost = validated.connectors.default;
84
- if (validated.frameworkEntities !== false && frameworkHost === void 0) this.log(`pas de connecteur "${FRAMEWORK_CONNECTOR}" (infrastructure déclarée non SQL) : schéma framework non déclaré sur drizzle — les stores durables vont au backend de cette infrastructure`, "INFO");
85
- if (validated.frameworkEntities !== false && frameworkHost !== void 0) {
84
+ if (validated.frameworkEntities && frameworkHost === void 0) this.log(`pas de connecteur "${FRAMEWORK_CONNECTOR}" (infrastructure déclarée non SQL) : schéma framework non déclaré sur drizzle — les stores durables vont au backend de cette infrastructure`, "INFO");
85
+ if (validated.frameworkEntities && frameworkHost !== void 0) {
86
86
  const dialect = frameworkHost.dialect;
87
87
  const report = registerDrizzleFrameworkStores(dialect);
88
88
  if (report.unported.length) this.log(`schéma framework : entités non portées sur "${dialect}" → stores drizzle indisponibles pour [${report.unported.join(", ")}] (chantier multi-dialecte Ph.2.1)`, "WARNING");
@@ -246,7 +246,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
246
246
  ], opts.json, EXIT.actionRequired);
247
247
  return this;
248
248
  }
249
- return await this.#emit({
249
+ return this.#emit({
250
250
  formatVersion: 1,
251
251
  connector,
252
252
  generated: false,
@@ -289,11 +289,11 @@ var OrmGenerate = class extends OrmMigrateCommand {
289
289
  ], opts.json, EXIT.actionRequired, discovery);
290
290
  return this;
291
291
  }
292
- return await this.#emit({
292
+ return this.#emit({
293
293
  formatVersion: 1,
294
294
  connector,
295
295
  generated: true,
296
- tag: added[added.length - 1],
296
+ tag: added.at(-1) ?? null,
297
297
  files: written,
298
298
  warnings,
299
299
  unreadable,
@@ -27,8 +27,8 @@ function applyEnvOverrides(config, declaredDefault) {
27
27
  delete config.connectors.default;
28
28
  return config;
29
29
  }
30
- if (database && database.family === "sql" && database.dialect) {
31
- const target = config.connectors.default ? "default" : Object.keys(config.connectors)[0];
30
+ if (database?.family === "sql" && database.dialect) {
31
+ const target = Object.hasOwn(config.connectors, "default") ? "default" : Object.keys(config.connectors)[0];
32
32
  const connector = target ? config.connectors[target] : void 0;
33
33
  if (connector) {
34
34
  connector.dialect = database.dialect;
@@ -136,7 +136,7 @@ function userTableColumns(table, dialect) {
136
136
  const columns = dialect === "postgres" ? getTableConfig$1(table).columns : dialect === "mysql" ? getTableConfig$2(table).columns : getTableConfig(table).columns;
137
137
  return new Map(columns.map((column) => [column.name, {
138
138
  notNull: column.notNull,
139
- isUnique: column.isUnique ?? false,
139
+ isUnique: column.isUnique,
140
140
  primary: column.primary
141
141
  }]));
142
142
  }
@@ -109,7 +109,7 @@ function registerDrizzleFrameworkStores(dialect) {
109
109
  if (getTokenStoreFactory("drizzle")) return;
110
110
  registerTokenStore("drizzle", (ctx) => {
111
111
  const orm = resolveConnectedOrm(`tokenStore "drizzle"`, dialect);
112
- const days = ctx?.config?.tokenStore?.retentionRevokedDays;
112
+ const days = ctx.config.tokenStore.retentionRevokedDays;
113
113
  return DrizzleTokenStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
114
114
  });
115
115
  });
@@ -117,7 +117,7 @@ function registerDrizzleFrameworkStores(dialect) {
117
117
  if (getAuditStoreFactory("drizzle")) return;
118
118
  registerAuditStore("drizzle", (ctx) => {
119
119
  const orm = resolveConnectedOrm(`audit.store "drizzle"`, dialect);
120
- const days = ctx?.config?.audit?.retentionDays;
120
+ const days = ctx.config.audit.retentionDays;
121
121
  return DrizzleAuditStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
122
122
  });
123
123
  });
@@ -82,7 +82,7 @@ var DrizzleService = class extends Service {
82
82
  */
83
83
  #watchers = null;
84
84
  constructor(module) {
85
- super(serviceName, module.container, module.notificationsCenter, module.options ?? {});
85
+ super(serviceName, module.container, module.notificationsCenter, module.options);
86
86
  this.module = module;
87
87
  this.module.hookKernel("onBoot", async () => {
88
88
  queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
@@ -96,11 +96,25 @@ var DrizzleService = class extends Service {
96
96
  await this.disconnectAll().catch(() => {});
97
97
  });
98
98
  }
99
- /** Config validée (Zod) exposée par le Module (`this.module.config`). */
99
+ /**
100
+ * Config du module telle que le service la TROUVE.
101
+ *
102
+ * Validée par Zod au boot du kernel ; mais le service se construit aussi hors
103
+ * kernel (bancs, usage direct) avec une config partielle, voire absente — et
104
+ * « config absente → rien à connecter » est un contrat couvert par
105
+ * `DrizzleService.test.ts`. Le type le dit, donc chaque lecture garde.
106
+ */
100
107
  #config() {
101
108
  return this.module.config;
102
109
  }
103
110
  /**
111
+ * Config VALIDÉE, pour les lecteurs de migrations du plan d'administration :
112
+ * seul un kernel les appelle, et un kernel a validé la config au boot.
113
+ */
114
+ #validatedConfig() {
115
+ return this.module.config;
116
+ }
117
+ /**
104
118
  * Connecte tous les connecteurs déclarés en config (validée Zod).
105
119
  *
106
120
  * @param connect - `false` pour ENREGISTRER les ORM sans ouvrir de connexion.
@@ -127,7 +141,7 @@ var DrizzleService = class extends Service {
127
141
  /** Connecte un connecteur (crée le dossier de la base SQLite si nécessaire). */
128
142
  async #connectOne(name, cfg, connect = true) {
129
143
  const dialect = cfg.dialect ?? "sqlite";
130
- const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), connectorVersionsMigrations(name, appMigrationsDir(this.kernel, this.#config().migrations?.dir ?? "migrations")), cfg.filename === MEMORY_DATABASE);
144
+ const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), connectorVersionsMigrations(name, appMigrationsDir(this.kernel, this.#config()?.migrations?.dir ?? "migrations")), cfg.filename === MEMORY_DATABASE);
131
145
  let filename;
132
146
  if (dialect === "sqlite") {
133
147
  filename = cfg.filename ?? this.#defaultFilename(name);
@@ -141,17 +155,17 @@ var DrizzleService = class extends Service {
141
155
  deriveSchema: ddl === "auto",
142
156
  migrationStatus: async () => {
143
157
  const { migrationStatusFor } = await import("../src/migrator/status.js");
144
- const result = await migrationStatusFor(name, this.#config(), this.kernel);
158
+ const result = await migrationStatusFor(name, this.#validatedConfig(), this.kernel);
145
159
  return result.ok ? result.report : result.failure;
146
160
  },
147
161
  migrationPlan: async () => {
148
162
  const { migrationPlanFor } = await import("../src/migrator/status.js");
149
- const result = await migrationPlanFor(name, this.#config(), this.kernel);
163
+ const result = await migrationPlanFor(name, this.#validatedConfig(), this.kernel);
150
164
  return result.ok ? result.plan : result.failure;
151
165
  },
152
166
  applyMigrations: async () => {
153
167
  const { applyMigrationsFor } = await import("../src/migrator/status.js");
154
- const result = await applyMigrationsFor(name, this.#config(), this.kernel);
168
+ const result = await applyMigrationsFor(name, this.#validatedConfig(), this.kernel);
155
169
  return result.ok ? result.run : result.failure;
156
170
  }
157
171
  });
@@ -195,9 +209,9 @@ var DrizzleService = class extends Service {
195
209
  const kernel = this.kernel;
196
210
  const config = this.#config();
197
211
  const env = readMigrationEnv(kernel);
198
- const check = resolveCheckMode(config.migrations?.check, env);
212
+ const check = resolveCheckMode(config?.migrations?.check, env);
199
213
  const adviseOnly = ddl === "auto";
200
- const appDir = appMigrationsDir(kernel, config.migrations?.dir ?? "migrations");
214
+ const appDir = appMigrationsDir(kernel, config?.migrations?.dir ?? "migrations");
201
215
  const hasMigrations = ownsSharedMigrations(name) || connectorVersionsMigrations(name, appDir);
202
216
  if (adviseOnly && (!hasMigrations || !adviseMigrations(check, env, filename))) return;
203
217
  if (!hasMigrations) this.log(`Drizzle « ${name} » : \`ddl: "migrate"\` sans migration à appliquer — « ${name} » n'est pas le connecteur du framework (« ${FRAMEWORK_CONNECTOR} ») et aucune migration n'est versionnée sous son dossier. Son schéma n'est ni dérivé ni migré par le framework : \`nodefony orm:generate --connector ${name}\` écrit sa première migration.`, "WARNING");
@@ -207,14 +221,14 @@ var DrizzleService = class extends Service {
207
221
  url: cfg.url
208
222
  };
209
223
  const sources = await defaultMigrationSources(appDir, {
210
- framework: config.frameworkEntities !== false,
224
+ framework: config?.frameworkEntities !== false,
211
225
  connector: name
212
226
  });
213
227
  const migrator = new DrizzleMigrator({
214
228
  connector: name,
215
229
  ...target,
216
230
  sources,
217
- lockTimeoutMs: config.migrations?.lockTimeoutMs
231
+ lockTimeoutMs: config?.migrations?.lockTimeoutMs
218
232
  });
219
233
  if (adviseOnly) {
220
234
  await this.#publishReadiness(name, migrator, ddl, "warn");
@@ -256,7 +270,7 @@ var DrizzleService = class extends Service {
256
270
  if (!kernel) return;
257
271
  try {
258
272
  const plan = await migrator.status();
259
- const mode = this.#config().migrations?.divergence ?? "report";
273
+ const mode = this.#config()?.migrations?.divergence ?? "report";
260
274
  const report = buildReport(plan, {
261
275
  ddl,
262
276
  divergence: mode === "off" ? null : await describeDivergence(plan),
@@ -281,7 +295,7 @@ var DrizzleService = class extends Service {
281
295
  */
282
296
  #watch(name, migrator, ddl, check) {
283
297
  if (check === "off") return;
284
- if (this.#watchers === null) this.#watchers = /* @__PURE__ */ new Map();
298
+ this.#watchers ??= /* @__PURE__ */ new Map();
285
299
  const timer = setInterval(() => {
286
300
  this.#publishReadiness(name, migrator, ddl, check).catch(() => void 0);
287
301
  }, READINESS_POLL_MS);
@@ -145,7 +145,7 @@ var DrizzleAuditStore = class DrizzleAuditStore {
145
145
  const hasNext = rows.length > limit;
146
146
  const pageRows = hasNext ? rows.slice(0, limit) : rows;
147
147
  const items = pageRows.map((row) => this.#toEvent(row));
148
- const last = pageRows[pageRows.length - 1];
148
+ const last = pageRows.at(-1);
149
149
  return {
150
150
  items,
151
151
  limit,
@@ -45,7 +45,7 @@ function defaultConnectorFilename(kernel, name) {
45
45
  * @returns les coordonnées, avec le fichier SQLite TOUJOURS résolu.
46
46
  */
47
47
  function resolveConnectorTarget(kernel, name, cfg) {
48
- const dialect = cfg.dialect ?? "sqlite";
48
+ const dialect = cfg.dialect;
49
49
  if (dialect !== "sqlite") return {
50
50
  dialect,
51
51
  url: cfg.url
@@ -376,7 +376,7 @@ var DrizzleMigrator = class {
376
376
  * @throws MigrationVerdictError portant le verdict structuré.
377
377
  */
378
378
  #assertApplicable(plan, files, options) {
379
- const first = plan.failed[0];
379
+ const first = plan.failed.at(0);
380
380
  if (first) throw this.#verdict("NF_MIGRATE_FAILED_MARKER", {
381
381
  source: first.source,
382
382
  tag: first.tag,
@@ -386,7 +386,7 @@ var DrizzleMigrator = class {
386
386
  },
387
387
  actions: this.#recoveryActions()
388
388
  }, `La migration « ${first.tag} » (source « ${first.source} ») a échoué ou n'a jamais fini. Inspecter la base, puis réparer avant de reprendre — une reprise aveugle n'est jamais sûre.\n\n${REPRISE_SANS_DESTRUCTION}`);
389
- const drift = plan.drifted[0];
389
+ const drift = plan.drifted.at(0);
390
390
  if (drift) throw this.#verdict("NF_MIGRATE_HASH_MISMATCH", {
391
391
  source: drift.source,
392
392
  tag: drift.tag,
@@ -411,7 +411,7 @@ var DrizzleMigrator = class {
411
411
  ]
412
412
  }]
413
413
  }, `Le fichier de la migration « ${drift.tag} » (source « ${drift.source} ») a changé depuis son application. Une migration déjà jouée est immuable : écrire une NOUVELLE migration, ou assumer le ré-alignement.`);
414
- const gone = plan.missing[0];
414
+ const gone = plan.missing.at(0);
415
415
  if (gone && options.ignoreMissing !== true) throw this.#verdict("NF_MIGRATE_MISSING_FILE", {
416
416
  source: gone.source,
417
417
  tag: gone.tag,
@@ -113,7 +113,7 @@ async function snapshotTables(outDir) {
113
113
  try {
114
114
  const raw = await fs.readFile(path.join(outDir, "meta", "0000_snapshot.json"), "utf8");
115
115
  const snapshot = JSON.parse(raw);
116
- return Object.values(snapshot.tables ?? {}).map((t, i) => t.name ?? Object.keys(snapshot.tables ?? {})[i] ?? "");
116
+ return Object.entries(snapshot.tables ?? {}).map(([key, t]) => t.name ?? key);
117
117
  } catch {
118
118
  return [];
119
119
  }
@@ -103,8 +103,7 @@ function rebuiltTables(sql) {
103
103
  const out = /* @__PURE__ */ new Set();
104
104
  const renames = joint.matchAll(new RegExp(`\\bALTER\\s+TABLE\\s+${IDENT}\\s+RENAME\\s+TO\\s+${IDENT}`, "gi"));
105
105
  for (const rename of renames) {
106
- const staging = rename[1];
107
- const target = rename[2];
106
+ const [, staging, target] = rename;
108
107
  if (staging === void 0 || target === void 0) continue;
109
108
  const created = new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"']?${staging}[\`"']?\\b`, "i");
110
109
  const copied = new RegExp(`\\bINSERT\\s+INTO\\s+[\`"']?${staging}[\`"']?[\\s\\S]{0,4000}?\\bFROM\\s+[\`"']?${target}[\`"']?\\b`, "i");
@@ -124,7 +123,7 @@ function rebuiltTables(sql) {
124
123
  * @returns les noms supprimés, en minuscules.
125
124
  */
126
125
  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());
126
+ return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
128
127
  }
129
128
  /**
130
129
  * L'instruction fait-elle partie de la mécanique d'une reconstruction ?
@@ -68,7 +68,7 @@ var MysqlMigrationDriver = class {
68
68
  }
69
69
  const cx = await createConnection(this.#url);
70
70
  cx.on("error", (error) => {
71
- this.#lost = error?.message ?? String(error);
71
+ this.#lost = error instanceof Error ? error.message : String(error);
72
72
  });
73
73
  this.#lost = null;
74
74
  this.#cxOrNull = cx;
@@ -56,7 +56,7 @@ var PostgresMigrationDriver = class {
56
56
  }
57
57
  const client = new ClientCtor({ connectionString: this.#url });
58
58
  client.on("error", (error) => {
59
- this.#lost = error?.message ?? String(error);
59
+ this.#lost = error instanceof Error ? error.message : String(error);
60
60
  });
61
61
  await client.connect();
62
62
  this.#lost = null;
@@ -309,18 +309,15 @@ function summaryOf(plan, verdict, divergence = null) {
309
309
  }
310
310
  case "drift": {
311
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
- }
312
+ const [d] = plan.drifted;
313
+ if (d !== void 0) parts.push(`le fichier « ${d.source}/${d.tag} » a été modifié APRÈS avoir été appliqué (son empreinte a changé)`);
314
+ const [m] = plan.missing;
315
+ if (m !== void 0) parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
320
316
  return `Le connecteur « ${c} » ne concorde plus avec son historique : ${parts.join(" ; ")}.`;
321
317
  }
322
318
  case "failed": {
323
- const f = plan.failed[0];
319
+ const [f] = plan.failed;
320
+ if (f === void 0) return `Le connecteur « ${c} » porte une migration qui n'a pas abouti.`;
324
321
  const when = f.startedAt ? new Date(f.startedAt).toISOString() : "à une date inconnue";
325
322
  const why = f.error ? ` (erreur : ${f.error})` : "";
326
323
  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}.`;
@@ -516,7 +513,7 @@ function describeDiscovery(facts, style) {
516
513
  */
517
514
  function renderRefusal(verdict, message, style, ddl) {
518
515
  const enMode = ddl ? refusalInMode(verdict.code, ddl, verdict.connector) : null;
519
- const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code] ?? "";
516
+ const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code];
520
517
  const actions = enMode?.actions ?? verdict.nextActions;
521
518
  return `${style.red(style.bold("Migration refusée"))} ${style.dim(`[${verdict.code}]`)}\n\n` + renderBlocks(style, message, meaning, actions);
522
519
  }
@@ -98,8 +98,8 @@ async function readHistory(driver) {
98
98
  finishedAt: row.finished_at === null ? null : Number(row.finished_at),
99
99
  executionMs: row.execution_ms === null ? null : Number(row.execution_ms),
100
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)
101
+ error: toNullableText(row.error),
102
+ appliedBy: toNullableText(row.applied_by)
103
103
  }));
104
104
  }
105
105
  /**
@@ -194,6 +194,29 @@ async function forgetEntries(driver, entries) {
194
194
  return removed;
195
195
  }
196
196
  /**
197
+ * Décode une colonne texte NULLABLE de l'historique.
198
+ *
199
+ * Une colonne texte rend une chaîne ; un `Buffer` (collation binaire MySQL) est
200
+ * lu en UTF-8, comme le faisait `String()`. Un objet inattendu est sérialisé en
201
+ * JSON plutôt que réduit à `[object Object]`, qui effacerait la cause.
202
+ *
203
+ * @param value - valeur brute rendue par le driver.
204
+ * @returns le texte, ou `null` si la colonne est nulle.
205
+ */
206
+ function toNullableText(value) {
207
+ if (value === null) return null;
208
+ switch (typeof value) {
209
+ case "string": return value;
210
+ case "number":
211
+ case "bigint":
212
+ case "boolean":
213
+ case "undefined": return String(value);
214
+ case "symbol": return value.toString();
215
+ case "function": return "function";
216
+ default: return Buffer.isBuffer(value) ? value.toString("utf8") : JSON.stringify(value);
217
+ }
218
+ }
219
+ /**
197
220
  * Normalise un booléen tel que le rend la base.
198
221
  *
199
222
  * PostgreSQL rend `true`, SQLite et MySQL rendent `1` — et un `1` textuel
@@ -69,7 +69,7 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
69
69
  cwd,
70
70
  encoding: "utf8"
71
71
  });
72
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
72
+ const output = toolOutput(result);
73
73
  if (result.status !== 0 || !generationHappened(output)) {
74
74
  const awaited = topLevelAwaitFailure(output);
75
75
  if (awaited !== null) throw new MigrationToolError(generationTopLevelAwait(label, awaited.file, awaited.line, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
@@ -107,7 +107,7 @@ function runIntrospect({ cwd, configRel, label }) {
107
107
  cwd,
108
108
  encoding: "utf8"
109
109
  });
110
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
110
+ const output = toolOutput(result);
111
111
  if (result.status !== 0) throw new MigrationToolError(introspectFailed(label, result.status, output));
112
112
  return output;
113
113
  }
@@ -228,9 +228,9 @@ const BLOCKING_PATTERNS = [
228
228
  id: "colonne-neuve-puis-index-unique",
229
229
  pattern: /\bCREATE\s+UNIQUE\s+INDEX\b/i,
230
230
  detect: (code) => {
231
- const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].map((m) => m[1].toLowerCase());
231
+ const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
232
232
  if (added.length === 0) return false;
233
- 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)));
233
+ return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some(([, columns = ""]) => columns.split(",").map((c) => c.trim().replace(/[`"']/g, "").toLowerCase()).some((c) => added.includes(c)));
234
234
  },
235
235
  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",
236
236
  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"
@@ -287,11 +287,11 @@ function isInteractivePromptFailure(output) {
287
287
  */
288
288
  function topLevelAwaitFailure(output) {
289
289
  const plain = output.replace(/\u001B\[[0-9;]*m/g, "");
290
- const m = /(?:^|\n)[^\S\n]*(\S[^\n]*?\.[cm]?[jt]sx?):(\d+):\d+: ERROR: Top-level await is currently not supported with the \\?"cjs\\?" output format/u.exec(plain);
291
- return m ? {
292
- file: m[1],
293
- line: Number(m[2])
294
- } : null;
290
+ const [, file, line] = /(?:^|\n)[^\S\n]*(\S[^\n]*?\.[cm]?[jt]sx?):(\d+):\d+: ERROR: Top-level await is currently not supported with the \\?"cjs\\?" output format/u.exec(plain) ?? [];
291
+ return file === void 0 ? null : {
292
+ file,
293
+ line: Number(line)
294
+ };
295
295
  }
296
296
  /**
297
297
  * Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
@@ -316,5 +316,19 @@ function stampFormatMarker(dir) {
316
316
  }
317
317
  return stamped;
318
318
  }
319
+ /**
320
+ * Sortie complète (standard puis erreur) d'un `spawnSync`.
321
+ *
322
+ * Malgré leur type, `stdout` et `stderr` valent `null` quand le processus n'a
323
+ * pas pu démarrer (`ENOENT`, `EACCES`) : Node les tire alors d'un `output` nul.
324
+ *
325
+ * @param result - le retour de `spawnSync` en mode `encoding: "utf8"`.
326
+ * @returns les deux flux concaténés, vide si rien n'a été écrit.
327
+ */
328
+ function toolOutput(result) {
329
+ const stdout = result.stdout;
330
+ const stderr = result.stderr;
331
+ return `${stdout ?? ""}${stderr ?? ""}`;
332
+ }
319
333
  //#endregion
320
334
  export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker, topLevelAwaitFailure };
@@ -58,7 +58,6 @@ function repairRebuildCopy(sql, columnsOf) {
58
58
  const groups = args[args.length - 1];
59
59
  if (groups === void 0) return match;
60
60
  const { head, into, middle, select, tail, source } = groups;
61
- if (head === void 0 || into === void 0 || middle === void 0 || select === void 0 || tail === void 0 || source === void 0) return match;
62
61
  const known = columnsOf(source);
63
62
  if (known === null) return match;
64
63
  const intoColumns = splitColumns(into);
@@ -68,13 +67,14 @@ function repairRebuildCopy(sql, columnsOf) {
68
67
  const keptInto = [];
69
68
  const keptSelect = [];
70
69
  const removed = [];
71
- for (let i = 0; i < intoColumns.length; i++) {
72
- const target = bareName(intoColumns[i]);
73
- const read = bareName(selectColumns[i]);
70
+ for (const [i, intoColumn] of intoColumns.entries()) {
71
+ const selectColumn = selectColumns[i] ?? "";
72
+ const target = bareName(intoColumn);
73
+ const read = bareName(selectColumn);
74
74
  if (target === null || read === null) return match;
75
75
  if (carried.has(read)) {
76
- keptInto.push(intoColumns[i]);
77
- keptSelect.push(selectColumns[i]);
76
+ keptInto.push(intoColumn);
77
+ keptSelect.push(selectColumn);
78
78
  } else removed.push(`${source}.${read}`);
79
79
  }
80
80
  if (removed.length === 0) return match;
@@ -224,10 +224,10 @@ function describeOwner(name) {
224
224
  driver: ""
225
225
  };
226
226
  }
227
- const klass = orm?.constructor?.name;
227
+ const klass = orm.constructor?.name;
228
228
  let driver = "";
229
229
  try {
230
- driver = orm?.describeConnection?.().driver ?? "";
230
+ driver = orm.describeConnection?.().driver ?? "";
231
231
  } catch {
232
232
  driver = "";
233
233
  }
@@ -268,7 +268,7 @@ const SQL_DRIVERS = /* @__PURE__ */ new Set([
268
268
  */
269
269
  function knownConnectors(config) {
270
270
  const names = new Set(ormRegistry.list());
271
- for (const name of Object.keys(config.connectors ?? {})) names.add(name);
271
+ for (const name of Object.keys(config.connectors)) names.add(name);
272
272
  return [...names].sort();
273
273
  }
274
274
  /**
@@ -292,7 +292,7 @@ function knownConnectors(config) {
292
292
  * @returns la réponse, discriminée par `kind`.
293
293
  */
294
294
  function resolveConnector(connector, config, env, kernel, options = {}) {
295
- const declared = config.connectors?.[connector];
295
+ const declared = config.connectors[connector];
296
296
  if (!declared) {
297
297
  if (ormRegistry.has(connector)) {
298
298
  const owner = describeOwner(connector);
@@ -71,7 +71,8 @@ async function readJournal(dir, source) {
71
71
  }
72
72
  let journal;
73
73
  try {
74
- journal = JSON.parse(raw);
74
+ const parsed = JSON.parse(raw);
75
+ journal = typeof parsed === "object" && parsed !== null ? parsed : {};
75
76
  } catch (e) {
76
77
  throw new MigrationVerdictError({
77
78
  code: "NF_MIGRATE_UNKNOWN_FORMAT",
@@ -91,21 +92,25 @@ async function readJournal(dir, source) {
91
92
  facts: { file },
92
93
  nextActions: []
93
94
  }, `Le journal des migrations « ${file} » ne porte pas de liste « entries » : ce n'est pas un journal que ce format sait lire.`);
94
- if (!SUPPORTED_JOURNAL_VERSIONS.includes(String(journal.version))) throw new MigrationVerdictError({
95
+ const version = String(journal.version);
96
+ if (!SUPPORTED_JOURNAL_VERSIONS.includes(version)) throw new MigrationVerdictError({
95
97
  code: "NF_MIGRATE_UNKNOWN_FORMAT",
96
98
  connector: "",
97
99
  source,
98
100
  facts: {
99
101
  file,
100
- journalVersion: String(journal.version),
102
+ journalVersion: version,
101
103
  supported: SUPPORTED_JOURNAL_VERSIONS
102
104
  },
103
105
  nextActions: [{
104
106
  command: "npm update @nodefony/drizzle",
105
107
  args: ["update", "@nodefony/drizzle"]
106
108
  }]
107
- }, `Le journal de migrations de la source « ${source} » est en version ${String(journal.version)}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
108
- return journal;
109
+ }, `Le journal de migrations de la source « ${source} » est en version ${version}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
110
+ return {
111
+ version,
112
+ entries: journal.entries
113
+ };
109
114
  }
110
115
  /**
111
116
  * Lit un fichier de migration, valide son format et découpe ses statements.
@@ -370,8 +375,8 @@ function scanLine(line, needle, grammar, curseur) {
370
375
  */
371
376
  function delimiteurDollar(line, debut) {
372
377
  let i = debut + 1;
373
- while (i < line.length && /[A-Za-z0-9_]/.test(line[i])) {
374
- if (i === debut + 1 && /[0-9]/.test(line[i])) return null;
378
+ while (i < line.length && /[A-Za-z0-9_]/.test(line.charAt(i))) {
379
+ if (i === debut + 1 && /[0-9]/.test(line.charAt(i))) return null;
375
380
  i += 1;
376
381
  }
377
382
  return line[i] === "$" ? line.slice(debut, i + 1) : null;
@@ -390,7 +395,7 @@ function delimiteurDollar(line, debut) {
390
395
  function createdTables(files) {
391
396
  const found = /* @__PURE__ */ new Set();
392
397
  const pattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z0-9_$]+)[`"']?/gi;
393
- for (const file of files) for (const statement of file.statements) for (const match of statement.matchAll(pattern)) found.add(match[1]);
398
+ for (const file of files) for (const statement of file.statements) for (const [, table] of statement.matchAll(pattern)) if (table !== void 0) found.add(table);
394
399
  return [...found];
395
400
  }
396
401
  /**