@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,610 @@
|
|
|
1
|
+
import { likeCond } from "../likeSql.js";
|
|
2
|
+
import { RequestContext, redactSecrets } from "nodefony";
|
|
3
|
+
import { UnknownCriteriaField, assertOrderOption, isFieldOperators, isUpdateOperators, queryFlowMonitor } from "@nodefony/orm-core";
|
|
4
|
+
import { and, asc, count, countDistinct, desc, eq, getTableColumns, getTableName, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
|
|
5
|
+
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
6
|
+
import { getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
|
|
7
|
+
import { getTableConfig as getTableConfig$2 } from "drizzle-orm/mysql-core";
|
|
8
|
+
//#region nodefony/src/orm-core/DrizzleRepository.ts
|
|
9
|
+
/**
|
|
10
|
+
* Vue d'exécution canonique (typage sqlite) d'une table multi-dialecte —
|
|
11
|
+
* **LE point unique** de conversion vers les builders (cf {@link DrizzleDb} :
|
|
12
|
+
* la surface builder est structurellement identique sqlite/pg, prouvée e2e).
|
|
13
|
+
* Consommé aussi par les stores à requêtes builder (`DrizzleAuditStore`).
|
|
14
|
+
*/
|
|
15
|
+
function execTable(table) {
|
|
16
|
+
return table;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Borne du cache de formes préparées d'un repository — au-delà, les formes
|
|
20
|
+
* excédentaires s'exécutent par le chemin non préparé (repli, jamais d'éviction :
|
|
21
|
+
* une entrée retient un statement natif, l'éviction re-préparerait en boucle).
|
|
22
|
+
*/
|
|
23
|
+
const PREPARED_CACHE_MAX = 128;
|
|
24
|
+
/** Séquence des noms de prepared statements (PG les exige uniques par process). */
|
|
25
|
+
let preparedNameSeq = 0;
|
|
26
|
+
/**
|
|
27
|
+
* Repository portable (contrat {@link IRepository}) au-dessus d'une table Drizzle
|
|
28
|
+
* + driver `better-sqlite3`.
|
|
29
|
+
*
|
|
30
|
+
* 3ᵉ adapter du banc orm-core (ADR-0003) : valide le contrat sur un ORM
|
|
31
|
+
* **type-safe-first** dont le `WHERE` est un *builder* d'expressions (pas un objet
|
|
32
|
+
* plat). Spécificités traduites ici :
|
|
33
|
+
* - critère portable → expressions Drizzle (`eq`/`and`/`gt`/`inArray`/`like`...),
|
|
34
|
+
* opérateurs riches inclus (résolution ADR-0003 risque #3) ;
|
|
35
|
+
* - **eager-load manuel** (`options.relations`) : une requête `IN (...)` par
|
|
36
|
+
* relation déclarée, puis regroupement en mémoire — portable sans déclarer la
|
|
37
|
+
* couche `relations()` de Drizzle ;
|
|
38
|
+
* - liaison transactionnelle via {@link DrizzleRepository.withTransaction} (le
|
|
39
|
+
* handle de transaction *est* un db Drizzle → réutilisé tel quel).
|
|
40
|
+
*
|
|
41
|
+
* @typeParam T - forme plate de l'entité gérée.
|
|
42
|
+
*/
|
|
43
|
+
var DrizzleRepository = class DrizzleRepository {
|
|
44
|
+
#db;
|
|
45
|
+
#table;
|
|
46
|
+
#relations;
|
|
47
|
+
/** Connecteur ORM (clé du registre) — tag des métriques de flux. */
|
|
48
|
+
#connector;
|
|
49
|
+
/** Dialecte SQL du connecteur — route les rares divergences syntaxiques
|
|
50
|
+
* (OFFSET-sans-LIMIT, introspection PK composite). */
|
|
51
|
+
#dialect;
|
|
52
|
+
/**
|
|
53
|
+
* Colonnes de la clé primaire (lazy — résolu au premier `*One`) : `null` =
|
|
54
|
+
* aucune PK déclarée (fallback `rowid`, SQLite-only) ; `undefined` = pas
|
|
55
|
+
* encore résolu. Rien d'alloué au constructeur (règle perf).
|
|
56
|
+
*/
|
|
57
|
+
#pk;
|
|
58
|
+
/**
|
|
59
|
+
* Cache des `SELECT` préparés, indexé par FORME de requête (lazy — alloué au
|
|
60
|
+
* premier `find` mémoïsable, règle perf). Vit aussi longtemps que le
|
|
61
|
+
* repository — que `DrizzleOrm.getRepository` mémoïse et jette au
|
|
62
|
+
* `disconnect()` avec les statements qu'il retient.
|
|
63
|
+
*/
|
|
64
|
+
#preparedSelects = null;
|
|
65
|
+
/** `true` = repository lié à une transaction : mémoïsation coupée (cf ctor). */
|
|
66
|
+
#transactional;
|
|
67
|
+
/**
|
|
68
|
+
* @param db - handle Drizzle (instance racine ou transaction).
|
|
69
|
+
* @param table - table Drizzle de l'entité (variante du dialecte).
|
|
70
|
+
* @param relations - relations résolues (eager-load), indexées par champ.
|
|
71
|
+
* @param connector - nom de la connexion (clé du registre) — défaut `"default"`.
|
|
72
|
+
* @param dialect - dialecte SQL du connecteur — défaut `"sqlite"`.
|
|
73
|
+
* @param transactional - `true` quand `db` est un handle de transaction :
|
|
74
|
+
* l'instance est jetable (une par `withTransaction`) et, en pg, sa connexion
|
|
75
|
+
* dédiée est rendue au commit — préparer dessus coûterait sans jamais
|
|
76
|
+
* s'amortir. Les transactions gardent le chemin non préparé.
|
|
77
|
+
*/
|
|
78
|
+
constructor(db, table, relations, connector = "default", dialect = "sqlite", transactional = false) {
|
|
79
|
+
this.#db = db;
|
|
80
|
+
this.#table = table;
|
|
81
|
+
this.#relations = relations;
|
|
82
|
+
this.#connector = connector;
|
|
83
|
+
this.#dialect = dialect;
|
|
84
|
+
this.#transactional = transactional;
|
|
85
|
+
}
|
|
86
|
+
/** Colonne Drizzle d'une table par nom logique. */
|
|
87
|
+
#col(table, name) {
|
|
88
|
+
return table[name];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Colonnes de la PK de la table (mémoïsées) : colonnes inline `.primaryKey()`
|
|
92
|
+
* d'abord (introspection core, valide sur tous les dialectes — couvre toutes
|
|
93
|
+
* les entités framework), sinon PK composite déclarée via `primaryKey({
|
|
94
|
+
* columns })` (extraConfig du dialecte, best-effort), sinon `null`.
|
|
95
|
+
*/
|
|
96
|
+
#pkColumns() {
|
|
97
|
+
if (this.#pk !== void 0) return this.#pk;
|
|
98
|
+
let cols = Object.values(getTableColumns(this.#table)).filter((col) => col.primary);
|
|
99
|
+
if (cols.length === 0) try {
|
|
100
|
+
cols = this.#dialect === "postgres" ? [...getTableConfig$1(this.#table).primaryKeys[0]?.columns ?? []] : this.#dialect === "mysql" ? [...getTableConfig$2(this.#table).primaryKeys[0]?.columns ?? []] : [...getTableConfig(this.#table).primaryKeys[0]?.columns ?? []];
|
|
101
|
+
} catch {
|
|
102
|
+
cols = [];
|
|
103
|
+
}
|
|
104
|
+
this.#pk = cols.length > 0 ? cols : null;
|
|
105
|
+
return this.#pk;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Prédicat `WHERE` qui borne un UPDATE/DELETE à **au plus une** ligne :
|
|
109
|
+
* `pk IN (SELECT pk FROM (SELECT pk FROM t WHERE … LIMIT 1) AS picked)`.
|
|
110
|
+
*
|
|
111
|
+
* POURQUOI cette forme (et pas `rowid` / `LIMIT` direct) :
|
|
112
|
+
* - `rowid` est SQLite-only — c'était le SQL dialect-spécifique n°1 du
|
|
113
|
+
* repository (audit comparatif ORM 2026-07, garde-fou G3) ;
|
|
114
|
+
* - `UPDATE … LIMIT 1` n'est pas du SQL standard (PG le rejette) ;
|
|
115
|
+
* - la **table dérivée** intermédiaire est requise par MySQL, qui interdit à
|
|
116
|
+
* la fois `LIMIT` dans une sous-requête `IN` directe ET la re-lecture de
|
|
117
|
+
* la table cible d'un UPDATE/DELETE en sous-requête (ERROR 1093) — la
|
|
118
|
+
* dérivée force la matérialisation. SQLite et PG l'acceptent telle quelle
|
|
119
|
+
* → une seule forme pour les trois dialectes ;
|
|
120
|
+
* - PK composite : row-values `(a, b) IN (…)` (SQLite ≥ 3.15 / PG / MySQL).
|
|
121
|
+
*
|
|
122
|
+
* Fallback sans PK déclarée : `rowid` (SQLite-only — toutes les entités
|
|
123
|
+
* framework ont une PK ; une table d'app sans PK est un cas sqlite assumé).
|
|
124
|
+
*/
|
|
125
|
+
#pickOne(where) {
|
|
126
|
+
const pk = this.#pkColumns();
|
|
127
|
+
if (!pk) return where ? sql`rowid in (select rowid from ${this.#table} where ${where} limit 1)` : sql`rowid in (select rowid from ${this.#table} limit 1)`;
|
|
128
|
+
const qualified = sql.join(pk, sql.raw(", "));
|
|
129
|
+
const inner = where ? sql`select ${qualified} from ${this.#table} where ${where} limit 1` : sql`select ${qualified} from ${this.#table} limit 1`;
|
|
130
|
+
const output = sql.join(pk.map((col) => sql.identifier(col.name)), sql.raw(", "));
|
|
131
|
+
const first = pk[0];
|
|
132
|
+
const target = pk.length === 1 && first ? sql`${first}` : sql`(${qualified})`;
|
|
133
|
+
return sql`${target} in (select ${output} from (${inner}) as picked)`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Lignes affectées, normalisé par driver : better-sqlite3 `{changes}` /
|
|
137
|
+
* pg `{rowCount}` / mysql2 tuple `[ResultSetHeader{affectedRows}, fields]`.
|
|
138
|
+
*/
|
|
139
|
+
#affected(result) {
|
|
140
|
+
if (Array.isArray(result)) return result[0]?.affectedRows ?? 0;
|
|
141
|
+
const r = result;
|
|
142
|
+
return r.changes ?? r.rowCount ?? 0;
|
|
143
|
+
}
|
|
144
|
+
/** Tronque + redacte un SQL paramétré pour l'affichage (jamais de valeur). */
|
|
145
|
+
#safeSql(builder) {
|
|
146
|
+
let statement;
|
|
147
|
+
try {
|
|
148
|
+
statement = builder.toSQL().sql;
|
|
149
|
+
} catch {
|
|
150
|
+
statement = "<drizzle query>";
|
|
151
|
+
}
|
|
152
|
+
return redactSecrets(statement.length > 2e3 ? `${statement.slice(0, 2e3)}…` : statement);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Tap dev-only : exécute le builder en mesurant la durée, alimente **deux**
|
|
156
|
+
* sondes complémentaires (sans surcoût quand les deux sont inactives) :
|
|
157
|
+
* 1. **profiler par-requête** (buffer de scope ALS, debug bar) — capture le
|
|
158
|
+
* SQL paramétré de CHAQUE requête tracée ;
|
|
159
|
+
* 2. **flux ORM agrégé** ({@link queryFlowMonitor}, process-wide) — compte le
|
|
160
|
+
* débit + la latence ; n'extrait le SQL que sur le chemin **lent** (rare).
|
|
161
|
+
*
|
|
162
|
+
* POURQUOI lecture directe de l'ALS (≠ tap par-requête d'un autre ORM) :
|
|
163
|
+
* `better-sqlite3` est **synchrone**, sans pool → l'ALS reste valide pendant
|
|
164
|
+
* `await builder`. Les deux drapeaux sont lus **avant toute allocation** →
|
|
165
|
+
* coût nul quand rien n'observe (prod, bancs de charge hors kernel).
|
|
166
|
+
*
|
|
167
|
+
* Sécurité : `toSQL()` renvoie le SQL **paramétré** (placeholders `?`, jamais
|
|
168
|
+
* les valeurs) → credentials hors texte ; `redactSecrets` en défense en profondeur.
|
|
169
|
+
*
|
|
170
|
+
* @param builder - requête Drizzle (thenable + `toSQL()`).
|
|
171
|
+
* @returns le résultat de la requête.
|
|
172
|
+
*/
|
|
173
|
+
async #prof(builder) {
|
|
174
|
+
if (!RequestContext.get()?.queries && !queryFlowMonitor.enabled) return builder;
|
|
175
|
+
return this.#measure(() => builder, () => this.#safeSql(builder));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Tap des `SELECT` préparés — même contrat que {@link DrizzleRepository.#prof}
|
|
179
|
+
* (mêmes deux sondes, mêmes drapeaux), le SQL redacté étant ici précalculé à
|
|
180
|
+
* la construction de l'entrée (zéro `toSQL()` par requête).
|
|
181
|
+
*/
|
|
182
|
+
async #profPrepared(entry, params) {
|
|
183
|
+
if (!RequestContext.get()?.queries && !queryFlowMonitor.enabled) return entry.prepared.execute(params);
|
|
184
|
+
return this.#measure(() => entry.prepared.execute(params), () => entry.sqlSafe);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Cœur de mesure partagé des deux taps : exécute, chronomètre, alimente le
|
|
188
|
+
* flux agrégé ({@link queryFlowMonitor} — texte SQL sur le chemin lent
|
|
189
|
+
* seulement) et le buffer par-requête (ALS, debug bar). N'est appelé QUE
|
|
190
|
+
* quand une sonde est active — il peut donc relire les deux drapeaux sans
|
|
191
|
+
* coût pour le chemin nominal.
|
|
192
|
+
*/
|
|
193
|
+
async #measure(run, sqlSafe) {
|
|
194
|
+
const buf = RequestContext.get()?.queries;
|
|
195
|
+
const flow = queryFlowMonitor.enabled;
|
|
196
|
+
const start = performance.now();
|
|
197
|
+
const result = await run();
|
|
198
|
+
const durationMs = performance.now() - start;
|
|
199
|
+
if (flow) {
|
|
200
|
+
const statement = durationMs >= queryFlowMonitor.slowMs ? sqlSafe() : void 0;
|
|
201
|
+
queryFlowMonitor.record(this.#connector, durationMs, statement);
|
|
202
|
+
}
|
|
203
|
+
if (buf) buf.push({
|
|
204
|
+
sql: sqlSafe(),
|
|
205
|
+
startMs: start,
|
|
206
|
+
durationMs,
|
|
207
|
+
rows: Array.isArray(result) ? result.length : void 0,
|
|
208
|
+
connector: "drizzle"
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
/** Empile les conditions d'un objet d'opérateurs riches sur une colonne. */
|
|
213
|
+
#pushOperators(conds, col, ops) {
|
|
214
|
+
if (ops.$eq !== void 0) conds.push(eq(col, ops.$eq));
|
|
215
|
+
if (ops.$ne !== void 0) conds.push(ne(col, ops.$ne));
|
|
216
|
+
if (ops.$gt !== void 0) conds.push(gt(col, ops.$gt));
|
|
217
|
+
if (ops.$gte !== void 0) conds.push(gte(col, ops.$gte));
|
|
218
|
+
if (ops.$lt !== void 0) conds.push(lt(col, ops.$lt));
|
|
219
|
+
if (ops.$lte !== void 0) conds.push(lte(col, ops.$lte));
|
|
220
|
+
if (ops.$in !== void 0) conds.push(inArray(col, [...ops.$in]));
|
|
221
|
+
if (ops.$nin !== void 0) conds.push(notInArray(col, [...ops.$nin]));
|
|
222
|
+
if (ops.$like !== void 0) conds.push(likeCond(this.#dialect, sql`${col}`, ops.$like));
|
|
223
|
+
if (ops.$null !== void 0) conds.push(ops.$null ? isNull(col) : isNotNull(col));
|
|
224
|
+
}
|
|
225
|
+
/** Traduit un critère portable en expression `WHERE` Drizzle (ou `undefined`). */
|
|
226
|
+
#where(criteria) {
|
|
227
|
+
if (!criteria) return;
|
|
228
|
+
const conds = [];
|
|
229
|
+
for (const [field, value] of Object.entries(criteria)) {
|
|
230
|
+
if (field === "$or") {
|
|
231
|
+
if (!Array.isArray(value)) throw new Error(`DrizzleRepository(${getTableName(this.#table)}): $or attend un tableau de critères.`);
|
|
232
|
+
const branches = value.map((branch) => this.#where(branch)).filter((branch) => branch !== void 0);
|
|
233
|
+
if (branches.length > 0) conds.push(branches.length === 1 ? branches[0] : or(...branches));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const col = this.#col(this.#table, field);
|
|
237
|
+
if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
|
|
238
|
+
if (isFieldOperators(value)) this.#pushOperators(conds, col, value);
|
|
239
|
+
else if (value === null) conds.push(isNull(col));
|
|
240
|
+
else conds.push(eq(col, value));
|
|
241
|
+
}
|
|
242
|
+
if (conds.length === 0) return;
|
|
243
|
+
return conds.length === 1 ? conds[0] : and(...conds);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* FORME d'un `SELECT` mémoïsable + valeurs à binder, ou `null` si la requête
|
|
247
|
+
* doit rester sur le chemin non préparé.
|
|
248
|
+
*
|
|
249
|
+
* La forme = ce qui décide du TEXTE SQL : champs du critère dans l'ordre
|
|
250
|
+
* (un `null` y est marqué à part — `IS NULL` n'est pas paramétrable), tri,
|
|
251
|
+
* présence de limit/offset. Les VALEURS n'en font jamais partie : elles
|
|
252
|
+
* deviennent des placeholders (`p<i>`, `lim`, `off`) bindés à l'exécution.
|
|
253
|
+
*
|
|
254
|
+
* Repli (`null`) par construction : `$or` et opérateurs riches (le SQL varie
|
|
255
|
+
* avec la cardinalité — `$in` — ou la combinaison d'opérateurs), valeur
|
|
256
|
+
* `undefined` (le chemin actuel la rejette, même contrat), transaction
|
|
257
|
+
* (cf ctor), cache plein ({@link PREPARED_CACHE_MAX}).
|
|
258
|
+
*/
|
|
259
|
+
#selectShape(criteria, options) {
|
|
260
|
+
let key = "";
|
|
261
|
+
const params = {};
|
|
262
|
+
if (criteria) {
|
|
263
|
+
let i = 0;
|
|
264
|
+
for (const [field, value] of Object.entries(criteria)) {
|
|
265
|
+
if (field === "$or" || value === void 0 || isFieldOperators(value)) return null;
|
|
266
|
+
if (value === null) key += `${field}\x01\x00`;
|
|
267
|
+
else {
|
|
268
|
+
key += `${field}\x00`;
|
|
269
|
+
params[`p${i}`] = value;
|
|
270
|
+
}
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (options?.order?.length) {
|
|
275
|
+
key += "";
|
|
276
|
+
for (const [field, dir] of options.order) key += `${field}\x03${dir}\x00`;
|
|
277
|
+
}
|
|
278
|
+
if (options?.limit !== void 0) {
|
|
279
|
+
key += "L";
|
|
280
|
+
params["lim"] = options.limit;
|
|
281
|
+
}
|
|
282
|
+
if (options?.offset !== void 0) {
|
|
283
|
+
key += "O";
|
|
284
|
+
params["off"] = options.offset;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
key,
|
|
288
|
+
params
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Construit puis PRÉPARE le `SELECT` d'une forme — même composition que le
|
|
293
|
+
* chemin non préparé de {@link DrizzleRepository.#runSelect}, les valeurs
|
|
294
|
+
* remplacées par `sql.placeholder()`. Payé UNE fois par forme :
|
|
295
|
+
* - sqlite : le statement natif better-sqlite3 est compilé ici et réutilisé
|
|
296
|
+
* (`client.prepare` dans le `prepareQuery` du driver) ;
|
|
297
|
+
* - postgres : requête NOMMÉE node-postgres → plan caché par connexion du
|
|
298
|
+
* pool (le nom, unique par process, est exigé par `prepare(name)`) ;
|
|
299
|
+
* - mysql : drizzle passe par `client.query()` (jamais de prepare protocole)
|
|
300
|
+
* → le gain se limite à ne plus refaire `sqlToQuery` à chaque requête.
|
|
301
|
+
*/
|
|
302
|
+
#buildPreparedSelect(criteria, options) {
|
|
303
|
+
let query = this.#db.select().from(execTable(this.#table)).$dynamic();
|
|
304
|
+
if (criteria) {
|
|
305
|
+
const conds = [];
|
|
306
|
+
let i = 0;
|
|
307
|
+
for (const [field, value] of Object.entries(criteria)) {
|
|
308
|
+
const col = this.#col(this.#table, field);
|
|
309
|
+
if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
|
|
310
|
+
conds.push(value === null ? isNull(col) : eq(col, sql.param(sql.placeholder(`p${i}`), col)));
|
|
311
|
+
i++;
|
|
312
|
+
}
|
|
313
|
+
if (conds.length > 0) query = query.where(conds.length === 1 ? conds[0] : and(...conds));
|
|
314
|
+
}
|
|
315
|
+
if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#col(this.#table, field)) : asc(this.#col(this.#table, field))));
|
|
316
|
+
if (options?.limit !== void 0) query = query.limit(sql.placeholder("lim"));
|
|
317
|
+
else if (options?.offset !== void 0 && this.#dialect === "sqlite") query = query.limit(sql`-1`);
|
|
318
|
+
else if (options?.offset !== void 0 && this.#dialect === "mysql") query = query.limit(Number.MAX_SAFE_INTEGER);
|
|
319
|
+
if (options?.offset !== void 0) query = query.offset(sql.placeholder("off"));
|
|
320
|
+
const sqlSafe = this.#safeSql(query);
|
|
321
|
+
return {
|
|
322
|
+
prepared: query.prepare(`nf_ps_${++preparedNameSeq}`),
|
|
323
|
+
sqlSafe
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Entrée préparée d'un `SELECT` + valeurs à binder, ou `null` (repli chemin
|
|
328
|
+
* non préparé). C'est LE remède au goulot mesuré du cycle ORM : drizzle
|
|
329
|
+
* refabriquait (`build` ~39 % du CPU du chemin read) et re-préparait (~9 %)
|
|
330
|
+
* la MÊME requête à chaque requête HTTP — la forme ne changeait jamais, seules
|
|
331
|
+
* les valeurs bindées changeaient.
|
|
332
|
+
*/
|
|
333
|
+
#preparedSelect(criteria, options) {
|
|
334
|
+
if (this.#transactional) return null;
|
|
335
|
+
const shape = this.#selectShape(criteria, options);
|
|
336
|
+
if (shape === null) return null;
|
|
337
|
+
let cache = this.#preparedSelects;
|
|
338
|
+
if (cache === null) cache = this.#preparedSelects = /* @__PURE__ */ new Map();
|
|
339
|
+
let entry = cache.get(shape.key);
|
|
340
|
+
if (entry === void 0) {
|
|
341
|
+
if (cache.size >= PREPARED_CACHE_MAX) return null;
|
|
342
|
+
entry = this.#buildPreparedSelect(criteria, options);
|
|
343
|
+
cache.set(shape.key, entry);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
entry,
|
|
347
|
+
params: shape.params
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Exécute le `SELECT` (critère + pagination + tri), retourne des objets plats.
|
|
352
|
+
*
|
|
353
|
+
* Point de passage UNIQUE des options de lecture pour cet adapter (`find` y mène,
|
|
354
|
+
* `findOne` passe par `find`) : la garde sur `order` s'y pose donc une seule fois,
|
|
355
|
+
* en amont des trois endroits qui construisent un `ORDER BY` — chemin préparé,
|
|
356
|
+
* forme mémoïsée et chemin direct.
|
|
357
|
+
*
|
|
358
|
+
* @throws InvalidOrderOption si `options.order` n'est pas un tableau de couples.
|
|
359
|
+
*/
|
|
360
|
+
async #runSelect(criteria, options) {
|
|
361
|
+
assertOrderOption(options?.order, getTableName(this.#table));
|
|
362
|
+
const memo = this.#preparedSelect(criteria, options);
|
|
363
|
+
if (memo !== null) return this.#profPrepared(memo.entry, memo.params);
|
|
364
|
+
let query = this.#db.select().from(execTable(this.#table)).$dynamic();
|
|
365
|
+
const where = this.#where(criteria);
|
|
366
|
+
if (where) query = query.where(where);
|
|
367
|
+
if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#col(this.#table, field)) : asc(this.#col(this.#table, field))));
|
|
368
|
+
if (options?.limit !== void 0) query = query.limit(options.limit);
|
|
369
|
+
else if (options?.offset !== void 0 && this.#dialect === "sqlite") query = query.limit(sql`-1`);
|
|
370
|
+
else if (options?.offset !== void 0 && this.#dialect === "mysql") query = query.limit(Number.MAX_SAFE_INTEGER);
|
|
371
|
+
if (options?.offset !== void 0) query = query.offset(options.offset);
|
|
372
|
+
return await this.#prof(query);
|
|
373
|
+
}
|
|
374
|
+
/** Eager-load manuel des relations déclarées (1 requête `IN (...)` par relation). */
|
|
375
|
+
async #populate(rows, relations) {
|
|
376
|
+
if (rows.length === 0) return;
|
|
377
|
+
for (const name of relations) {
|
|
378
|
+
const rel = this.#relations[name];
|
|
379
|
+
if (!rel) throw new Error(`DrizzleRepository(${name}): relation "${name}" non déclarée.`);
|
|
380
|
+
if (rel.type === "one-to-many") {
|
|
381
|
+
const parentIds = rows.map((row) => row[rel.localKey]);
|
|
382
|
+
const fkCol = this.#col(rel.targetTable, rel.foreignKey);
|
|
383
|
+
const children = await this.#prof(this.#db.select().from(execTable(rel.targetTable)).where(inArray(fkCol, parentIds)));
|
|
384
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
385
|
+
for (const child of children) {
|
|
386
|
+
const key = child[rel.foreignKey];
|
|
387
|
+
const bucket = byParent.get(key);
|
|
388
|
+
if (bucket) bucket.push(child);
|
|
389
|
+
else byParent.set(key, [child]);
|
|
390
|
+
}
|
|
391
|
+
for (const row of rows) row[name] = byParent.get(row[rel.localKey]) ?? [];
|
|
392
|
+
} else {
|
|
393
|
+
const fkValues = rows.map((row) => row[rel.foreignKey]).filter((value) => value !== null && value !== void 0);
|
|
394
|
+
const idCol = this.#col(rel.targetTable, rel.targetKey);
|
|
395
|
+
const parents = fkValues.length > 0 ? await this.#prof(this.#db.select().from(execTable(rel.targetTable)).where(inArray(idCol, fkValues))) : [];
|
|
396
|
+
const byId = new Map(parents.map((p) => [p[rel.targetKey], p]));
|
|
397
|
+
for (const row of rows) row[name] = byId.get(row[rel.foreignKey]) ?? null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async find(criteria, options) {
|
|
402
|
+
const rows = await this.#runSelect(criteria, options);
|
|
403
|
+
if (options?.relations?.length) await this.#populate(rows, options.relations);
|
|
404
|
+
return rows;
|
|
405
|
+
}
|
|
406
|
+
async findOne(criteria, options) {
|
|
407
|
+
return (await this.find(criteria, {
|
|
408
|
+
...options,
|
|
409
|
+
limit: 1
|
|
410
|
+
}))[0] ?? null;
|
|
411
|
+
}
|
|
412
|
+
async create(data) {
|
|
413
|
+
if (this.#dialect === "mysql") return (await this.#mysqlInsertReturning([data]))[0];
|
|
414
|
+
return (await this.#prof(this.#db.insert(execTable(this.#table)).values(data).returning()))[0];
|
|
415
|
+
}
|
|
416
|
+
async createMany(data) {
|
|
417
|
+
if (data.length === 0) return [];
|
|
418
|
+
if (this.#dialect === "mysql") return await this.#mysqlInsertReturning(data);
|
|
419
|
+
return await this.#prof(this.#db.insert(execTable(this.#table)).values(data).returning());
|
|
420
|
+
}
|
|
421
|
+
async updateOne(criteria, data) {
|
|
422
|
+
if (this.#dialect === "mysql") return this.#mysqlUpdateOneReturning(data, criteria);
|
|
423
|
+
const pick = this.#pickOne(this.#where(criteria));
|
|
424
|
+
return (await this.#prof(this.#db.update(execTable(this.#table)).set(data).where(pick).returning()))[0] ?? null;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Traduit le `update` d'un upsert en deux vues : ce qu'on ÉCRIT à l'insertion
|
|
428
|
+
* (valeurs brutes) et ce qu'on ré-applique au conflit (`SET`), où les
|
|
429
|
+
* {@link UpdateOperators} deviennent une expression du dialecte.
|
|
430
|
+
*
|
|
431
|
+
* `$max`/`$min` → `MAX(col, ?)` en sqlite, `GREATEST/LEAST(col, ?)` en
|
|
432
|
+
* postgres/mysql : `col` y désigne la valeur EXISTANTE, et la valeur proposée
|
|
433
|
+
* est bindée. À l'insertion il n'y a rien à comparer → valeur brute.
|
|
434
|
+
*/
|
|
435
|
+
#writeSet(update) {
|
|
436
|
+
const set = {};
|
|
437
|
+
const values = {};
|
|
438
|
+
for (const [field, value] of Object.entries(update)) {
|
|
439
|
+
if (!isUpdateOperators(value)) {
|
|
440
|
+
set[field] = value;
|
|
441
|
+
values[field] = value;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const col = this.#col(this.#table, field);
|
|
445
|
+
if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
|
|
446
|
+
const apply = (fn, v) => {
|
|
447
|
+
set[field] = sql`${sql.raw(fn)}(${col}, ${v})`;
|
|
448
|
+
values[field] = v;
|
|
449
|
+
};
|
|
450
|
+
if (value.$max !== void 0) apply(this.#dialect === "sqlite" ? "MAX" : "GREATEST", value.$max);
|
|
451
|
+
if (value.$min !== void 0) apply(this.#dialect === "sqlite" ? "MIN" : "LEAST", value.$min);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
set,
|
|
455
|
+
values
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async upsert(criteria, update, insertOnly) {
|
|
459
|
+
const target = Object.keys(criteria).map((field) => {
|
|
460
|
+
const col = this.#col(this.#table, field);
|
|
461
|
+
if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
|
|
462
|
+
return col;
|
|
463
|
+
});
|
|
464
|
+
const write = this.#writeSet(update);
|
|
465
|
+
const values = {
|
|
466
|
+
...criteria,
|
|
467
|
+
...insertOnly ?? {},
|
|
468
|
+
...write.values
|
|
469
|
+
};
|
|
470
|
+
if (this.#dialect === "mysql") {
|
|
471
|
+
await this.#prof(this.#db.insert(execTable(this.#table)).values(values).onDuplicateKeyUpdate({ set: write.set }));
|
|
472
|
+
const conds = target.map((col) => eq(col, values[col.name]));
|
|
473
|
+
return (await this.#prof(this.#db.select().from(execTable(this.#table)).where(conds.length === 1 ? conds[0] : and(...conds)).limit(1)))[0];
|
|
474
|
+
}
|
|
475
|
+
return (await this.#prof(this.#db.insert(execTable(this.#table)).values(values).onConflictDoUpdate({
|
|
476
|
+
target,
|
|
477
|
+
set: write.set
|
|
478
|
+
}).returning()))[0];
|
|
479
|
+
}
|
|
480
|
+
async updateMany(criteria, data) {
|
|
481
|
+
const where = this.#where(criteria);
|
|
482
|
+
const builder = this.#db.update(execTable(this.#table)).set(data);
|
|
483
|
+
const result = await this.#prof(where ? builder.where(where) : builder);
|
|
484
|
+
return this.#affected(result);
|
|
485
|
+
}
|
|
486
|
+
async increment(criteria, changes) {
|
|
487
|
+
const setObj = {};
|
|
488
|
+
for (const [field, delta] of Object.entries(changes)) {
|
|
489
|
+
const col = this.#col(this.#table, field);
|
|
490
|
+
if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
|
|
491
|
+
setObj[field] = sql`${col} + ${delta}`;
|
|
492
|
+
}
|
|
493
|
+
if (this.#dialect === "mysql") return this.#mysqlUpdateOneReturning(setObj, criteria);
|
|
494
|
+
const pick = this.#pickOne(this.#where(criteria));
|
|
495
|
+
return (await this.#prof(this.#db.update(execTable(this.#table)).set(setObj).where(pick).returning()))[0] ?? null;
|
|
496
|
+
}
|
|
497
|
+
async delete(criteria) {
|
|
498
|
+
const where = this.#where(criteria);
|
|
499
|
+
const builder = this.#db.delete(execTable(this.#table));
|
|
500
|
+
const result = await this.#prof(where ? builder.where(where) : builder);
|
|
501
|
+
return this.#affected(result);
|
|
502
|
+
}
|
|
503
|
+
async deleteOne(criteria) {
|
|
504
|
+
if (this.#dialect === "mysql") return await this.#mysqlDeleteOneReturning(criteria) !== null;
|
|
505
|
+
return (await this.#deleteOneReturning(criteria)).length > 0;
|
|
506
|
+
}
|
|
507
|
+
async findOneAndDelete(criteria) {
|
|
508
|
+
if (this.#dialect === "mysql") return this.#mysqlDeleteOneReturning(criteria);
|
|
509
|
+
return (await this.#deleteOneReturning(criteria))[0] ?? null;
|
|
510
|
+
}
|
|
511
|
+
/** DELETE atomique d'AU PLUS une ligne (PK via `#pickOne`), RETURNING la ligne. */
|
|
512
|
+
async #deleteOneReturning(criteria) {
|
|
513
|
+
const pick = this.#pickOne(this.#where(criteria));
|
|
514
|
+
return await this.#prof(this.#db.delete(execTable(this.#table)).where(pick).returning());
|
|
515
|
+
}
|
|
516
|
+
/** PK obligatoire en mysql (les verbes re-SELECTent par PK). Fail-loud sinon. */
|
|
517
|
+
#requirePk(verb) {
|
|
518
|
+
const pk = this.#pkColumns();
|
|
519
|
+
if (!pk) throw new Error(`DrizzleRepository(${getTableName(this.#table)}): "${verb}" on mysql requires a declared primary key (no RETURNING → rows are re-read by PK).`);
|
|
520
|
+
return pk;
|
|
521
|
+
}
|
|
522
|
+
/** WHERE d'égalité sur la PK (valeurs plates lues par nom de colonne). */
|
|
523
|
+
#pkWhere(pk, values) {
|
|
524
|
+
const conds = pk.map((col) => eq(col, values[col.name]));
|
|
525
|
+
return conds.length === 1 ? conds[0] : and(...conds);
|
|
526
|
+
}
|
|
527
|
+
/** SELECT d'UNE ligne par valeurs de PK (relecture post-mutation mysql). */
|
|
528
|
+
async #selectByPk(pk, values) {
|
|
529
|
+
return (await this.#prof(this.#db.select().from(execTable(this.#table)).where(this.#pkWhere(pk, values)).limit(1)))[0] ?? null;
|
|
530
|
+
}
|
|
531
|
+
/** Valeurs de PK d'une ligne cible, après application d'un `set` éventuel
|
|
532
|
+
* (un set qui réécrit une colonne PK avec une VALEUR plate est honoré ;
|
|
533
|
+
* les fragments SQL — increment — retombent sur la valeur d'origine). */
|
|
534
|
+
#finalPkValues(pk, target, set) {
|
|
535
|
+
const out = {};
|
|
536
|
+
for (const col of pk) {
|
|
537
|
+
const v = set?.[col.name];
|
|
538
|
+
out[col.name] = v !== void 0 && (typeof v !== "object" || v === null) ? v : target[col.name];
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
/** UPDATE mysql « au plus une, ligne rendue » : cible par critère → UPDATE
|
|
543
|
+
* borné PK + critère re-vérifié → relecture par PK. */
|
|
544
|
+
async #mysqlUpdateOneReturning(set, criteria) {
|
|
545
|
+
const pk = this.#requirePk("updateOne");
|
|
546
|
+
const where = this.#where(criteria);
|
|
547
|
+
const target = (await this.#runSelect(criteria, { limit: 1 }))[0];
|
|
548
|
+
if (!target) return null;
|
|
549
|
+
const pkCond = this.#pkWhere(pk, target);
|
|
550
|
+
const result = await this.#prof(this.#db.update(execTable(this.#table)).set(set).where(where ? and(pkCond, where) : pkCond));
|
|
551
|
+
if (this.#affected(result) === 0) return null;
|
|
552
|
+
return await this.#selectByPk(pk, this.#finalPkValues(pk, target, set));
|
|
553
|
+
}
|
|
554
|
+
/** DELETE mysql « au plus une, ligne rendue » : cible par critère → DELETE
|
|
555
|
+
* borné PK + critère re-vérifié → ligne lue rendue. */
|
|
556
|
+
async #mysqlDeleteOneReturning(criteria) {
|
|
557
|
+
const pk = this.#requirePk("deleteOne");
|
|
558
|
+
const where = this.#where(criteria);
|
|
559
|
+
const target = (await this.#runSelect(criteria, { limit: 1 }))[0];
|
|
560
|
+
if (!target) return null;
|
|
561
|
+
const pkCond = this.#pkWhere(pk, target);
|
|
562
|
+
const result = await this.#prof(this.#db.delete(execTable(this.#table)).where(where ? and(pkCond, where) : pkCond));
|
|
563
|
+
return this.#affected(result) > 0 ? target : null;
|
|
564
|
+
}
|
|
565
|
+
/** INSERT mysql, ligne(s) rendue(s) : `$returningId()` (PK générées côté JS
|
|
566
|
+
* par `$defaultFn` ou auto-increment) complété par les PK passées dans les
|
|
567
|
+
* données, puis relecture par PK (ordre d'insertion préservé). */
|
|
568
|
+
async #mysqlInsertReturning(data) {
|
|
569
|
+
const pk = this.#requirePk("create");
|
|
570
|
+
const ids = await this.#prof(this.#db.insert(execTable(this.#table)).values(data).$returningId());
|
|
571
|
+
const out = [];
|
|
572
|
+
for (let i = 0; i < data.length; i++) {
|
|
573
|
+
const values = {};
|
|
574
|
+
for (const col of pk) values[col.name] = data[i]?.[col.name] ?? ids[i]?.[col.name];
|
|
575
|
+
const row = await this.#selectByPk(pk, values);
|
|
576
|
+
if (!row) throw new Error(`DrizzleRepository(${getTableName(this.#table)}): mysql insert succeeded but the row could not be re-read by primary key.`);
|
|
577
|
+
out.push(row);
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
async count(criteria) {
|
|
582
|
+
const where = this.#where(criteria);
|
|
583
|
+
const builder = this.#db.select({ value: count() }).from(execTable(this.#table)).$dynamic();
|
|
584
|
+
const rows = await this.#prof(where ? builder.where(where) : builder);
|
|
585
|
+
return Number(rows[0]?.value ?? 0);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* `COUNT(DISTINCT col)` natif — la déduplication reste dans le moteur, aucune
|
|
589
|
+
* ligne n'est rapatriée. `COUNT(DISTINCT …)` ignore les `NULL` sur les trois
|
|
590
|
+
* dialectes, ce qui donne au contrat sa sémantique sans clause supplémentaire.
|
|
591
|
+
*/
|
|
592
|
+
async countDistinct(field, criteria) {
|
|
593
|
+
const where = this.#where(criteria);
|
|
594
|
+
const builder = this.#db.select({ value: countDistinct(this.#col(this.#table, field)) }).from(execTable(this.#table)).$dynamic();
|
|
595
|
+
const rows = await this.#prof(where ? builder.where(where) : builder);
|
|
596
|
+
return Number(rows[0]?.value ?? 0);
|
|
597
|
+
}
|
|
598
|
+
async exists(criteria) {
|
|
599
|
+
const where = this.#where(criteria);
|
|
600
|
+
let query = this.#db.select({ one: sql`1` }).from(execTable(this.#table)).$dynamic();
|
|
601
|
+
if (where) query = query.where(where);
|
|
602
|
+
query = query.limit(1);
|
|
603
|
+
return (await this.#prof(query)).length > 0;
|
|
604
|
+
}
|
|
605
|
+
withTransaction(tx) {
|
|
606
|
+
return new DrizzleRepository(tx.getNative(), this.#table, this.#relations, this.#connector, this.#dialect, true);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
//#endregion
|
|
610
|
+
export { DrizzleRepository, execTable };
|