@nodefony/drizzle 10.0.0-alpha.7 → 10.0.0-alpha.9

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 (61) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +11 -7
  4. package/dist/nodefony/command/migrateShared.js +17 -1
  5. package/dist/nodefony/command/orm-generate.js +81 -13
  6. package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
  7. package/dist/nodefony/command/orm-reset.js +1 -0
  8. package/dist/nodefony/config/defineModuleConfig.js +21 -5
  9. package/dist/nodefony/entity/userTable.js +3 -0
  10. package/dist/nodefony/registerStores.js +2 -26
  11. package/dist/nodefony/service/DrizzleService.js +19 -7
  12. package/dist/nodefony/src/DrizzleAuditStore.js +13 -1
  13. package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
  14. package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
  15. package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
  16. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
  17. package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
  18. package/dist/nodefony/src/SessionStorage.js +8 -0
  19. package/dist/nodefony/src/frameworkConnector.js +29 -0
  20. package/dist/nodefony/src/memoryDatabase.js +13 -0
  21. package/dist/nodefony/src/migrator/DrizzleMigrator.js +17 -1
  22. package/dist/nodefony/src/migrator/appSchema.js +44 -1
  23. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
  24. package/dist/nodefony/src/migrator/explain.js +42 -1
  25. package/dist/nodefony/src/migrator/kit.js +23 -2
  26. package/dist/nodefony/src/migrator/paths.js +55 -1
  27. package/dist/nodefony/src/migrator/refusals.js +50 -1
  28. package/dist/nodefony/src/migrator/resolve.js +126 -6
  29. package/dist/nodefony/src/orm-core/DrizzleOrm.js +123 -22
  30. package/dist/nodefony/src/orm-core/ddlPlan.js +95 -0
  31. package/dist/nodefony/src/orm-core/index.js +2 -1
  32. package/dist/nodefony/src/orm-core/seedRow.js +65 -0
  33. package/dist/nodefony/src/safeTarget.js +1 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
  36. package/dist/types/nodefony/command/orm-generate.d.ts +12 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
  38. package/dist/types/nodefony/entity/userTable.d.ts +46 -3
  39. package/dist/types/nodefony/registerStores.d.ts +4 -3
  40. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
  41. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
  42. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
  43. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
  44. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
  45. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
  46. package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
  47. package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
  48. package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
  49. package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
  50. package/dist/types/nodefony/src/migrator/explain.d.ts +19 -0
  51. package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
  52. package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
  53. package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
  54. package/dist/types/nodefony/src/migrator/resolve.d.ts +97 -1
  55. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +11 -0
  56. package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +96 -0
  57. package/dist/types/nodefony/src/orm-core/index.d.ts +1 -0
  58. package/dist/types/nodefony/src/orm-core/seedRow.d.ts +23 -0
  59. package/docs/index.md +28 -25
  60. package/docs/migrations.md +69 -4
  61. package/package.json +19 -20
@@ -40,6 +40,16 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
40
40
  get location() {
41
41
  return this.#location;
42
42
  }
43
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleTotpSecretStore.from}. */
44
+ #connector;
45
+ /**
46
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
47
+ * registre des stores : la console rattache la brique à SON connecteur au
48
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
49
+ */
50
+ get connector() {
51
+ return this.#connector;
52
+ }
43
53
  /**
44
54
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
45
55
  * (`registerTotpSecretEntity`) doit avoir été enregistrée **avant** `orm.connect()`.
@@ -47,7 +57,9 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
47
57
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
48
58
  */
49
59
  static from(orm) {
50
- return new DrizzleTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY), orm.location);
60
+ const store = new DrizzleTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY), orm.location);
61
+ store.#connector = orm.name;
62
+ return store;
51
63
  }
52
64
  /** Row plate → secret du contrat (recoveryCodes copié = mutable indépendant). */
53
65
  #toSecret(row) {
@@ -39,6 +39,16 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
39
39
  get location() {
40
40
  return this.#location;
41
41
  }
42
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebAuthnCredentialStore.from}. */
43
+ #connector;
44
+ /**
45
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
46
+ * registre des stores : la console rattache la brique à SON connecteur au
47
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
48
+ */
49
+ get connector() {
50
+ return this.#connector;
51
+ }
42
52
  /**
43
53
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
44
54
  * (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
@@ -47,7 +57,9 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
47
57
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
48
58
  */
