@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,1154 @@
1
+ import { schemaReader, toDollarParams } from "../migrator/catalog.js";
2
+ import { notConfigured } from "../migrator/refusals.js";
3
+ import { additiveSql, compareSchema, hasGap } from "../migrator/schemaDiff.js";
4
+ import { describeTargetSafely } from "../safeTarget.js";
5
+ import { failureFrom } from "../migrator/status.js";
6
+ import { DrizzleRepository } from "./DrizzleRepository.js";
7
+ import { DrizzleTransaction } from "./DrizzleTransaction.js";
8
+ import { createRequire } from "node:module";
9
+ import { Orm, entityRegistry } from "@nodefony/orm-core";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { is } from "drizzle-orm";
13
+ import { SQLiteSyncDialect, SQLiteTable, getTableConfig } from "drizzle-orm/sqlite-core";
14
+ import { PgDialect, PgTable, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
15
+ import { MySqlDialect, MySqlTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/mysql-core";
16
+ //#region nodefony/src/orm-core/DrizzleOrm.ts
17
+ /**
18
+ * Rend le prédicat d'une contrainte `CHECK` en SQL littéral, avec la grammaire
19
+ * du dialecte demandé.
20
+ *
21
+ * Un `CHECK` ne peut porter **aucun paramètre lié** : une définition de table
22
+ * n'a pas de place où en fournir la valeur. Le prédicat qui en produirait un
23
+ * est donc écarté plutôt qu'émis avec un `?` que le serveur refuserait — le
24
+ * colKit, seul producteur de contraintes du framework, compose exclusivement en
25
+ * `sql.raw`.
26
+ *
27
+ * @param check - contrainte lue sur la table Drizzle.
28
+ * @param dialect - dialecte SQL cible (grammaire de citation et de paramètre).
29
+ * @returns le prédicat littéral, ou `null` s'il porte un paramètre lié.
30
+ */
31
+ function renderCheck(check, dialect) {
32
+ const query = dialect === "postgres" ? new PgDialect().sqlToQuery(check.value) : dialect === "mysql" ? new MySqlDialect().sqlToQuery(check.value) : new SQLiteSyncDialect().sqlToQuery(check.value);
33
+ return query.params.length === 0 ? query.sql : null;
34
+ }
35
+ /**
36
+ * Adapter Drizzle (driver `better-sqlite3`) **branché sur `@nodefony/orm-core`**
37
+ * — 3ᵉ adapter du banc multi-ORM (P7.4), choix SQL #1 moderne.
38
+ *
39
+ * Particularité vs les autres ORM : Drizzle est **schema-as-code** — il n'y
40
+ * a pas de « compilation » de modèle. `entity.schema` *est* déjà une table
41
+ * Drizzle (`sqliteTable(...)`). L'adapter :
42
+ * - dérive le DDL de chaque table via `getTableConfig()` et le crée (dev/test ;
43
+ * la prod passera par `drizzle-kit`) — pas de `sync()` natif côté Drizzle ;
44
+ * - résout les relations déclaratives ({@link IEntityRelation}) en métadonnées
45
+ * d'**eager-load manuel** (cf {@link DrizzleRepository}), pour rester générique
46
+ * sans imposer la couche `relations()` de Drizzle ;
47
+ * - pilote les transactions à la main (`BEGIN`/`COMMIT`/`ROLLBACK`) car
48
+ * `better-sqlite3` est synchrone (cf {@link DrizzleTransaction}).
49
+ *
50
+ * Trappe SQL brut : {@link DrizzleOrm.getNativeConnection} expose le db Drizzle
51
+ * (tag `sql`) pour les jointures arbitraires (ADR-0003 risque #1).
52
+ */
53
+ var DrizzleOrm = class DrizzleOrm extends Orm {
54
+ #client = null;
55
+ /**
56
+ * Détacheurs des listeners de cycle de vie posés sur le pool natif.
57
+ * `null` tant qu'aucun pool réseau n'est ouvert (SQLite n'en a pas) — lazy,
58
+ * et surtout : un `disconnect()`/`connect()` répété empilerait sinon un jeu
59
+ * de listeners par cycle.
60
+ */
61
+ #unwire = null;
62
+ /**
63
+ * Ce que cet adapter sait de l'état de sa connexion, **par dialecte**.
64
+ *
65
+ * `postgres`/`mysql` traduisent les signaux de leur pool → `"events"`.
66
+ * `sqlite` est une base EMBARQUÉE : il n'y a ni serveur à perdre ni socket
67
+ * à surveiller, donc rien à constater — dire `"assumed"` n'est pas un aveu
68
+ * de faiblesse, c'est la description exacte de la situation.
69
+ */
70
+ get liveness() {
71
+ return this.#dialect === "sqlite" ? "assumed" : "events";
72
+ }
73
+ /** Pool `pg` (dialecte postgres) — `null` hors postgres ou non connecté. */
74
+ #pgPool = null;
75
+ /** Pool `mysql2/promise` (dialecte mysql) — `null` hors mysql ou non connecté. */
76
+ #mysqlPool = null;
77
+ #db = null;
78
+ /**
79
+ * Ouvre une transaction sur le driver du connecteur — posée par le
80
+ * `#connectX` du dialecte, `null` hors connexion.
81
+ *
82
+ * **Pourquoi une fabrique par dialecte plutôt qu'un `switch` dans
83
+ * {@link DrizzleOrm.transaction}** : une transaction postgres/mysql exige une
84
+ * connexion DÉDIÉE empruntée au pool (le `BEGIN` et les écritures doivent
85
+ * tomber sur la MÊME connexion, sinon aucune atomicité) et le db Drizzle qui
86
+ * lui est lié — donc la factory `drizzle` du driver, importée en LAZY au
87
+ * connect. La closure capture ce que seul le connect connaît, et
88
+ * `transaction()` reste un chemin unique, sans réimport ni branche.
89
+ */
90
+ #beginTx = null;
91
+ /**
92
+ * File d'attente des transactions **sqlite** (lazy, `null` au repos) — dernier
93
+ * maillon de la chaîne : chaque transaction attend le précédent, et libère le
94
+ * suivant en se terminant.
95
+ *
96
+ * **Pourquoi** : la connexion `better-sqlite3` est UNIQUE → c'est un pool de
97
+ * taille 1. Sans file, deux transactions concurrentes (= deux requêtes HTTP
98
+ * simultanées) émettent deux `BEGIN` sur la même connexion et la seconde
99
+ * échoue (`cannot start a transaction within a transaction`) — un framework
100
+ * qui assume sqlite en prod mono-nœud doit encaisser ça. postgres/mysql n'en
101
+ * ont pas besoin : leur pool EST la file d'attente (prouvé au banc — 15
102
+ * transactions simultanées sur un pool de 10 passent).
103
+ */
104
+ #sqliteTxGate = null;
105
+ /**
106
+ * Tables Drizzle indexées par nom logique d'entité (lazy) — union
107
+ * multi-dialecte {@link DrizzleTable} (variante sqlite OU pg selon le
108
+ * connecteur), consommée telle quelle par le `DrizzleRepository` porté.
109
+ */
110
+ #tables = null;
111
+ /** Relations résolues : entité → champ → relation eager-load (lazy). */
112
+ #relations = null;
113
+ /** Repositories mémoïsés par nom d'entité (lazy). */
114
+ #repositories = null;
115
+ #dialect;
116
+ /**
117
+ * Le schéma est-il dérivé du code à la connexion ? (cf `deriveSchema`)
118
+ *
119
+ * Fixé au constructeur : changer d'avis en cours de vie ferait qu'une
120
+ * reconnexion poserait des tables qu'un exploitant croyait sous contrôle des
121
+ * migrations.
122
+ */
123
+ #deriveSchema;
124
+ /**
125
+ * Écart CONSTATÉ entre le schéma déclaré et la base — `null` tant qu'il n'y
126
+ * en a pas, ce qui est le cas courant : rien n'est alloué pour un état sain.
127
+ */
128
+ #drift = null;
129
+ #filename;
130
+ #url;
131
+ /** Injecté par le service — cf {@link DrizzleOrmOptions.migrationStatus}. */
132
+ #migrationStatus;
133
+ /** Injecté par le service — cf {@link DrizzleOrmOptions.migrationPlan}. */
134
+ #migrationPlan;
135
+ /** Injecté par le service — cf {@link DrizzleOrmOptions.applyMigrations}. */
136
+ #applyMigrations;
137
+ /**
138
+ * @param name - clé unique de l'ORM dans le `ormRegistry` (ex. `"db_test"`).
139
+ * @param options - options de connexion (`dialect`, `filename` sqlite, `url` pg).
140
+ */
141
+ constructor(name, options = {}) {
142
+ super(name);
143
+ this.#dialect = options.dialect ?? "sqlite";
144
+ this.#deriveSchema = options.deriveSchema !== false;
145
+ this.#filename = options.filename ?? ":memory:";
146
+ this.#url = options.url;
147
+ this.#migrationStatus = options.migrationStatus;
148
+ this.#migrationPlan = options.migrationPlan;
149
+ this.#applyMigrations = options.applyMigrations;
150
+ }
151
+ /**
152
+ * Le schéma est-il dérivé du code à la connexion, ou appartient-il aux
153
+ * migrations ?
154
+ *
155
+ * Se lire de l'extérieur a un usage précis : une brique qui s'apprête à
156
+ * interroger une table doit pouvoir dire si quelqu'un la crée. Quand la
157
+ * réponse est « non » et que la table ne figure dans aucune migration, un refus
158
+ * au démarrage vaut infiniment mieux qu'une erreur de colonne inconnue à la
159
+ * première requête d'un utilisateur.
160
+ *
161
+ * @returns `true` en mode `auto` (développement, test), `false` sinon.
162
+ */
163
+ get derivesSchema() {
164
+ return this.#deriveSchema;
165
+ }
166
+ /** Dialecte SQL de ce connecteur. */
167
+ get dialect() {
168
+ return this.#dialect;
169
+ }
170
+ /**
171
+ * Écart constaté entre le schéma déclaré par le code et la base — `null`
172
+ * quand la base est conforme.
173
+ *
174
+ * Publié pour que trois surfaces disent la MÊME chose : le corps d'erreur
175
+ * rendu au client, la sonde de disponibilité, et l'écran d'administration.
176
+ * En mode dérivé il est renseigné au démarrage, après rattrapage de ce qui se
177
+ * rattrapait ; ailleurs il ne l'est que sur demande explicite
178
+ * ({@link DrizzleOrm.compareToDeclared}).
179
+ */
180
+ get schemaDrift() {
181
+ return this.#drift;
182
+ }
183
+ /**
184
+ * Compare la base à ce que le code déclare — sans jamais rien modifier.
185
+ *
186
+ * C'est la **troisième source** : les outils de migration croisent les
187
+ * fichiers et l'historique, et concluent « tout est appliqué » sans avoir
188
+ * regardé la base. Croiser le schéma réel rend visible l'incident qu'aucun
189
+ * d'eux ne voit — historique complet, rien en attente, et pourtant la base ne
190
+ * correspond pas au code (un `ALTER` passé à la main, un correctif jamais
191
+ * reporté, deux environnements qui ont divergé).
192
+ *
193
+ * La lecture passe par la connexion DÉJÀ ouverte de ce connecteur : en
194
+ * ouvrir une seconde sur `:memory:` désignerait une base vide et rendrait un
195
+ * verdict faux.
196
+ *
197
+ * @returns les écarts, séparés selon qu'ils se rattrapent ou non.
198
+ */
199
+ compareToDeclared() {
200
+ return compareSchema(schemaReader(this.#dialect, this.#rawQuery), this.describeTables());
201
+ }
202
+ /**
203
+ * Emplacement PHYSIQUE lisible de la base, pour l'écran Studio « Stores »
204
+ * (« où sont écrites mes données ? »). Fichier SQLite **relativisé** au cwd
205
+ * (anti info-leak, cf {@link DrizzleOrm.#safeTarget}) — `undefined` pour
206
+ * `:memory:` (volatil) et pour un backend RÉSEAU (postgres/mysql), dont
207
+ * l'emplacement EST l'infra déclarée, déjà surfacée à part (le Studio dérive
208
+ * alors « backend réseau — voir l'infra »). Stable dès la construction
209
+ * (`#filename` fixé au ctor, indépendant du connect).
210
+ */
211
+ get location() {
212
+ if (this.#dialect !== "sqlite" || this.#filename === ":memory:") return;
213
+ return this.#safeTarget();
214
+ }
215
+ /** Entités enregistrées dans `entityRegistry` ciblant cet ORM. */
216
+ #ownEntities() {
217
+ return entityRegistry.list().filter((entity) => entity.connector === this.name);
218
+ }
219
+ /** FK déterministe camelCase `<entité>Id` (parité avec Mongoose). */
220
+ #foreignKey(entityName) {
221
+ return `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Id`;
222
+ }
223
+ /**
224
+ * Générateur de `CREATE TABLE IF NOT EXISTS` partagé entre dialectes (dev/test).
225
+ * Émet uniquement les contraintes colonne (PK / NOT NULL / UNIQUE) à partir du
226
+ * type SQL natif de chaque colonne (`getSQLType()` rend `text`/`integer` en
227
+ * SQLite, `text`/`bigint`/`jsonb` en Postgres, `varchar(512)`/`json` en MySQL
228
+ * → DDL adapté sans code spécifique). Le quoting d'identifiants diverge :
229
+ * `"…"` (SQL standard, SQLite/PG) vs backtick MySQL (qui ne lit `"…"` qu'en
230
+ * mode ANSI_QUOTES, jamais garanti). La prod reste pilotée par drizzle-kit.
231
+ */
232
+ #buildCreateTable(name, columns, quote = "\"", checks = [], dialect = "sqlite") {
233
+ const defs = columns.map((col) => {
234
+ const parts = [`${quote}${col.name}${quote}`, col.getSQLType()];
235
+ if (col.primary) parts.push("PRIMARY KEY");
236
+ if (col.notNull) parts.push("NOT NULL");
237
+ if (col.isUnique) parts.push("UNIQUE");
238
+ return parts.join(" ");
239
+ });
240
+ for (const check of checks) {
241
+ const predicate = renderCheck(check, dialect);
242
+ if (predicate === null) continue;
243
+ defs.push(`CONSTRAINT ${quote}${check.name}${quote} CHECK (${predicate})`);
244
+ }
245
+ return `CREATE TABLE IF NOT EXISTS ${quote}${name}${quote} (${defs.join(", ")})`;
246
+ }
247
+ /**
248
+ * Dérive les `CREATE INDEX` des index déclarés sur la table (dev/test).
249
+ *
250
+ * Les index étaient construits sur la table Drizzle mais jamais émis : une
251
+ * colonne déclarée indexée ne l'était nulle part, et rien ne le disait. La
252
+ * requête restait correcte, seulement lente — le pire genre d'écart, celui qui
253
+ * ne se voit qu'en charge.
254
+ *
255
+ * Pourquoi ici et pas dans `#buildCreateTable` : un index est un objet SÉPARÉ
256
+ * de la table en SQL standard, et `CREATE TABLE IF NOT EXISTS` ne le porterait
257
+ * pas. Émis à part, il arrive AUSSI sur une base de développement déjà créée —
258
+ * ce qu'aucune clause de table ne permettrait.
259
+ *
260
+ * Les clés étrangères, elles, ne sont toujours PAS émises : elles se déclarent
261
+ * DANS le `CREATE TABLE`, donc elles n'atteindraient jamais une base existante,
262
+ * et elles imposeraient de créer les tables dans l'ordre de leurs dépendances
263
+ * (indécidable sur un cycle). C'est le travail du DDL de production
264
+ * (drizzle-kit), pas d'un dérivé de développement.
265
+ *
266
+ * @param table - nom de la table portant les index.
267
+ * @param indexes - index déclarés (`getTableConfig(...).indexes`).
268
+ * @param quote - caractère de citation des identifiants du dialecte.
269
+ * @returns une instruction par index (vide s'il n'y en a aucun).
270
+ */
271
+ #buildCreateIndexes(table, indexes, quote = "\"") {
272
+ const statements = [];
273
+ for (const entry of indexes) {
274
+ const { name, unique, columns } = entry.config;
275
+ const names = columns.map((column) => column.name).filter((column) => typeof column === "string");
276
+ if (names.length === 0 || names.length !== columns.length) continue;
277
+ const cols = names.map((column) => `${quote}${column}${quote}`).join(", ");
278
+ statements.push(`CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${quote}${name}${quote} ON ${quote}${table}${quote} (${cols})`);
279
+ }
280
+ return statements;
281
+ }
282
+ /** Dérive le `CREATE TABLE` SQLite depuis la table Drizzle (dev/test). */
283
+ #createTableSQL(table) {
284
+ const { name, columns, checks } = getTableConfig(table);
285
+ return this.#buildCreateTable(name, columns, "\"", checks, "sqlite");
286
+ }
287
+ /** Dérive les `CREATE INDEX` SQLite depuis la table Drizzle (dev/test). */
288
+ #createIndexesSQL(table) {
289
+ const { name, indexes } = getTableConfig(table);
290
+ return this.#buildCreateIndexes(name, indexes);
291
+ }
292
+ /** Dérive le `CREATE TABLE` Postgres depuis la table Drizzle (dev/test). */
293
+ #createTablePgSQL(table) {
294
+ const { name, columns, checks } = getTableConfig$1(table);
295
+ return this.#buildCreateTable(name, columns, "\"", checks, "postgres");
296
+ }
297
+ /** Dérive les `CREATE INDEX` Postgres depuis la table Drizzle (dev/test). */
298
+ #createIndexesPgSQL(table) {
299
+ const { name, indexes } = getTableConfig$1(table);
300
+ return this.#buildCreateIndexes(name, indexes);
301
+ }
302
+ /** Dérive le `CREATE TABLE` MySQL depuis la table Drizzle (dev/test). */
303
+ #createTableMysqlSQL(table) {
304
+ const { name, columns, checks } = getTableConfig$2(table);
305
+ return this.#buildCreateTable(name, columns, "`", checks, "mysql");
306
+ }
307
+ /**
308
+ * Dérive les `CREATE INDEX` MySQL depuis la table Drizzle (dev/test).
309
+ *
310
+ * MySQL ne connaît pas `CREATE INDEX IF NOT EXISTS` : la clause est retirée, et
311
+ * l'exécution tolère l'erreur « index déjà existant » (le seul cas où rejouer
312
+ * le DDL de développement doit rester silencieux).
313
+ */
314
+ #createIndexesMysqlSQL(table) {
315
+ const { name, indexes } = getTableConfig$2(table);
316
+ return this.#buildCreateIndexes(name, indexes, "`").map((statement) => statement.replace(" IF NOT EXISTS", ""));
317
+ }
318
+ /** Résout les relations déclaratives d'une entité en métadonnées eager-load. */
319
+ #resolveRelations(entity, tables) {
320
+ const resolved = {};
321
+ for (const relation of entity.relations ?? []) {
322
+ const target = tables[relation.target];
323
+ if (!target) throw new Error(`DrizzleOrm "${this.name}": relation target "${relation.target}" (from "${entity.name}.${relation.field}") not registered for this ORM.`);
324
+ resolved[relation.field] = this.#resolveOne(entity, relation, target);
325
+ }
326
+ return resolved;
327
+ }
328
+ /** Construit une relation résolue (FK déterministe selon la cardinalité). */
329
+ #resolveOne(entity, relation, target) {
330
+ switch (relation.type) {
331
+ case "one-to-many": return {
332
+ type: "one-to-many",
333
+ targetTable: target,
334
+ foreignKey: relation.foreignKey ?? this.#foreignKey(entity.name),
335
+ localKey: "id",
336
+ targetKey: "id"
337
+ };
338
+ case "many-to-one":
339
+ case "one-to-one": return {
340
+ type: relation.type,
341
+ targetTable: target,
342
+ foreignKey: relation.foreignKey ?? this.#foreignKey(relation.target),
343
+ localKey: "id",
344
+ targetKey: "id"
345
+ };
346
+ case "many-to-many": throw new Error(`DrizzleOrm "${this.name}": many-to-many ("${entity.name}.${relation.field}") non portable — déclarer via getNativeConnection().`);
347
+ }
348
+ }
349
+ async onConnect() {
350
+ await this.#releasePrevious();
351
+ this.#tables = Object.create(null);
352
+ this.#relations = Object.create(null);
353
+ const entities = this.#ownEntities();
354
+ switch (this.#dialect) {
355
+ case "sqlite":
356
+ await this.#connectSqlite(entities);
357
+ break;
358
+ case "postgres":
359
+ await this.#connectPostgres(entities);
360
+ break;
361
+ case "mysql": await this.#connectMysql(entities);
362
+ }
363
+ if (this.#deriveSchema) {
364
+ await this.#reconcileSchema();
365
+ await this.#createIndexes(entities);
366
+ }
367
+ for (const entity of entities) this.#relations[entity.name] = this.#resolveRelations(entity, this.#tables);
368
+ }
369
+ /**
370
+ * Connexion SQLite + création des tables (dev/test).
371
+ *
372
+ * Le pilote est chargé en LAZY, même patron que `#connectPostgres` et
373
+ * `#connectMysql` : les trois dialectes sont symétriques, et une application
374
+ * n'embarque que celui qu'elle ouvre. `better-sqlite3` est un binaire natif
375
+ * compilé à l'installation — l'imposer à une application PostgreSQL coûtait
376
+ * un `node-gyp` et des mégaoctets pour rien.
377
+ *
378
+ * La méthode devient asynchrone pour cette seule raison ; le pilote, lui,
379
+ * reste synchrone une fois chargé (`client.exec`, `client.pragma`).
380
+ */
381
+ async #connectSqlite(entities) {
382
+ let Sqlite;
383
+ let sqliteDrizzle;
384
+ try {
385
+ const ns = await import("better-sqlite3");
386
+ const resolved = ns.default ?? ns;
387
+ if (typeof resolved !== "function") throw new Error("`better-sqlite3` did not expose a constructor");
388
+ Sqlite = resolved;
389
+ sqliteDrizzle = (await import("drizzle-orm/better-sqlite3")).drizzle;
390
+ } catch (e) {
391
+ throw new Error(`DrizzleOrm "${this.name}": the sqlite dialect needs the optional driver \`better-sqlite3\` (run \`npm i better-sqlite3\`). ${e.message}`, { cause: e });
392
+ }
393
+ const client = new Sqlite(this.#filename);
394
+ if (this.#filename !== ":memory:") {
395
+ client.pragma("journal_mode = WAL");
396
+ client.pragma("synchronous = NORMAL");
397
+ }
398
+ this.#client = client;
399
+ const db = sqliteDrizzle(client);
400
+ this.#db = db;
401
+ this.#beginTx = async () => {
402
+ const previous = this.#sqliteTxGate;
403
+ let release;
404
+ const mine = new Promise((resolve) => {
405
+ release = resolve;
406
+ });
407
+ this.#sqliteTxGate = mine;
408
+ if (previous) await previous;
409
+ client.exec("BEGIN");
410
+ return new DrizzleTransaction(db, {
411
+ exec: (sql) => {
412
+ client.exec(sql);
413
+ return Promise.resolve();
414
+ },
415
+ quoteIdent: (name) => `"${name}"`,
416
+ release: () => {
417
+ if (this.#sqliteTxGate === mine) this.#sqliteTxGate = null;
418
+ release();
419
+ }
420
+ });
421
+ };
422
+ for (const entity of entities) {
423
+ this.#assertDialectTable(entity, SQLiteTable, "sqlite");
424
+ const table = entity.schema;
425
+ this.#tables[entity.name] = table;
426
+ entity.model = table;
427
+ if (!this.#deriveSchema) continue;
428
+ client.exec(this.#createTableSQL(table));
429
+ }
430
+ }
431
+ /**
432
+ * Exécute une requête paramétrée sur la connexion COURANTE du connecteur.
433
+ *
434
+ * Réservé à la lecture du catalogue et au rattrapage de schéma, au démarrage.
435
+ * Ce n'est pas une trappe publique : `getNativeConnection()` reste le chemin
436
+ * documenté pour du SQL applicatif.
437
+ *
438
+ * @param sql - requête, paramètres écrits `?` (traduits pour PostgreSQL).
439
+ * @param params - valeurs bindées.
440
+ * @returns les lignes rendues.
441
+ */
442
+ #rawQuery = async (sql, params = []) => {
443
+ switch (this.#dialect) {
444
+ case "sqlite": {
445
+ const statement = this.#client.prepare(sql);
446
+ return statement.reader ? statement.all(...params) : [];
447
+ }
448
+ case "postgres": return (await this.#pgPool.query(toDollarParams(sql), params)).rows;
449
+ case "mysql": {
450
+ const [rows] = await this.#mysqlPool.query(sql, params);
451
+ return Array.isArray(rows) ? rows : [];
452
+ }
453
+ }
454
+ };
455
+ /**
456
+ * Exécute une instruction sans résultat sur la connexion courante.
457
+ *
458
+ * @param sql - instruction DDL.
459
+ */
460
+ async #rawExec(sql) {
461
+ switch (this.#dialect) {
462
+ case "sqlite":
463
+ this.#client.exec(sql);
464
+ return;
465
+ case "postgres":
466
+ await this.#pgPool.query(sql);
467
+ return;
468
+ case "mysql":
469
+ await this.#mysqlPool.query(sql);
470
+ return;
471
+ }
472
+ }
473
+ /**
474
+ * Confronte le schéma DÉCLARÉ par le code à celui que la base porte
475
+ * vraiment, répare ce qui se répare, et publie le reste.
476
+ *
477
+ * **Le cas fréquent se répare tout seul** : le back ajoute un champ qui
478
+ * accepte le vide, la table existe déjà, la colonne manque — elle est ajoutée
479
+ * et journalisée en clair. Le développeur front tire la branche, le serveur
480
+ * redémarre, ça marche : il n'a rien à taper, et n'a pas eu à comprendre un
481
+ * domaine qui n'est pas le sien.
482
+ *
483
+ * **Ce qui ne se répare pas est ÉNONCÉ, jamais deviné** : une colonne
484
+ * obligatoire exigerait d'inventer une valeur pour les lignes existantes, et
485
+ * c'est une décision métier. Une table absente relève des migrations. Dans
486
+ * les deux cas l'écart est publié ({@link DrizzleOrm.schemaDrift}), journalisé
487
+ * avec le geste exact, et **le démarrage continue** : un serveur qui refuse de
488
+ * démarrer n'apprend rien de plus à celui qui le lance, et lui retire le seul
489
+ * outil qui pourrait le renseigner.
490
+ *
491
+ * ⚠️ Ne tourne QUE lorsque le schéma est dérivé du code (développement). En
492
+ * `migrate`/`none`, le même calcul est demandé explicitement par
493
+ * {@link DrizzleOrm.compareToDeclared} — il constate, il ne répare jamais.
494
+ */
495
+ async #reconcileSchema() {
496
+ const comparison = await this.compareToDeclared();
497
+ if (!hasGap(comparison)) {
498
+ this.#drift = null;
499
+ return;
500
+ }
501
+ for (const gap of comparison.additive) {
502
+ await this.#rawExec(additiveSql(gap, this.#dialect));
503
+ this.log(`schéma rattrapé : colonne « ${gap.column} » (${gap.type}) ajoutée à la table « ${gap.table} » — le code la déclarait, la base ne l'avait pas.`, "INFO");
504
+ }
505
+ const remaining = {
506
+ additive: [],
507
+ blocking: comparison.blocking,
508
+ missingTables: comparison.missingTables
509
+ };
510
+ this.#drift = hasGap(remaining) ? remaining : null;
511
+ if (this.#drift) this.log(this.#explainDrift(this.#drift), "CRITIC");
512
+ }
513
+ /**
514
+ * Met en mots un écart que le démarrage ne sait pas réparer.
515
+ *
516
+ * @param drift - l'écart restant après rattrapage.
517
+ * @returns le texte à journaliser, geste compris.
518
+ */
519
+ #explainDrift(drift) {
520
+ const lines = [`La base du connecteur « ${this.name} » ne correspond pas au code, et l'écart ne se rattrape pas tout seul :`];
521
+ for (const table of drift.missingTables) lines.push(` · table « ${table} » absente`);
522
+ for (const gap of drift.blocking) lines.push(` · colonne « ${gap.table}.${gap.column} » (${gap.type}) absente et OBLIGATOIRE — la poser exigerait d'inventer une valeur pour les lignes déjà présentes`);
523
+ lines.push(` Les index et les requêtes qui portent sur ces colonnes échoueront.`, ` En développement, le geste est : nodefony orm:reset --connector ${this.name}`, ` (il SUPPRIME et recrée la base — jamais hors développement).`, ` Sur une base qui porte des données, c'est une migration qu'il faut : nodefony orm:migrate:status --connector ${this.name}`);
524
+ return lines.join("\n");
525
+ }
526
+ /**
527
+ * Crée les index déclarés, table par table.
528
+ *
529
+ * **Séparé de la création des tables, et joué APRÈS le rattrapage** : un
530
+ * index porte sur des colonnes, et le poser sur une table à laquelle il en
531
+ * manque une échoue. C'était le mode de défaillance réel avant ce
532
+ * découpage — le serveur de développement ne démarrait plus du tout, sur une
533
+ * erreur de pilote (`no such column`) qui ne nommait ni le connecteur, ni le
534
+ * geste.
535
+ *
536
+ * @param entities - entités du connecteur.
537
+ */
538
+ async #createIndexes(entities) {
539
+ const empechees = new Set(this.#drift?.missingTables ?? []);
540
+ for (const gap of this.#drift?.blocking ?? []) empechees.add(gap.table);
541
+ for (const entity of entities) {
542
+ const table = this.#tables?.[entity.name];
543
+ if (!table) continue;
544
+ switch (this.#dialect) {
545
+ case "sqlite": {
546
+ const t = table;
547
+ if (empechees.has(getTableConfig(t).name)) continue;
548
+ for (const statement of this.#createIndexesSQL(t)) this.#client.exec(statement);
549
+ break;
550
+ }
551
+ case "postgres": {
552
+ const t = table;
553
+ if (empechees.has(getTableConfig$1(t).name)) continue;
554
+ for (const statement of this.#createIndexesPgSQL(t)) await this.#pgPool.query(statement);
555
+ break;
556
+ }
557
+ case "mysql": {
558
+ const t = table;
559
+ if (empechees.has(getTableConfig$2(t).name)) continue;
560
+ for (const statement of this.#createIndexesMysqlSQL(t)) try {
561
+ await this.#mysqlPool.query(statement);
562
+ } catch (error) {
563
+ if (error.code !== "ER_DUP_KEYNAME") throw error;
564
+ }
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ }
570
+ /**
571
+ * Garde fail-loud : une entité dont la table n'est pas du dialecte du
572
+ * connecteur (ex. variante sqlite enregistrée sur un connecteur postgres)
573
+ * doit échouer avec un message ACTIONNABLE — pas le `TypeError: Cannot
574
+ * convert undefined or null to object` cryptique de `getTableConfig`.
575
+ */
576
+ #assertDialectTable(entity, tableCtor, dialect) {
577
+ if (!is(entity.schema, tableCtor)) throw new Error(`DrizzleOrm "${this.name}" (${dialect}): entity "${entity.name}" is not ported to this dialect (its schema is not a ${dialect} table). Port it via a createXTable("${dialect}") factory, or keep this connector on a dialect the entity supports (multi-dialect worksite).`);
578
+ }
579
+ /**
580
+ * Connexion **Postgres** : le driver `pg` (optionalDependency) et l'adapter
581
+ * `drizzle-orm/node-postgres` sont chargés en LAZY (`await import`) — un
582
+ * déploiement SQLite ne les tire jamais. Pool partagé ; DDL dérivé pour dev/test
583
+ * (prod = drizzle-kit). Échec d'import → message actionnable (`npm i pg`).
584
+ */
585
+ async #connectPostgres(entities) {
586
+ if (!this.#url) throw new Error(`DrizzleOrm "${this.name}": dialect "postgres" requires a connection \`url\`.`);
587
+ let PoolCtor;
588
+ let pgDrizzle;
589
+ try {
590
+ const pgNs = await import("pg");
591
+ const resolved = pgNs.Pool ?? pgNs.default?.Pool;
592
+ if (!resolved) throw new Error("`pg` did not expose a `Pool` constructor");
593
+ PoolCtor = resolved;
594
+ pgDrizzle = (await import("drizzle-orm/node-postgres")).drizzle;
595
+ } catch (e) {
596
+ throw new Error(`DrizzleOrm "${this.name}": the postgres dialect needs the optional driver \`pg\` (run \`npm i pg\`). ${e.message}`, { cause: e });
597
+ }
598
+ const pool = new PoolCtor({
599
+ connectionString: this.#url,
600
+ keepAlive: true,
601
+ keepAliveInitialDelayMillis: 1e4
602
+ });
603
+ this.#wirePgLifecycle(pool);
604
+ try {
605
+ await pool.query("SELECT 1");
606
+ } catch (e) {
607
+ this.#unwireAll();
608
+ await pool.end().catch(() => void 0);
609
+ throw e;
610
+ }
611
+ this.#pgPool = pool;
612
+ this.#db = pgDrizzle(pool);
613
+ try {
614
+ await this.#finishPostgres(entities, pool, pgDrizzle);
615
+ } catch (e) {
616
+ this.#unwireAll();
617
+ await pool.end().catch(() => void 0);
618
+ this.#pgPool = null;
619
+ this.#db = null;
620
+ this.#beginTx = null;
621
+ throw e;
622
+ }
623
+ }
624
+ /** Suite de la connexion postgres — isolée pour rendre `connect()` atomique. */
625
+ async #finishPostgres(entities, pool, pgDrizzle) {
626
+ this.#beginTx = async () => {
627
+ const cx = await pool.connect();
628
+ const puitsTx = (err) => {
629
+ this.connectionLost(`pg (transaction) : ${err?.message ?? String(err)}`);
630
+ };
631
+ cx.on("error", puitsTx);
632
+ /** Rend la connexion, en retirant d'abord NOTRE puits. */
633
+ const giveBack = (err) => {
634
+ cx.removeListener("error", puitsTx);
635
+ cx.release(err === void 0 ? void 0 : err instanceof Error ? err : true);
636
+ };
637
+ try {
638
+ await cx.query("BEGIN");
639
+ } catch (e) {
640
+ giveBack(e);
641
+ throw e;
642
+ }
643
+ return new DrizzleTransaction(pgDrizzle(cx), {
644
+ exec: async (sql) => {
645
+ await cx.query(sql);
646
+ },
647
+ quoteIdent: (name) => `"${name}"`,
648
+ release: giveBack
649
+ });
650
+ };
651
+ for (const entity of entities) {
652
+ this.#assertDialectTable(entity, PgTable, "postgres");
653
+ const table = entity.schema;
654
+ this.#tables[entity.name] = table;
655
+ entity.model = table;
656
+ if (!this.#deriveSchema) continue;
657
+ await pool.query(this.#createTablePgSQL(table));
658
+ }
659
+ }
660
+ /**
661
+ * Referme ce qu'un établissement précédent avait ouvert, s'il y en a eu un.
662
+ * Silencieux et idempotent : au premier `connect()` il n'y a rien à faire.
663
+ */
664
+ async #releasePrevious() {
665
+ if (!this.#pgPool && !this.#mysqlPool && !this.#client) return;
666
+ this.#unwireAll();
667
+ const sink = () => void 0;
668
+ this.#pgPool?.on("error", sink);
669
+ try {
670
+ this.#client?.close();
671
+ await this.#pgPool?.end();
672
+ await this.#mysqlPool?.end();
673
+ } catch {}
674
+ this.#client = null;
675
+ this.#pgPool = null;
676
+ this.#mysqlPool = null;
677
+ }
678
+ /** Détache tous les listeners de cycle de vie posés sur les pools natifs. */
679
+ #unwireAll() {
680
+ if (!this.#unwire) return;
681
+ for (const off of this.#unwire) off();
682
+ this.#unwire = null;
683
+ }
684
+ /**
685
+ * Traduit le cycle de vie du pool **`pg`** en signaux du contrat `orm-core`.
686
+ *
687
+ * 🔴 Ce listener n'est pas du confort, il empêche un CRASH. `pg-pool` fait
688
+ * `pool.emit("error", …)` quand un client **inactif** tombe (serveur
689
+ * redémarré, coupure réseau, `pg_terminate_backend`) ; un `EventEmitter` qui
690
+ * émet `error` sans auditeur **lève**, et rien n'installe de
691
+ * `uncaughtException` dans le framework — le pod tombait. Constaté au banc :
692
+ * `docker stop` du serveur PostgreSQL ⇒ process mort, code 1.
693
+ *
694
+ * Le RETOUR se lit sur `connect` **et** `acquire`. `connect` seul ne suffit
695
+ * pas : `pg` ne recrée un client que si aucun n'est disponible, donc après
696
+ * une coupure brève le pool peut reprendre du service en réutilisant un
697
+ * client sain — sans jamais émettre `connect`, laissant l'ORM marqué tombé
698
+ * alors que la base répond. `acquire` couvre les deux cas.
699
+ * `connectionRestored()` étant idempotent, les acquisitions d'un pool en
700
+ * bonne santé ne comptent rien.
701
+ */
702
+ #wirePgLifecycle(pool) {
703
+ const onError = (err) => {
704
+ this.connectionLost(`pg: ${err?.message ?? String(err)}`);
705
+ };
706
+ const onConnect = () => {
707
+ this.connectionRestored();
708
+ };
709
+ pool.on("error", onError);
710
+ pool.on("connect", onConnect);
711
+ pool.on("acquire", onConnect);
712
+ (this.#unwire ??= []).push(() => {
713
+ pool.removeListener("error", onError);
714
+ pool.removeListener("connect", onConnect);
715
+ pool.removeListener("acquire", onConnect);
716
+ });
717
+ }
718
+ /**
719
+ * Traduit le cycle de vie du pool **`mysql2`** en signaux du contrat.
720
+ *
721
+ * `mysql2` n'expose PAS d'événement `error` sur le pool : ce sont les
722
+ * `PoolConnection` qui émettent (elles posent d'ailleurs leur propre
723
+ * `once("error")` pour se retirer du pool — c'est la seule raison pour
724
+ * laquelle MySQL ne fait pas tomber le process là où `pg` le fait). On
725
+ * s'abonne donc à chaque connexion créée ; `connection` sert aussi de
726
+ * signal de RETOUR, le pool en ouvrant une neuve dès que le serveur répond.
727
+ *
728
+ * 🔴 **Mais cet `error` n'arrive PAS quand le serveur tombe.** Mesuré, un
729
+ * `docker stop` sous une connexion établie : le socket rend `end` puis
730
+ * `close`, la requête en vol est rejetée en `PROTOCOL_CONNECTION_LOST`
731
+ * (`fatal: true`) — et la connexion n'émet rien, `mysql2` délivrant l'erreur
732
+ * fatale au demandeur plutôt qu'à l'émetteur. L'écoute ci-dessus ne couvre
733
+ * donc que les erreurs survenues connexion INACTIVE ; s'y fier seul laissait
734
+ * l'ORM marqué connecté jusqu'au battement suivant, quand `pg` bascule
735
+ * aussitôt par `pool.on("error")`.
736
+ *
737
+ * D'où la seconde écoute, sur le **socket** — le seul à parler dans ce cas.
738
+ * Sa fermeture n'est pas une preuve (une connexion inactive recyclée en
739
+ * ferme un aussi), donc elle ne conclut rien : elle déclenche un battement
740
+ * ANTICIPÉ, qui tranche par une requête. Cleanup : `once` se retire en
741
+ * partant, et le socket meurt avec la connexion qui le porte.
742
+ */
743
+ #wireMysqlLifecycle(pool) {
744
+ const onConnection = (cx) => {
745
+ this.connectionRestored();
746
+ cx.on("error", (err) => {
747
+ if (this.#mysqlPool !== pool) return;
748
+ this.connectionLost(`mysql: ${err?.message ?? String(err)}`);
749
+ });
750
+ cx.stream?.once("close", () => {
751
+ if (this.#mysqlPool !== pool) return;
752
+ this.beatNow();
753
+ });
754
+ };
755
+ const emitter = pool;
756
+ emitter.on("connection", onConnection);
757
+ (this.#unwire ??= []).push(() => {
758
+ emitter.removeListener("connection", onConnection);
759
+ });
760
+ }
761
+ /**
762
+ * Connexion **MySQL** : le driver `mysql2` (optionalDependency) et l'adapter
763
+ * `drizzle-orm/mysql2` sont chargés en LAZY (`await import`) — même patron que
764
+ * `#connectPostgres`. Pool promise ouvert en `timezone: "Z"` : les colonnes
765
+ * `datetime(3)` (kind `dateMs` du colKit) sont écrites/relues en UTC — mêmes
766
+ * instants que `timestamptz` PG, sans dépendre de la timezone du serveur.
767
+ */
768
+ async #connectMysql(entities) {
769
+ if (!this.#url) throw new Error(`DrizzleOrm "${this.name}": dialect "mysql" requires a connection \`url\`.`);
770
+ let pool;
771
+ let mysqlDrizzle;
772
+ try {
773
+ const mysqlNs = await import("mysql2/promise");
774
+ const createPool = mysqlNs.createPool ?? mysqlNs.default?.createPool;
775
+ if (!createPool) throw new Error("`mysql2/promise` did not expose `createPool`");
776
+ mysqlDrizzle = (await import("drizzle-orm/mysql2")).drizzle;
777
+ pool = createPool({
778
+ uri: this.#url,
779
+ timezone: "Z",
780
+ enableKeepAlive: true,
781
+ keepAliveInitialDelay: 1e4
782
+ });
783
+ this.#wireMysqlLifecycle(pool);
784
+ } catch (e) {
785
+ throw new Error(`DrizzleOrm "${this.name}": the mysql dialect needs the optional driver \`mysql2\` (run \`npm i mysql2\`). ${e.message}`, { cause: e });
786
+ }
787
+ try {
788
+ await pool.query("SELECT 1");
789
+ } catch (e) {
790
+ this.#unwireAll();
791
+ await pool.end().catch(() => void 0);
792
+ throw e;
793
+ }
794
+ this.#mysqlPool = pool;
795
+ this.#db = mysqlDrizzle(pool);
796
+ try {
797
+ await this.#finishMysql(entities, pool, mysqlDrizzle);
798
+ } catch (e) {
799
+ this.#unwireAll();
800
+ await pool.end().catch(() => void 0);
801
+ this.#mysqlPool = null;
802
+ this.#db = null;
803
+ this.#beginTx = null;
804
+ throw e;
805
+ }
806
+ }
807
+ /** Suite de la connexion mysql — isolée pour rendre `connect()` atomique. */
808
+ async #finishMysql(entities, pool, mysqlDrizzle) {
809
+ this.#beginTx = async () => {
810
+ const cx = await pool.getConnection();
811
+ try {
812
+ await cx.query("BEGIN");
813
+ } catch (e) {
814
+ cx.destroy();
815
+ throw e;
816
+ }
817
+ return new DrizzleTransaction(mysqlDrizzle(cx), {
818
+ exec: async (sql) => {
819
+ await cx.query(sql);
820
+ },
821
+ quoteIdent: (name) => `\`${name}\``,
822
+ release: (err) => {
823
+ if (err === void 0) cx.release();
824
+ else cx.destroy();
825
+ }
826
+ });
827
+ };
828
+ for (const entity of entities) {
829
+ this.#assertDialectTable(entity, MySqlTable, "mysql");
830
+ const table = entity.schema;
831
+ this.#tables[entity.name] = table;
832
+ entity.model = table;
833
+ if (!this.#deriveSchema) continue;
834
+ await pool.query(this.#createTableMysqlSQL(table));
835
+ }
836
+ }
837
+ async disconnect() {
838
+ this.alive = false;
839
+ this.stopHeartbeat();
840
+ this.#unwireAll();
841
+ const sink = () => void 0;
842
+ this.#pgPool?.on("error", sink);
843
+ if (this.#client) this.#client.close();
844
+ if (this.#pgPool) await this.#pgPool.end();
845
+ if (this.#mysqlPool) await this.#mysqlPool.end();
846
+ this.#client = null;
847
+ this.#pgPool = null;
848
+ this.#mysqlPool = null;
849
+ this.#db = null;
850
+ this.#beginTx = null;
851
+ this.#sqliteTxGate = null;
852
+ this.#tables = null;
853
+ this.#relations = null;
854
+ this.#repositories = null;
855
+ }
856
+ getRepository(name) {
857
+ const table = this.#tables?.[name];
858
+ if (!table || !this.#db) throw new Error(`DrizzleOrm "${this.name}": no entity table registered under "${name}".`);
859
+ if (this.#repositories === null) this.#repositories = Object.create(null);
860
+ let repository = this.#repositories[name];
861
+ if (repository === void 0) {
862
+ repository = new DrizzleRepository(this.#db, table, this.#relations?.[name] ?? {}, this.name, this.#dialect);
863
+ this.#repositories[name] = repository;
864
+ }
865
+ return repository;
866
+ }
867
+ /**
868
+ * Exécute `work` dans une transaction, sur les TROIS dialectes : commit si la
869
+ * closure résout, rollback si elle rejette (cf {@link DrizzleTransaction}).
870
+ *
871
+ * Un repository n'entre dans la transaction que lié par `withTransaction(tx)` :
872
+ * en postgres/mysql, la transaction tient une connexion dédiée du pool, tandis
873
+ * que `getRepository()` écrit via le pool — donc hors transaction.
874
+ *
875
+ * @param work - travail transactionnel ; reçoit la transaction à lier aux repositories.
876
+ * @returns la valeur rendue par `work`.
877
+ * @throws `not connected` hors connexion ; sinon l'erreur de `work`, après rollback.
878
+ */
879
+ async transaction(work) {
880
+ const begin = this.#beginTx;
881
+ if (!begin) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
882
+ const tx = await begin();
883
+ try {
884
+ const result = await work(tx);
885
+ await tx.commit();
886
+ return result;
887
+ } catch (error) {
888
+ await tx.rollback().catch(() => void 0);
889
+ throw error;
890
+ }
891
+ }
892
+ getNativeConnection() {
893
+ if (!this.#db) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
894
+ return this.#db;
895
+ }
896
+ /**
897
+ * État des migrations de ce connecteur — la MÊME charge utile que
898
+ * `orm:migrate:status --json`, jamais un second calcul.
899
+ *
900
+ * Le travail est fait par le lecteur injecté à la construction : lui seul
901
+ * détient la configuration (fichiers, mode `ddl`, coordonnées), quand cet
902
+ * objet ne connaît que sa connexion. Sans lecteur — un ORM construit à la
903
+ * main dans un banc —, la réponse dit exactement cela, et surtout PAS « ne
904
+ * porte pas de migrations » : ce serait faux d'un connecteur SQL, et un
905
+ * message faux publié est appris par les scripts qui le lisent.
906
+ *
907
+ * @returns l'état, ou l'empêchement qui explique pourquoi il n'y en a pas.
908
+ */
909
+ async migrationStatus() {
910
+ if (this.#migrationStatus) return this.#migrationStatus();
911
+ return failureFrom(this.name, notConfigured(this.name, this.#dialect));
912
+ }
913
+ /**
914
+ * Ce qui S'APPLIQUERAIT, avec son SQL — lecture seule.
915
+ *
916
+ * @returns le plan, ou l'empêchement.
917
+ */
918
+ async migrationPlan() {
919
+ if (this.#migrationPlan) return this.#migrationPlan();
920
+ return failureFrom(this.name, notConfigured(this.name, this.#dialect));
921
+ }
922
+ /**
923
+ * Applique les migrations en attente — **développement seulement**, le
924
+ * lecteur injecté refuse ailleurs en le disant.
925
+ *
926
+ * @returns ce qui a été appliqué, ou l'empêchement.
927
+ */
928
+ async applyMigrations() {
929
+ if (this.#applyMigrations) return this.#applyMigrations();
930
+ return failureFrom(this.name, notConfigured(this.name, this.#dialect));
931
+ }
932
+ /**
933
+ * Ping bas-coût : `SELECT 1` — round-trip RÉEL vers la base, routé par
934
+ * dialecte (pool pg/mysql, client better-sqlite3 synchrone).
935
+ *
936
+ * @throws si le connecteur n'est pas connecté.
937
+ */
938
+ async ping() {
939
+ if (this.#pgPool) {
940
+ await this.#pgPool.query("SELECT 1");
941
+ return;
942
+ }
943
+ if (this.#mysqlPool) {
944
+ await this.#mysqlPool.query("SELECT 1");
945
+ return;
946
+ }
947
+ if (!this.#client) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
948
+ this.#client.prepare("SELECT 1").get();
949
+ }
950
+ /**
951
+ * Sonde driver-spécifique, **routée par dialecte** — alimente le data plane
952
+ * admin (panneau Studio ORM) :
953
+ * - **sqlite** : stockage via PRAGMA (synchrone, bon marché) — taille
954
+ * (`page_count × page_size`), mode de journal (WAL ?), pages libres ;
955
+ * - **postgres / mysql** : état du **pool**, la métrique qui compte sur une
956
+ * base serveur (sa saturation est une falaise de débit) — lu sur des
957
+ * compteurs EN MÉMOIRE, donc sans requête réseau.
958
+ *
959
+ * Le stockage d'une base serveur n'est PAS sondé : il coûterait une requête
960
+ * (`pg_database_size`…) à chaque appel, pour une donnée que l'admin du SGBD
961
+ * expose déjà. Mieux vaut ne rien promettre que promettre en silence.
962
+ *
963
+ * Best-effort — `{}` seulement si le connecteur n'est pas connecté.
964
+ *
965
+ * @returns sonde `storage` (sqlite) ou `pool` (serveur), `{}` hors connexion.
966
+ */
967
+ async probe() {
968
+ if (this.#pgPool) return this.#probePgPool(this.#pgPool);
969
+ if (this.#mysqlPool) return this.#probeMysqlPool(this.#mysqlPool);
970
+ const client = this.#client;
971
+ if (!client) return {};
972
+ try {
973
+ const num = (sql, key) => {
974
+ const v = client.prepare(sql).get()?.[key];
975
+ return typeof v === "number" ? v : void 0;
976
+ };
977
+ const str = (sql, key) => {
978
+ const v = client.prepare(sql).get()?.[key];
979
+ return typeof v === "string" ? v : void 0;
980
+ };
981
+ const pages = num("PRAGMA page_count", "page_count");
982
+ const pageSize = num("PRAGMA page_size", "page_size");
983
+ return {
984
+ storage: {
985
+ pages,
986
+ pageSize,
987
+ sizeBytes: pages !== void 0 && pageSize !== void 0 ? pages * pageSize : void 0,
988
+ journalMode: str("PRAGMA journal_mode", "journal_mode"),
989
+ freePages: num("PRAGMA freelist_count", "freelist_count")
990
+ },
991
+ extra: { connections: 1 }
992
+ };
993
+ } catch {
994
+ return {};
995
+ }
996
+ }
997
+ /**
998
+ * Sonde du pool **postgres** — compteurs publics du driver `pg`, lus en
999
+ * mémoire (aucune requête).
1000
+ *
1001
+ * @param pool - pool `pg` du connecteur.
1002
+ * @returns sonde `pool` (taille max, idle, empruntées, en attente).
1003
+ */
1004
+ #probePgPool(pool) {
1005
+ const max = pool.options?.max;
1006
+ return { pool: {
1007
+ size: typeof max === "number" ? max : 10,
1008
+ available: pool.idleCount,
1009
+ borrowed: pool.totalCount - pool.idleCount,
1010
+ pending: pool.waitingCount
1011
+ } };
1012
+ }
1013
+ /**
1014
+ * Sonde du pool **mysql** — `mysql2` ne publie AUCUN compteur (contrairement
1015
+ * à `pg`) : seuls des champs internes portent l'état, sous le pool callback
1016
+ * (`promisePool.pool`). Accès défensif (chaque champ narrowé, jamais de cast
1017
+ * aveugle) → un renommage chez `mysql2` rend une sonde PARTIELLE, jamais une
1018
+ * exception ; et le banc de contrat, qui exige ces champs sur les dialectes
1019
+ * serveur, vire au rouge pour le dire.
1020
+ *
1021
+ * @param pool - pool `mysql2/promise` du connecteur.
1022
+ * @returns sonde `pool` (champs omis si le driver ne les expose plus).
1023
+ */
1024
+ #probeMysqlPool(pool) {
1025
+ const internals = pool.pool;
1026
+ const len = (q) => typeof q?.length === "number" ? q.length : void 0;
1027
+ const all = len(internals?._allConnections);
1028
+ const free = len(internals?._freeConnections);
1029
+ return { pool: {
1030
+ size: internals?.config?.connectionLimit,
1031
+ available: free,
1032
+ borrowed: all !== void 0 && free !== void 0 ? all - free : void 0,
1033
+ pending: len(internals?._connectionQueue)
1034
+ } };
1035
+ }
1036
+ /**
1037
+ * Colonnes normalisées d'une entité, dérivées du DDL Drizzle
1038
+ * (`getTableConfig`) — alimente le graphe canonique / ERD / contexte IA.
1039
+ *
1040
+ * @param name - nom logique de l'entité.
1041
+ * @returns colonnes (`[]` si l'entité n'est pas connue de cet ORM).
1042
+ */
1043
+ describeEntity(name) {
1044
+ const table = this.#tables?.[name];
1045
+ if (!table) return [];
1046
+ return (this.#dialect === "postgres" ? getTableConfig$1(table).columns : this.#dialect === "mysql" ? getTableConfig$2(table).columns : getTableConfig(table).columns).map((col) => ({
1047
+ name: col.name,
1048
+ type: col.getSQLType(),
1049
+ primaryKey: col.primary,
1050
+ nullable: !col.notNull,
1051
+ unique: col.isUnique ?? false
1052
+ }));
1053
+ }
1054
+ /**
1055
+ * Décrit le schéma ENTIER attendu par le code : une entrée par table.
1056
+ *
1057
+ * Même calcul que {@link DrizzleOrm.describeEntity}, à l'échelle du
1058
+ * connecteur — et c'est le point : le rattrapage de colonnes au démarrage, le
1059
+ * constat de divergence et l'écran d'administration comparent tous « ce que
1060
+ * le code déclare » à « ce que la base contient ». Trois lecteurs, un seul
1061
+ * producteur ; recopier ce parcours ailleurs le ferait diverger du jour où
1062
+ * un dialecte s'ajoute.
1063
+ *
1064
+ * @returns une entrée par table, avec son NOM EN BASE (≠ nom d'entité).
1065
+ */
1066
+ describeTables() {
1067
+ const out = [];
1068
+ for (const entity of this.#ownEntities()) {
1069
+ const table = this.#tables?.[entity.name];
1070
+ if (!table) continue;
1071
+ const name = this.#dialect === "postgres" ? getTableConfig$1(table).name : this.#dialect === "mysql" ? getTableConfig$2(table).name : getTableConfig(table).name;
1072
+ out.push({
1073
+ entity: entity.name,
1074
+ table: name,
1075
+ columns: this.describeEntity(entity.name)
1076
+ });
1077
+ }
1078
+ return out;
1079
+ }
1080
+ /**
1081
+ * Décrit la connexion : driver `sqlite` (better-sqlite3) + cible (chemin du
1082
+ * fichier, `:memory:` pour les tests). Aucun credential (SQLite = fichier local).
1083
+ * Le chemin est **relativisé** à la racine du process : on ne fuite jamais
1084
+ * l'arborescence absolue du serveur (home, structure FS) dans le data plane.
1085
+ *
1086
+ * @returns driver + cible (chemin relatif, basename si hors projet, ou `:memory:`).
1087
+ */
1088
+ describeConnection() {
1089
+ if (this.#dialect === "postgres" || this.#dialect === "mysql") return {
1090
+ driver: this.#dialect,
1091
+ target: this.#safeUrlTarget(),
1092
+ ormVersion: DrizzleOrm.#ormVersion()
1093
+ };
1094
+ return {
1095
+ driver: "sqlite",
1096
+ target: this.#safeTarget(),
1097
+ version: this.#sqliteVersion(),
1098
+ ormVersion: DrizzleOrm.#ormVersion()
1099
+ };
1100
+ }
1101
+ /** Cible réseau affichable : `host:port/db`, **sans** user/password (anti-leak). */
1102
+ #safeUrlTarget() {
1103
+ return describeTargetSafely({
1104
+ dialect: this.#dialect,
1105
+ url: this.#url
1106
+ });
1107
+ }
1108
+ /** Version de la lib `drizzle-orm` (résolue + cachée une seule fois). */
1109
+ static #cachedOrmVersion;
1110
+ static #ormVersion() {
1111
+ if (DrizzleOrm.#cachedOrmVersion === void 0) DrizzleOrm.#cachedOrmVersion = DrizzleOrm.#resolvePkgVersion("drizzle-orm") ?? null;
1112
+ return DrizzleOrm.#cachedOrmVersion ?? void 0;
1113
+ }
1114
+ /**
1115
+ * Version d'un package npm via son `package.json` — `createRequire` +
1116
+ * remontée FS. (`require("<pkg>/package.json")` direct échoue souvent :
1117
+ * `exports` ne publie pas toujours `./package.json`.)
1118
+ */
1119
+ static #resolvePkgVersion(name) {
1120
+ try {
1121
+ const req = createRequire(import.meta.url);
1122
+ let dir = path.dirname(req.resolve(name));
1123
+ for (let i = 0; i < 8; i++) {
1124
+ const pkgPath = path.join(dir, "package.json");
1125
+ if (fs.existsSync(pkgPath)) {
1126
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1127
+ if (pkg.name === name) return pkg.version;
1128
+ }
1129
+ const parent = path.dirname(dir);
1130
+ if (parent === dir) break;
1131
+ dir = parent;
1132
+ }
1133
+ } catch {}
1134
+ }
1135
+ /** Cible affichable : `:memory:` tel quel, sinon chemin relatif au cwd
1136
+ * (basename si hors projet) — jamais d'absolu (anti info-leak). */
1137
+ #safeTarget() {
1138
+ return describeTargetSafely({
1139
+ dialect: "sqlite",
1140
+ filename: this.#filename
1141
+ });
1142
+ }
1143
+ /** Version du moteur SQLite (`SELECT sqlite_version()`), si connecté. */
1144
+ #sqliteVersion() {
1145
+ if (!this.#client) return void 0;
1146
+ try {
1147
+ return this.#client.prepare("SELECT sqlite_version() AS v").get()?.v;
1148
+ } catch {
1149
+ return;
1150
+ }
1151
+ }
1152
+ };
1153
+ //#endregion
1154
+ export { DrizzleOrm };