@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.
- package/LICENSE +544 -0
- package/README.md +162 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +105 -0
- package/dist/nodefony/command/migrateShared.js +247 -0
- package/dist/nodefony/command/orm-generate.js +356 -0
- package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
- package/dist/nodefony/command/orm-migrate-repair.js +114 -0
- package/dist/nodefony/command/orm-migrate-status.js +67 -0
- package/dist/nodefony/command/orm-migrate.js +141 -0
- package/dist/nodefony/command/orm-reset.js +166 -0
- package/dist/nodefony/config/config.js +107 -0
- package/dist/nodefony/config/defineModuleConfig.js +63 -0
- package/dist/nodefony/entity/auditEventEntity.js +93 -0
- package/dist/nodefony/entity/colKit.js +260 -0
- package/dist/nodefony/entity/idempotencyEntity.js +74 -0
- package/dist/nodefony/entity/sessionEntity.js +75 -0
- package/dist/nodefony/entity/tokenEntity.js +198 -0
- package/dist/nodefony/entity/totpSecretEntity.js +98 -0
- package/dist/nodefony/entity/userTable.js +141 -0
- package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
- package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
- package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/migrations-schema/mysql.js +48 -0
- package/dist/nodefony/migrations-schema/postgres.js +48 -0
- package/dist/nodefony/migrations-schema/sqlite.js +48 -0
- package/dist/nodefony/registerStores.js +218 -0
- package/dist/nodefony/service/DrizzleService.js +282 -0
- package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
- package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
- package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
- package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
- package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
- package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
- package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
- package/dist/nodefony/src/SessionStorage.js +259 -0
- package/dist/nodefony/src/connectorTarget.js +59 -0
- package/dist/nodefony/src/likeSql.js +50 -0
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
- package/dist/nodefony/src/migrator/adopt.js +553 -0
- package/dist/nodefony/src/migrator/appSchema.js +414 -0
- package/dist/nodefony/src/migrator/catalog.js +76 -0
- package/dist/nodefony/src/migrator/destructive.js +213 -0
- package/dist/nodefony/src/migrator/divergence.js +84 -0
- package/dist/nodefony/src/migrator/drivers/index.js +39 -0
- package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
- package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
- package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
- package/dist/nodefony/src/migrator/explain.js +565 -0
- package/dist/nodefony/src/migrator/hash.js +47 -0
- package/dist/nodefony/src/migrator/history.js +219 -0
- package/dist/nodefony/src/migrator/index.js +16 -0
- package/dist/nodefony/src/migrator/kit.js +296 -0
- package/dist/nodefony/src/migrator/name.js +68 -0
- package/dist/nodefony/src/migrator/paths.js +88 -0
- package/dist/nodefony/src/migrator/refusals.js +143 -0
- package/dist/nodefony/src/migrator/resolve.js +281 -0
- package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
- package/dist/nodefony/src/migrator/sources.js +419 -0
- package/dist/nodefony/src/migrator/status.js +231 -0
- package/dist/nodefony/src/migrator/types.js +91 -0
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
- package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
- package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
- package/dist/nodefony/src/orm-core/index.js +4 -0
- package/dist/nodefony/src/queryKit.js +318 -0
- package/dist/nodefony/src/safeTarget.js +55 -0
- package/dist/types/index.d.ts +76 -0
- package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
- package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
- package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
- package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
- package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
- package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
- package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
- package/dist/types/nodefony/config/config.d.ts +110 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
- package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
- package/dist/types/nodefony/entity/colKit.d.ts +130 -0
- package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
- package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
- package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
- package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
- package/dist/types/nodefony/entity/userTable.d.ts +65 -0
- package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
- package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
- package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
- package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
- package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
- package/dist/types/nodefony/registerStores.d.ts +52 -0
- package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
- package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
- package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
- package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
- package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
- package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
- package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
- package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
- package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
- package/dist/types/nodefony/src/likeSql.d.ts +29 -0
- package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
- package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
- package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
- package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
- package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
- package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
- package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
- package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
- package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
- package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
- package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
- package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
- package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
- package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
- package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
- package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
- package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
- package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
- package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
- package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
- package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
- package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
- package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
- package/dist/types/nodefony/src/queryKit.d.ts +136 -0
- package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
- package/docs/index.md +954 -0
- package/docs/migrations.md +691 -0
- package/migrations/mysql/0000_framework_init.sql +137 -0
- package/migrations/mysql/meta/_journal.json +13 -0
- package/migrations/postgres/0000_framework_init.sql +128 -0
- package/migrations/postgres/meta/_journal.json +13 -0
- package/migrations/sqlite/0000_framework_init.sql +127 -0
- package/migrations/sqlite/meta/_journal.json +13 -0
- 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 };
|