49
59
  static from(orm) {
50
- return new DrizzleWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY), orm.location);
60
+ const store = new DrizzleWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY), orm.location);
61
+ store.#connector = orm.name;
62
+ return store;
51
63
  }
52
64
  /** Row plate → credential du contrat (`nickname?` omis si `null`). */
53
65
  #toCredential(row) {
@@ -59,6 +59,16 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
59
59
  get location() {
60
60
  return this.#location;
61
61
  }
62
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebhookStore.from}. */
63
+ #connector;
64
+ /**
65
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
66
+ * registre des stores : la console rattache la brique à SON connecteur au
67
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
68
+ */
69
+ get connector() {
70
+ return this.#connector;
71
+ }
62
72
  /**
63
73
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
64
74
  * (`registerWebhookEndpointEntity`) doit avoir été enregistrée **avant**
@@ -67,7 +77,9 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
67
77
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
68
78
  */
69
79
  static from(orm) {
70
- return new DrizzleWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), orm.location, orm.getNativeConnection(), orm.dialect);
80
+ const store = new DrizzleWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), orm.location, orm.getNativeConnection(), orm.dialect);
81
+ store.#connector = orm.name;
82
+ return store;
71
83
  }
72
84
  /** Row plate → endpoint du contrat (`events` mutable accepté en `readonly`). */
73
85
  #toEndpoint(row) {
@@ -41,6 +41,14 @@ var SessionStorage = class SessionStorage {
41
41
  return typeof loc === "string" && loc.length > 0 ? loc : void 0;
42
42
  }
43
43
  /**
44
+ * Connecteur ORM qui porte les sessions — lu par `readStoreConnector` pour le
45
+ * registre des stores. `undefined` tant que l'ORM de ce connecteur n'est pas
46
+ * enregistré : on ne publie pas un rattachement que rien ne porte.
47
+ */
48
+ get connector() {
49
+ return ormRegistry.has("default") ? SESSION_CONNECTOR : void 0;
50
+ }
51
+ /**
44
52
  * Repository de l'entité session, ou `null` si l'ORM n'est pas (ou plus)
45
53
  * connecté.
46
54
  *
@@ -0,0 +1,29 @@
1
+ //#region nodefony/src/frameworkConnector.ts
2
+ /**
3
+ * Connecteur conventionnel qui héberge le schéma framework (même convention que
4
+ * `SESSION_CONNECTOR` : `"default"` pour Drizzle, `"nodefony"` pour Mongoose).
5
+ *
6
+ * Vit dans ce fichier sans dépendance parce que DEUX familles le lisent : le
7
+ * registre des stores (où résoudre les briques du framework) et les migrations
8
+ * (qui les reçoit). Une copie du littéral de chaque côté dériverait en silence.
9
+ */
10
+ const FRAMEWORK_CONNECTOR = "default";
11
+ /**
12
+ * Ce connecteur possède-t-il les migrations du framework et de l'application ?
13
+ *
14
+ * **Seul le connecteur du framework les possède.** Le dossier des migrations de
15
+ * l'application (`migrations/<dialect>`) ne porte aucune notion de connecteur :
16
+ * il décrit UNE base, celle où vivent les briques du framework et les entités
17
+ * de l'application. Les attribuer à tout connecteur faisait qu'une base
18
+ * secondaire (analyse, base d'un module) en mode `migrate` y créait des tables
19
+ * qui ne lui appartiennent pas, et qu'en mode `auto` elle annonçait à chaque
20
+ * démarrage des migrations qui ne la concernent pas.
21
+ *
22
+ * @param connector - nom du connecteur (clé de `connectors`).
23
+ * @returns `true` pour le connecteur du framework, `false` pour tout autre.
24
+ */
25
+ function ownsSharedMigrations(connector) {
26
+ return connector === FRAMEWORK_CONNECTOR;
27
+ }
28
+ //#endregion
29
+ export { FRAMEWORK_CONNECTOR, ownsSharedMigrations };
@@ -0,0 +1,13 @@
1
+ //#region nodefony/src/memoryDatabase.ts
2
+ /**
3
+ * Nom de fichier d'une base SQLite en MÉMOIRE — volatile, elle repart vide à
4
+ * chaque démarrage.
5
+ *
6
+ * Module FEUILLE (aucun import) : l'adapter, le migrateur, les commandes et le
7
+ * service le consultent tous, et le placer dans l'un d'eux ferait importer le
8
+ * migrateur par l'adapter. Une seule écriture du littéral : c'est d'elle que
9
+ * dépend la règle « une base volatile dérive son schéma » (`resolveDdlMode`).
10
+ */
11
+ const MEMORY_DATABASE = ":memory:";
12
+ //#endregion
13
+ export { MEMORY_DATABASE };
@@ -1,4 +1,5 @@
1
1
  import { MIGRATE_URL_ENV, MigrationLockTimeoutError, MigrationVerdictError } from "./types.js";
