@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,419 @@
1
+ import { FORMAT_MARKER, MigrationVerdictError, STATEMENT_BREAKPOINT } from "./types.js";
2
+ import { frameworkMigrationsDir } from "./paths.js";
3
+ import { migrationHash, normalizeSql } from "./hash.js";
4
+ import path from "node:path";
5
+ import fs from "node:fs/promises";
6
+ //#region nodefony/src/migrator/sources.ts
7
+ /**
8
+ * Version du journal drizzle-kit que cet applicateur sait lire.
9
+ *
10
+ * On adopte le format d'un outil tiers : le lire « au mieux » reviendrait à
11
+ * découvrir un défaut de découpe APRÈS publication, quand le corriger
12
+ * changerait le sens de fichiers déjà livrés chez des utilisateurs.
13
+ */
14
+ const SUPPORTED_JOURNAL_VERSIONS = ["7"];
15
+ /**
16
+ * Ordonne un registre de sources : rang croissant, nom en départage.
17
+ *
18
+ * `framework` porte le rang 0 (les entités d'application peuvent référencer ses
19
+ * tables), `app` le rang le plus élevé. Entre les deux, les modules à leur
20
+ * ordre de chargement. Le nom départage à rang égal pour que l'ordre soit
21
+ * **déterministe** : deux pods qui liraient le même registre dans deux ordres
22
+ * différents appliqueraient deux plans différents.
23
+ *
24
+ * @param sources - registre, dans n'importe quel ordre.
25
+ * @returns le même registre, trié.
26
+ */
27
+ function orderSources(sources) {
28
+ return [...sources].sort((a, b) => a.rank - b.rank || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
29
+ }
30
+ /**
31
+ * Charge toutes les sources d'un registre pour un dialecte donné.
32
+ *
33
+ * @param sources - registre de sources (espace de noms ouvert).
34
+ * @param dialect - dialecte du connecteur ; sélectionne le sous-dossier.
35
+ * @returns les fichiers ordonnés et les sources absentes, nommées.
36
+ * @throws MigrationVerdictError si un fichier ne porte pas le format attendu.
37
+ */
38
+ async function loadSources(sources, dialect) {
39
+ const files = [];
40
+ const absent = [];
41
+ for (const source of orderSources(sources)) {
42
+ const dir = path.join(source.dir, dialect);
43
+ const journal = await readJournal(dir, source.name);
44
+ if (journal === null) {
45
+ absent.push(source.name);
46
+ continue;
47
+ }
48
+ for (const entry of [...journal.entries].sort((a, b) => a.idx - b.idx)) files.push(await readMigrationFile(dir, source.name, entry, dialect));
49
+ }
50
+ return {
51
+ files,
52
+ absent
53
+ };
54
+ }
55
+ /**
56
+ * Lit le journal d'une source, ou `null` si la source n'est pas installée.
57
+ *
58
+ * @param dir - dossier `<source>/<dialecte>`.
59
+ * @param source - nom logique de la source, pour nommer un refus.
60
+ * @returns le journal validé, ou `null` si le dossier n'existe pas.
61
+ * @throws MigrationVerdictError si la version du journal n'est pas reconnue.
62
+ */
63
+ async function readJournal(dir, source) {
64
+ const file = path.join(dir, "meta", "_journal.json");
65
+ let raw;
66
+ try {
67
+ raw = await fs.readFile(file, "utf8");
68
+ } catch (e) {
69
+ if (e.code === "ENOENT") return null;
70
+ throw e;
71
+ }
72
+ let journal;
73
+ try {
74
+ journal = JSON.parse(raw);
75
+ } catch (e) {
76
+ throw new MigrationVerdictError({
77
+ code: "NF_MIGRATE_UNKNOWN_FORMAT",
78
+ connector: "",
79
+ source,
80
+ facts: {
81
+ file,
82
+ cause: e instanceof Error ? e.message : String(e)
83
+ },
84
+ nextActions: []
85
+ }, `Le journal des migrations « ${file} » n'est pas lisible : ${e instanceof Error ? e.message : String(e)}. La cause la plus fréquente est un conflit de fusion non résolu — deux branches ayant généré chacune une migration. Ouvrir le fichier, résoudre le conflit, et vérifier que chaque migration du dossier y figure une fois. La base n'est pour rien dans cette erreur.`);
86
+ }
87
+ if (!Array.isArray(journal.entries)) throw new MigrationVerdictError({
88
+ code: "NF_MIGRATE_UNKNOWN_FORMAT",
89
+ connector: "",
90
+ source,
91
+ facts: { file },
92
+ nextActions: []
93
+ }, `Le journal des migrations « ${file} » ne porte pas de liste « entries » : ce n'est pas un journal que ce format sait lire.`);
94
+ if (!SUPPORTED_JOURNAL_VERSIONS.includes(String(journal.version))) throw new MigrationVerdictError({
95
+ code: "NF_MIGRATE_UNKNOWN_FORMAT",
96
+ connector: "",
97
+ source,
98
+ facts: {
99
+ file,
100
+ journalVersion: String(journal.version),
101
+ supported: SUPPORTED_JOURNAL_VERSIONS
102
+ },
103
+ nextActions: [{
104
+ command: "npm update @nodefony/drizzle",
105
+ args: ["update", "@nodefony/drizzle"]
106
+ }]
107
+ }, `Le journal de migrations de la source « ${source} » est en version ${String(journal.version)}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
108
+ return journal;
109
+ }
110
+ /**
111
+ * Lit un fichier de migration, valide son format et découpe ses statements.
112
+ *
113
+ * @param dir - dossier `<source>/<dialecte>`.
114
+ * @param source - nom logique de la source.
115
+ * @param entry - entrée du journal.
116
+ * @param dialect - dialecte du connecteur ; choisit la grammaire de découpe.
117
+ * @returns le fichier chargé, empreinte comprise.
118
+ * @throws MigrationVerdictError si le marqueur de format est absent ou autre.
119
+ */
120
+ async function readMigrationFile(dir, source, entry, dialect) {
121
+ const file = path.join(dir, `${entry.tag}.sql`);
122
+ let content;
123
+ try {
124
+ content = await fs.readFile(file, "utf8");
125
+ } catch (cause) {
126
+ throw new MigrationVerdictError({
127
+ code: "NF_MIGRATE_JOURNAL_MISMATCH",
128
+ connector: "",
129
+ source,
130
+ tag: entry.tag,
131
+ facts: {
132
+ file,
133
+ reason: cause instanceof Error ? cause.message : String(cause)
134
+ },
135
+ nextActions: [{
136
+ command: "nodefony orm:migrate:status --json",
137
+ args: ["orm:migrate:status", "--json"]
138
+ }]
139
+ }, `Le journal de la source « ${source} » annonce la migration « ${entry.tag} », mais son fichier « ${file} » est introuvable. Cette source est incohérente avec elle-même : rien n'a été appliqué, et l'applicateur ne devine jamais le contenu d'un fichier absent. Deux issues, et le statut ci-dessus dit laquelle : si « ${entry.tag} » figure dans l'historique, la migration a DÉJÀ été appliquée et c'est le fichier qu'il faut rendre (contrôle de version) ; si elle n'y figure pas, retirer son entrée de « ${path.join(dir, "meta", "_journal.json")} ». Ne PAS refaire la base : elle porte des données, et rien ici ne les concerne.`);
140
+ }
141
+ const normalized = normalizeSql(content);
142
+ const marker = normalized.split("\n", 1)[0]?.trim() ?? "";
143
+ if (marker !== "-- nodefony:migration format=1") throw new MigrationVerdictError({
144
+ code: "NF_MIGRATE_UNKNOWN_FORMAT",
145
+ connector: "",
146
+ source,
147
+ tag: entry.tag,
148
+ facts: {
149
+ file,
150
+ found: marker,
151
+ expected: FORMAT_MARKER
152
+ },
153
+ nextActions: [{
154
+ command: `nodefony orm:generate --custom --name ${entry.tag}`,
155
+ args: [
156
+ "orm:generate",
157
+ "--custom",
158
+ "--name",
159
+ entry.tag
160
+ ]
161
+ }]
162
+ }, `Le fichier « ${file} » ne porte pas le format de migration attendu (« ${FORMAT_MARKER} » ; lu : « ${marker} »). Il n'a PAS été appliqué. Un fichier de migration ne s'écrit pas à la main : « --custom » dépose le gabarit, son marqueur et son entrée de journal, et c'est dedans que le SQL libre se met. Sinon, poser « ${FORMAT_MARKER} » en première ligne du fichier.`);
163
+ return {
164
+ source,
165
+ tag: entry.tag,
166
+ idx: entry.idx,
167
+ hash: migrationHash(content),
168
+ statements: splitStatements(normalized, dialect),
169
+ path: file
170
+ };
171
+ }
172
+ /**
173
+ * Découpe un fichier de migration en statements exécutables.
174
+ *
175
+ * ## Le séparateur EST un commentaire SQL — et c'est tout le problème
176
+ *
177
+ * `--> statement-breakpoint` commence par deux tirets : pour le moteur, c'est
178
+ * un commentaire, et rien ne le distingue d'un autre à l'œil nu. Deux ordres
179
+ * naïfs échouent donc, chacun pour sa raison, et les deux ont été constatés :
180
+ *
181
+ * - **découper puis retirer les commentaires** fait d'un séparateur écrit DANS
182
+ * un commentaire un vrai séparateur. La ligne est coupée en deux, et le
183
+ * fragment de droite — qui ne commence plus par deux tirets — part au pilote
184
+ * comme une instruction. Le produit se le faisait à lui-même : le gabarit
185
+ * qu'écrit `orm:generate --custom` porte la phrase qui NOMME le séparateur,
186
+ * si bien que toute migration libre écrite en suivant son aide échouait sur
187
+ * une erreur de syntaxe, en laissant dans l'historique une migration `failed`
188
+ * — c'est-à-dire une base bloquée, à réparer à la main ;
189
+ * - **retirer les commentaires puis découper** emporte les séparateurs
190
+ * eux-mêmes, et fond toutes les instructions du fichier en une seule.
191
+ *
192
+ * Il n'y a donc pas d'ordre à trouver : les deux décisions se prennent au MÊME
193
+ * moment, ligne par ligne, en sachant si l'on est dans une chaîne littérale.
194
+ * Un commentaire ne peut pas ouvrir un commentaire : dès que la ligne commence
195
+ * par deux tirets sans être le séparateur, tout ce qu'elle porte est du texte.
196
+ *
197
+ * Le séparateur n'est pas reconnu « ligne entière » pour autant : drizzle-kit
198
+ * le colle en fin d'instruction (`CREATE INDEX …;--> statement-breakpoint`), et
199
+ * l'exiger seul sur sa ligne ferait fusionner toutes les instructions d'une
200
+ * migration du framework.
201
+ *
202
+ * ## La grammaire de chaîne est celle du MOTEUR, pas une moyenne des trois
203
+ *
204
+ * Savoir si l'on est dans une chaîne n'a pas la même réponse partout : MySQL
205
+ * échappe l'apostrophe par une contre-oblique, PostgreSQL délimite les corps
206
+ * par `$tag$`. Une grammaire fausse ne lève AUCUNE erreur — le scanner croit
207
+ * sortir d'une chaîne où il est encore, la ligne suivante commençant par deux
208
+ * tirets est retirée comme un commentaire alors qu'elle est de la DONNÉE, ou
209
+ * un séparateur porté par un texte coupe l'instruction en deux. La migration
210
+ * s'inscrit ensuite en succès avec l'empreinte du fichier entier : plus aucun
211
+ * verdict ne peut le voir. Le dialecte est donc un paramètre REQUIS, jamais un
212
+ * défaut — le fichier vit déjà sous `<source>/<dialecte>/`, l'appelant l'a.
213
+ *
214
+ * @param normalized - contenu normalisé (fins de ligne en LF).
215
+ * @param dialect - dialecte du connecteur ; choisit la grammaire de chaîne.
216
+ * @returns les statements non vides, dans l'ordre du fichier.
217
+ */
218
+ function splitStatements(normalized, dialect) {
219
+ const grammar = STRING_GRAMMARS[dialect];
220
+ const statements = [];
221
+ let current = [];
222
+ const curseur = {
223
+ open: "",
224
+ coupe: -1
225
+ };
226
+ /** Clôt le statement en cours — vide, il ne compte pas. */
227
+ const clore = () => {
228
+ const statement = current.join("\n").trim();
229
+ if (statement.length > 0) statements.push(statement);
230
+ current = [];
231
+ curseur.open = "";
232
+ };
233
+ for (const line of normalized.split("\n")) {
234
+ if (curseur.open === "") {
235
+ const tete = line.trimStart();
236
+ if (tete.startsWith("--> statement-breakpoint")) {
237
+ clore();
238
+ continue;
239
+ }
240
+ if (tete.startsWith("--")) continue;
241
+ scanLine(line, STATEMENT_BREAKPOINT, grammar, curseur);
242
+ const coupe = curseur.coupe;
243
+ if (coupe >= 0) {
244
+ current.push(line.slice(0, coupe));
245
+ clore();
246
+ const remainder = line.slice(coupe + STATEMENT_BREAKPOINT.length);
247
+ if (remainder.trim() !== "") {
248
+ current.push(remainder);
249
+ scanLine(remainder, "", grammar, curseur);
250
+ }
251
+ continue;
252
+ }
253
+ current.push(line);
254
+ continue;
255
+ }
256
+ current.push(line);
257
+ scanLine(line, "", grammar, curseur);
258
+ }
259
+ clore();
260
+ return statements;
261
+ }
262
+ /**
263
+ * La grammaire de chaîne de chaque moteur supporté.
264
+ *
265
+ * SQLite suit le standard et rien de plus : une contre-oblique y est un
266
+ * caractère ordinaire, et `$$` n'est pas un délimiteur. Lui appliquer la
267
+ * grammaire de MySQL serait aussi faux que l'inverse — c'est pourquoi le
268
+ * routage se fait dans les DEUX sens, et pas seulement pour ajouter des cas.
269
+ */
270
+ const STRING_GRAMMARS = {
271
+ sqlite: {
272
+ backslashInSingleQuote: false,
273
+ apostropheEchappeePrefixee: false,
274
+ dollarQuote: false
275
+ },
276
+ postgres: {
277
+ backslashInSingleQuote: false,
278
+ apostropheEchappeePrefixee: true,
279
+ dollarQuote: true
280
+ },
281
+ mysql: {
282
+ backslashInSingleQuote: true,
283
+ apostropheEchappeePrefixee: false,
284
+ dollarQuote: false
285
+ }
286
+ };
287
+ /** Vrai si `c` peut faire partie d'un identifiant SQL non quoté. */
288
+ function isIdentifierChar(c) {
289
+ return c !== void 0 && /[A-Za-z0-9_$]/.test(c);
290
+ }
291
+ /**
292
+ * Balaye une ligne : met à jour l'état de chaîne, et repère le motif.
293
+ *
294
+ * UNE seule implémentation pour les deux questions que pose la découpe — « où
295
+ * couper ? » et « la ligne suivante peut-elle porter un commentaire ? ». Elles
296
+ * se répondaient auparavant dans deux fonctions jumelles, chacune avec sa copie
297
+ * de la grammaire : deux copies divergent, et chacune reste verte dans son
298
+ * propre test.
299
+ *
300
+ * Le balayage s'ARRÊTE sur le motif : l'appelant repart alors du reste de la
301
+ * ligne, avec une chaîne close par construction (un statement neuf commence).
302
+ * Passer un motif vide balaye donc la ligne entière.
303
+ *
304
+ * @param line - ligne à balayer.
305
+ * @param needle - texte cherché hors chaîne ; vide pour ne rien chercher.
306
+ * @param grammar - grammaire de chaîne du moteur visé.
307
+ * @param curseur - état lu ET écrit : chaîne ouverte, indice du motif.
308
+ */
309
+ function scanLine(line, needle, grammar, curseur) {
310
+ curseur.coupe = -1;
311
+ let open = curseur.open;
312
+ for (let i = 0; i < line.length; i += 1) {
313
+ const c = line[i];
314
+ if (open.startsWith("$")) {
315
+ if (line.startsWith(open, i)) {
316
+ i += open.length - 1;
317
+ open = "";
318
+ }
319
+ continue;
320
+ }
321
+ if (open !== "") {
322
+ if (c === "\\" && (grammar.backslashInSingleQuote || open === "e'")) {
323
+ i += 1;
324
+ continue;
325
+ }
326
+ if (c === "'") {
327
+ if (line[i + 1] === "'") {
328
+ i += 1;
329
+ continue;
330
+ }
331
+ open = "";
332
+ }
333
+ continue;
334
+ }
335
+ if (c === "'") {
336
+ open = "'";
337
+ continue;
338
+ }
339
+ if (grammar.apostropheEchappeePrefixee && (c === "E" || c === "e") && line[i + 1] === "'" && !isIdentifierChar(line[i - 1])) {
340
+ open = "e'";
341
+ i += 1;
342
+ continue;
343
+ }
344
+ if (grammar.dollarQuote && c === "$") {
345
+ const tag = delimiteurDollar(line, i);
346
+ if (tag !== null) {
347
+ open = tag;
348
+ i += tag.length - 1;
349
+ continue;
350
+ }
351
+ }
352
+ if (needle !== "" && line.startsWith(needle, i)) {
353
+ curseur.coupe = i;
354
+ curseur.open = open;
355
+ return;
356
+ }
357
+ }
358
+ curseur.open = open;
359
+ }
360
+ /**
361
+ * Le délimiteur `$tag$` qui commence à `debut`, ou `null`.
362
+ *
363
+ * Le tag est optionnel (`$$`) et suit les règles d'un identifiant : ce qui
364
+ * exclut `$1`, un paramètre de requête, qu'il ne faut surtout pas lire comme
365
+ * l'ouverture d'une chaîne.
366
+ *
367
+ * @param line - ligne balayée.
368
+ * @param debut - indice du `$` candidat.
369
+ * @returns le délimiteur complet, ou `null` si ce n'en est pas un.
370
+ */
371
+ function delimiteurDollar(line, debut) {
372
+ let i = debut + 1;
373
+ while (i < line.length && /[A-Za-z0-9_]/.test(line[i])) {
374
+ if (i === debut + 1 && /[0-9]/.test(line[i])) return null;
375
+ i += 1;
376
+ }
377
+ return line[i] === "$" ? line.slice(debut, i + 1) : null;
378
+ }
379
+ /**
380
+ * Noms des tables qu'un lot de migrations CRÉE.
381
+ *
382
+ * Sert la garde d'adoption : un historique vide alors que ces tables existent
383
+ * déjà signale une base antérieure aux migrations, pas une base neuve. La
384
+ * liste est DÉRIVÉE des fichiers — une liste codée en dur mentirait dès la
385
+ * première migration livrée par un module tiers.
386
+ *
387
+ * @param files - fichiers de migration chargés.
388
+ * @returns les noms de tables, sans doublon.
389
+ */
390
+ function createdTables(files) {
391
+ const found = /* @__PURE__ */ new Set();
392
+ const pattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z0-9_$]+)[`"']?/gi;
393
+ for (const file of files) for (const statement of file.statements) for (const match of statement.matchAll(pattern)) found.add(match[1]);
394
+ return [...found];
395
+ }
396
+ /**
397
+ * Tables que le FRAMEWORK construit — dérivées de ses fichiers de migration.
398
+ *
399
+ * Dérivées, jamais listées à la main : une liste codée en dur mentirait dès la
400
+ * première migration livrée par une version suivante.
401
+ *
402
+ * Une seule implémentation, parce que deux appelants en dépendent pour la même
403
+ * décision — le générateur les exclut de ce qu'il écrit, l'adoption les exclut
404
+ * de ce qu'elle lit. Deux copies divergeraient en silence, chacune verte dans
405
+ * son propre test.
406
+ *
407
+ * @param dialect - dialecte du connecteur visé.
408
+ * @returns les noms de tables du framework.
409
+ */
410
+ async function frameworkTables(dialect) {
411
+ const { files } = await loadSources([{
412
+ name: "framework",
413
+ dir: await frameworkMigrationsDir(),
414
+ rank: 0
415
+ }], dialect);
416
+ return createdTables(files);
417
+ }
418
+ //#endregion
419
+ export { SUPPORTED_JOURNAL_VERSIONS, createdTables, frameworkTables, loadSources, orderSources, splitStatements };
@@ -0,0 +1,231 @@
1
+ import { MigrationVerdictError } from "./types.js";
2
+ import { action, buildReport } from "./explain.js";
3
+ import { buildMigrator, readMigrationEnv, resetAllowed, resolveConnector } from "./resolve.js";
4
+ import { describeResolutionRefusal, moduleAbsent } from "./refusals.js";
5
+ import { describeDivergence } from "./divergence.js";
6
+ import { describeTargetSafely } from "../safeTarget.js";
7
+ //#region nodefony/src/migrator/status.ts
8
+ /**
9
+ * Habille un refus en charge utile publiée — la MÊME que celle de `--json`.
10
+ *
11
+ * @param connector - connecteur demandé.
12
+ * @param refusal - ce qu'il y a à en dire.
13
+ * @returns la charge utile d'arrêt.
14
+ */
15
+ function failureFrom(connector, refusal) {
16
+ return {
17
+ formatVersion: 1,
18
+ connector,
19
+ exitCode: refusal.exitCode,
20
+ error: {
21
+ code: refusal.code,
22
+ summary: refusal.summary,
23
+ meaning: refusal.meaning,
24
+ nextActions: refusal.nextActions
25
+ }
26
+ };
27
+ }
28
+ /**
29
+ * Compose la charge utile d'un état — le SEUL endroit où le contexte du rendu
30
+ * est assemblé.
31
+ *
32
+ * Les quatre commandes de migration ET le plan d'administration publient le
33
+ * même objet ; recopier son assemblage les faisait déjà diverger d'un champ à
34
+ * l'autre, et le prochain consommateur à naître aurait oublié celui du jour.
35
+ * La troisième source ne se paie qu'ici, une fois : `describeDivergence`
36
+ * s'abstient toute seule quand le verdict est déjà décidé.
37
+ *
38
+ * @param plan - plan calculé par l'applicateur, en lecture seule.
39
+ * @param resolution - connecteur prêt (porte le mode de schéma effectif).
40
+ * @param config - configuration validée du module.
41
+ * @param kernel - kernel courant, pour constater l'environnement.
42
+ * @returns la charge utile, prête pour `--json` comme pour l'écran.
43
+ */
44
+ async function composeReport(plan, resolution, config, kernel) {
45
+ const mode = config.migrations.divergence;
46
+ return buildReport(plan, {
47
+ ddl: resolution.ddl,
48
+ divergence: mode === "off" ? null : await describeDivergence(plan),
49
+ divergenceMode: mode,
50
+ canReset: resetAllowed(readMigrationEnv(kernel)),
51
+ target: describeTargetSafely(resolution.target),
52
+ fromMigrateUrl: resolution.fromMigrateUrl
53
+ });
54
+ }
55
+ /**
56
+ * Résout le connecteur pour les trois verbes — ou rend le refus.
57
+ *
58
+ * Les trois posent la même question dans le même ordre : le module est-il
59
+ * chargé, le connecteur existe-t-il, est-il migrable. Trois copies auraient
60
+ * fini par répondre trois choses différentes au même cas.
61
+ *
62
+ * @param wanted - connecteur demandé.
63
+ * @param config - configuration validée, `null` si le module n'est pas chargé.
64
+ * @param kernel - kernel courant.
65
+ * @returns le connecteur prêt et sa configuration, ou le refus.
66
+ */
67
+ function prepare(wanted, config, kernel) {
68
+ if (!config) return {
69
+ ok: false,
70
+ failure: failureFrom(wanted, moduleAbsent())
71
+ };
72
+ const resolution = resolveConnector(wanted, config, readMigrationEnv(kernel), kernel, { allowMigrateUrl: false });
73
+ if (resolution.kind !== "ready") return {
74
+ ok: false,
75
+ failure: failureFrom(wanted, describeResolutionRefusal(wanted, resolution, config))
76
+ };
77
+ return {
78
+ ok: true,
79
+ resolution,
80
+ config
81
+ };
82
+ }
83
+ /**
84
+ * Traduit n'importe quel échec d'exécution en charge utile publiée.
85
+ *
86
+ * Un refus de l'applicateur porte DÉJÀ son verdict et ses gestes : on les rend
87
+ * tels quels. Tout le reste — base injoignable, droits manquants, verrou
88
+ * impossible — est une panne, et l'écran doit la MONTRER : un tableau vide qui
89
+ * ressemble à « tout va bien » est pire qu'une erreur.
90
+ *
91
+ * @param connector - connecteur concerné.
92
+ * @param e - ce qui a été levé.
93
+ * @returns la charge utile d'arrêt.
94
+ */
95
+ function failure(connector, e) {
96
+ if (e instanceof MigrationVerdictError) return {
97
+ formatVersion: 1,
98
+ connector,
99
+ exitCode: e.verdict.code === "NF_MIGRATE_LOCK_TIMEOUT" ? 2 : 1,
100
+ error: {
101
+ code: e.verdict.code,
102
+ summary: e.message,
103
+ meaning: "",
104
+ nextActions: [...e.verdict.nextActions]
105
+ }
106
+ };
107
+ return {
108
+ formatVersion: 1,
109
+ connector,
110
+ exitCode: 2,
111
+ error: {
112
+ code: "NF_MIGRATE_UNAVAILABLE",
113
+ summary: `Le connecteur « ${connector} » n'a pas pu être lu : ${e instanceof Error ? e.message : String(e)}`,
114
+ meaning: "Toucher une base échoue quand celle-ci est injoignable, quand le compte n'a pas le droit de lire l'historique, ou quand le verrou est tenu par un autre travail.",
115
+ nextActions: [action(`nodefony orm:migrate:status --connector ${connector}`)]
116
+ }
117
+ };
118
+ }
119
+ /**
120
+ * Lit l'état des migrations d'un connecteur, sans rien appliquer.
121
+ *
122
+ * @param wanted - connecteur demandé (`default` quand rien n'est précisé).
123
+ * @param config - configuration validée du module, `null` s'il n'est pas chargé.
124
+ * @param kernel - kernel courant.
125
+ * @returns l'état, ou le refus qui explique pourquoi il n'y en a pas.
126
+ */
127
+ async function migrationStatusFor(wanted, config, kernel) {
128
+ const prepared = prepare(wanted, config, kernel);
129
+ if (!prepared.ok) return prepared;
130
+ try {
131
+ return {
132
+ ok: true,
133
+ report: await composeReport(await (await buildMigrator(prepared.resolution, prepared.config, kernel)).status(), prepared.resolution, prepared.config, kernel)
134
+ };
135
+ } catch (e) {
136
+ return {
137
+ ok: false,
138
+ failure: failure(prepared.resolution.connector, e)
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Ce qui S'APPLIQUERAIT, avec son SQL — lecture seule.
144
+ *
145
+ * Sert la confirmation d'un geste d'application : une modification de schéma
146
+ * ne se confirme pas sur une promesse, elle se confirme sur les instructions
147
+ * qui vont être exécutées.
148
+ *
149
+ * @param wanted - connecteur demandé.
150
+ * @param config - configuration validée du module, `null` s'il n'est pas chargé.
151
+ * @param kernel - kernel courant.
152
+ * @returns le plan, ou le refus.
153
+ */
154
+ async function migrationPlanFor(wanted, config, kernel) {
155
+ const prepared = prepare(wanted, config, kernel);
156
+ if (!prepared.ok) return prepared;
157
+ try {
158
+ const plan = await (await buildMigrator(prepared.resolution, prepared.config, kernel)).status();
159
+ return {
160
+ ok: true,
161
+ plan: {
162
+ formatVersion: 1,
163
+ connector: plan.connector,
164
+ pending: plan.pending.map((f) => ({
165
+ source: f.source,
166
+ tag: f.tag,
167
+ statements: [...f.statements]
168
+ }))
169
+ }
170
+ };
171
+ } catch (e) {
172
+ return {
173
+ ok: false,
174
+ failure: failure(prepared.resolution.connector, e)
175
+ };
176
+ }
177
+ }
178
+ /**
179
+ * Applique les migrations en attente — **DÉVELOPPEMENT seulement**.
180
+ *
181
+ * 🔴 Le refus hors développement n'est pas une précaution d'interface, c'est la
182
+ * doctrine : en production, les migrations s'appliquent dans un travail
183
+ * d'orchestrateur qui se termine AVANT que le premier nouvel exemplaire ne
184
+ * démarre. Les appliquer au clic de quelqu'un qui regarde une console pendant
185
+ * que le trafic passe, c'est modifier un schéma sous les pieds des exemplaires
186
+ * en service. La garde vit ICI, dans le produit — jamais dans l'écran, qui ne
187
+ * protège que celui qui le regarde.
188
+ *
189
+ * @param wanted - connecteur demandé.
190
+ * @param config - configuration validée du module, `null` s'il n'est pas chargé.
191
+ * @param kernel - kernel courant.
192
+ * @returns ce qui a été appliqué, ou le refus.
193
+ */
194
+ async function applyMigrationsFor(wanted, config, kernel) {
195
+ const env = readMigrationEnv(kernel);
196
+ if (!resetAllowed(env)) return {
197
+ ok: false,
198
+ failure: failureFrom(wanted, {
199
+ code: "NF_MIGRATE_NOT_DEVELOPMENT",
200
+ summary: "Appliquer les migrations depuis la console d'administration est réservé au développement. Rien n'a été appliqué.",
201
+ meaning: "En production, les migrations s'appliquent dans un travail dédié qui se termine AVANT que le premier nouvel exemplaire ne démarre — le fichier `deploy/migrate-job.yaml` d'une application générée en est la recette. Les appliquer depuis un serveur qui sert le trafic reviendrait à changer le schéma sous les pieds des exemplaires en service.",
202
+ nextActions: [action(`nodefony orm:migrate --connector ${wanted}`), action(`nodefony orm:migrate:status --connector ${wanted}`)],
203
+ exitCode: 1
204
+ })
205
+ };
206
+ const prepared = prepare(wanted, config, kernel);
207
+ if (!prepared.ok) return prepared;
208
+ try {
209
+ const run = await (await buildMigrator(prepared.resolution, prepared.config, kernel)).migrate();
210
+ return {
211
+ ok: true,
212
+ run: {
213
+ formatVersion: 1,
214
+ connector: prepared.resolution.connector,
215
+ runId: run.runId,
216
+ applied: run.applied.map((a) => ({
217
+ source: a.source,
218
+ tag: a.tag,
219
+ executionMs: a.executionMs
220
+ }))
221
+ }
222
+ };
223
+ } catch (e) {
224
+ return {
225
+ ok: false,
226
+ failure: failure(prepared.resolution.connector, e)
227
+ };
228
+ }
229
+ }
230
+ //#endregion
231
+ export { applyMigrationsFor, composeReport, failureFrom, migrationPlanFor, migrationStatusFor };