@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,414 @@
|
|
|
1
|
+
import { FORMAT_MARKER } from "./types.js";
|
|
2
|
+
import "./kit.js";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
4
|
+
import { entityRegistry } from "@nodefony/orm-core";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Table, getTableName, is } from "drizzle-orm";
|
|
7
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
8
|
+
import { PgTable } from "drizzle-orm/pg-core";
|
|
9
|
+
import { MySqlTable } from "drizzle-orm/mysql-core";
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
//#region nodefony/src/migrator/appSchema.ts
|
|
13
|
+
/**
|
|
14
|
+
* Ce qu'une APPLICATION donne à lire à `drizzle-kit` — et comment on s'assure
|
|
15
|
+
* qu'elle lui donne bien TOUT.
|
|
16
|
+
*
|
|
17
|
+
* ## Le mécanisme, et pourquoi c'est celui-là
|
|
18
|
+
*
|
|
19
|
+
* `drizzle-kit` est un process séparé qui lit des **fichiers** : il ne sait rien
|
|
20
|
+
* d'un registre vivant. Et une entité enregistrée ne porte **aucune provenance
|
|
21
|
+
* de fichier** — `IEntity.schema` est un objet d'exécution. On ne peut donc pas
|
|
22
|
+
* « matérialiser le schéma depuis le registre » : cette voie a été explorée et
|
|
23
|
+
* écartée, elle n'existe pas.
|
|
24
|
+
*
|
|
25
|
+
* Le sens est l'inverse : **les fichiers fournissent, le registre valide.**
|
|
26
|
+
*
|
|
27
|
+
* 1. On découvre les fichiers d'entités par la convention du générateur —
|
|
28
|
+
* `nodefony/entity/*.ts`, dans l'application et dans chacun de ses modules.
|
|
29
|
+
* 2. On les IMPORTE pour voir ce qu'ils exportent vraiment : une table Drizzle
|
|
30
|
+
* se reconnaît (`is(value, Table)`), elle ne se devine pas à un nom.
|
|
31
|
+
* 3. On écrit un module temporaire qui les **ré-exporte à plat**.
|
|
32
|
+
* 4. Le registre sert de CONTRÔLE : une entité enregistrée que plus aucun
|
|
33
|
+
* fichier ne fournit est un refus qui la NOMME, jamais une migration
|
|
34
|
+
* silencieusement amputée — laquelle se graverait à vie dans le journal.
|
|
35
|
+
*
|
|
36
|
+
* ## Le ré-export doit être PLAT, sans exception
|
|
37
|
+
*
|
|
38
|
+
* `drizzle-kit` ne collecte que les tables exportées directement. Une table
|
|
39
|
+
* nichée dans un objet exporté est ignorée **sans un mot** (mesuré : un fichier
|
|
40
|
+
* exportant une table plate et une table nichée rend « 1 tables »). C'est
|
|
41
|
+
* pourquoi le module temporaire ré-exporte chaque table sous un nom propre, et
|
|
42
|
+
* jamais l'objet qui la contient.
|
|
43
|
+
*/
|
|
44
|
+
/** Dossier des entités, relatif à une cible de scaffold. */
|
|
45
|
+
const ENTITY_DIR = ["nodefony", "entity"];
|
|
46
|
+
/**
|
|
47
|
+
* Dialecte tel que `drizzle-kit` le nomme dans sa configuration.
|
|
48
|
+
*
|
|
49
|
+
* Il ne s'écrit pas comme le nôtre — `postgresql` chez lui, `postgres` chez
|
|
50
|
+
* nous — et cette table est le seul endroit où l'écart existe.
|
|
51
|
+
*/
|
|
52
|
+
const KIT_DIALECT = {
|
|
53
|
+
sqlite: "sqlite",
|
|
54
|
+
postgres: "postgresql",
|
|
55
|
+
mysql: "mysql"
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Fichiers d'entités d'une cible (l'application, ou l'un de ses modules).
|
|
59
|
+
*
|
|
60
|
+
* Les `*.schema.ts` sont écartés : c'est la convention du générateur pour les
|
|
61
|
+
* contrats d'entrée (Zod), qui n'ont rien à faire dans un schéma de base.
|
|
62
|
+
*
|
|
63
|
+
* @param targetDir - dossier d'une cible (contient `index.ts` + `nodefony/`).
|
|
64
|
+
* @returns les chemins absolus, triés — l'ordre doit être le même partout.
|
|
65
|
+
*/
|
|
66
|
+
async function entityFilesOf(targetDir) {
|
|
67
|
+
const dir = path.join(targetDir, ...ENTITY_DIR);
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = await fs.readdir(dir);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e.code !== "ENOENT") throw e;
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return names.filter((n) => n.endsWith(".ts") && !n.endsWith(".schema.ts")).sort().map((n) => path.join(dir, n));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Importe des fichiers d'entités et relève les tables qu'ils exportent.
|
|
79
|
+
*
|
|
80
|
+
* L'import est la seule façon HONNÊTE de savoir ce qu'un fichier fournit : lire
|
|
81
|
+
* un nom d'export au motif qu'il finit par `Table` marcherait sur le code que
|
|
82
|
+
* le générateur écrit, et sur rien d'autre — or ces fichiers sont faits pour
|
|
83
|
+
* être modifiés à la main, c'est même écrit dans leur en-tête.
|
|
84
|
+
*
|
|
85
|
+
* Un fichier qui refuse de s'importer n'est PAS ignoré : il est rendu à
|
|
86
|
+
* l'appelant avec sa cause. C'est presque toujours le même défaut — un accès au
|
|
87
|
+
* kernel à l'évaluation du module — et le taire produirait une migration
|
|
88
|
+
* amputée là où il faut une phrase.
|
|
89
|
+
*
|
|
90
|
+
* @param files - chemins absolus des fichiers d'entités.
|
|
91
|
+
* @returns les tables trouvées et les fichiers illisibles.
|
|
92
|
+
*/
|
|
93
|
+
async function collectTables(files) {
|
|
94
|
+
const tables = [];
|
|
95
|
+
const unreadable = [];
|
|
96
|
+
const hooks = registerExtensionlessResolution();
|
|
97
|
+
try {
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
let mod;
|
|
100
|
+
try {
|
|
101
|
+
mod = await import(pathToFileURL(file).href);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
unreadable.push({
|
|
104
|
+
file,
|
|
105
|
+
cause: e instanceof Error ? e.message : String(e)
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const [exportName, value] of Object.entries(mod)) {
|
|
110
|
+
if (!is(value, Table)) continue;
|
|
111
|
+
tables.push({
|
|
112
|
+
file,
|
|
113
|
+
exportName,
|
|
114
|
+
tableName: getTableName(value),
|
|
115
|
+
dialect: dialectOf(value)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
hooks.deregister();
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
tables,
|
|
124
|
+
unreadable
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Moteur pour lequel une table est écrite.
|
|
129
|
+
*
|
|
130
|
+
* Une entité d'application est du Drizzle **natif** : `sqliteTable`, `pgTable` et
|
|
131
|
+
* `mysqlTable` produisent trois objets différents, et une table écrite pour un
|
|
132
|
+
* moteur est simplement IGNORÉE par l'outil quand il en génère un autre — sans
|
|
133
|
+
* un mot, comme d'habitude. Constaté sur une application témoin : six tables
|
|
134
|
+
* découvertes, quatre écrites, et un message qui annonçait six.
|
|
135
|
+
*
|
|
136
|
+
* @param table - table relevée dans un fichier d'entité.
|
|
137
|
+
* @returns le dialecte, ou `null` si ce n'est aucun des trois.
|
|
138
|
+
*/
|
|
139
|
+
function dialectOf(table) {
|
|
140
|
+
if (is(table, SQLiteTable)) return "sqlite";
|
|
141
|
+
if (is(table, PgTable)) return "postgres";
|
|
142
|
+
if (is(table, MySqlTable)) return "mysql";
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Fait résoudre `./voisin` comme le ferait un bundler — le temps de la lecture.
|
|
147
|
+
*
|
|
148
|
+
* Un fichier d'entité qui factorise ses colonnes dans un voisin l'importe en
|
|
149
|
+
* TypeScript, donc **sans extension** : c'est ce que tout le monde écrit, ce que
|
|
150
|
+
* les éditeurs proposent, et ce que `moduleResolution: "Bundler"` autorise. Node,
|
|
151
|
+
* lui, applique la règle ESM et exige l'extension — un tel fichier est donc
|
|
152
|
+
* illisible pour lui, alors qu'il se compile parfaitement.
|
|
153
|
+
*
|
|
154
|
+
* Constaté sur les entités du framework lui-même : neuf fichiers sur neuf
|
|
155
|
+
* refusaient de s'importer, tous pour la même raison. Sans cette résolution, la
|
|
156
|
+
* découverte n'aurait marché que sur les fichiers qui n'importent AUCUN voisin
|
|
157
|
+
* — c'est-à-dire sur ce que le générateur écrit, et sur rien de ce qu'on écrit
|
|
158
|
+
* ensuite.
|
|
159
|
+
*
|
|
160
|
+
* La portée est bornée au plus court : posée pour la lecture, retirée juste
|
|
161
|
+
* après, y compris en cas d'échec.
|
|
162
|
+
*
|
|
163
|
+
* @returns le jeton de retrait des hooks.
|
|
164
|
+
*/
|
|
165
|
+
function registerExtensionlessResolution() {
|
|
166
|
+
return registerHooks({ resolve(specifier, context, nextResolve) {
|
|
167
|
+
try {
|
|
168
|
+
return nextResolve(specifier, context);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (specifier.startsWith(".") && path.extname(specifier) === "") return nextResolve(`${specifier}.ts`, context);
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
} });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Spécificateur d'import de `from` vers `to`, écrit pour VOYAGER.
|
|
177
|
+
*
|
|
178
|
+
* Un spécificateur s'écrit en `/` sur les trois systèmes — c'est du texte que
|
|
179
|
+
* lit un outil, pas un accès disque. L'extension est explicite : le module
|
|
180
|
+
* temporaire est compilé par l'outil, qui doit savoir quoi ouvrir sans deviner.
|
|
181
|
+
*
|
|
182
|
+
* @param fromFile - fichier qui contiendra l'import.
|
|
183
|
+
* @param toFile - fichier visé.
|
|
184
|
+
* @returns un spécificateur relatif, toujours préfixé `./` ou `../`.
|
|
185
|
+
*/
|
|
186
|
+
function importSpecifier(fromFile, toFile) {
|
|
187
|
+
const posix = path.relative(path.dirname(fromFile), toFile).split(path.sep).join("/");
|
|
188
|
+
return posix.startsWith(".") ? posix : `./${posix}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Écrit le module temporaire que `drizzle-kit` lira comme « le schéma ».
|
|
192
|
+
*
|
|
193
|
+
* Chaque table reçoit un alias unique et neutre : deux fichiers peuvent très
|
|
194
|
+
* bien exporter deux tables sous le même identifiant JS, et un ré-export en
|
|
195
|
+
* étoile les rendrait AMBIGUËS — l'ambiguïté n'est pas une erreur en ESM, c'est
|
|
196
|
+
* une absence silencieuse. Le nom de la table en base, lui, n'est pas touché :
|
|
197
|
+
* il vit dans l'appel `…Table("nom")`, pas dans l'identifiant.
|
|
198
|
+
*
|
|
199
|
+
* @param file - chemin du module à écrire.
|
|
200
|
+
* @param tables - tables à ré-exporter, dans l'ordre de découverte.
|
|
201
|
+
* @returns le contenu écrit (rendu pour les bancs).
|
|
202
|
+
*/
|
|
203
|
+
async function writeSchemaModule(file, tables) {
|
|
204
|
+
const lines = [
|
|
205
|
+
"// Fichier ENGENDRÉ par `nodefony orm:generate` — il est réécrit à chaque",
|
|
206
|
+
"// exécution et effacé à la fin. Ne rien y mettre à la main.",
|
|
207
|
+
"//",
|
|
208
|
+
"// Les ré-exports sont PLATS : drizzle-kit ne collecte que les tables",
|
|
209
|
+
"// exportées directement, et ignore sans un mot celles qui sont nichées.",
|
|
210
|
+
""
|
|
211
|
+
];
|
|
212
|
+
tables.forEach((t, i) => {
|
|
213
|
+
lines.push(`export { ${t.exportName} as nf_${i}_${t.tableName.replace(/[^A-Za-z0-9_]/g, "_")} } from "${importSpecifier(file, t.file)}";`);
|
|
214
|
+
});
|
|
215
|
+
const body = `${lines.join("\n")}\n`;
|
|
216
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
217
|
+
await fs.writeFile(file, body, "utf8");
|
|
218
|
+
return body;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Schéma PostgreSQL réellement visé par une URL de connexion.
|
|
222
|
+
*
|
|
223
|
+
* 🔴 L'outil d'introspection ne suit PAS le `search_path` porté par l'URL : il
|
|
224
|
+
* lit `public`, quoi qu'on lui donne. Sans cette dérivation, adopter une
|
|
225
|
+
* application logée dans un schéma dédié — le montage habituel d'une base
|
|
226
|
+
* mutualisée — écrivait la référence des tables de `public`, c'est-à-dire
|
|
227
|
+
* celles de quelqu'un d'autre, et déclarait absentes les siennes. Constaté sur
|
|
228
|
+
* un serveur réel : la référence décrivait trois tables étrangères au projet.
|
|
229
|
+
*
|
|
230
|
+
* Deux formes sont reconnues, parce que les deux circulent : le `search_path`
|
|
231
|
+
* passé dans `options`, et le paramètre `schema` que posent certains outils.
|
|
232
|
+
* Une URL sans rien rend `null` — l'appelant laisse alors le défaut de l'outil,
|
|
233
|
+
* qui est le bon.
|
|
234
|
+
*
|
|
235
|
+
* @param url - URL de connexion, telle que le connecteur la porte.
|
|
236
|
+
* @returns le premier schéma du chemin de recherche, ou `null`.
|
|
237
|
+
*/
|
|
238
|
+
function postgresSchemaOf(url) {
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = new URL(url);
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const direct = parsed.searchParams.get("schema");
|
|
246
|
+
if (direct !== null && direct.trim() !== "") return direct.trim();
|
|
247
|
+
const options = parsed.searchParams.get("options");
|
|
248
|
+
if (options === null) return null;
|
|
249
|
+
const premier = /(?:^|\s)-c\s*search_path=([^\s]+)/u.exec(options)?.[1]?.split(",")[0]?.trim();
|
|
250
|
+
return premier !== void 0 && premier !== "" ? premier : null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Écrit la configuration `drizzle-kit` d'une génération d'application.
|
|
254
|
+
*
|
|
255
|
+
* 🔴 **Les chemins y sont RELATIFS, et ce n'est pas un style.** L'outil préfixe
|
|
256
|
+
* son dossier de sortie par `./` : un chemin absolu devient `.//Users/…`, la
|
|
257
|
+
* lecture échoue — et l'échec se présente comme un succès, puisque l'outil rend
|
|
258
|
+
* 0 quand il rate. La configuration est donc écrite pour être lue depuis la
|
|
259
|
+
* racine de l'application, qui est le dossier d'exécution.
|
|
260
|
+
*
|
|
261
|
+
* `tablesFilter` exclut ce que l'application ne possède pas : les tables du
|
|
262
|
+
* framework et la table d'historique. Sans lui, une entité d'application qui
|
|
263
|
+
* référence une table du framework la ferait entrer dans le diff, et la
|
|
264
|
+
* migration porterait un second `CREATE TABLE` de cette table — qui échoue en
|
|
265
|
+
* production, sur toute base déjà migrée.
|
|
266
|
+
*
|
|
267
|
+
* @param options - où écrire, quoi lire, où sortir, quoi exclure.
|
|
268
|
+
* @returns le contenu écrit (rendu pour les bancs).
|
|
269
|
+
*/
|
|
270
|
+
async function writeKitConfig({ file, projectRoot, schemaFile, outDir, dialect, excludedTables, dbUrl }) {
|
|
271
|
+
const rel = (target) => `./${path.relative(projectRoot, target).split(path.sep).join("/")}`;
|
|
272
|
+
const filters = ["*", ...excludedTables.map((t) => `!${t}`)];
|
|
273
|
+
const schema = dbUrl !== void 0 && dialect === "postgres" ? postgresSchemaOf(dbUrl) : null;
|
|
274
|
+
const body = `// Fichier ENGENDRÉ par \`nodefony orm:generate\` — réécrit puis effacé.
|
|
275
|
+
import { defineConfig } from "drizzle-kit";
|
|
276
|
+
|
|
277
|
+
export default defineConfig({
|
|
278
|
+
dialect: ${JSON.stringify(KIT_DIALECT[dialect])},\n schema: ${JSON.stringify(rel(schemaFile))},\n out: ${JSON.stringify(rel(outDir))},\n migrations: { prefix: "index" },\n tablesFilter: ${JSON.stringify(filters)},\n` + (schema === null ? "" : ` schemaFilter: ${JSON.stringify([schema])},\n`) + (dbUrl === void 0 ? "" : ` dbCredentials: { url: ${JSON.stringify(dbUrl)} },\n`) + `});\n`;
|
|
279
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
280
|
+
await fs.writeFile(file, body, "utf8");
|
|
281
|
+
return body;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Version d'ENTRÉE écrite par `drizzle-kit` selon le dialecte.
|
|
285
|
+
*
|
|
286
|
+
* Elle n'est pas la version du journal (« 7 » partout) : c'est celle du format
|
|
287
|
+
* d'instantané, et elle diffère par moteur. Les valeurs sont RELEVÉES sur les
|
|
288
|
+
* journaux que l'outil a produits pour le framework, jamais devinées — une
|
|
289
|
+
* entrée écrite à la main doit être indistinguable des siennes, sans quoi la
|
|
290
|
+
* génération suivante repartirait de travers.
|
|
291
|
+
*/
|
|
292
|
+
const ENTRY_VERSION = {
|
|
293
|
+
sqlite: "6",
|
|
294
|
+
postgres: "7",
|
|
295
|
+
mysql: "5"
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Écrit une migration LIBRE et son entrée de journal, sans `drizzle-kit`.
|
|
299
|
+
*
|
|
300
|
+
* C'est la porte de sortie du modèle déclaratif : une vue, un déclencheur, une
|
|
301
|
+
* clé étrangère réelle, un remplissage de données ne se DÉDUISENT d'aucun
|
|
302
|
+
* schéma. Sans elle, on n'aurait le choix qu'entre renoncer et écrire un
|
|
303
|
+
* fichier à la main dans un journal dont le format n'est pas documenté — deux
|
|
304
|
+
* façons de casser l'historique.
|
|
305
|
+
*
|
|
306
|
+
* Le fichier est vide de toute instruction : un squelette qui « propose » du
|
|
307
|
+
* SQL est un squelette qu'on applique sans le lire.
|
|
308
|
+
*
|
|
309
|
+
* @param options - dossier de sortie, dialecte, nom de la migration.
|
|
310
|
+
* @returns le tag attribué et le chemin du fichier écrit.
|
|
311
|
+
* @throws Error si le journal existant est illisible — on n'en réécrit JAMAIS
|
|
312
|
+
* un par-dessus : il porte l'historique déjà appliqué en production.
|
|
313
|
+
*/
|
|
314
|
+
async function writeCustomMigration({ outDir, dialect, name, now = Date.now() }) {
|
|
315
|
+
const journalFile = path.join(outDir, "meta", "_journal.json");
|
|
316
|
+
let journal = {
|
|
317
|
+
version: "7",
|
|
318
|
+
dialect: KIT_DIALECT[dialect],
|
|
319
|
+
entries: []
|
|
320
|
+
};
|
|
321
|
+
let raw = null;
|
|
322
|
+
try {
|
|
323
|
+
raw = await fs.readFile(journalFile, "utf8");
|
|
324
|
+
} catch {}
|
|
325
|
+
if (raw !== null) try {
|
|
326
|
+
journal = JSON.parse(raw);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
throw new Error(`Le journal « ${journalFile} » est illisible (${e instanceof Error ? e.message : String(e)}). Rien n'a été écrit : ce fichier porte la liste de ce qui a DÉJÀ été appliqué en production, et en réécrire un neuf ferait rejouer toutes les migrations sur une base qui les a déjà reçues.`, { cause: e });
|
|
329
|
+
}
|
|
330
|
+
const entries = [...journal.entries ?? []];
|
|
331
|
+
const idx = entries.reduce((max, e) => Math.max(max, e.idx + 1), 0);
|
|
332
|
+
const tag = `${String(idx).padStart(4, "0")}_${name}`;
|
|
333
|
+
const file = path.join(outDir, `${tag}.sql`);
|
|
334
|
+
await fs.mkdir(path.join(outDir, "meta"), { recursive: true });
|
|
335
|
+
await fs.writeFile(file, `${FORMAT_MARKER}\n-- Migration LIBRE « ${name} » — à écrire à la main.\n--\n-- Elle est déjà inscrite au journal : elle sera appliquée telle quelle,\n-- une seule fois, dans l'ordre, et son empreinte sera gravée. La modifier\n-- après application sera REFUSÉ — écrire une migration de plus, alors.\n--\n-- Séparer les instructions par « --> statement-breakpoint ».\n`, "utf8");
|
|
336
|
+
entries.push({
|
|
337
|
+
idx,
|
|
338
|
+
version: ENTRY_VERSION[dialect],
|
|
339
|
+
when: now,
|
|
340
|
+
tag,
|
|
341
|
+
breakpoints: true
|
|
342
|
+
});
|
|
343
|
+
await fs.writeFile(journalFile, `${JSON.stringify({
|
|
344
|
+
...journal,
|
|
345
|
+
entries
|
|
346
|
+
}, null, 2)}\n`, "utf8");
|
|
347
|
+
return {
|
|
348
|
+
tag,
|
|
349
|
+
file
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Ce que le registre attend et que les fichiers ne fournissent PAS.
|
|
354
|
+
*
|
|
355
|
+
* C'est le seul contrôle qu'un registre puisse rendre et qu'aucun outil de
|
|
356
|
+
* génération ne rendra jamais : `drizzle-kit` ne sait pas ce qu'une application
|
|
357
|
+
* a déclaré, il ne voit que ce qu'on lui donne à lire. Sans cette confrontation,
|
|
358
|
+
* une entité dont le fichier a été déplacé, renommé, ou rendu illisible
|
|
359
|
+
* disparaît de la migration **sans un mot** — et une migration ne se corrige
|
|
360
|
+
* pas : elle est immuable dès qu'une base l'a reçue.
|
|
361
|
+
*
|
|
362
|
+
* Les tables du framework sont écartées des deux côtés : elles sont fournies par
|
|
363
|
+
* une autre source, appliquée avant.
|
|
364
|
+
*
|
|
365
|
+
* @param expected - entités du registre, sur le connecteur visé.
|
|
366
|
+
* @param providedTables - noms des tables que les fichiers découverts fournissent.
|
|
367
|
+
* @param frameworkTables - noms des tables construites par le framework.
|
|
368
|
+
* @returns les entités sans fournisseur, dans l'ordre reçu.
|
|
369
|
+
*/
|
|
370
|
+
function missingProviders(expected, providedTables, frameworkTables) {
|
|
371
|
+
return expected.filter(({ table }) => !providedTables.has(table) && !frameworkTables.has(table));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Les tables de l'application qui usurpent une table du framework.
|
|
375
|
+
*
|
|
376
|
+
* Deux `CREATE TABLE` pour un même nom : la migration passe sur une base vierge
|
|
377
|
+
* et échoue sur toute base déjà migrée — c'est-à-dire en production, et nulle
|
|
378
|
+
* part ailleurs. C'est le pire endroit pour découvrir la faute, donc on la dit
|
|
379
|
+
* avant d'écrire quoi que ce soit.
|
|
380
|
+
*
|
|
381
|
+
* @param tables - tables fournies par les fichiers de l'application.
|
|
382
|
+
* @param frameworkTables - noms des tables construites par le framework.
|
|
383
|
+
* @returns les tables en conflit, avec le fichier qui les exporte.
|
|
384
|
+
*/
|
|
385
|
+
function usurpedTables(tables, frameworkTables) {
|
|
386
|
+
return tables.filter((t) => frameworkTables.has(t.tableName));
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Tables attendues sur un connecteur, telles que le REGISTRE les connaît.
|
|
390
|
+
*
|
|
391
|
+
* Le registre est la seule source qui sache ce que l'application DÉCLARE :
|
|
392
|
+
* les fichiers disent ce qu'elle fournit, la base ce qu'elle porte, et c'est
|
|
393
|
+
* le croisement des trois qui fait les verdicts. Deux commandes en dépendent —
|
|
394
|
+
* la génération pour repérer une entité sans fichier, l'adoption pour ne lire
|
|
395
|
+
* QUE les tables de l'application — et une seconde copie divergerait en
|
|
396
|
+
* silence.
|
|
397
|
+
*
|
|
398
|
+
* @param connector - connecteur visé.
|
|
399
|
+
* @returns les entités et leur table, dans l'ordre du registre.
|
|
400
|
+
*/
|
|
401
|
+
function registeredTables(connector) {
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const entity of entityRegistry.list()) {
|
|
404
|
+
if (entity.connector !== connector) continue;
|
|
405
|
+
const schema = entity.schema;
|
|
406
|
+
if (is(schema, Table)) out.push({
|
|
407
|
+
entity: entity.name,
|
|
408
|
+
table: getTableName(schema)
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region nodefony/src/migrator/catalog.ts
|
|
2
|
+
/**
|
|
3
|
+
* Les moteurs dont la résolution des noms de COLONNES ignore la casse.
|
|
4
|
+
*
|
|
5
|
+
* PostgreSQL n'y est pas, et ce n'est pas un oubli : il stocke la casse d'un
|
|
6
|
+
* identifiant cité et la compare exactement.
|
|
7
|
+
*/
|
|
8
|
+
const CASE_INSENSITIVE_COLUMN_DIALECTS = /* @__PURE__ */ new Set(["sqlite", "mysql"]);
|
|
9
|
+
/**
|
|
10
|
+
* Compare deux noms de COLONNE selon la résolution du moteur.
|
|
11
|
+
*
|
|
12
|
+
* Exportée parce qu'elle porte une RÈGLE : la recopier ailleurs la ferait
|
|
13
|
+
* diverger, et une divergence de casse ne se voit que sur une base adoptée,
|
|
14
|
+
* c'est-à-dire chez l'utilisateur.
|
|
15
|
+
*
|
|
16
|
+
* Elle ne vaut PAS pour les tables — voir {@link ISchemaReader.sameColumnName},
|
|
17
|
+
* qui dit pourquoi leur sensibilité se constate au lieu de se déduire.
|
|
18
|
+
*
|
|
19
|
+
* @param dialect - moteur qui résoudrait le nom.
|
|
20
|
+
* @param declared - nom tel que le code le déclare.
|
|
21
|
+
* @param actual - nom tel que la base le rend.
|
|
22
|
+
* @returns `true` si le moteur les résoudrait vers la même colonne.
|
|
23
|
+
*/
|
|
24
|
+
function sameColumnName(dialect, declared, actual) {
|
|
25
|
+
return CASE_INSENSITIVE_COLUMN_DIALECTS.has(dialect) ? declared.toLowerCase() === actual.toLowerCase() : declared === actual;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compose un lecteur de catalogue au-dessus d'un exécuteur de requêtes.
|
|
29
|
+
*
|
|
30
|
+
* @param dialect - dialecte du serveur interrogé.
|
|
31
|
+
* @param query - exécuteur du porteur (pilote de migration, ou ORM connecté).
|
|
32
|
+
* @returns le lecteur, sans état ni connexion propre.
|
|
33
|
+
*/
|
|
34
|
+
function schemaReader(dialect, query) {
|
|
35
|
+
return {
|
|
36
|
+
sameColumnName(declared, actual) {
|
|
37
|
+
return sameColumnName(dialect, declared, actual);
|
|
38
|
+
},
|
|
39
|
+
async tableExists(table) {
|
|
40
|
+
switch (dialect) {
|
|
41
|
+
case "sqlite": return (await query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? COLLATE NOCASE", [table])).length > 0;
|
|
42
|
+
case "postgres": return (await query("SELECT 1 AS found FROM information_schema.tables WHERE table_name = ? AND table_schema = ANY(current_schemas(false))", [table])).length > 0;
|
|
43
|
+
case "mysql": {
|
|
44
|
+
const rows = await query("SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?", [table]);
|
|
45
|
+
return Number(rows[0]?.n ?? 0) > 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async columnsOf(table) {
|
|
50
|
+
switch (dialect) {
|
|
51
|
+
case "sqlite":
|
|
52
|
+
if (!await this.tableExists(table)) return [];
|
|
53
|
+
return (await query(`PRAGMA table_info("${table.replace(/"/g, "\"\"")}")`)).map((row) => row.name);
|
|
54
|
+
case "postgres": return (await query("SELECT column_name FROM information_schema.columns WHERE table_name = ? AND table_schema = ANY(current_schemas(false))", [table])).map((row) => row.column_name);
|
|
55
|
+
case "mysql": return (await query("SELECT column_name AS name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ?", [table])).map((row) => String(row.name));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Traduit les paramètres `?` du dialecte commun en `$n` PostgreSQL.
|
|
62
|
+
*
|
|
63
|
+
* L'applicateur écrit ses requêtes une seule fois, avec la forme la plus
|
|
64
|
+
* répandue ; chaque pilote l'adapte. Aucune des requêtes de l'applicateur ne
|
|
65
|
+
* contient de littéral `?` — les valeurs, elles, sont bindées, jamais
|
|
66
|
+
* concaténées.
|
|
67
|
+
*
|
|
68
|
+
* @param sql - requête écrite avec des `?`.
|
|
69
|
+
* @returns la même requête, paramètres numérotés.
|
|
70
|
+
*/
|
|
71
|
+
function toDollarParams(sql) {
|
|
72
|
+
let index = 0;
|
|
73
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { sameColumnName, schemaReader, toDollarParams };
|