2
+ import { alreadyInTargetState } from "./explain.js";
2
3
  import { openMigrationDriver } from "./drivers/index.js";
3
4
  import { deleteFailed, ensureHistorySchema, finishHistory, forgetEntries, insertHistory, readHistory } from "./history.js";
4
5
  import "./paths.js";
@@ -51,6 +52,21 @@ const ERROR_MAX_LENGTH = 2e3;
51
52
  * celle qu'il trouve seul est de recréer la base, ce qui emporte les données.
52
53
  */
53
54
  const REPRISE_SANS_DESTRUCTION = "REPRENDRE ne demande PAS de recréer la base. Deux voies, selon ce qui a échoué : corriger le fichier de migration puis lever le marqueur (« orm:migrate:repair ») et reprendre — un fichier corrigé après un échec est rejoué tel quel, son empreinte n'est pas comparée ; ou, si la mise au point demande plusieurs essais, la faire sur une base d'essai en posant « " + MIGRATE_URL_ENV + " », qui détourne la commande vers une AUTRE base et laisse celle-ci intacte — sous PowerShell, poser la variable s'écrit « $env:" + MIGRATE_URL_ENV + " = \"…\" », « env » n'y existant pas. Écrire la migration suivante est toujours préférable à défaire ce qui est déjà appliqué.";
55
+ /**
56
+ * La troisième voie de reprise — celle qu'aucun message ne nommait.
57
+ *
58
+ * Les deux voies écrites jusqu'ici (corriger le fichier, ou travailler sur une
59
+ * base d'essai) supposent toutes deux que le fichier a tort. Quand le moteur
60
+ * répond « c'est déjà ainsi », ni l'une ni l'autre ne mène quelque part : le
61
+ * SQL est juste, la base est conforme, et rejouer redira la même chose. Sans
62
+ * cette phrase, la boucle `repair` → `migrate` → même refus n'a pas de sortie
63
+ * — et la seule qu'un agent trouve seul est de recréer la base.
64
+ *
65
+ * @param source - source de la migration (`app` ou `framework`).
66
+ * @param tag - identité de la migration en échec.
67
+ * @returns le paragraphe à insérer dans le verdict.
68
+ */
69
+ const alreadyInTargetStateAdvice = (source, tag) => `Ce que dit ce message, c'est que la base est DÉJÀ dans l'état que cette migration vise : l'objet qu'elle crée existe, ou celui qu'elle retire n'a jamais existé sous ce nom. Corriger le fichier ne servira donc à rien, et le rejouer redira la même chose — c'est la boucle dans laquelle on tourne si on s'obstine. VÉRIFIE d'abord l'état réel (« nodefony inspect entities --json », ou le schéma de la table concernée) ; s'il correspond bien à ce que « ${source}/${tag} » voulait obtenir, la migration est SANS OBJET sur cette base : déclare-la appliquée plutôt que de la rejouer, avec « nodefony orm:migrate:baseline --up-to ${tag} ». Une cause courante en développement : la table a été créée par le schéma DÉRIVÉ du démarrage, qui pose les contraintes sans les nommer, là où une migration les nomme.`;
54
70
  var DrizzleMigrator = class {
55
71
  #options;
56
72
  #now;
@@ -592,7 +608,7 @@ var DrizzleMigrator = class {
592
608
  tag: file.tag,
593
609
  facts: { error: truncate(cause.message) },
594
610
  actions: this.#recoveryActions()
595
- }, `La migration « ${file.tag} » (source « ${file.source} ») a échoué : ${truncate(cause.message)}\n\n${state}\n\nSon échec est INSCRIT : le prochain passage refusera de reprendre tant que le marqueur n'aura pas été levé, après inspection.\n\n${REPRISE_SANS_DESTRUCTION}`);
611
+ }, `La migration « ${file.tag} » (source « ${file.source} ») a échoué : ${truncate(cause.message)}\n\n${state}\n\nSon échec est INSCRIT : le prochain passage refusera de reprendre tant que le marqueur n'aura pas été levé, après inspection.\n\n` + (alreadyInTargetState(cause.message) ? `${alreadyInTargetStateAdvice(file.source, file.tag)}\n\n` : "") + REPRISE_SANS_DESTRUCTION);
596
612
  }
