@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,356 @@
1
+ import { HISTORY_TABLE } from "../src/migrator/types.js";
2
+ import { EXIT, action } from "../src/migrator/explain.js";
3
+ import { openMigrationDriver } from "../src/migrator/drivers/index.js";
4
+ import { frameworkMigrationsDir } from "../src/migrator/paths.js";
5
+ import { createdTables, frameworkTables, loadSources } from "../src/migrator/sources.js";
6
+ import { appMigrationsDir } from "../src/migrator/resolve.js";
7
+ import { summarizeGap } from "../src/migrator/schemaDiff.js";
8
+ import { gapAgainstDeclared } from "../src/migrator/divergence.js";
9
+ import { auditMigrationSql, runGenerate, stampFormatMarker } from "../src/migrator/kit.js";
10
+ import { collectTables, entityFilesOf, missingProviders, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule } from "../src/migrator/appSchema.js";
11
+ import { checkMigrationName } from "../src/migrator/name.js";
12
+ import { readJournal, tablesPresentIn } from "../src/migrator/adopt.js";
13
+ import { OrmMigrateCommand } from "./migrateShared.js";
14
+ import { listTargets } from "nodefony";
15
+ import path from "node:path";
16
+ import fs from "node:fs/promises";
17
+ //#region nodefony/command/orm-generate.ts
18
+ /**
19
+ * `kernelEvent: "onPostReady"` — la commande LIT l'état de l'application.
20
+ *
21
+ * Le registre des entités est peuplé au démarrage, module par module. Une
22
+ * commande branchée plus tôt passerait avant lui : elle ne trouverait rien à
23
+ * contrôler, et laisserait passer une migration amputée. Aucun serveur n'écoute
24
+ * pour autant — le profil console est respecté.
25
+ */
26
+ const options = {
27
+ helpGroup: "BASE DE DONNÉES",
28
+ showBanner: false,
29
+ kernelEvent: "onPostReady"
30
+ };
31
+ /**
32
+ * `nodefony orm:generate` — écrit la migration qui fera passer la base au
33
+ * schéma que décrivent les entités de l'application.
34
+ *
35
+ * ## Ce que l'utilisateur n'a pas à connaître
36
+ *
37
+ * Rien de `drizzle-kit`. Ni son installation, ni sa configuration par dialecte,
38
+ * ni la notion de « schéma matérialisé », ni son dossier de sortie, ni le format
39
+ * de son journal. Il tape un verbe et un nom. La commande découvre les fichiers
40
+ * d'entités par la convention du générateur, écrit ce qu'il faut dans un dossier
41
+ * de travail qu'elle efface ensuite, et pilote l'outil — dont elle EXIGE la
42
+ * preuve qu'il a travaillé, parce qu'il rend 0 même quand il échoue.
43
+ *
44
+ * ## Ce qu'elle refuse, et pourquoi elle le refuse plutôt que de continuer
45
+ *
46
+ * Une migration est **immuable une fois appliquée**. Une migration amputée n'est
47
+ * pas un désagrément qu'on rattrape à la génération suivante : elle grave dans
48
+ * l'historique une table qui n'existera jamais, et toute base qui l'a reçue
49
+ * restera incomplète. Trois situations valent donc un arrêt qui NOMME :
50
+ *
51
+ * - un fichier d'entité qui ne s'importe pas (le schéma serait incomplet) ;
52
+ * - une entité enregistrée qu'aucun fichier ne fournit (idem, et c'est le
53
+ * contrôle que le registre est le seul à pouvoir faire) ;
54
+ * - un fichier de l'application qui fournit une table du FRAMEWORK — la
55
+ * migration porterait un second `CREATE TABLE` de cette table, qui échoue sur
56
+ * toute base déjà migrée, c'est-à-dire en production et nulle part ailleurs.
57
+ *
58
+ * @example Le cas courant
59
+ * ```bash
60
+ * nodefony orm:generate --name ajout_du_titre
61
+ * nodefony orm:migrate # ou : le travail de déploiement
62
+ * ```
63
+ *
64
+ * @example Ce que le modèle déclaratif ne peut pas déduire
65
+ * ```bash
66
+ * nodefony orm:generate --custom --name vue_des_ventes
67
+ * # → un fichier SQL vide, déjà inscrit au journal : à écrire à la main.
68
+ * ```
69
+ */
70
+ var OrmGenerate = class extends OrmMigrateCommand {
71
+ constructor(cli) {
72
+ super("orm:generate", "écrit la migration qui aligne la base sur les entités", cli, options);
73
+ this.addSharedOptions();
74
+ this.addOption("-n, --name <nom>", "nom de la migration, en minuscules et « _ » — il entre dans le tag, qui est immuable une fois publié");
75
+ this.addOption("--custom", "écrit un fichier SQL VIDE et son entrée de journal, sans rien déduire : vues, déclencheurs, clés étrangères, remplissages");
76
+ this.addOption("--allow-destructive", "accepte une migration qui SUPPRIME des données — à ne poser qu'après avoir relu le fichier produit");
77
+ }
78
+ /**
79
+ * Racine de l'application — c'est depuis là que l'outil est lancé.
80
+ *
81
+ * @returns le chemin absolu de la racine.
82
+ */
83
+ #projectRoot() {
84
+ return this.kernel.path;
85
+ }
86
+ /**
87
+ * Vérifie le nom, ou arrête la commande en disant pourquoi.
88
+ *
89
+ * @param opts - options reçues.
90
+ * @returns le nom validé, ou `null` si la commande est déjà arrêtée.
91
+ */
92
+ #nameOrFail(opts, connector) {
93
+ const verdict = checkMigrationName(opts.name);
94
+ if (verdict.ok) return verdict.name;
95
+ this.fail(connector, "NF_GENERATE_NAME", verdict.reason, "Le nom entre dans le tag du fichier, et un tag ne se renomme plus une fois la migration appliquée quelque part : c'est lui qui dit à chaque base ce qu'elle a déjà reçu. Il devient aussi un nom de fichier sur trois systèmes — un espace, un accent ou une majuscule produisent un fichier qui ne se retrouve pas d'une machine à l'autre. Choisis-le pour qu'il se lise dans six mois : ce qui change, pas quand.", [action(`nodefony orm:generate --name ${verdict.suggestion ?? "ajout_du_titre"}`)], opts.json, EXIT.actionRequired);
96
+ return null;
97
+ }
98
+ async generate(opts = {}) {
99
+ const resolved = this.resolveOrFail(opts, false);
100
+ if (!resolved) return this;
101
+ const { resolution, config } = resolved;
102
+ const connector = resolution.connector;
103
+ const name = this.#nameOrFail(opts, connector);
104
+ if (name === null) return this;
105
+ const root = this.#projectRoot();
106
+ const migrationsDir = appMigrationsDir(this.kernel, config.migrations.dir);
107
+ if (migrationsDir === void 0) {
108
+ this.fail(connector, "NF_MIGRATE_UNAVAILABLE", "La racine de l'application n'a pas pu être résolue : impossible de savoir où écrire les migrations.", "Le dossier des migrations se résout depuis la racine de l'application, jamais depuis le répertoire courant — sans quoi la commande serait juste ou fausse selon l'endroit d'où on la tape.", [action("nodefony inspect config --json")], opts.json);
109
+ return this;
110
+ }
111
+ const outDir = path.join(migrationsDir, resolution.dialect);
112
+ const relative = (p) => path.relative(root, p).split(path.sep).join("/");
113
+ try {
114
+ if (opts.custom === true) {
115
+ const { tag, file } = await writeCustomMigration({
116
+ outDir,
117
+ dialect: resolution.dialect,
118
+ name
119
+ });
120
+ return this.#emit({
121
+ formatVersion: 1,
122
+ connector,
123
+ generated: true,
124
+ tag,
125
+ files: [relative(file)],
126
+ warnings: [],
127
+ unreadable: [],
128
+ otherDialect: [],
129
+ driver: {
130
+ kind: "sql",
131
+ dialect: resolution.dialect,
132
+ dir: relative(outDir)
133
+ }
134
+ }, opts, `Migration LIBRE ${tag} — le fichier est VIDE, et déjà inscrit au journal.`);
135
+ }
136
+ return await this.#generateFromEntities(opts, connector, resolution.target, root, outDir, name, relative);
137
+ } catch (e) {
138
+ this.failFrom(e, connector, opts.json, resolution.ddl);
139
+ return this;
140
+ }
141
+ }
142
+ /**
143
+ * Le chemin nominal : découvrir, contrôler, générer, prouver.
144
+ *
145
+ * @returns `this`, la commande ayant déjà écrit sa sortie.
146
+ */
147
+ async #generateFromEntities(opts, connector, database, root, outDir, name, relative) {
148
+ const dialect = database.dialect;
149
+ const files = [];
150
+ for (const target of listTargets(root)) files.push(...await entityFilesOf(target.dir));
151
+ const { tables: found, unreadable: rawUnreadable } = await collectTables(files);
152
+ const unreadable = rawUnreadable.map((u) => ({
153
+ file: relative(u.file),
154
+ cause: u.cause
155
+ }));
156
+ const frameworkRoot = path.dirname(await frameworkMigrationsDir());
157
+ const belongsToFramework = (file) => !path.relative(frameworkRoot, file).startsWith("..");
158
+ const mine = found.filter((t) => !belongsToFramework(t.file));
159
+ const tables = mine.filter((t) => t.dialect === dialect);
160
+ const otherDialect = mine.filter((t) => t.dialect !== dialect).map((t) => ({
161
+ table: t.tableName,
162
+ dialect: t.dialect ?? "inconnu",
163
+ file: relative(t.file)
164
+ }));
165
+ const discovery = {
166
+ filesScanned: files.length,
167
+ tables: tables.map((t) => t.tableName),
168
+ otherDialect,
169
+ unreadable
170
+ };
171
+ const framework = new Set(await frameworkTables(dialect));
172
+ const usurped = usurpedTables(tables, framework);
173
+ if (usurped.length > 0) {
174
+ const list = usurped.map((t) => ` • « ${t.tableName} », exportée par ${relative(t.file)} (${t.exportName})`).join("\n");
175
+ this.fail(connector, "NF_GENERATE_FRAMEWORK_TABLE", `L'application fournit ${usurped.length} table(s) qui appartiennent au framework :\n${list}`, "Rien n'a été écrit. Ces tables sont créées par les migrations du framework, appliquées AVANT celles de l'application. Les décrire ici produirait un second « CREATE TABLE » pour la même table : la migration passerait sur une base vierge et échouerait sur toute base déjà migrée — c'est-à-dire en production, et nulle part ailleurs. Pour pointer vers une table du framework, garder la référence dans le code de l'entité sans la RÉ-EXPORTER ; pour une vraie clé étrangère SQL, écrire une migration libre (--custom).", [action(`nodefony orm:generate --custom --name lien_${name}`)], opts.json, EXIT.actionRequired);
176
+ return this;
177
+ }
178
+ const provided = new Set(tables.map((t) => t.tableName));
179
+ const missing = missingProviders(registeredTables(connector), provided, framework).map(({ entity, table }) => ` • entité « ${entity} » → table « ${table} »`);
180
+ if (missing.length > 0) {
181
+ const cause = unreadable.length > 0 ? `\n\nCe sont peut-être ces fichiers, qui n'ont pas pu être lus :\n${unreadable.map((u) => ` • ${u.file} — ${u.cause}`).join("\n")}` : "";
182
+ this.fail(connector, "NF_GENERATE_MISSING_ENTITY", `${missing.length} entité(s) enregistrée(s) ne sont fournies par aucun fichier lisible :\n${missing.join("\n")}${cause}`, "Rien n'a été écrit. Les migrations se produisent à partir des FICHIERS — l'outil qui les écrit est un process séparé, il ne voit pas les objets d'une application démarrée — et ces fichiers sont cherchés sous « nodefony/entity/ » dans l'application et dans chacun de ses modules. Une entité déclarée ailleurs, ou portée par un fichier qui ne s'importe pas seul, est invisible pour la génération : la migration serait écrite SANS sa table, et une migration ne se corrige pas — elle se remplace par une suivante, sur toutes les bases qui ont déjà reçu la première.", [action("nodefony inspect entities --json")], opts.json, EXIT.actionRequired);
183
+ return this;
184
+ }
185
+ const work = path.join(root, "node_modules", ".cache", "nodefony", "orm-generate");
186
+ const schemaFile = path.join(work, `schema.${dialect}.ts`);
187
+ const configFile = path.join(work, `drizzle.${dialect}.config.ts`);
188
+ const before = await this.#tags(outDir);
189
+ const decrites = new Set(createdTables((await loadSources([{
190
+ name: "app",
191
+ dir: path.dirname(outDir),
192
+ rank: 1
193
+ }], dialect)).files));
194
+ if (!tables.some((t) => decrites.has(t.tableName))) {
195
+ const refusal = await this.#alreadyInDatabase(opts, connector, name, tables, database);
196
+ if (refusal !== null) return refusal;
197
+ }
198
+ try {
199
+ await writeSchemaModule(schemaFile, tables);
200
+ await writeKitConfig({
201
+ file: configFile,
202
+ projectRoot: root,
203
+ schemaFile,
204
+ outDir,
205
+ dialect,
206
+ excludedTables: [...framework, HISTORY_TABLE]
207
+ });
208
+ runGenerate({
209
+ cwd: root,
210
+ configRel: relative(configFile),
211
+ name,
212
+ label: `le connecteur « ${connector} » (${dialect})`,
213
+ regenerateCommand: `nodefony orm:generate --name ${name}`
214
+ });
215
+ } finally {
216
+ await fs.rm(work, {
217
+ recursive: true,
218
+ force: true
219
+ });
220
+ }
221
+ const added = (await this.#tags(outDir)).filter((t) => !before.includes(t));
222
+ if (added.length === 0) {
223
+ const gap = await gapAgainstDeclared(connector);
224
+ if (gap !== null) {
225
+ this.fail(connector, "NF_GENERATE_DATABASE_BEHIND", `Rien à écrire, et pourtant la base ne porte pas le schéma déclaré : ${summarizeGap(gap)}.`, "Les fichiers de migration décrivent déjà ce que le code déclare — il n'y a donc rien de neuf à générer. Mais la base, elle, ne l'a pas reçu : son historique affirme des migrations qu'elle n'a pas exécutées. C'est l'HISTORIQUE qu'il faut reprendre, pas le schéma — et surtout pas la base, qu'il ne sert à rien de refaire. Le premier geste MONTRE : le statut dit, source par source, ce que l'historique prétend appliqué. Repère la ou les migrations qui décrivent ce qui manque ci-dessus, puis désinscris-les NOMMÉMENT : elles seront rejouées au passage suivant. La base n'est pas touchée par cette désinscription ; si une migration avait bien été appliquée, son rejeu échouera, et c'est ce qu'on veut.", [
226
+ action(`nodefony orm:migrate:status --connector ${connector} --json`),
227
+ action(`nodefony orm:migrate:repair --connector ${connector} --forget app/<tag>`),
228
+ action(`nodefony orm:migrate --connector ${connector}`)
229
+ ], opts.json, EXIT.actionRequired);
230
+ return this;
231
+ }
232
+ return this.#emit({
233
+ formatVersion: 1,
234
+ connector,
235
+ generated: false,
236
+ tag: null,
237
+ files: [],
238
+ warnings: [],
239
+ unreadable,
240
+ otherDialect,
241
+ driver: {
242
+ kind: "sql",
243
+ dialect,
244
+ dir: relative(outDir)
245
+ }
246
+ }, opts, "Le schéma n'a pas bougé : il n'y avait rien à écrire.");
247
+ }
248
+ stampFormatMarker(outDir);
249
+ const destructive = [];
250
+ const warnings = [];
251
+ const written = [];
252
+ for (const tag of added) {
253
+ const file = path.join(outDir, `${tag}.sql`);
254
+ written.push(relative(file));
255
+ const audit = auditMigrationSql(await fs.readFile(file, "utf8"), dialect);
256
+ destructive.push(...audit.destructive);
257
+ warnings.push(...audit.blocking);
258
+ }
259
+ if (destructive.length > 0 && opts.allowDestructive !== true) {
260
+ const list = destructive.map((r) => ` • ${r.id} : ${r.what}\n → ${r.todo}`).join("\n");
261
+ this.fail(connector, "NF_GENERATE_DESTRUCTIVE", `Cette migration DÉTRUIT des données :\n${list}\n\nLes fichiers ont été écrits — les RELIRE avant toute décision :\n${written.map((f) => ` ${f}`).join("\n")}`, "Ils ne sont pas effacés : ce sont eux qu'il faut lire pour décider, et les supprimer priverait de la seule chose à regarder. C'est leur mise en service qui est refusée. S'il s'agit d'un renommage mal interprété, annuler ces fichiers avec l'outil de gestion de versions puis regénérer dans un terminal interactif, et répondre « renamed » : l'outil produit alors un RENAME, et les données suivent. Si la perte est ASSUMÉE, il n'y a rien à regénérer — les fichiers sont là : c'est l'application qui décide, et elle a sa propre garde.", [
262
+ action(`git checkout -- ${relative(outDir)}`),
263
+ action(`nodefony orm:migrate --connector ${connector} --dry-run`),
264
+ action(`nodefony orm:migrate --connector ${connector}`)
265
+ ], opts.json, EXIT.actionRequired, discovery);
266
+ return this;
267
+ }
268
+ return this.#emit({
269
+ formatVersion: 1,
270
+ connector,
271
+ generated: true,
272
+ tag: added[added.length - 1],
273
+ files: written,
274
+ warnings,
275
+ unreadable,
276
+ otherDialect,
277
+ driver: {
278
+ kind: "sql",
279
+ dialect,
280
+ dir: relative(outDir)
281
+ }
282
+ }, opts, `Migration ${added.join(", ")} écrite depuis ${tables.length} table(s).`);
283
+ }
284
+ /**
285
+ * Demande à la BASE lesquelles de ces tables elle porte déjà.
286
+ *
287
+ * Ouvre un pilote le temps du contrôle, et le referme quoi qu'il arrive : la
288
+ * commande vit dans un processus court, mais une connexion laissée ouverte
289
+ * retient un descripteur et, sur un serveur, une place dans le réservoir.
290
+ *
291
+ * Une base injoignable rend une liste VIDE, jamais une erreur : ce contrôle
292
+ * existe pour éviter un refus infondé, il ne doit pas en créer un. Qui
293
+ * travaille hors connexion garde le droit d'écrire sa migration.
294
+ *
295
+ * @param target - coordonnées de la base visée.
296
+ * @param tables - tables que la migration créerait.
297
+ * @returns celles que la base porte déjà, éventuellement aucune.
298
+ */
299
+ async #presentInDatabase(target, tables) {
300
+ let driver = null;
301
+ try {
302
+ driver = await openMigrationDriver(target);
303
+ return await tablesPresentIn(driver, tables);
304
+ } catch {
305
+ return [];
306
+ } finally {
307
+ if (driver !== null) try {
308
+ await driver.close();
309
+ } catch {}
310
+ }
311
+ }
312
+ /**
313
+ * Refuse d'écrire le schéma initial sur une base qui porte déjà ces tables.
314
+ *
315
+ * @param opts - options reçues (porte le choix du format de sortie).
316
+ * @param connector - connecteur visé.
317
+ * @param name - nom demandé, cité dans le geste à rejouer après adoption.
318
+ * @param tables - tables que l'application fournit pour ce dialecte.
319
+ * @returns `this` si la commande a refusé, `null` s'il n'y a rien à redire.
320
+ */
321
+ async #alreadyInDatabase(opts, connector, name, tables, target) {
322
+ const presentTables = await this.#presentInDatabase(target, tables.map((t) => t.tableName));
323
+ if (presentTables.length === 0) return null;
324
+ const adopter = action(`nodefony orm:migrate:baseline --from-database --connector ${connector}`);
325
+ const complete = presentTables.length === tables.length;
326
+ this.fail(connector, "NF_GENERATE_DATABASE_NOT_ADOPTED", complete ? `Rien n'a été écrit : aucune migration n'existe encore, et la base porte déjà TOUTES les tables déclarées (${presentTables.join(", ")}).` : `Rien n'a été écrit : aucune migration n'existe encore, et la base porte déjà ${presentTables.length} des ${tables.length} tables à créer (${presentTables.join(", ")}).`, complete ? "Une première migration décrit la création du schéma. Écrite ici, elle porterait un « CREATE TABLE » de tables qui existent, avec leurs données : elle ne s'appliquerait jamais. Et comme la base porte déjà tout ce que le code déclare, il n'y a AUCUN écart à écrire. La commande ci-dessous lit le schéma SUR la base et en fait votre première migration — rien n'est exécuté dessus, et elle s'applique telle quelle sur une base vierge. Inutile de regénérer ensuite : il n'y aurait rien de plus à écrire. La suite redevient ordinaire — le champ que vous ajouterez produira un « ALTER TABLE »." : "Une première migration décrit la création du schéma. Écrite ici, elle porterait un « CREATE TABLE » de tables qui existent, avec leurs données : elle ne s'appliquerait jamais, et l'adopter graverait dans l'historique un schéma que la base n'a pas. Cette base doit d'abord être ADOPTÉE — sa migration de référence se lit sur elle, pas sur le code, et rien n'est exécuté dessus. La suite redevient ordinaire : le champ ajouté produit alors un « ALTER TABLE ».", complete ? [adopter] : [adopter, action(`nodefony orm:generate --name ${name} --connector ${connector}`)], opts.json, EXIT.actionRequired);
327
+ return this;
328
+ }
329
+ /**
330
+ * Tags actuellement inscrits au journal d'un dossier de sortie.
331
+ *
332
+ * @param outDir - dossier `<migrations>/<dialecte>`.
333
+ * @returns les tags, ou `[]` si rien n'a encore été généré.
334
+ */
335
+ async #tags(outDir) {
336
+ return ((await readJournal(outDir))?.entries ?? []).map((e) => e.tag);
337
+ }
338
+ /**
339
+ * Écrit le rapport, en machine ou pour un humain.
340
+ *
341
+ * @returns `this`.
342
+ */
343
+ #emit(report, opts, headline) {
344
+ const style = this.style;
345
+ let human = `${style.green("✓")} ${headline}\n`;
346
+ if (report.files.length > 0) human += `\n${report.files.map((f) => ` + ${f}`).join("\n")}\n`;
347
+ if (report.otherDialect.length > 0) human += `\n${style.dim(`Écrites pour un autre moteur, donc hors de cette migration :`)}\n` + report.otherDialect.map((o) => ` • ${o.table} (${o.dialect}) — ${o.file}`).join("\n") + "\n";
348
+ if (report.unreadable.length > 0) human += `\n${style.dim("Fichiers d'entités non lus (aucune entité enregistrée n'en dépend) :")}\n` + report.unreadable.map((u) => ` • ${u.file} — ${u.cause}`).join("\n") + "\n";
349
+ if (report.warnings.length > 0) human += `\n${style.bold("⚠️ À REGARDER avant d'appliquer")} (rien n'est détruit — mais l'application peut cesser de répondre le temps de l'opération, ou la migration ÉCHOUER si la table porte déjà des lignes) :\n` + report.warnings.map((r) => ` • ${r.id} : ${r.what}\n → ${r.todo}`).join("\n") + "\n";
350
+ if (report.generated) human += `\n${style.bold("À faire :")}\n ${style.dim("relire le fichier, puis")} ${style.green("nodefony orm:migrate")}\n`;
351
+ this.respond(report, human, EXIT.ok, opts.json);
352
+ return this;
353
+ }
354
+ };
355
+ //#endregion
356
+ export { OrmGenerate as default };
@@ -0,0 +1,208 @@
1
+ import { HISTORY_TABLE } from "../src/migrator/types.js";
2
+ import { EXIT, MIGRATION_FORMAT_VERSION, action, renderStatus } from "../src/migrator/explain.js";
3
+ import { frameworkTables } from "../src/migrator/sources.js";
4
+ import { appMigrationsDir } from "../src/migrator/resolve.js";
5
+ import { summarizeGap } from "../src/migrator/schemaDiff.js";
6
+ import { gapAgainstDeclared } from "../src/migrator/divergence.js";
7
+ import { stampFormatMarker } from "../src/migrator/kit.js";
8
+ import { registeredTables } from "../src/migrator/appSchema.js";
9
+ import { checkMigrationName } from "../src/migrator/name.js";
10
+ import { adoptFromDatabase, readJournal } from "../src/migrator/adopt.js";
11
+ import { OrmMigrateCommand } from "./migrateShared.js";
12
+ import path from "node:path";
13
+ //#region nodefony/command/orm-migrate-baseline.ts
14
+ const options = {
15
+ helpGroup: "BASE DE DONNÉES",
16
+ showBanner: false,
17
+ kernelEvent: "onPostReady"
18
+ };
19
+ /**
20
+ * `nodefony orm:migrate:baseline` — déclare une base existante « à niveau »,
21
+ * SANS exécuter une seule instruction de schéma.
22
+ *
23
+ * ## Quand on en a besoin
24
+ *
25
+ * La base contient déjà les tables — parce qu'on branche Nodefony sur une base
26
+ * qui existait avant, ou parce que le schéma a été créé autrement (mode `auto`
27
+ * en développement) — mais aucune migration n'y est enregistrée. Appliquer les
28
+ * migrations dans cet état exécuterait des créations de tables qui existent
29
+ * déjà, et échouerait.
30
+ *
31
+ * ## Pourquoi ce n'est PAS automatique
32
+ *
33
+ * Adopter tout seul retirerait le filet qui protège de la pire erreur : se
34
+ * tromper de base. Une adresse de base héritée d'un autre environnement, et
35
+ * l'outil déclarerait « à niveau » une base qui n'a rien à voir — puis
36
+ * appliquerait dessus les migrations suivantes. La documentation de Flyway
37
+ * elle-même met en garde contre son propre mode automatique pour cette raison.
38
+ *
39
+ * **Vérifie que c'est la bonne base avant de taper cette commande.** Elle ne
40
+ * modifie pas le schéma, mais elle change ce que le framework CROIT du schéma —
41
+ * et tout le reste en découle.
42
+ *
43
+ * ## Ce qu'elle fait exactement
44
+ *
45
+ * Elle inscrit dans l'historique, comme appliquées avec succès et une durée
46
+ * nulle, les migrations qui n'y sont pas encore. Rejouée, elle n'inscrit que ce
47
+ * qui manque : elle est donc sûre à relancer.
48
+ *
49
+ * ## Ce qu'elle REFUSE
50
+ *
51
+ * Adopter, c'est affirmer que la base est à l'état que décrivent ces
52
+ * migrations. Quand la base s'écarte du schéma déclaré, l'affirmation serait
53
+ * fausse — et une affirmation fausse gravée dans l'historique n'est rattrapable
54
+ * par aucune commande. La commande constate donc la base AVANT d'écrire, et
55
+ * refuse (`NF_MIGRATE_BASELINE_AMBIGUOUS`) en nommant ce qui manque.
56
+ *
57
+ * Trois cas y échappent, et pour la même raison — la garde n'a plus rien à
58
+ * deviner : `--up-to`, qui borne l'adoption explicitement ; `--from-database`,
59
+ * dont la référence DÉCRIT la base et rend donc l'affirmation vraie par
60
+ * construction ; et une cible détournée par `NF_MIGRATE_DATABASE_URL`, où la
61
+ * comparaison porterait sur la base de la configuration et non sur celle qu'on
62
+ * migre.
63
+ *
64
+ * ## Une base qui n'a JAMAIS eu de migrations — `--from-database`
65
+ *
66
+ * Adopter suppose des fichiers à inscrire. Une application passée du mode
67
+ * dérivé, où le démarrage fabrique le schéma, au mode de production n'en a
68
+ * aucun : sa base porte tout, son dossier de migrations est vide. Sans
69
+ * référence, la première génération décrirait ce que le CODE déclare — un
70
+ * schéma que cette base n'a peut-être jamais eu, si quelqu'un vient de changer
71
+ * une entité — et l'inscrire graverait une affirmation fausse.
72
+ *
73
+ * `--from-database` LIT le schéma de la base et en écrit la migration de
74
+ * référence, puis l'inscrit. Rien n'est exécuté sur la base. La suite redevient
75
+ * ordinaire : le champ ajouté produit un `ALTER TABLE`.
76
+ *
77
+ * @example Adopter jusqu'à une migration précise
78
+ * ```bash
79
+ * nodefony orm:migrate:baseline --up-to 0003_audit_severity
80
+ * nodefony orm:migrate # applique la suite normalement
81
+ * ```
82
+ *
83
+ * @example Reprendre une base qui existait avant toute migration
84
+ * ```bash
85
+ * nodefony orm:migrate:baseline --from-database # la référence est LUE sur la base
86
+ * nodefony orm:generate --name ajout_du_slug # produit un ALTER, plus un CREATE
87
+ * nodefony orm:migrate # applique
88
+ * ```
89
+ */
90
+ var OrmMigrateBaseline = class extends OrmMigrateCommand {
91
+ constructor(cli) {
92
+ super("orm:migrate:baseline", "déclare une base déjà peuplée comme à niveau", cli, options);
93
+ this.addSharedOptions();
94
+ this.addOption("--up-to <tag>", "dernière migration inscrite (incluse) — ex. 0003_audit_severity ; toutes si omis");
95
+ this.addOption("--from-database", "LIT le schéma de la base pour en écrire la migration de référence, puis l'inscrit — pour une base qui existait avant toute migration");
96
+ this.addOption("--name <nom>", "nom de la migration de référence écrite par --from-database (défaut : base_existante)");
97
+ }
98
+ /**
99
+ * Écrit la migration de référence en LISANT la base, puis l'inscrit.
100
+ *
101
+ * ## Pourquoi la référence vient de la base, et non du code
102
+ *
103
+ * Le générateur ne connaît que les fichiers. Sans instantané de départ, il
104
+ * décrit ce que le CODE déclare — c'est-à-dire, si quelqu'un vient de changer
105
+ * une entité, un schéma que la base n'a jamais eu. L'adopter graverait cette
106
+ * affirmation dans l'historique, et la colonne manquante ne s'appliquerait
107
+ * plus jamais. Lue sur la base, la référence est vraie par construction : elle
108
+ * décrit ce qui est là.
109
+ *
110
+ * ## Ce qui est écrit, et ce qui ne l'est pas
111
+ *
112
+ * Un fichier de migration et son instantané, dans le dossier de l'application.
113
+ * **Aucune instruction n'est exécutée sur la base** — l'inscription qui suit
114
+ * déclare appliquée une migration qui décrit un état déjà atteint.
115
+ *
116
+ * @param opts - options reçues.
117
+ * @param resolution - connecteur prêt.
118
+ * @param config - configuration validée du module.
119
+ * @returns ce qui a été écrit, ou `null` si la commande a déjà refusé.
120
+ */
121
+ async #fromDatabase(opts, resolution, config) {
122
+ const root = this.kernel.path;
123
+ const dir = appMigrationsDir(this.kernel, config.migrations.dir);
124
+ if (dir === void 0) {
125
+ this.fail(resolution.connector, "NF_MIGRATE_UNAVAILABLE", "La racine de l'application n'a pas pu être résolue : impossible de savoir où écrire la migration de référence.", "Le dossier des migrations se résout depuis la racine de l'application, jamais depuis le répertoire courant — sans quoi la commande serait juste ou fausse selon l'endroit d'où on la tape.", [action("nodefony inspect config --json")], opts.json);
126
+ return null;
127
+ }
128
+ const outDir = path.join(dir, resolution.dialect);
129
+ const journal = await readJournal(outDir);
130
+ if (journal !== null && journal.entries.length > 0) {
131
+ this.fail(resolution.connector, "NF_MIGRATE_BASELINE_NOT_EMPTY", `Rien n'a été écrit : ce dossier porte déjà ${journal.entries.length} migration(s) (${outDir}).`, "La lecture de la base ne sert qu'à DÉMARRER un historique qui n'existe pas encore. Ici il en existe un : c'est lui qui fait foi, et la base s'adopte alors telle quelle. Si ces migrations décrivent bien l'état de la base, `orm:migrate:baseline` sans option les inscrit ; si elles décrivent autre chose, c'est un écart à regarder, pas une référence à réécrire.", [action(`nodefony orm:migrate:status --connector ${resolution.connector} --json`), action(`nodefony orm:migrate:baseline --connector ${resolution.connector}`)], opts.json, EXIT.actionRequired);
132
+ return null;
133
+ }
134
+ const verdict = checkMigrationName(opts.name ?? "base_existante");
135
+ if (!verdict.ok) {
136
+ this.fail(resolution.connector, "NF_GENERATE_NAME", verdict.reason, "Le nom entre dans le tag de la migration de référence, et un tag ne se renomme plus une fois inscrit : c'est lui qui dit à chaque base ce qu'elle a déjà reçu.", [action(`nodefony orm:migrate:baseline --from-database --name ${verdict.suggestion ?? "base_existante"}`)], opts.json, EXIT.actionRequired);
137
+ return null;
138
+ }
139
+ const adopted = await adoptFromDatabase({
140
+ projectRoot: root,
141
+ outDir,
142
+ dialect: resolution.dialect,
143
+ target: resolution.target,
144
+ excludedTables: [...await frameworkTables(resolution.dialect), HISTORY_TABLE],
145
+ declaredTables: registeredTables(resolution.connector).map((t) => t.table),
146
+ name: verdict.name,
147
+ workDir: path.join(root, "node_modules", ".cache", "nodefony", "orm-adopt")
148
+ });
149
+ stampFormatMarker(outDir);
150
+ return adopted;
151
+ }
152
+ async generate(opts = {}) {
153
+ const resolved = this.resolveOrFail(opts, true);
154
+ if (!resolved) return this;
155
+ const { resolution, config } = resolved;
156
+ const style = this.style;
157
+ try {
158
+ let reference = null;
159
+ if (opts.fromDatabase === true) {
160
+ reference = await this.#fromDatabase(opts, resolution, config);
161
+ if (reference === null) return this;
162
+ }
163
+ const migrator = await this.migrator(resolution, config);
164
+ if (opts.upTo === void 0 && opts.fromDatabase !== true && !resolution.fromMigrateUrl) {
165
+ const gap = await gapAgainstDeclared(resolution.connector);
166
+ if (gap !== null) {
167
+ this.fail(resolution.connector, "NF_MIGRATE_BASELINE_AMBIGUOUS", `La base ne correspond pas au schéma déclaré : ${summarizeGap(gap)}. Rien n'a été inscrit.`, "Adopter reviendrait à déclarer appliquées des migrations que cette base n'a jamais reçues — une affirmation fausse gravée dans l'historique, qu'aucune commande ne peut ensuite rattraper. Dis jusqu'où la base suit avec `--up-to <tag>` : les migrations postérieures resteront en attente et s'appliqueront normalement.", [action(`nodefony orm:migrate:status --connector ${resolution.connector}`), action(`nodefony orm:migrate:baseline --up-to <tag> --connector ${resolution.connector}`)], opts.json, EXIT.actionRequired);
168
+ return this;
169
+ }
170
+ }
171
+ const adopted = await migrator.baseline(opts.upTo);
172
+ const plan = await migrator.status();
173
+ const report = await this.report(plan, resolution, config);
174
+ const payload = {
175
+ ...report,
176
+ adopted: adopted.map((a) => ({
177
+ source: a.source,
178
+ tag: a.tag
179
+ })),
180
+ ...reference === null ? {} : { reference: {
181
+ tag: reference.tag,
182
+ file: reference.file,
183
+ runnable: reference.runnable,
184
+ extraTables: reference.extraTables
185
+ } }
186
+ };
187
+ let human = "";
188
+ if (reference !== null) {
189
+ human += `${style.green(style.bold(`✓ Référence ${reference.tag} écrite en LISANT la base`))} ${style.dim("— aucune instruction n'a été exécutée dessus")}\n ${style.dim(reference.file)}\n`;
190
+ if (reference.extraTables.length > 0) human += `${style.bold("⚠️ Tables LUES sans être déclarées par l'application")} : ${reference.extraTables.join(", ")}.\n ${style.dim("La prochaine génération proposera de les SUPPRIMER — relire le fichier avant.")}\n`;
191
+ if (!reference.runnable) human += `${style.bold("⚠️ Son corps est resté en COMMENTAIRE")} — une base montée depuis ces fichiers sortirait VIDE. Relire le fichier avant de créer un environnement.\n`;
192
+ human += "\n";
193
+ }
194
+ if (adopted.length === 0) human += `${style.green("Rien à déclarer : toutes les migrations connues sont déjà enregistrées.")}\n\n` + renderStatus(report, style);
195
+ else {
196
+ human += `${style.green(style.bold(`✓ ${adopted.length} migration(s) déclarée(s) comme appliquée(s)`))} ${style.dim("— aucun SQL n'a été exécuté")}\n`;
197
+ for (const a of adopted) human += ` ${style.green("=")} ${a.source}/${a.tag}\n`;
198
+ human += `\n${renderStatus(report, style)}`;
199
+ }
200
+ this.respond(payload, human, report.exitCode, opts.json);
201
+ } catch (e) {
202
+ this.failFrom(e, resolution.connector, opts.json, resolution.ddl);
203
+ }
204
+ return this;
205
+ }
206
+ };
207
+ //#endregion
208
+ export { MIGRATION_FORMAT_VERSION, action, OrmMigrateBaseline as default };