@nodefony/drizzle 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +162 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +105 -0
  6. package/dist/nodefony/command/migrateShared.js +247 -0
  7. package/dist/nodefony/command/orm-generate.js +356 -0
  8. package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
  9. package/dist/nodefony/command/orm-migrate-repair.js +114 -0
  10. package/dist/nodefony/command/orm-migrate-status.js +67 -0
  11. package/dist/nodefony/command/orm-migrate.js +141 -0
  12. package/dist/nodefony/command/orm-reset.js +166 -0
  13. package/dist/nodefony/config/config.js +107 -0
  14. package/dist/nodefony/config/defineModuleConfig.js +63 -0
  15. package/dist/nodefony/entity/auditEventEntity.js +93 -0
  16. package/dist/nodefony/entity/colKit.js +260 -0
  17. package/dist/nodefony/entity/idempotencyEntity.js +74 -0
  18. package/dist/nodefony/entity/sessionEntity.js +75 -0
  19. package/dist/nodefony/entity/tokenEntity.js +198 -0
  20. package/dist/nodefony/entity/totpSecretEntity.js +98 -0
  21. package/dist/nodefony/entity/userTable.js +141 -0
  22. package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
  23. package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
  24. package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/migrations-schema/mysql.js +48 -0
  27. package/dist/nodefony/migrations-schema/postgres.js +48 -0
  28. package/dist/nodefony/migrations-schema/sqlite.js +48 -0
  29. package/dist/nodefony/registerStores.js +218 -0
  30. package/dist/nodefony/service/DrizzleService.js +282 -0
  31. package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
  32. package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
  33. package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
  34. package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
  35. package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
  36. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
  37. package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
  38. package/dist/nodefony/src/SessionStorage.js +259 -0
  39. package/dist/nodefony/src/connectorTarget.js +59 -0
  40. package/dist/nodefony/src/likeSql.js +50 -0
  41. package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
  42. package/dist/nodefony/src/migrator/adopt.js +553 -0
  43. package/dist/nodefony/src/migrator/appSchema.js +414 -0
  44. package/dist/nodefony/src/migrator/catalog.js +76 -0
  45. package/dist/nodefony/src/migrator/destructive.js +213 -0
  46. package/dist/nodefony/src/migrator/divergence.js +84 -0
  47. package/dist/nodefony/src/migrator/drivers/index.js +39 -0
  48. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
  49. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
  50. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
  51. package/dist/nodefony/src/migrator/explain.js +565 -0
  52. package/dist/nodefony/src/migrator/hash.js +47 -0
  53. package/dist/nodefony/src/migrator/history.js +219 -0
  54. package/dist/nodefony/src/migrator/index.js +16 -0
  55. package/dist/nodefony/src/migrator/kit.js +296 -0
  56. package/dist/nodefony/src/migrator/name.js +68 -0
  57. package/dist/nodefony/src/migrator/paths.js +88 -0
  58. package/dist/nodefony/src/migrator/refusals.js +143 -0
  59. package/dist/nodefony/src/migrator/resolve.js +281 -0
  60. package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
  61. package/dist/nodefony/src/migrator/sources.js +419 -0
  62. package/dist/nodefony/src/migrator/status.js +231 -0
  63. package/dist/nodefony/src/migrator/types.js +91 -0
  64. package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
  65. package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
  66. package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
  67. package/dist/nodefony/src/orm-core/index.js +4 -0
  68. package/dist/nodefony/src/queryKit.js +318 -0
  69. package/dist/nodefony/src/safeTarget.js +55 -0
  70. package/dist/types/index.d.ts +76 -0
  71. package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
  72. package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
  73. package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
  74. package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
  75. package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
  76. package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
  77. package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
  78. package/dist/types/nodefony/config/config.d.ts +110 -0
  79. package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
  80. package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
  81. package/dist/types/nodefony/entity/colKit.d.ts +130 -0
  82. package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
  83. package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
  84. package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
  85. package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
  86. package/dist/types/nodefony/entity/userTable.d.ts +65 -0
  87. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
  88. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
  89. package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  91. package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
  92. package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
  93. package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
  94. package/dist/types/nodefony/registerStores.d.ts +52 -0
  95. package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
  96. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
  97. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
  98. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
  99. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
  100. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
  101. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
  102. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
  103. package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
  104. package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
  105. package/dist/types/nodefony/src/likeSql.d.ts +29 -0
  106. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
  107. package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
  108. package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
  109. package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
  110. package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
  111. package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
  112. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
  113. package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
  114. package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
  115. package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
  116. package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
  117. package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
  118. package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
  119. package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
  120. package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
  121. package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
  122. package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
  123. package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
  124. package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
  125. package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
  126. package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
  127. package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
  128. package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
  129. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
  130. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
  131. package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
  132. package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
  133. package/dist/types/nodefony/src/queryKit.d.ts +136 -0
  134. package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
  135. package/docs/index.md +954 -0
  136. package/docs/migrations.md +691 -0
  137. package/migrations/mysql/0000_framework_init.sql +137 -0
  138. package/migrations/mysql/meta/_journal.json +13 -0
  139. package/migrations/postgres/0000_framework_init.sql +128 -0
  140. package/migrations/postgres/meta/_journal.json +13 -0
  141. package/migrations/sqlite/0000_framework_init.sql +127 -0
  142. package/migrations/sqlite/meta/_journal.json +13 -0
  143. package/package.json +126 -0