597
613
  /**
598
614
  * Les gestes qui sortent d'une migration en échec — **sans toucher à la base**.
@@ -1,4 +1,5 @@
1
1
  import { FORMAT_MARKER } from "./types.js";
2
+ import { ownsSharedMigrations } from "../frameworkConnector.js";
2
3
  import "./kit.js";
3
4
  import { registerHooks } from "node:module";
4
5
  import { entityRegistry } from "@nodefony/orm-core";
@@ -410,5 +411,47 @@ function registeredTables(connector) {
410
411
  }
411
412
  return out;
412
413
  }
414
+ /**
415
+ * Tables d'un connecteur parmi celles que les fichiers fournissent — le
416
+ * partage qui fait qu'une migration ne décrit que SA base.
417
+ *
418
+ * Une table appartient au connecteur de l'entité qui la déclare (le REGISTRE
419
+ * le sait, un fichier non). Une table qu'aucune entité ne réclame reste au
420
+ * connecteur du framework, comme avant que les connecteurs secondaires aient
421
+ * leurs migrations. Sans ce partage, la table d'une entité secondaire entrait
422
+ * AUSSI dans les migrations du framework : créée dans une base qui ne s'en sert
423
+ * jamais, et proposée à la suppression le jour où elle n'y figure plus.
424
+ *
425
+ * @param tables - tables fournies par les fichiers de l'application.
426
+ * @param connector - connecteur visé.
427
+ * @param owners - `table → connecteurs` qui la déclarent ; par défaut, le registre.
428
+ * @returns les tables de ce connecteur, dans l'ordre reçu.
429
+ */
430
+ function tablesOfConnector(tables, connector, owners = registeredOwners()) {
431
+ return tables.filter((t) => {
432
+ const claimed = owners.get(t.tableName);
433
+ return claimed === void 0 ? ownsSharedMigrations(connector) : claimed.has(connector);
434
+ });
435
+ }
436
+ /**
437
+ * Pour chaque table enregistrée, les connecteurs dont une entité la déclare.
438
+ *
439
+ * @returns la table de correspondance, lue dans le registre des entités.
440
+ */
441
+ function registeredOwners() {
442
+ const owners = /* @__PURE__ */ new Map();
443
+ for (const entity of entityRegistry.list()) {
444
+ const schema = entity.schema;
445
+ if (!is(schema, Table)) continue;
446
+ const table = getTableName(schema);
447
+ let set = owners.get(table);
448
+ if (set === void 0) {
449
+ set = /* @__PURE__ */ new Set();
450
+ owners.set(table, set);
451
+ }
452
+ set.add(entity.connector);
453
+ }
454
+ return owners;
455
+ }
413
456
  //#endregion
414
- export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
457
+ export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredOwners, registeredTables, tablesOfConnector, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
@@ -1,5 +1,6 @@
1
1
  import { HISTORY_TABLE } from "../types.js";
2
2
  import { schemaReader } from "../catalog.js";
3
+ import "../../memoryDatabase.js";
3
4
  //#region nodefony/src/migrator/drivers/sqliteDriver.ts
