@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,553 @@
1
+ import { openMigrationDriver } from "./drivers/index.js";
2
+ import { runIntrospect } from "./kit.js";
3
+ import { postgresSchemaOf, writeKitConfig } from "./appSchema.js";
4
+ import path from "node:path";
5
+ import fs from "node:fs/promises";
6
+ //#region nodefony/src/migrator/adopt.ts
7
+ /**
8
+ * Les coordonnées de connexion, dans la forme que l'outil d'introspection lit.
9
+ *
10
+ * SQLite désigne un FICHIER, les autres une URL — et c'est le seul endroit où
11
+ * cette distinction se fait, pour qu'elle ne se redécide pas à chaque appelant.
12
+ *
13
+ * @param target - cible résolue du connecteur.
14
+ * @returns l'URL de connexion, ou `null` si la cible n'en porte aucune.
15
+ */
16
+ function introspectionUrl(target) {
17
+ if (target.dialect === "sqlite") return target.filename ?? null;
18
+ return target.url ?? null;
19
+ }
20
+ /** L'en-tête que l'outil pose devant un corps commenté. */
21
+ const INTROSPECTION_HEADER = /^\s*--[^\n]*generated after introspecting[^\n]*\n(?:\s*--[^\n]*\n)*/u;
22
+ /**
23
+ * Rend exécutable le corps qu'une introspection a mis en commentaire.
24
+ *
25
+ * Fonction PURE — c'est ce qui la rend éprouvable sans base et sans outil
26
+ * tiers : une règle qui exige un sous-processus pour être vue rouge n'est
27
+ * jamais vue rouge.
28
+ *
29
+ * Ne touche à RIEN si la forme attendue n'est pas là : rendre `null` laisse
30
+ * l'appelant dire ce qu'il a constaté, au lieu de publier un fichier tronqué
31
+ * par une expression écrite pour une autre version de l'outil.
32
+ *
33
+ * @param sql - le fichier tel que l'outil l'a écrit.
34
+ * @returns le SQL exécutable, ou `null` si aucun bloc commenté n'a été trouvé.
35
+ */
36
+ function uncommentIntrospection(sql) {
37
+ const withoutHeader = sql.replace(INTROSPECTION_HEADER, "");
38
+ const debut = withoutHeader.indexOf("/*");
39
+ const fin = withoutHeader.lastIndexOf("*/");
40
+ if (debut === -1 || fin === -1 || fin < debut) return null;
41
+ const body = withoutHeader.slice(debut + 2, fin);
42
+ if (`${withoutHeader.slice(0, debut)}${withoutHeader.slice(fin + 2)}`.trim() !== "") return null;
43
+ return `${body.trim()}\n`;
44
+ }
45
+ /**
46
+ * Lit le journal des FICHIERS d'un dossier de migrations.
47
+ *
48
+ * ⚠️ Ce journal n'est pas l'historique. Il vit dans le dépôt, il est versionné,
49
+ * et il dit ce que le code CONNAÎT. L'historique, lui, vit dans la base et dit
50
+ * ce qu'ELLE a reçu. Les confondre fait conclure « tout est appliqué » sur une
51
+ * base qui n'a rien vu.
52
+ *
53
+ * @param outDir - dossier `<migrations>/<dialecte>`.
54
+ * @returns le journal, ou `null` s'il n'y en a pas encore.
55
+ */
56
+ async function readJournal(outDir) {
57
+ try {
58
+ const raw = await fs.readFile(path.join(outDir, "meta", "_journal.json"), "utf8");
59
+ return JSON.parse(raw);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Donne à la migration de référence le nom que l'utilisateur a choisi.
66
+ *
67
+ * L'outil d'introspection tire un nom au hasard (`0000_futuristic_spectrum`) et
68
+ * n'accepte pas qu'on le lui impose. Or ce tag est une IDENTITÉ : il est
69
+ * enregistré dans chaque base qui reçoit la migration, et il ne se renomme plus
70
+ * ensuite. Le fixer ici, une fois, avant que quiconque l'ait vu.
71
+ *
72
+ * @param outDir - dossier `<migrations>/<dialecte>`.
73
+ * @param oldTag - tag tiré par l'outil.
74
+ * @param newTag - tag voulu.
75
+ * @returns le chemin du fichier SQL, sous son nom final.
76
+ */
77
+ async function renameTag(outDir, oldTag, newTag) {
78
+ const targetFile = path.join(outDir, `${newTag}.sql`);
79
+ if (oldTag !== newTag) {
80
+ await fs.rename(path.join(outDir, `${oldTag}.sql`), targetFile);
81
+ const journal = await readJournal(outDir);
82
+ if (journal) {
83
+ for (const entry of journal.entries) if (entry.tag === oldTag) entry.tag = newTag;
84
+ await fs.writeFile(path.join(outDir, "meta", "_journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
85
+ }
86
+ }
87
+ return targetFile;
88
+ }
89
+ /**
90
+ * Retire les modules que l'introspection dépose à côté de sa migration.
91
+ *
92
+ * `schema.ts` et `relations.ts` décrivent la base en TypeScript — utile à qui
93
+ * repart de zéro, hors sujet ici : les entités de l'application sont déjà la
94
+ * source du schéma. Les laisser dans le dossier des migrations y mettrait deux
95
+ * descriptions du même schéma, dont une que personne ne met à jour.
96
+ *
97
+ * @param outDir - dossier `<migrations>/<dialecte>`.
98
+ */
99
+ async function dropIntrospectionModules(outDir) {
100
+ await Promise.all(["schema.ts", "relations.ts"].map((f) => fs.rm(path.join(outDir, f), { force: true })));
101
+ }
102
+ /**
103
+ * Les tables que l'instantané fraîchement écrit décrit.
104
+ *
105
+ * Lues sur l'instantané et non sur le SQL : c'est lui qui sert de référence au
106
+ * prochain diff, donc c'est lui qui dit ce qui a réellement été adopté. Le
107
+ * fichier `.sql`, lui, n'est qu'un rendu.
108
+ *
109
+ * @param outDir - dossier `<migrations>/<dialecte>`.
110
+ * @returns les noms de tables, ou `[]` si l'instantané est illisible.
111
+ */
112
+ async function snapshotTables(outDir) {
113
+ try {
114
+ const raw = await fs.readFile(path.join(outDir, "meta", "0000_snapshot.json"), "utf8");
115
+ const snapshot = JSON.parse(raw);
116
+ return Object.values(snapshot.tables ?? {}).map((t, i) => t.name ?? Object.keys(snapshot.tables ?? {})[i] ?? "");
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+ /**
122
+ * Rend utilisable une référence que l'introspection vient d'écrire.
123
+ *
124
+ * L'outil rend ce qu'il a LU, fidèlement. Deux fidélités rendent pourtant le
125
+ * fichier inutilisable, et il faut les défaire — c'est la même famille de
126
+ * geste que le décommentage du corps : l'artefact est exact, personne ne peut
127
+ * s'en servir.
128
+ *
129
+ * ## 1. Les objets d'une table EXCLUE
130
+ *
131
+ * L'exclusion porte sur les tables, jamais sur ce qui gravite autour. La table
132
+ * d'historique est écartée — c'est le framework qui la crée —, mais sa
133
+ * SÉQUENCE, elle, entre dans la référence. Deux conséquences, toutes deux
134
+ * constatées sur un serveur : la référence rejouée sur un environnement neuf
135
+ * échoue (la séquence existe déjà, posée par les migrations du framework), et
136
+ * la génération suivante propose de la SUPPRIMER — ce que la base refuse,
137
+ * puisque la table d'historique en dépend. L'adoption se retrouve alors dans
138
+ * l'état qu'elle existe pour éviter : un historique en place, et plus aucune
139
+ * commande qui passe.
140
+ *
141
+ * ## 2. La qualification de schéma
142
+ *
143
+ * Une application peut vivre dans un schéma autre que `public` — le montage
144
+ * habituel d'une base mutualisée, obtenu par le chemin de recherche de la
145
+ * connexion. Les entités, elles, ne portent aucun schéma : `pgTable("article")`
146
+ * désigne `public.article` pour l'outil de comparaison, quand l'introspection
147
+ * rend `nf_app.article`. Au premier champ ajouté, l'outil voit une table qui
148
+ * disparaît et une autre qui apparaît, et demande s'il s'agit d'un RENOMMAGE :
149
+ * sans terminal la commande s'arrête, avec un terminal répondre « oui » écrit
150
+ * un `ALTER TABLE … RENAME` qui déplacerait les données.
151
+ *
152
+ * On retire donc la qualification, ce qui rétablit la symétrie avec les
153
+ * entités. À l'exécution, le chemin de recherche de la connexion place les
154
+ * tables là où elles doivent être : c'est déjà lui qui décide, l'adoption
155
+ * cesse simplement de le contredire.
156
+ *
157
+ * ## 3. Le booléen de MySQL
158
+ *
159
+ * `BOOLEAN` est un SYNONYME de `TINYINT(1)` en MySQL : le serveur ne garde que
160
+ * la forme physique, et l'introspection ne peut donc rendre que `tinyint(1)`.
161
+ * L'outil de comparaison, lui, compare des noms de type : il voit une
162
+ * différence là où la base n'en a aucune, et propose un `MODIFY COLUMN` sur
163
+ * chaque booléen — refusé ensuite comme destructif. Une application MySQL qui
164
+ * adopte sa base se retrouve donc bloquée au premier champ ajouté, pour une
165
+ * table qu'elle n'a jamais touchée.
166
+ *
167
+ * La forme déclarée est rétablie. Elle ne peut écraser aucune intention : dans
168
+ * une entité, `tinyint()` rend `tinyint` sans largeur — vérifié au source
169
+ * (`drizzle-orm/mysql-core/columns/tinyint.js`) —, et seul `boolean()` produit
170
+ * `tinyint(1)`. Sur un DDL écrit à la main hors de l'ORM, la réécriture reste
171
+ * exacte : les deux formes désignent la même colonne.
172
+ *
173
+ * @param outDir - dossier `<migrations>/<dialecte>`.
174
+ * @param options.dialect - dialecte lu ; seul MySQL a le synonyme booléen.
175
+ * @param file - fichier SQL de la référence.
176
+ * @param options.schema - schéma effectivement lu, ou `null` (hors PostgreSQL,
177
+ * ou lecture dans `public` : il n'y a alors rien à déqualifier).
178
+ * @param options.excludedTables - tables écartées de la lecture, dont les
179
+ * objets satellites doivent l'être aussi.
180
+ * @param options.stripTables - tables que l'outil a LUES faute de pouvoir les
181
+ * exclure, et qu'il faut retirer du résultat (cf `lectureSansExclusion`).
182
+ */
183
+ async function normalizeIntrospection(outDir, file, options) {
184
+ const { schema, excludedTables } = options;
185
+ const stripTables = options.stripTables ?? [];
186
+ const qualifie = schema !== null && schema !== "public";
187
+ /** Une séquence appartient-elle à une table qu'on a écartée ? */
188
+ const satellite = (objectName) => excludedTables.some((t) => objectName.startsWith(`${t}_`));
189
+ const snapshotFile = path.join(outDir, "meta", "0000_snapshot.json");
190
+ const removedSequences = [];
191
+ /** Colonnes booléennes rendues à leur forme déclarée (MySQL). */
192
+ let booleans = 0;
193
+ let brut = null;
194
+ try {
195
+ brut = await fs.readFile(snapshotFile, "utf8");
196
+ } catch {
197
+ brut = null;
198
+ }
199
+ if (brut !== null) {
200
+ const doc = JSON.parse(brut);
201
+ let changed = false;
202
+ if (doc.sequences !== void 0) {
203
+ const sequences = {};
204
+ for (const [key, seq] of Object.entries(doc.sequences)) {
205
+ const sequenceName = seq.name ?? key.split(".").pop() ?? key;
206
+ if (satellite(sequenceName)) {
207
+ removedSequences.push(sequenceName);
208
+ changed = true;
209
+ continue;
210
+ }
211
+ const publicKey = qualifie && key.startsWith(`${schema}.`) ? `public.${key.slice(schema.length + 1)}` : key;
212
+ sequences[publicKey] = qualifie ? {
213
+ ...seq,
214
+ schema: "public"
215
+ } : seq;
216
+ if (publicKey !== key) changed = true;
217
+ }
218
+ doc.sequences = sequences;
219
+ }
220
+ if (qualifie && doc.tables !== void 0) {
221
+ const tables = {};
222
+ for (const [key, table] of Object.entries(doc.tables)) {
223
+ const publicKey = key.startsWith(`${schema}.`) ? `public.${key.slice(schema.length + 1)}` : key;
224
+ tables[publicKey] = {
225
+ ...table,
226
+ schema: ""
227
+ };
228
+ if (publicKey !== key) changed = true;
229
+ }
230
+ doc.tables = tables;
231
+ if (doc.schemas !== void 0) doc.schemas = {};
232
+ }
233
+ if (options.dialect === "mysql" && doc.tables !== void 0) {
234
+ for (const table of Object.values(doc.tables)) for (const column of Object.values(table.columns ?? {})) if (column.type === "tinyint(1)") {
235
+ column.type = "boolean";
236
+ booleans += 1;
237
+ changed = true;
238
+ }
239
+ }
240
+ if (stripTables.length > 0 && doc.tables !== void 0) {
241
+ const toRemove = new Set(stripTables);
242
+ const kept = {};
243
+ for (const [key, table] of Object.entries(doc.tables)) {
244
+ if (toRemove.has(key.split(".").pop() ?? key)) {
245
+ changed = true;
246
+ continue;
247
+ }
248
+ kept[key] = table;
249
+ }
250
+ doc.tables = kept;
251
+ }
252
+ if (changed) await fs.writeFile(snapshotFile, JSON.stringify(doc, null, 2), "utf8");
253
+ }
254
+ if (!qualifie && removedSequences.length === 0 && stripTables.length === 0 && booleans === 0) return;
255
+ let sql = await fs.readFile(file, "utf8");
256
+ if (booleans > 0) sql = sql.replace(/\btinyint\(1\)/giu, "boolean");
257
+ if (qualifie) sql = sql.replace(new RegExp(`^CREATE SCHEMA "${schema}";\\s*(?:-->[^\\n]*\\n)?`, "gmu"), "").replaceAll(`"${schema}".`, "");
258
+ for (const removedSequence of removedSequences) sql = sql.replace(new RegExp(`^CREATE SEQUENCE "${removedSequence}"[^;]*;(?:\\s*-->[^\\n]*)?\\n?`, "gmu"), "");
259
+ for (const tableName of stripTables) {
260
+ const ident = `[\`"]?${tableName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}[\`"]?`;
261
+ sql = sql.replace(new RegExp(`^CREATE TABLE ${ident} \\([\\s\\S]*?^\\);(?:\\s*-->[^\\n]*)?\\n?`, "gmu"), "").replace(new RegExp(`^CREATE (?:UNIQUE )?INDEX [^;]*? ON ${ident}[^;]*;(?:\\s*-->[^\\n]*)?\\n?`, "gmu"), "").replace(new RegExp(`^ALTER TABLE ${ident}[^;]*;(?:\\s*-->[^\\n]*)?\\n?`, "gmu"), "");
262
+ }
263
+ await fs.writeFile(file, sql, "utf8");
264
+ }
265
+ /**
266
+ * Parmi les tables qu'on s'apprête à CRÉER, celles que la base porte déjà.
267
+ *
268
+ * 🔴 **La base est la seule source, et elle est INTERROGÉE.** Il serait tentant
269
+ * de déduire la présence d'une table de son absence dans une comparaison au
270
+ * code : c'est ce que faisait la première version, et le raccourci a produit un
271
+ * refus mensonger. La comparaison ne connaît que le REGISTRE — les entités
272
+ * qu'une application enregistre à son démarrage — alors que la génération part
273
+ * des FICHIERS, qui en contiennent d'autres : celles d'un module désactivé,
274
+ * d'un connecteur différent, ou d'un module pas encore câblé. Une table hors
275
+ * registre n'est ni manquante ni présente : elle est INCONNUE, et déduire sa
276
+ * présence faisait refuser la première migration d'une application en nommant
277
+ * sept tables qui n'existaient nulle part — puis renvoyait vers une adoption
278
+ * qui échouait pour la raison inverse. Deux commandes qui se prescrivent l'une
279
+ * l'autre en se refusant : une impasse, exactement celle que ce chantier
280
+ * existe pour fermer.
281
+ *
282
+ * Le lecteur est INJECTÉ plutôt que construit ici : c'est ce qui rend la règle
283
+ * éprouvable sans kernel ni serveur, et une règle qu'on ne peut pas voir rouge
284
+ * n'est pas une règle.
285
+ *
286
+ * Le coût est d'une requête de catalogue par table, et l'appelant ne s'en sert
287
+ * que lorsque le journal est vide — une fois dans la vie d'une application.
288
+ *
289
+ * @param reader - lecteur de catalogue ouvert sur la base visée.
290
+ * @param tables - tables que la migration créerait.
291
+ * @returns les tables déjà en base, dans l'ordre reçu.
292
+ */
293
+ async function tablesPresentIn(reader, tables) {
294
+ const presentTables = [];
295
+ for (const candidateTable of tables) if (await reader.tableExists(candidateTable)) presentTables.push(candidateTable);
296
+ return presentTables;
297
+ }
298
+ /**
299
+ * Ce que le serveur dit de LUI-MÊME — constaté, jamais déduit d'un port.
300
+ *
301
+ * Sert uniquement à expliquer un échec : c'est le seul moment où la question
302
+ * se pose, et le coût d'une requête est alors sans importance.
303
+ *
304
+ * @param target - cible du connecteur.
305
+ * @returns la bannière de version, ou `null` si le serveur n'a pas répondu.
306
+ */
307
+ async function serverVersion(target) {
308
+ try {
309
+ const driver = await openMigrationDriver(target);
310
+ try {
311
+ return (await driver.query("SELECT VERSION() AS v"))[0]?.v ?? null;
312
+ } finally {
313
+ await driver.close();
314
+ }
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ /**
320
+ * Explique l'échec d'une lecture de schéma, avec ce qu'on en SAIT.
321
+ *
322
+ * L'outil meurt sans un mot — code non nul, sortie d'erreur vide. Le laisser
323
+ * remonter tel quel enverrait chercher du côté des identifiants ou du réseau,
324
+ * là où il n'y a rien. Une cause est connue et reproductible, elle mérite
325
+ * d'être nommée.
326
+ *
327
+ * @param target - cible du connecteur.
328
+ * @param cause - l'erreur telle que l'outil l'a produite.
329
+ * @returns l'erreur à lever.
330
+ */
331
+ async function explainFailure(target, cause) {
332
+ const version = await serverVersion(target);
333
+ if (version !== null && /mariadb/i.test(version)) return new Error(`La lecture du schéma a échoué sur ce serveur (MariaDB ${version}).\n\n MariaDB ne porte pas de type JSON natif : il l'écrit en « longtext »\n assorti d'une contrainte « CHECK (json_valid(…)) ». L'outil qui lit les\n schémas ne sait pas lire ces contraintes, et il s'arrête sans rien dire —\n y compris quand les tables concernées ne sont PAS celles qu'on adopte :\n il lit la base ENTIÈRE avant de filtrer. Les tables du framework en\n portent, donc le cas est systématique ici.\n\n Le repli, sur ce serveur : écrire la référence à la main.\n 1. relever le schéma existant — SHOW CREATE TABLE <table>\n 2. nodefony orm:generate --custom --name base_existante\n 3. coller ce schéma dans le fichier produit\n 4. nodefony orm:migrate:baseline\n\n Sur MySQL Community, PostgreSQL et SQLite, « --from-database » fait ces\n quatre gestes seul.`, { cause });
334
+ return cause;
335
+ }
336
+ /**
337
+ * Les contraintes d'unicité de COLONNE que l'introspection n'a pas rendues.
338
+ *
339
+ * 🔴 En SQLite, `col text UNIQUE` ne crée pas d'index nommé : le moteur pose un
340
+ * index INTERNE (`sqlite_autoindex_…`) que l'outil d'introspection ne liste
341
+ * pas. La référence adoptée sortait donc SANS la contrainte, alors que la base
342
+ * la porte — et un index unique COMPOSITE, lui, survivait, ce qui rendait
343
+ * l'écart d'autant plus difficile à voir.
344
+ *
345
+ * La perte ne se constate pas sur la base adoptée : elle a déjà sa contrainte.
346
+ * Elle frappe la base SUIVANTE — un environnement de test, un exemplaire neuf —
347
+ * recréée depuis ce fichier, qui accepte alors des doublons que le schéma
348
+ * interdisait. Aucune erreur n'est levée : c'est la donnée qui devient fausse.
349
+ *
350
+ * On les rend sous forme de `CREATE UNIQUE INDEX`, et non en réécrivant le
351
+ * `CREATE TABLE` : un index séparé est portable, s'ajoute sans toucher au corps
352
+ * produit par l'outil, et porte exactement la même garantie.
353
+ *
354
+ * Les autres moteurs n'ont pas ce défaut — leur catalogue expose la contrainte,
355
+ * et l'introspection la rend (vérifié : PostgreSQL et MySQL passent le cas qui
356
+ * fait tomber SQLite).
357
+ *
358
+ * @param target - cible du connecteur adopté.
359
+ * @param tables - tables retenues par l'adoption.
360
+ * @param sql - la référence telle que l'outil l'a écrite.
361
+ * @returns les instructions à ajouter, vides s'il n'en manque aucune.
362
+ */
363
+ /**
364
+ * Les colonnes d'index, telles que PostgreSQL les RÉÉCRIT lui-même.
365
+ *
366
+ * 🔴 L'introspection rend UNE classe d'opérateur pour tout l'index et
367
+ * l'applique à CHAQUE colonne. Sur un index composite dont les colonnes n'ont
368
+ * pas le même type, le résultat est du SQL qui ne s'exécute pas :
369
+ * `("author" timestamptz_ops, "created_at" timestamptz_ops)` quand `author`
370
+ * est un `uuid` — « operator class "timestamptz_ops" does not accept data
371
+ * type uuid ».
372
+ *
373
+ * La perte ne se constate pas sur la base adoptée : elle a déjà son index.
374
+ * Elle frappe l'exemplaire SUIVANT — et elle frappe mal, car la migration
375
+ * s'arrête sur ce `CREATE INDEX` : aucune table suivante n'est créée, et les
376
+ * erreurs que l'on voit ensuite ne parlent plus que de tables absentes, à
377
+ * l'autre bout de la chaîne. Même famille que les contraintes d'unicité
378
+ * perdues : l'outil est fidèle à ce qu'il croit avoir lu, pas à la base.
379
+ *
380
+ * On ne DEVINE pas la bonne classe, on la demande au moteur. `pg_indexes`
381
+ * rend la définition que PostgreSQL écrirait lui-même pour recréer l'index,
382
+ * et il n'y nomme que les classes qui ne sont PAS celles du type — ce qui
383
+ * préserve une classe volontaire (`varchar_pattern_ops`) sans jamais en
384
+ * inventer une.
385
+ *
386
+ * @param target - coordonnées de la base visée.
387
+ * @param schema - schéma PostgreSQL où les tables ont été lues.
388
+ * @param tables - tables adoptées, dont on relit les index.
389
+ * @returns pour chaque nom d'index, la liste de colonnes que le moteur écrit.
390
+ */
391
+ async function actualIndexColumns(target, schema, tables) {
392
+ const byName = /* @__PURE__ */ new Map();
393
+ if (target.dialect !== "postgres" || tables.length === 0) return byName;
394
+ const driver = await openMigrationDriver(target);
395
+ try {
396
+ const placeholders = tables.map(() => "?").join(",");
397
+ const rows = await driver.query(`SELECT indexname, indexdef FROM pg_indexes
398
+ WHERE schemaname = ? AND tablename IN (${placeholders})`, [schema, ...tables]);
399
+ for (const row of rows) {
400
+ const columns = /\(([^()]*(?:\([^()]*\)[^()]*)*)\)\s*$/u.exec(row.indexdef);
401
+ if (columns?.[1] !== void 0) byName.set(row.indexname, columns[1].trim());
402
+ }
403
+ } finally {
404
+ await driver.close();
405
+ }
406
+ return byName;
407
+ }
408
+ /**
409
+ * Réécrit les listes de colonnes d'index que l'outil a rendues fausses.
410
+ *
411
+ * Agit sur les DEUX artefacts, et c'est nécessaire : le fichier SQL est ce
412
+ * qu'on applique, l'instantané est ce à quoi la génération SUIVANTE compare.
413
+ * Ne corriger que le premier laisserait un instantané qui décrit un index que
414
+ * la base n'a pas — la génération d'après proposerait de le refaire, sans que
415
+ * personne comprenne pourquoi.
416
+ *
417
+ * @param sql - corps exécutable de la référence.
418
+ * @param outDir - dossier de sortie, qui porte `meta/0000_snapshot.json`.
419
+ * @param actual - listes de colonnes rendues par le moteur, par nom d'index.
420
+ * @returns le SQL corrigé.
421
+ */
422
+ async function rewriteCompositeIndexes(sql, outDir, actual) {
423
+ if (actual.size === 0) return sql;
424
+ let fixed = sql;
425
+ for (const [indexName, columns] of actual) {
426
+ const echappe = indexName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
427
+ fixed = fixed.replace(new RegExp(`(CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+"${echappe}"[^;]*?USING\\s+\\w+\\s*)\\([^;]*?\\)`, "giu"), `$1(${columns})`);
428
+ }
429
+ const snapshotFile = path.join(outDir, "meta", "0000_snapshot.json");
430
+ try {
431
+ const doc = JSON.parse(await fs.readFile(snapshotFile, "utf8"));
432
+ let changed = false;
433
+ for (const table of Object.values(doc.tables ?? {})) for (const [key, index] of Object.entries(table.indexes ?? {})) {
434
+ const columns = actual.get(index.name ?? key);
435
+ if (columns === void 0) continue;
436
+ for (const column of index.columns ?? []) {
437
+ const columnName = column.expression;
438
+ const named = columnName === void 0 || column.isExpression === true ? void 0 : new RegExp(`(?:^|,)\\s*"?${columnName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}"?\\s+(\\w+_ops)\\b`, "u").exec(columns)?.[1];
439
+ if (named === void 0) {
440
+ if (column.opclass !== void 0) {
441
+ delete column.opclass;
442
+ changed = true;
443
+ }
444
+ } else if (column.opclass !== named) {
445
+ column.opclass = named;
446
+ changed = true;
447
+ }
448
+ }
449
+ }
450
+ if (changed) await fs.writeFile(snapshotFile, JSON.stringify(doc, null, 2), "utf8");
451
+ } catch {}
452
+ return fixed;
453
+ }
454
+ async function lostColumnUniques(target, tables, sql) {
455
+ if (target.dialect !== "sqlite") return [];
456
+ const additions = [];
457
+ const driver = await openMigrationDriver(target);
458
+ try {
459
+ for (const table of tables) {
460
+ const ident = `"${table.replace(/"/gu, "\"\"")}"`;
461
+ const index = await driver.query(`PRAGMA index_list(${ident})`);
462
+ for (const idx of index) {
463
+ if (Number(idx.unique) !== 1 || idx.origin !== "u") continue;
464
+ const names = (await driver.query(`PRAGMA index_info("${String(idx.name).replace(/"/gu, "\"\"")}")`)).map((c) => c.name).filter((n) => n !== null);
465
+ if (names.length === 0) continue;
466
+ const constraintName = `${table}_${names.join("_")}_key`;
467
+ if (sql.includes(constraintName)) continue;
468
+ const quotedNames = names.map((n) => `\`${n}\``).join(",");
469
+ additions.push(`CREATE UNIQUE INDEX \`${constraintName}\` ON \`${table}\` (${quotedNames});`);
470
+ }
471
+ }
472
+ } finally {
473
+ await driver.close();
474
+ }
475
+ return additions;
476
+ }
477
+ /**
478
+ * Fabrique la migration de RÉFÉRENCE d'une base déjà en place.
479
+ *
480
+ * Orchestration complète du côté fichiers — la connexion est lue, rien n'est
481
+ * écrit dans la base : c'est l'appelant qui décide ensuite d'inscrire cette
482
+ * migration dans l'historique.
483
+ *
484
+ * Le dossier de travail ne survit à rien, ni au succès ni à l'échec : un module
485
+ * temporaire laissé derrière serait relu par le prochain outil qui balaie les
486
+ * sources, et personne ne saurait d'où il sort.
487
+ *
488
+ * @param options - racine du projet, dossier de sortie, cible, tables exclues,
489
+ * tables déclarées, nom voulu pour la migration, dossier de travail.
490
+ * @returns le tag final, le fichier, et si son corps est exécutable.
491
+ * @throws Error si la base ne porte aucune table à adopter, ou si l'outil échoue.
492
+ */
493
+ async function adoptFromDatabase({ projectRoot, outDir, dialect, target, excludedTables, declaredTables, name, workDir }) {
494
+ if (declaredTables.length === 0) throw new Error("Cette application ne déclare aucune table sur ce connecteur : il n'y a rien à adopter.");
495
+ const url = introspectionUrl(target);
496
+ if (url === null) throw new Error("La base visée n'a pas de coordonnées de connexion : impossible de lire son schéma.");
497
+ const configFile = path.join(workDir, `introspect.${dialect}.config.ts`);
498
+ const relatif = (targetFile) => path.relative(projectRoot, targetFile).split(path.sep).join("/");
499
+ const lectureSansExclusion = dialect === "mysql" && excludedTables.length > 0;
500
+ try {
501
+ await writeKitConfig({
502
+ file: configFile,
503
+ projectRoot,
504
+ schemaFile: path.join(workDir, `schema.${dialect}.ts`),
505
+ outDir,
506
+ dialect,
507
+ excludedTables: lectureSansExclusion ? [] : excludedTables,
508
+ dbUrl: url
509
+ });
510
+ try {
511
+ runIntrospect({
512
+ cwd: projectRoot,
513
+ configRel: relatif(configFile),
514
+ label: `la base du connecteur (${dialect})`
515
+ });
516
+ } catch (e) {
517
+ throw await explainFailure(target, e);
518
+ }
519
+ } finally {
520
+ await fs.rm(workDir, {
521
+ recursive: true,
522
+ force: true
523
+ });
524
+ }
525
+ const premiere = (await readJournal(outDir))?.entries[0];
526
+ if (!premiere) throw new Error(`La lecture du schéma n'a produit aucune migration : cette base ne porte aucune des tables de l'application (${declaredTables.join(", ")}).`);
527
+ const tag = `0000_${name}`;
528
+ const file = await renameTag(outDir, premiere.tag, tag);
529
+ await dropIntrospectionModules(outDir);
530
+ const schema = dialect === "postgres" ? postgresSchemaOf(url) : null;
531
+ await normalizeIntrospection(outDir, file, {
532
+ schema,
533
+ excludedTables,
534
+ stripTables: lectureSansExclusion ? excludedTables : [],
535
+ dialect
536
+ });
537
+ const readTables = await snapshotTables(outDir);
538
+ const declarees = new Set(declaredTables);
539
+ const executable = uncommentIntrospection(await fs.readFile(file, "utf8"));
540
+ if (executable !== null) {
541
+ const missing = await lostColumnUniques(target, readTables, executable);
542
+ const fixed = await rewriteCompositeIndexes(missing.length === 0 ? executable : `${executable.trimEnd()}\n--> statement-breakpoint\n${missing.join("\n--> statement-breakpoint\n")}\n`, outDir, await actualIndexColumns(target, schema ?? "public", readTables));
543
+ await fs.writeFile(file, fixed, "utf8");
544
+ }
545
+ return {
546
+ tag,
547
+ file,
548
+ runnable: executable !== null,
549
+ extraTables: readTables.filter((t) => !declarees.has(t))
550
+ };
551
+ }
552
+ //#endregion
553
+ export { adoptFromDatabase, dropIntrospectionModules, introspectionUrl, normalizeIntrospection, readJournal, renameTag, snapshotTables, tablesPresentIn, uncommentIntrospection };