@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,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 };
|