@@ -0,0 +1,114 @@
1
+ import { renderStatus } from "../src/migrator/explain.js";
2
+ import { OrmMigrateCommand } from "./migrateShared.js";
3
+ //#region nodefony/command/orm-migrate-repair.ts
4
+ const options = {
5
+ helpGroup: "BASE DE DONNÉES",
6
+ showBanner: false,
7
+ kernelEvent: "onPostReady"
8
+ };
9
+ /**
10
+ * `nodefony orm:migrate:repair` — lève les marqueurs d'échec, **après**
11
+ * inspection humaine.
12
+ *
13
+ * ## Ce que « réparer » veut dire ici, et ce que ça ne veut pas dire
14
+ *
15
+ * Cette commande **ne répare pas la base**. Elle efface la trace d'une
16
+ * migration qui n'a pas abouti, pour que l'applicateur accepte de reprendre.
17
+ * C'est une déclaration : « j'ai regardé la base, elle est dans l'état que je
18
+ * crois ».
19
+ *
20
+ * Pourquoi ce n'est pas automatique : quand une migration s'arrête en cours de
21
+ * route, l'état laissé dépend de la base. PostgreSQL et SQLite annulent la
22
+ * migration fautive entière — l'état est net. MySQL valide chaque instruction
23
+ * de schéma au fur et à mesure, sans retour possible : la moitié des
24
+ * changements peut être en place. Aucun programme ne peut décider à la place
25
+ * d'un humain si cette moitié est acceptable.
26
+ *
27
+ * **L'ordre des gestes, et il ne s'inverse pas :**
28
+ *
29
+ * 1. `nodefony orm:migrate:status --json` — voir ce qui a échoué et pourquoi ;
30
+ * 2. regarder la base elle-même, et la remettre d'aplomb si besoin ;
31
+ * 3. `nodefony orm:migrate:repair` — lever le marqueur ;
32
+ * 4. `nodefony orm:migrate` — reprendre.
33
+ *
34
+ * ## `--update-hashes` : à ne taper que sur instruction d'un message
35
+ *
36
+ * Ré-aligner les empreintes déclare que les fichiers modifiés APRÈS avoir été
37
+ * appliqués sont réputés conformes. C'est presque toujours faux : les autres
38
+ * bases ont reçu l'ancienne version et ne recevront jamais la nouvelle. Le
39
+ * geste normal face à un fichier modifié est de le restaurer
40
+ * (`git checkout -- migrations/`) et d'écrire une NOUVELLE migration.
41
+ *
42
+ * L'option existe pour le cas où l'on sait que la modification était sans effet
43
+ * — une reformulation, un commentaire. Elle ne touche jamais la base.
44
+ *
45
+ * ## `--forget <source>/<tag>` : l'issue d'un historique qui MENT
46
+ *
47
+ * Il existait un état dont aucune commande ne sortait : une migration inscrite
48
+ * comme réussie que personne n'a jamais exécutée — une adoption mal bornée, ou
49
+ * une base héritée d'une version antérieure aux gardes. La base ne porte pas
50
+ * les tables, l'historique affirme le contraire, et rien n'est « en attente ».
51
+ * Le générateur disait alors « c'est l'historique qu'il faut reprendre » et
52
+ * renvoyait ici ; mais cette commande ne savait lever que des marqueurs
53
+ * d'ÉCHEC, et répondait « rien à réparer ». Trois messages vrais, aucun geste
54
+ * — et le seul chemin restant était de détruire la base.
55
+ *
56
+ * `--forget` désinscrit UNE entrée nommée, pour qu'elle soit rejouée au
57
+ * passage suivant. Bornée par construction : il faut la nommer
58
+ * (`--forget app/0003_ajout_facture`), il n'y a ni motif ni lot. La base n'est
59
+ * pas touchée — si la migration avait réellement été appliquée, son rejeu
60
+ * échouera, bruyamment, ce qui est le comportement voulu.
61
+ */
62
+ var OrmMigrateRepair = class extends OrmMigrateCommand {
63
+ constructor(cli) {
64
+ super("orm:migrate:repair", "lève un marqueur d'échec, sans toucher à la base", cli, options);
65
+ this.addSharedOptions();
66
+ this.addOption("-s, --source <nom>", "ne réparer que cette source (framework, app, un module) — toutes si omis");
67
+ this.addOption("--forget <source/tag...>", "désinscrit une migration nommée pour qu'elle soit REJOUÉE — l'issue quand l'historique affirme une migration jamais exécutée");
68
+ this.addOption("--update-hashes", "déclare conformes les fichiers modifiés après application — presque toujours FAUX, à ne taper que si un message le demande");
69
+ }
70
+ async generate(opts = {}) {
71
+ const resolved = this.resolveOrFail(opts, true);
72
+ if (!resolved) return this;
73
+ const { resolution, config } = resolved;
74
+ const style = this.style;
75
+ try {
76
+ const migrator = await this.migrator(resolution, config);
77
+ const forget = (opts.forget ?? []).map((brut) => {
78
+ const coupe = brut.indexOf("/");
79
+ if (coupe <= 0 || coupe === brut.length - 1) throw new Error(`« ${brut} » n'est pas une entrée d'historique. La forme attendue est « source/tag », par exemple « app/0003_ajout_facture » : sans la source, deux migrations de même tag ne se distinguent pas.`);
80
+ return {
81
+ source: brut.slice(0, coupe),
82
+ tag: brut.slice(coupe + 1)
83
+ };
84
+ });
85
+ const done = await migrator.repair({
86
+ source: opts.source,
87
+ updateHashes: opts.updateHashes === true,
88
+ forget
89
+ });
90
+ const plan = await migrator.status();
91
+ const report = await this.report(plan, resolution, config);
92
+ const payload = {
93
+ ...report,
94
+ repaired: done
95
+ };
96
+ let human = "";
97
+ if (done.cleared.length === 0 && done.rehashed.length === 0 && done.forgotten.length === 0) human = `${style.green("Rien à réparer : aucun marqueur d'échec, aucune empreinte à ré-aligner.")}\n\n`;
98
+ else {
99
+ human = `${style.green(style.bold("✓ historique réparé"))} ${style.dim("— la base n'a pas été modifiée")}\n`;
100
+ for (const c of done.cleared) human += ` ${style.green("−")} marqueur d'échec levé : ${c.source}/${c.tag}\n`;
101
+ for (const r of done.rehashed) human += ` ${style.yellow("≈")} empreinte ré-alignée : ${r.source}/${r.tag}\n`;
102
+ for (const f of done.forgotten) human += ` ${style.yellow("↺")} désinscrite, sera REJOUÉE : ${f.source}/${f.tag}\n`;
103
+ human += "\n";
104
+ }
105
+ human += renderStatus(report, style);
106
+ this.respond(payload, human, report.exitCode, opts.json);
107
+ } catch (e) {
108
+ this.failFrom(e, resolution.connector, opts.json, resolution.ddl);
109
+ }
110
+ return this;
111
+ }
112
+ };
113
+ //#endregion
114
+ export { OrmMigrateRepair as default };
@@ -0,0 +1,67 @@
1
+ import { renderStatus } from "../src/migrator/explain.js";
2
+ import { OrmMigrateCommand } from "./migrateShared.js";
3
+ //#region nodefony/command/orm-migrate-status.ts
4
+ /**
5
+ * `kernelEvent: "onPostReady"` — la commande LIT l'état de l'application.
6
+ *
7
+ * Le registre des connecteurs est peuplé au démarrage par le service du module.
8
+ * Une commande branchée plus tôt passerait avant lui et ne trouverait rien.
9
+ * Aucun serveur n'écoute pour autant : le profil console est respecté.
10
+ */
11
+ const options = {
12
+ helpGroup: "BASE DE DONNÉES",
13
+ showBanner: false,
14
+ kernelEvent: "onPostReady"
15
+ };
16
+ /**
17
+ * `nodefony orm:migrate:status` — dit ce que la base a reçu, ce qui reste, et
18
+ * ce qu'il faut taper.
19
+ *
20
+ * **Lecture seule et sans verrou.** Elle n'écrit rien, pas même la table
21
+ * d'historique : un état qui se consulte ne doit pas modifier ce qu'il
22
+ * observe — et une sonde qui écrit dans la base n'est plus une sonde.
23
+ *
24
+ * ## Son autre métier : barrière d'intégration continue
25
+ *
26
+ * Le code de sortie porte le verdict, et il ne changera jamais de sens :
27
+ *
28
+ * | Code | Ce que ça veut dire |
29
+ * | ---- | ------------------------------------------------------------------ |
30
+ * | `0` | à jour — rien à faire |
31
+ * | `1` | une action humaine est requise (migrations en attente, écart, échec) |
32
+ * | `2` | la commande n'a pas pu travailler (base injoignable, verrou, usage) |
33
+ *
34
+ * Une passe de déploiement peut donc s'arrêter dessus sans lire un mot :
35
+ *
36
+ * ```bash
37
+ * nodefony orm:migrate:status --json || exit 1
38
+ * ```
39
+ *
40
+ * @example Ce qu'un agent lit
41
+ * ```bash
42
+ * nodefony orm:migrate:status --json | jq -r '.verdict, .nextActions[0].command'
43
+ * # pending
44
+ * # nodefony orm:migrate
45
+ * ```
46
+ */
47
+ var OrmMigrateStatus = class extends OrmMigrateCommand {
48
+ constructor(cli) {
49
+ super("orm:migrate:status", "l'état des migrations d'un connecteur, sans écrire", cli, options);
50
+ this.addSharedOptions();
51
+ }
52
+ async generate(opts = {}) {
53
+ const resolved = this.resolveOrFail(opts, true);
54
+ if (!resolved) return this;
55
+ const { resolution, config } = resolved;
56
+ try {
57
+ const plan = await (await this.migrator(resolution, config)).status();
58
+ const report = await this.report(plan, resolution, config);
59
+ this.emitReport(report, renderStatus(report, this.style), opts.json);
60
+ } catch (e) {
61
+ this.failFrom(e, resolution.connector, opts.json, resolution.ddl);
62
+ }
63
+ return this;
64
+ }
65
+ };
66
+ //#endregion
67
+ export { OrmMigrateStatus as default };
@@ -0,0 +1,141 @@
1
+ import { EXIT, action, renderStatus } from "../src/migrator/explain.js";
2
+ import { readMigrationEnv } from "../src/migrator/resolve.js";
3
+ import { checkDataAdvice, dataLoss, destructiveActions, renderDestructive, scanDestructive, summarizeDestructive, touchesExistingRows } from "../src/migrator/destructive.js";
4
+ import { OrmMigrateCommand } from "./migrateShared.js";
5
+ //#region nodefony/command/orm-migrate.ts
6
+ const options = {
7
+ helpGroup: "BASE DE DONNÉES",
8
+ showBanner: false,
9
+ kernelEvent: "onPostReady"
10
+ };
11
+ /**
12
+ * `nodefony orm:migrate` — applique les migrations restantes, sous verrou.
13
+ *
14
+ * ## Ce que la commande fait, dans cet ordre exact
15
+ *
16
+ * 1. **prend le verrou** de la base (verrou natif du serveur : il se libère
17
+ * tout seul si le processus meurt — aucun verrou fantôme à débloquer à la
18
+ * main) ;
19
+ * 2. **crée la table d'historique** si elle n'existe pas ;
20
+ * 3. **valide TOUT** — empreintes, ordre, échecs passés, fichiers manquants ;
21
+ * 4. **applique** les migrations une par une, chacune dans sa transaction là où
22
+ * la base le permet.
23
+ *
24
+ * La validation précède toute écriture : **un refus laisse la base
25
+ * intacte**. C'est ce qui rend sûr de relancer la commande après un refus.
26
+ *
27
+ * ## Elle ne s'exécute jamais toute seule au démarrage — sauf si on le demande
28
+ *
29
+ * Appliquer des migrations au démarrage n'est pas un défaut, et ce n'est pas de
30
+ * la prudence : au démarrage, plusieurs exemplaires partent en même temps. Le
31
+ * mode `ddl: "migrate"` existe pour les déploiements à exemplaire unique et
32
+ * s'écrit à la main. En production orchestrée, c'est un travail séparé qui
33
+ * lance cette commande AVANT que les nouveaux exemplaires ne démarrent.
34
+ *
35
+ * ## Le compte qui migre n'est pas le compte qui sert
36
+ *
37
+ * `NF_MIGRATE_DATABASE_URL` remplace, pour cette commande seulement, la
38
+ * connexion du connecteur. C'est le véhicule du moindre privilège : le secret
39
+ * qui a le droit de modifier le schéma est monté dans le travail de migration,
40
+ * et nulle part ailleurs. Elle doit désigner une connexion **directe** — un
41
+ * répartiteur de connexions en mode transaction casse le verrou.
42
+ *
43
+ * @example Voir sans rien appliquer
44
+ * ```bash
45
+ * nodefony orm:migrate --dry-run
46
+ * ```
47
+ *
48
+ * @example Dans un travail de déploiement
49
+ * ```bash
50
+ * NF_MIGRATE_DATABASE_URL="postgres://migrator:…@db:5432/app" nodefony orm:migrate --json
51
+ * ```
52
+ */
53
+ var OrmMigrate = class extends OrmMigrateCommand {
54
+ constructor(cli) {
55
+ super("orm:migrate", "applique les migrations en attente", cli, options);
56
+ this.addSharedOptions();
57
+ this.addOption("-n, --dry-run", "n'applique RIEN : valide et affiche le SQL qui serait exécuté");
58
+ this.addOption("--out-of-order", "accepte une migration plus ancienne que la dernière appliquée (trace d'une fusion de branches — à ne taper que si le message le demande)");
59
+ this.addOption("--ignore-missing", "accepte qu'une migration enregistrée n'ait plus de fichier (à ne taper que si le message le demande)");
60
+ this.addOption("--allow-destructive", "assume une migration qui SUPPRIME des données (colonne, table, lignes) — hors développement, elle est refusée sans ce drapeau");
61
+ }
62
+ async generate(opts = {}) {
63
+ const resolved = this.resolveOrFail(opts, true);
64
+ if (!resolved) return this;
65
+ const { resolution, config } = resolved;
66
+ const style = this.style;
67
+ try {
68
+ const migrator = await this.migrator(resolution, config);
69
+ const before = await migrator.status();
70
+ const findings = scanDestructive(before.pending);
71
+ const losses = dataLoss(findings);
72
+ const enDev = readMigrationEnv(this.kernel).runtime === "development";
73
+ if (losses.length > 0 && opts.dryRun !== true && opts.allowDestructive !== true && !enDev) {
74
+ this.fail(resolution.connector, "NF_MIGRATE_DESTRUCTIVE", summarizeDestructive(findings, resolution.connector), renderDestructive(findings, true), destructiveActions(resolution.connector).map((c) => action(c)), opts.json, EXIT.actionRequired);
75
+ return this;
76
+ }
77
+ if (findings.length > 0) process.stderr.write(style.yellow(renderDestructive(findings, false)) + "\n");
78
+ const run = await migrator.migrate({
79
+ dryRun: opts.dryRun === true,
80
+ outOfOrder: opts.outOfOrder === true,
81
+ ignoreMissing: opts.ignoreMissing === true
82
+ });
83
+ const plan = await migrator.status();
84
+ const report = await this.report(plan, resolution, config);
85
+ if (opts.dryRun === true) {
86
+ const pending = plan.pending;
87
+ const payload = {
88
+ formatVersion: 1,
89
+ connector: report.connector,
90
+ verdict: report.verdict,
91
+ exitCode: report.exitCode,
92
+ dryRun: true,
93
+ summary: report.summary,
94
+ nextActions: report.nextActions,
95
+ sources: report.sources,
96
+ driver: report.driver,
97
+ statements: pending.map((f) => ({
98
+ source: f.source,
99
+ tag: f.tag,
100
+ sql: [...f.statements]
101
+ })),
102
+ destructive: findings
103
+ };
104
+ let human = `${style.bold("Essai — RIEN n'a été appliqué.")}\n\n`;
105
+ if (findings.length > 0) human += `${style.yellow(renderDestructive(findings, false))}\n`;
106
+ if (pending.length === 0) human += `${style.green("Aucune migration en attente : il n'y a rien à appliquer.")}\n`;
107
+ else {
108
+ for (const f of pending) {
109
+ human += `${style.bold(`── ${f.source}/${f.tag}`)}\n`;
110
+ for (const sql of f.statements) human += `${style.dim(sql)};\n`;
111
+ human += "\n";
112
+ }
113
+ human += `${style.bold("Pour appliquer :")}\n ${style.green(`nodefony orm:migrate${resolution.connector === "default" ? "" : ` --connector ${resolution.connector}`}`)}\n`;
114
+ }
115
+ this.respond(payload, human, report.exitCode, opts.json);
116
+ return this;
117
+ }
118
+ if (run.applied.length === 0) {
119
+ this.emitReport(report, `${style.green("Rien à appliquer : la base est déjà à jour.")}\n\n${renderStatus(report, style)}`, opts.json);
120
+ return this;
121
+ }
122
+ let human = `${style.green(style.bold(`✓ ${run.applied.length} migration(s) appliquée(s)`))} ${style.dim(`(exécution ${run.runId})`)}\n`;
123
+ for (const a of run.applied) human += ` ${style.green("+")} ${a.source}/${a.tag} ${style.dim(`— ${a.executionMs} ms`)}\n`;
124
+ human += `\n${renderStatus(report, style)}`;
125
+ if (touchesExistingRows(before.pending)) {
126
+ const conseil = checkDataAdvice(resolution.connector, report.driver);
127
+ if (opts.json === true) process.stderr.write(`${conseil}\n`);
128
+ else human += `\n${style.dim(conseil)}\n`;
129
+ }
130
+ this.emitReport(findings.length > 0 ? {
131
+ ...report,
132
+ destructive: findings
133
+ } : report, human, opts.json);
134
+ } catch (e) {
135
+ this.failFrom(e, resolution.connector, opts.json, resolution.ddl);
136
+ }
137
+ return this;
138
+ }
139
+ };
140
+ //#endregion
141
+ export { EXIT, action, OrmMigrate as default };
@@ -0,0 +1,166 @@
1
+ import { action } from "../src/migrator/explain.js";
2
+ import { openMigrationDriver } from "../src/migrator/drivers/index.js";
3
+ import { readMigrationEnv, resetAllowed } from "../src/migrator/resolve.js";
4
+ import { OrmMigrateCommand } from "./migrateShared.js";
5
+ //#region nodefony/command/orm-reset.ts
6
+ const options = {
7
+ helpGroup: "BASE DE DONNÉES",
8
+ showBanner: false,
9
+ kernelEvent: "onPostReady"
10
+ };
11
+ /**
12
+ * Requête qui liste les tables du schéma COURANT de la connexion.
13
+ *
14
+ * Le schéma courant, et pas « toutes les bases du serveur » : c'est le
15
+ * périmètre que la connexion adresse déjà, donc celui que l'utilisateur a
16
+ * désigné en configurant son connecteur. Élargir serait détruire ce qu'il n'a
17
+ * pas montré.
18
+ */
19
+ const LIST_TABLES = {
20
+ sqlite: "SELECT name AS name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
21
+ postgres: "SELECT tablename AS name FROM pg_tables WHERE schemaname = current_schema()",
22
+ mysql: "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'"
23
+ };
24
+ /** Échappe un identifiant venu du catalogue de la base. */
25
+ function quoteIdent(name, dialect) {
26
+ return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, "\"\"")}"`;
27
+ }
28
+ /**
29
+ * `nodefony orm:reset` — vide la base de DÉVELOPPEMENT et la laisse prête à
30
+ * être recréée.
31
+ *
32
+ * ## Le geste qu'elle remplace
33
+ *
34
+ * Jusqu'ici, le générateur d'applications disait « supprime ta base de dev, ou
35
+ * passe par une migration » : deux gestes manuels, aucun outil, et chacun s'en
36
+ * tirait comme il pouvait. Une seule commande à retenir pour toute l'équipe,
37
+ * au lieu d'un arbre de décision.
38
+ *
39
+ * ## Ce qu'elle fait exactement
40
+ *
41
+ * Elle supprime **toutes les tables du schéma courant** de la connexion —
42
+ * l'historique des migrations compris. Elle ne supprime NI la base, NI le
43
+ * fichier : supprimer une base demande des droits d'administration que le
44
+ * compte de l'application n'a pas, et supprimer un fichier ouvert échoue sous
45
+ * Windows. Le résultat est le même — une base vide — et il s'obtient partout de
46
+ * la même façon.
47
+ *
48
+ * Ensuite : en mode `auto` (le défaut en développement), le prochain démarrage
49
+ * recrée le schéma depuis le code. Dans les autres modes, la commande dit quoi
50
+ * lancer.
51
+ *
52
+ * ## Le refus est une LISTE BLANCHE, pas une liste noire
53
+ *
54
+ * Elle n'accepte de travailler que si l'environnement est `development`. Pas
55
+ * « refusée en production » : `staging`, `preprod`, `test` et tout
56
+ * environnement inconnu refusent aussi. La différence n'est pas théorique — une
57
+ * garde écrite « si production » laisse passer tout ce qu'on n'a pas pensé à
58
+ * nommer, et c'est exactement là que l'accident arrive.
59
+ *
60
+ * `NF_MIGRATE_DATABASE_URL` n'est **pas** lue par cette commande. Cette
61
+ * variable porte le compte qui a le droit de modifier le schéma en
62
+ * production : lui laisser désigner la cible d'un effacement serait offrir la
63
+ * seule combinaison qu'il ne faut jamais rendre possible.
64
+ *
65
+ * @example
66
+ * ```bash
67
+ * nodefony orm:reset # demande confirmation en terminal
68
+ * nodefony orm:reset --yes # sans question (script)
69
+ * ```
70
+ */
71
+ var OrmReset = class extends OrmMigrateCommand {
72
+ constructor(cli) {
73
+ super("orm:reset", "vide la base de développement d'un connecteur", cli, options);
74
+ this.addSharedOptions();
75
+ this.addOption("-y, --yes", "ne pose pas la question — pour un script ; hors terminal, l'option est obligatoire");
76
+ }
77
+ async generate(opts = {}) {
78
+ const resolved = this.resolveOrFail(opts, false);
79
+ if (!resolved) return this;
80
+ const { resolution } = resolved;
81
+ const style = this.style;
82
+ const env = readMigrationEnv(this.kernel);
83
+ if (!resetAllowed(env)) {
84
+ const observed = env.nodeEnv ?? process.env.NF_ENV ?? process.env.APP_ENV ?? "non déclaré";
85
+ this.fail(resolution.connector, "NF_MIGRATE_NOT_DEVELOPMENT", `Cette commande efface des données : elle n'est acceptée qu'en développement. L'environnement constaté est « ${observed} ».`, "La règle est une liste blanche : seul `development` passe. Un environnement inconnu, un `staging`, un `test` sont refusés — c'est ce qui empêche un accident sur ce que personne n'avait pensé à nommer. Pour vider une base ailleurs, fais-le avec l'outil de ta base, en sachant ce que tu fais. Sur un poste de développement, c'est l'environnement de l'application qui doit dire `development` — pas une variable posée devant la commande.", [action(`nodefony orm:migrate:status --connector ${resolution.connector} --json`)], opts.json);
86
+ return this;
87
+ }
88
+ const target = resolution.dialect === "sqlite" ? resolution.target.filename ?? ":memory:" : redact(resolution.target.url ?? "");
89
+ let driver = null;
90
+ try {
91
+ driver = await openMigrationDriver(resolution.target);
92
+ const tables = (await driver.query(LIST_TABLES[resolution.dialect])).map((r) => String(r.name)).filter((n) => n.length > 0).sort();
93
+ if (tables.length === 0) {
94
+ this.respond({
95
+ formatVersion: 1,
96
+ connector: resolution.connector,
97
+ exitCode: 0,
98
+ dropped: []
99
+ }, `${style.green("La base est déjà vide")} ${style.dim(`(${resolution.dialect} : ${target})`)} — rien à faire.\n`, 0, opts.json);
100
+ return this;
101
+ }
102
+ if (opts.yes !== true) {
103
+ if (opts.json === true || !process.stdin.isTTY) {
104
+ this.fail(resolution.connector, "NF_MIGRATE_CONFIRM_REQUIRED", `Cette commande va supprimer ${tables.length} table(s) de « ${target} » (${resolution.dialect}) : ${tables.join(", ")}.`, "Hors terminal — ou en sortie machine, où un dialogue n'a pas sa place —, il n'y a personne pour répondre à une question, et un effacement ne se déduit pas d'un silence. Relance avec `--yes` si c'est bien ce que tu veux.", [action(`nodefony orm:reset --yes${resolution.connector === "default" ? "" : ` --connector ${resolution.connector}`}`)], opts.json, 1);
105
+ return this;
106
+ }
107
+ process.stdout.write(`${style.yellow(style.bold("Effacement de la base de développement"))}\n base : ${style.bold(target)} ${style.dim(`(${resolution.dialect})`)}\n tables : ${tables.join(", ")}\n\n`);
108
+ await this.loadPrompts();
109
+ if (!await this.prompts.confirm({
110
+ message: `Supprimer ces ${tables.length} table(s) ?`,
111
+ default: false
112
+ })) {
113
+ process.stdout.write(`${style.dim("Annulé — rien n'a été touché.")}\n`);
114
+ return this;
115
+ }
116
+ }
117
+ await this.#dropAll(driver, tables, resolution.dialect);
118
+ const suite = resolution.ddl === "auto" ? "Le prochain démarrage recrée le schéma depuis le code." : "Applique les migrations pour recréer le schéma.";
119
+ const actions = resolution.ddl === "auto" ? [action("nodefony development")] : [action(`nodefony orm:migrate${resolution.connector === "default" ? "" : ` --connector ${resolution.connector}`}`)];
120
+ this.respond({
121
+ formatVersion: 1,
122
+ connector: resolution.connector,
123
+ exitCode: 0,
124
+ dropped: tables,
125
+ nextActions: actions
126
+ }, `${style.green(style.bold(`✓ ${tables.length} table(s) supprimée(s)`))} ${style.dim(`(${resolution.dialect} : ${target})`)}\n${tables.map((t) => ` ${style.dim("−")} ${t}`).join("\n")}\n\n${suite}\n\n${style.bold("À faire :")}\n${actions.map((a) => ` ${style.green(a.command)}`).join("\n")}\n`, 0, opts.json);
127
+ } catch (e) {
128
+ this.failFrom(e, resolution.connector, opts.json);
129
+ } finally {
130
+ await driver?.close().catch(() => void 0);
131
+ }
132
+ return this;
133
+ }
134
+ /**
135
+ * Supprime les tables en désarmant les clés étrangères le temps de l'opération.
136
+ *
137
+ * Sans ce désarmement, l'ordre de suppression devrait être topologique — et il
138
+ * est indécidable sur un cycle de références. Le désarmement porte sur la
139
+ * connexion de la commande seulement, et elle se ferme juste après.
140
+ */
141
+ async #dropAll(driver, tables, dialect) {
142
+ if (dialect === "sqlite") await driver.exec("PRAGMA foreign_keys = OFF");
143
+ else if (dialect === "mysql") await driver.exec("SET FOREIGN_KEY_CHECKS = 0");
144
+ try {
145
+ for (const t of tables) {
146
+ const ident = quoteIdent(t, dialect);
147
+ await driver.exec(dialect === "postgres" ? `DROP TABLE IF EXISTS ${ident} CASCADE` : `DROP TABLE IF EXISTS ${ident}`);
148
+ }
149
+ } finally {
150
+ if (dialect === "sqlite") await driver.exec("PRAGMA foreign_keys = ON").catch(() => void 0);
151
+ else if (dialect === "mysql") await driver.exec("SET FOREIGN_KEY_CHECKS = 1").catch(() => void 0);
152
+ }
153
+ }
154
+ };
155
+ /** Retire le secret d'une URL de connexion avant de l'afficher. */
156
+ function redact(url) {
157
+ try {
158
+ const u = new URL(url);
159
+ if (u.password) u.password = "***";
160
+ return u.toString();
161
+ } catch {
162
+ return url.replace(/\/\/[^@]*@/, "//***@");
163
+ }
164
+ }
165
+ //#endregion
166
+ export { OrmReset as default };
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ //#region nodefony/config/config.ts
3
+ /**
4
+ * @nodefony/drizzle — CONFIGURATION DU MODULE (schéma Zod = source unique).
5
+ *
6
+ * ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` = la valeur d'usine ;
7
+ * changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
8
+ * surcharge via `use("@nodefony/...", { … })` dans SON `nodefony.config.ts`.
9
+ *
10
+ * RÈGLE D'OR (ADR-0006) : ce fichier porte le **schéma Zod commenté** (type +
11
+ * validation + défaut + doc) ET matérialise les défauts via `parse({})`. Aucune
12
+ * valeur n'est re-tapée ailleurs. Le builder (`defineDrizzleConfig`) et les types
13
+ * (`interfaces/IDrizzleConfig.ts`) importent le schéma D'ICI (nœud bas : ce fichier
14
+ * n'importe que `zod` → pas de cycle).
15
+ *
16
+ * Convention figée (cf `feedback_config_validation_zod` + audit config ORM
17
+ * 2026-06), alignée sur `@nodefony/mongoose`/`@nodefony/redis`/`@nodefony/realtime`.
18
+ *
19
+ * ⚠️ Le schéma reste **PUR** : `filename` est **optionnel SANS défaut** — le chemin
20
+ * SQLite par défaut dépend de `kernel.path` (indisponible à l'évaluation du schéma)
21
+ * et est résolu au boot par `DrizzleService`. Aucune lecture `process.env` ici
22
+ * (l'env est appliqué dans `defineDrizzleConfig`).
23
+ *
24
+ * SURCHARGE (précédence croissante — cf ADR-0006) :
25
+ * • App (typé) : `use("@nodefony/drizzle", { connectors: { … } })` ;
26
+ * • Par environnement : infra database `NF_DATABASE_URL`/`DATABASE_URL`
27
+ * (dialecte déduit du scheme, appliqué dans `defineDrizzleConfig`) ;
28
+ * • Déploiement/Docker : `NF__DRIZZLE__<CHEMIN>=valeur` (override env générique).
29
+ */
30
+ /**
31
+ * Dialectes SQL supportés par l'adapter Drizzle. `sqlite` (better-sqlite3) est le
32
+ * défaut bootable ; `postgres` (pg) / `mysql` (mysql2) sont des drivers chargés en
33
+ * LAZY (`optionalDependencies` + `await import` au connect) — un framework doit
34
+ * porter ses entités sur les bases majeures (cf chantier portabilité multi-dialecte).
35
+ */
36
+ const SQL_DIALECTS = [
37
+ "sqlite",
38
+ "postgres",
39
+ "mysql"
40
+ ];
41
+ /**
42
+ * Stratégies de fabrication du schéma d'un connecteur.
43
+ *
44
+ * Trois valeurs, et pas une de plus : ce qui fait le schéma est soit le
45
+ * démarrage à partir du code (`auto`), soit le démarrage à partir des fichiers
46
+ * de migration (`migrate`), soit personne (`none`). Un quatrième mode serait un
47
+ * mélange, donc un comportement que personne ne saurait décrire dans un
48
+ * incident.
49
+ */
50
+ const DDL_MODES = [
51
+ "auto",
52
+ "migrate",
53
+ "none"
54
+ ];
55
+ /**
56
+ * Conduites de la sonde de disponibilité quand le schéma est en retard.
57
+ *
58
+ * `fail` retient la mise en service (le processus ne reçoit pas de trafic),
59
+ * `warn` journalise et sert quand même, `off` ne regarde pas.
60
+ */
61
+ const MIGRATION_CHECK_MODES = [
62
+ "fail",
63
+ "warn",
64
+ "off"
65
+ ];
66
+ /**
67
+ * Conduites face à une base qui ne correspond plus au code alors que
68
+ * l'historique est complet.
69
+ *
70
+ * Le défaut est `report` — et il est structurel, pas prudent : une application
71
+ * qui écrit des migrations libres (vues, déclencheurs, colonnes ajoutées à une
72
+ * table d'entité) a une base légitimement différente du schéma déclaré, en
73
+ * permanence. Faire tomber sa mise en service dessus rendrait le constat
74
+ * inutilisable, donc mort. Superviser ne fait pas tomber un déploiement.
75
+ */
76
+ const DIVERGENCE_MODES = [
77
+ "report",
78
+ "fail",
79
+ "off"
80
+ ];
81
+ const connectorSchema = z.strictObject({
82
+ dialect: z.enum(SQL_DIALECTS).default("sqlite").describe("Dialecte SQL du connecteur : `sqlite` (défaut, driver better-sqlite3, `filename`) · `postgres` (driver `pg`, `url`) · `mysql` (driver `mysql2`, `url`). pg/mysql sont des `optionalDependencies` chargées en lazy au connect — l'app installe le driver de son déploiement."),
83
+ filename: z.string().min(1).optional().describe("Fichier SQLite du connecteur (dialecte `sqlite`). OMIS → résolu au boot sous le répertoire de données de l'application : `<app>/var/databases/nodefony-drizzle.db` pour le connecteur `default`, `nodefony-<connecteur>.db` pour les autres. `:memory:` = base éphémère en mémoire (tests). Surchargé par le infra database `NF_DATABASE_URL=sqlite:…` pour le connecteur primaire."),
84
+ url: z.string().min(1).optional().describe("Chaîne de connexion des dialectes `postgres`/`mysql` (`postgres://user:pass@host:port/db`, `mysql://…`). Requise pour ces dialectes (ignorée en `sqlite`). Porte le secret → jamais loggée (rédaction au describe)."),
85
+ ddl: z.enum(DDL_MODES).optional().describe("Qui fabrique le schéma de ce connecteur, et quand. `auto` : le démarrage crée les tables manquantes depuis le code ET ajoute les colonnes qui manquent quand elles acceptent le vide (développement — strictement additif, jamais destructeur). `migrate` : le démarrage applique les migrations sous verrou (un seul exemplaire assumé — VPS, docker compose, sqlite). `none` : personne ne touche au schéma au démarrage, un travail externe lance `nodefony orm:migrate` avant le déploiement (production orchestrée). OMIS → résolu par environnement au boot : développement et test → `auto`, tout le reste → `none`.")
86
+ }).describe("Définition d'une connexion Drizzle (driver selon `dialect` : better-sqlite3 / pg / mysql2).");
87
+ const drizzleConfigSchema = z.strictObject({
88
+ connectors: z.record(z.string(), connectorSchema).default(() => ({ default: connectorSchema.parse({}) })).describe("Connexions indexées par nom (= clé dans le `ormRegistry`). Défaut : un connecteur `default` (fichier SQLite résolu au boot). Le nom `default` (≠ `nodefony` de Mongoose) isole l'entité `session` dans le `entityRegistry` process-wide si les deux ORM cohabitent."),
89
+ migrations: z.strictObject({
90
+ dir: z.string().default("migrations").describe("Dossier des migrations de l'APPLICATION, relatif à la racine de l'application — celle que le kernel connaît, jamais le répertoire courant du processus (un espace de travail en a plusieurs). Un sous-dossier par dialecte (`migrations/postgres`). Le dossier du framework, lui, est livré dans le paquet et n'a rien à déclarer ici."),
91
+ check: z.enum(MIGRATION_CHECK_MODES).optional().describe("Conduite de la sonde de disponibilité quand `ddl` n'est pas `auto` et que le schéma est en retard. `fail` : la mise en service est RETENUE (`/readyz` répond 503) jusqu'à ce que les migrations soient appliquées — le processus redevient disponible tout seul, sans redéploiement. `warn` : journalisé, le trafic passe quand même. `off` : rien. OMIS → résolu par environnement : production → `fail`, reste → `warn`."),
92
+ lockTimeoutMs: z.number().int().positive().default(3e4).describe("Délai maximal d'attente du verrou d'application des migrations (ms). Passé ce délai la commande s'arrête sur le code 2 en disant qui tient le verrou : un processus qui attend sans limite derrière un travail mort n'annonce jamais sa panne."),
93
+ divergence: z.enum(DIVERGENCE_MODES).default("report").describe("Conduite quand l'historique est complet, rien n'est en attente, ET que la base ne correspond pourtant pas au schéma déclaré (modification faite à la main, correctif d'urgence non reporté). `report` (défaut) : journalisé et affiché, la sonde reste verte. `fail` : compte comme un écart et retient la mise en service — à n'activer que si aucune migration libre ne touche les tables d'entités. `off` : rien.")
94
+ }).default(() => ({
95
+ dir: "migrations",
96
+ lockTimeoutMs: 3e4,
97
+ divergence: "report"
98
+ })).describe("Réglages des migrations de schéma (dossier de l'application, sonde de disponibilité, verrou, dérive). Les commandes sont `nodefony orm:migrate`, `orm:migrate:status`, `orm:migrate:baseline`, `orm:migrate:repair` et `orm:reset`."),
99
+ frameworkEntities: z.boolean().default(true).describe("Déclare le schéma framework sur le connecteur `default` (tokens, audit, webauthn, webhooks, idempotence — tables créées au connect) et rend les stores correspondants sélectionnables par nom (`drizzle`). `false` = module data-only (aucune entité ni fabrique framework).")
100
+ }).describe("Configuration de @nodefony/drizzle.");
101
+ /**
102
+ * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
103
+ * valides par construction ; passés au `super(..., config)` du Module class.
104
+ */
105
+ const config = drizzleConfigSchema.parse({});
106
+ //#endregion
107
+ export { DDL_MODES, DIVERGENCE_MODES, MIGRATION_CHECK_MODES, SQL_DIALECTS, config as default, drizzleConfigSchema };
@@ -0,0 +1,63 @@
1
+ import { drizzleConfigSchema } from "./config.js";
2
+ import { parseModuleConfig, resolveInfra, sqliteFilenameFromUrl } from "nodefony";
3
+ import { z } from "zod";
4
+ //#region nodefony/config/defineModuleConfig.ts
5
+ /**
6
+ * Applique la surcharge par variables d'environnement APRÈS le parse Zod.
7
+ *
8
+ * Le schéma reste pur ; l'env est une couche explicite par-dessus. Précédence :
9
+ * env > config app > défauts.
10
+ *
11
+ * - Infra `database` (`NF_DATABASE_URL`, alias `DATABASE_URL`) de famille SQL →
12
+ * dialecte + cible du connecteur primaire (`default`, sinon le premier) :
13
+ * `sqlite:…` → `filename` ; `postgres://…`/`mysql://…` → `url`. Une URL
14
+ * `mongodb://` est IGNORÉE ici (l'infra appartient alors à `@nodefony/mongoose`).
15
+ */
16
+ function applyEnvOverrides(config) {
17
+ const database = resolveInfra(process.env).database;
18
+ if (database && database.family === "sql" && database.dialect) {
19
+ const target = config.connectors.default ? "default" : Object.keys(config.connectors)[0];
20
+ const connector = target ? config.connectors[target] : void 0;
21
+ if (connector) {
22
+ connector.dialect = database.dialect;
23
+ if (database.dialect === "sqlite") {
24
+ connector.filename = sqliteFilenameFromUrl(database.url);
25
+ delete connector.url;
26
+ } else {
27
+ connector.url = database.url;
28
+ delete connector.filename;
29
+ }
30
+ }
31
+ }
32
+ return config;
33
+ }
34
+ /**
35
+ * Builder type-safe de la configuration de `@nodefony/drizzle`.
36
+ *
37
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
38
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
39
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
40
+ * env + freeze) et publie le JSON Schema Studio.
41
+ *
42
+ * Aligné sur `defineMongooseConfig` (l'autre driver ORM) : source unique
43
+ * (`./config.ts`), VALIDE + applique l'ENV + GÈLE. Le **chemin SQLite par défaut**
44
+ * (kernel-dépendant) n'est PAS résolu ici (schéma pur) mais dans `DrizzleService`
45
+ * au boot — cf audit config ORM 2026-06 §3.2.
46
+ *
47
+ * @param config - configuration brute (sections omises = défauts sûrs).
48
+ * @returns config validée, surchargée par l'env, et gelée.
49
+ * @throws ZodError si la config est invalide.
50
+ */
51
+ function defineDrizzleConfig(config = {}) {
52
+ const parsed = parseModuleConfig(drizzleConfigSchema, config, "@nodefony/drizzle");
53
+ return Object.freeze(applyEnvOverrides(parsed));
54
+ }
55
+ /**
56
+ * JSON Schema introspectable de la config Drizzle — destiné au formulaire
57
+ * d'édition Studio (futur) et à la documentation générée.
58
+ */
59
+ function drizzleConfigJsonSchema() {
60
+ return z.toJSONSchema(drizzleConfigSchema);
61
+ }
62
+ //#endregion
63
+ export { defineDrizzleConfig, drizzleConfigJsonSchema };