4
5
  /**
5
6
  * Pilote SQLite de l'applicateur — connexion `better-sqlite3` dédiée.
@@ -450,6 +450,8 @@ function renderStatus(report, style) {
450
450
  if (report.sources.length === 0) out += ` ${style.dim("aucune source de migrations")}\n`;
451
451
  if (report.divergence && !namesEverything(report.divergence)) out += renderDivergence(report.divergence, style);
452
452
  out += `\n${renderBlocks(style, report.summary, meaningOf(report.verdict), report.nextActions)}`;
453
+ if (report.exitCode === EXIT.actionRequired) out += `\n${style.dim("code de sortie 1 — une action est requise, ce n'est pas un échec.")}\n`;
454
+ else if (report.exitCode === EXIT.error) out += `\n${style.dim("code de sortie 2 — la commande n'a pas pu travailler.")}\n`;
453
455
  return out;
454
456
  }
455
457
  /** Combien d'écarts on déroule à l'écran avant de renvoyer au `--json`. */
@@ -561,5 +563,44 @@ const REFUSAL_MEANING = {
561
563
  NF_MIGRATE_JOURNAL_MISMATCH: "Le journal d'une source annonce une migration dont le fichier n'est pas dans le dossier. La source est incohérente avec elle-même : une copie incomplète, un fichier ignoré par le gestionnaire de versions, ou un paquet publié sans ses migrations. Rien n'a été appliqué — l'applicateur ne devine jamais le contenu d'un fichier absent.",
562
564
  NF_MIGRATE_UNKNOWN_SOURCE: "La source demandée n'est pas déclarée par cette application. Filtrer sur un nom inconnu ne touche aucune ligne et rend pourtant « rien à réparer » : le marqueur d'échec resterait en place, et la migration suivante échouerait pour la même raison."
563
565
  };
566
+ /**
567
+ * Les moteurs, chacun dans sa langue, disant « c'est DÉJÀ ainsi ».
568
+ *
569
+ * Reconnaître ce cas change le conseil du tout au tout : la migration n'a pas
570
+ * échoué parce que la base est cassée, mais parce qu'elle est **déjà dans
571
+ * l'état visé**. Le geste n'est alors ni de corriger le SQL ni de recréer la
572
+ * base — c'est de déclarer la migration appliquée.
573
+ */
574
+ const ALREADY_IN_TARGET_STATE = [
575
+ /\bno such (index|column|table|trigger|view)\b/iu,
576
+ /\b(table|index|trigger|view)\b.*\balready exists\b/iu,
577
+ /\bduplicate column name\b/iu,
578
+ /\balready exists\b/iu,
579
+ /\bdoes not exist\b/iu,
580
+ /\bduplicate key name\b/iu,
581
+ /\bcan't drop\b/iu,
582
+ /\bcheck that (column|it|the) .*exists\b/iu
583
+ ];
584
+ /**
585
+ * L'échec vient-il de ce que la base est DÉJÀ dans l'état que la migration vise ?
586
+ *
587
+ * Le cas est fréquent et sa sortie n'était nommée nulle part : le message de
588
+ * reprise proposait de corriger le fichier ou de travailler sur une base
589
+ * d'essai, deux voies qui ne mènent à rien quand le fichier est juste et la
590
+ * base déjà conforme. Un agent placé devant ce refus tourne alors en rond
591
+ * — `repair` lève le marqueur, `migrate` rejoue, le moteur redit la même
592
+ * chose — puis finit par recréer la base, ce qui emporte les données.
593
+ *
594
+ * Constaté sur un agent tiers : un `DROP INDEX messages_content_unique` sur une
595
+ * base dont l'unicité avait été posée INLINE par le DDL de démarrage, donc sans
596
+ * index nommé. Trente-cinq minutes, deux bases détruites, la boucle brisée à la
597
+ * main.
598
+ *
599
+ * @param message - le message d'erreur rendu par le moteur.
600
+ * @returns `true` si l'échec dit « c'est déjà ainsi ».
601
+ */
602
+ function alreadyInTargetState(message) {
603
+ return ALREADY_IN_TARGET_STATE.some((pattern) => pattern.test(message));
604
+ }
564
605
  //#endregion
565
- export { EXIT, MIGRATION_FORMAT_VERSION, action, buildReport, describeDiscovery, divergenceIsBlocking, exitCodeOf, isAheadOnly, meaningOf, refusalInMode, renderRefusal, renderStatus, styleFor, verdictOf };
606
+ export { EXIT, MIGRATION_FORMAT_VERSION, action, alreadyInTargetState, buildReport, describeDiscovery, divergenceIsBlocking, exitCodeOf, isAheadOnly, meaningOf, refusalInMode, renderRefusal, renderStatus, styleFor, verdictOf };
@@ -1,5 +1,5 @@
1
1
  import { FORMAT_MARKER } from "./types.js";
2
- import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed } from "./refusals.js";
2
+ import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, generationTopLevelAwait, introspectFailed } from "./refusals.js";
3
3
  import { droppedTables, rebuiltTables } from "./destructive.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
@@ -71,6 +71,8 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
71
71
  });
72
72
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
73
73
  if (result.status !== 0 || !generationHappened(output)) {
74
+ const awaited = topLevelAwaitFailure(output);
75
+ if (awaited !== null) throw new MigrationToolError(generationTopLevelAwait(label, awaited.file, awaited.line, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
74
76
  if (isInteractivePromptFailure(output)) throw new MigrationToolError(generationNeedsAnswer(label, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
75
77
  throw new MigrationToolError(generationFailed(label, result.status, output));
76
78
  }
@@ -273,6 +275,25 @@ function isInteractivePromptFailure(output) {
273
275
  return /Interactive prompts require a TTY/i.test(output);
274
276
  }
275
277
  /**
278
+ * Reconnaît le refus de l'outil devant un `await` de premier niveau.
279
+ *
280
+ * C'est l'esbuild de `drizzle-kit` qui le détecte, en transpilant le fichier en
281
+ * CommonJS : sa ligne d'erreur nomme le fichier et la position. On la lit plutôt
282
+ * que d'analyser nous-mêmes les sources — l'outil qui refuse est aussi celui qui
283
+ * sait exactement où, sans faux positif. Fonction PURE, éprouvée sans process.
284
+ *
285
+ * @param output - sortie complète de l'outil.
286
+ * @returns le fichier et la ligne, ou `null` si l'échec est d'une autre nature.
287
+ */
288
+ function topLevelAwaitFailure(output) {
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;
295
+ }
296
+ /**
276
297
  * Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
277
298
  *
278
299
  * Écrit en fins de ligne `\n` quel que soit le système : le dépôt et le gabarit
@@ -296,4 +317,4 @@ function stampFormatMarker(dir) {
296
317
  return stamped;
297
318
  }
298
319
  //#endregion
299
- export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker };
320
+ export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker, topLevelAwaitFailure };
@@ -1,3 +1,4 @@
1
+ import { ownsSharedMigrations } from "../frameworkConnector.js";
1
2
  import path from "node:path";
2
3
  import fs from "node:fs/promises";
3
4
  import { fileURLToPath } from "node:url";
@@ -15,6 +16,47 @@ const FRAMEWORK_RANK = 0;
15
16
  * celles des modules ; l'inverse n'arrive jamais.
16
17
  */
17
18
  const APP_RANK = 1e6;
19
+ /**
20
+ * Noms qui ne peuvent PAS désigner le dossier d'un connecteur secondaire : ce
21
+ * sont ceux des dossiers de dialecte (et du journal) que le connecteur du
22
+ * framework range déjà sous `migrations/`. Un connecteur `postgres` y lirait les
23
+ * migrations de `default` comme les siennes.
24
+ */
25
+ const RESERVED_MIGRATION_DIRS = [
26
+ "sqlite",
27
+ "postgres",
28
+ "mysql",
29
+ "meta"
30
+ ];
31
+ /**
32
+ * Le nom de ce connecteur collisionne-t-il avec un dossier de dialecte ?
33
+ *
34
+ * @param connector - nom du connecteur (clé de `connectors`).
35
+ * @returns `true` pour un connecteur SECONDAIRE nommé comme un dossier réservé.
36
+ */
37
+ function isReservedConnectorName(connector) {
38
+ return !ownsSharedMigrations(connector) && RESERVED_MIGRATION_DIRS.includes(connector);
39
+ }
40
+ /**
41
+ * Dossier des migrations PROPRES à un connecteur — la seule règle qui dit où
42
+ * elles vivent.
43
+ *
44
+ * Le connecteur du framework garde la racine (`migrations/<dialecte>`) : ses
45
+ * migrations décrivent la base du framework et des entités de l'application.
46
+ * Un connecteur secondaire reçoit son sous-dossier (`migrations/<connecteur>/
47
+ * <dialecte>`) : sa base ne reçoit que ce qui y est écrit, et celle du
48
+ * framework n'en voit rien — le dossier de dialecte qu'elle lit n'est pas le
49
+ * même.
50
+ *
51
+ * @param appDir - dossier de migrations de l'application (`appMigrationsDir`).
52
+ * @param connector - nom du connecteur.
53
+ * @returns le dossier parent des sous-dossiers de dialecte, ou `undefined` sans
54
+ * racine d'application, ou pour un nom réservé ({@link isReservedConnectorName}).
55
+ */
56
+ function connectorMigrationsDir(appDir, connector) {
57
+ if (appDir === void 0 || isReservedConnectorName(connector)) return;
58
+ return ownsSharedMigrations(connector) ? appDir : path.join(appDir, connector);
59
+ }
18
60
  /** Dossier de migrations, mémoïsé — la remontée ne se fait qu'une fois. */
19
61
  let cachedDir = null;
20
62
  /**
@@ -68,10 +110,22 @@ async function frameworkMigrationsDir() {
68
110
  *
69
111
  * @param appDir - dossier de migrations de l'application, s'il y en a un.
70
112
  * @param options.framework - `false` quand le module est data-only.
113
+ * @param options.connector - connecteur visé ; un connecteur secondaire ne
114
+ * reçoit que SES migrations ({@link connectorMigrationsDir}), jamais celles
115
+ * du framework. Omis : le registre complet (usage direct).
71
116
  * @returns le registre, prêt pour l'applicateur.
72
117
  */
73
118
  async function defaultMigrationSources(appDir, options = {}) {
74
119
  const sources = [];
120
+ if (options.connector !== void 0 && !ownsSharedMigrations(options.connector)) {
121
+ const own = connectorMigrationsDir(appDir, options.connector);
122
+ if (own !== void 0) sources.push({
123
+ name: "app",
124
+ dir: own,
125
+ rank: APP_RANK
126
+ });
127
+ return sources;
128
+ }
75
129
  if (options.framework !== false) sources.push({
76
130
  name: FRAMEWORK_SOURCE,
77
131
  dir: await frameworkMigrationsDir(),
@@ -85,4 +139,4 @@ async function defaultMigrationSources(appDir, options = {}) {
85
139
  return sources;
86
140
  }
87
141
  //#endregion
88
- export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, defaultMigrationSources, frameworkMigrationsDir };
142
+ export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, RESERVED_MIGRATION_DIRS, connectorMigrationsDir, defaultMigrationSources, frameworkMigrationsDir, isReservedConnectorName };
@@ -1,5 +1,7 @@
1
1
  import { MIGRATE_URL_ENV } from "./types.js";
2
2
  import { action } from "./explain.js";
3
+ import { FRAMEWORK_CONNECTOR } from "../frameworkConnector.js";
4
+ import { RESERVED_MIGRATION_DIRS } from "./paths.js";
3
5
  import { knownConnectors } from "./resolve.js";
4
6
  //#region nodefony/src/migrator/refusals.ts
5
7
  /**
@@ -79,6 +81,30 @@ function generationNeedsAnswer(label, replay) {
79
81
  };
80
82
  }
81
83
  /**
84
+ * Un fichier lu par l'outil de génération porte un `await` de premier niveau.
85
+ *
86
+ * Nodefony est ESM, et l'application démarre très bien avec ce fichier. Mais
87
+ * l'outil qui écrit les migrations (`drizzle-kit`) relit les fichiers de schéma
88
+ * HORS de la chaîne de l'application, en les transpilant en CommonJS — où un
89
+ * `await` de premier niveau n'existe pas. Sans ce refus, l'utilisateur recevait
90
+ * une pile d'appels d'esbuild qui ne nomme ni l'outil ni le remède.
91
+ *
92
+ * @param label - ce qui était généré, tel qu'on le cite à l'utilisateur.
93
+ * @param file - fichier fautif, tel que l'outil l'a nommé.
94
+ * @param line - ligne de l'`await`.
95
+ * @param replay - la commande à relancer une fois le fichier corrigé.
96
+ * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
97
+ */
98
+ function generationTopLevelAwait(label, file, line, replay) {
99
+ return {
100
+ code: "NF_GENERATE_TOP_LEVEL_AWAIT",
101
+ summary: `${file}:${line} — un \`await\` de premier niveau empêche de générer ${label} ; rien n'a été écrit.`,
102
+ meaning: "L'application démarre avec ce fichier : elle est ESM. Mais l'outil qui écrit les migrations (drizzle-kit) relit les fichiers de schéma de son côté, en CommonJS, où un `await` de premier niveau n'existe pas. Un fichier d'entité DÉCLARE un schéma : ce qui se décide à l'exécution (choisir une entité selon l'infrastructure, importer un module à la demande) se place dans l'`index.ts` du module — `@entities([...])` accepte une liste calculée. La base n'est pas en cause : elle n'a pas été interrogée.",
103
+ nextActions: [action(replay)],
104
+ exitCode: 2
105
+ };
106
+ }
107
+ /**
82
108
  * L'outil de génération s'est arrêté, et ce qu'il a dit est remonté TEL QUEL.
83
109
  *
84
110
  * 🔴 Ne JAMAIS remplacer sa sortie par une hypothèse. Le fourre-tout des
@@ -176,6 +202,29 @@ function notConfigured(connector, driver) {
176
202
  };
177
203
  }
178
204
  /**
205
+ * Écrire une migration pour un connecteur secondaire dont le NOM est celui d'un
206
+ * dossier de dialecte — refusé.
207
+ *
208
+ * Un connecteur secondaire range ses migrations sous `migrations/<connecteur>/`,
209
+ * à côté des dossiers de dialecte du connecteur du framework. Nommé `postgres`,
210
+ * son dossier SERAIT celui des migrations PostgreSQL du framework : il les
211
+ * lirait comme les siennes, et les siennes seraient appliquées à la base du
212
+ * framework. On le refuse en nommant la collision, plutôt que de le découvrir
213
+ * au premier déploiement.
214
+ *
215
+ * @param connector - connecteur demandé.
216
+ * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
217
+ */
218
+ function reservedConnectorName(connector) {
219
+ return {
220
+ code: "NF_MIGRATE_SECONDARY_CONNECTOR",
221
+ summary: `Le connecteur « ${connector} » porte le nom d'un dossier réservé (${RESERVED_MIGRATION_DIRS.join(", ")}) : ses migrations ne peuvent pas avoir de dossier propre. Rien n'a été écrit.`,
222
+ meaning: `Les migrations d'un connecteur secondaire vivent sous \`migrations/<connecteur>/<dialecte>\`, à côté des dossiers de dialecte du connecteur « ${FRAMEWORK_CONNECTOR} ». Le dossier \`migrations/${connector}\` est déjà l'un d'eux : les deux connecteurs y liraient les migrations de l'autre, et chacune serait appliquée à la mauvaise base. Renomme le connecteur dans \`connectors\` (par exemple « ${connector}_data ») : son nom ne sert qu'à le désigner.`,
223
+ nextActions: [action("nodefony inspect config --json")],
224
+ exitCode: 2
225
+ };
226
+ }
227
+ /**
179
228
  * Traduit une résolution qui n'est PAS `ready` en refus lisible.
180
229
  *
181
230
  * @param wanted - nom demandé par l'appelant.
@@ -222,4 +271,4 @@ function describeResolutionRefusal(wanted, resolution, config) {
222
271
  };
223
272
  }
224
273
  //#endregion
225
- export { MigrationToolError, describeResolutionRefusal, formatToolOutput, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed, moduleAbsent, notConfigured };
274
+ export { MigrationToolError, describeResolutionRefusal, formatToolOutput, generationFailed, generationNeedsAnswer, generationToolMissing, generationTopLevelAwait, introspectFailed, moduleAbsent, notConfigured, reservedConnectorName };