@nodefony/drizzle 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +162 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +105 -0
  6. package/dist/nodefony/command/migrateShared.js +247 -0
  7. package/dist/nodefony/command/orm-generate.js +356 -0
  8. package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
  9. package/dist/nodefony/command/orm-migrate-repair.js +114 -0
  10. package/dist/nodefony/command/orm-migrate-status.js +67 -0
  11. package/dist/nodefony/command/orm-migrate.js +141 -0
  12. package/dist/nodefony/command/orm-reset.js +166 -0
  13. package/dist/nodefony/config/config.js +107 -0
  14. package/dist/nodefony/config/defineModuleConfig.js +63 -0
  15. package/dist/nodefony/entity/auditEventEntity.js +93 -0
  16. package/dist/nodefony/entity/colKit.js +260 -0
  17. package/dist/nodefony/entity/idempotencyEntity.js +74 -0
  18. package/dist/nodefony/entity/sessionEntity.js +75 -0
  19. package/dist/nodefony/entity/tokenEntity.js +198 -0
  20. package/dist/nodefony/entity/totpSecretEntity.js +98 -0
  21. package/dist/nodefony/entity/userTable.js +141 -0
  22. package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
  23. package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
  24. package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/migrations-schema/mysql.js +48 -0
  27. package/dist/nodefony/migrations-schema/postgres.js +48 -0
  28. package/dist/nodefony/migrations-schema/sqlite.js +48 -0
  29. package/dist/nodefony/registerStores.js +218 -0
  30. package/dist/nodefony/service/DrizzleService.js +282 -0
  31. package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
  32. package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
  33. package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
  34. package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
  35. package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
  36. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
  37. package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
  38. package/dist/nodefony/src/SessionStorage.js +259 -0
  39. package/dist/nodefony/src/connectorTarget.js +59 -0
  40. package/dist/nodefony/src/likeSql.js +50 -0
  41. package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
  42. package/dist/nodefony/src/migrator/adopt.js +553 -0
  43. package/dist/nodefony/src/migrator/appSchema.js +414 -0
  44. package/dist/nodefony/src/migrator/catalog.js +76 -0
  45. package/dist/nodefony/src/migrator/destructive.js +213 -0
  46. package/dist/nodefony/src/migrator/divergence.js +84 -0
  47. package/dist/nodefony/src/migrator/drivers/index.js +39 -0
  48. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
  49. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
  50. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
  51. package/dist/nodefony/src/migrator/explain.js +565 -0
  52. package/dist/nodefony/src/migrator/hash.js +47 -0
  53. package/dist/nodefony/src/migrator/history.js +219 -0
  54. package/dist/nodefony/src/migrator/index.js +16 -0
  55. package/dist/nodefony/src/migrator/kit.js +296 -0
  56. package/dist/nodefony/src/migrator/name.js +68 -0
  57. package/dist/nodefony/src/migrator/paths.js +88 -0
  58. package/dist/nodefony/src/migrator/refusals.js +143 -0
  59. package/dist/nodefony/src/migrator/resolve.js +281 -0
  60. package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
  61. package/dist/nodefony/src/migrator/sources.js +419 -0
  62. package/dist/nodefony/src/migrator/status.js +231 -0
  63. package/dist/nodefony/src/migrator/types.js +91 -0
  64. package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
  65. package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
  66. package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
  67. package/dist/nodefony/src/orm-core/index.js +4 -0
  68. package/dist/nodefony/src/queryKit.js +318 -0
  69. package/dist/nodefony/src/safeTarget.js +55 -0
  70. package/dist/types/index.d.ts +76 -0
  71. package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
  72. package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
  73. package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
  74. package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
  75. package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
  76. package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
  77. package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
  78. package/dist/types/nodefony/config/config.d.ts +110 -0
  79. package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
  80. package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
  81. package/dist/types/nodefony/entity/colKit.d.ts +130 -0
  82. package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
  83. package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
  84. package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
  85. package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
  86. package/dist/types/nodefony/entity/userTable.d.ts +65 -0
  87. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
  88. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
  89. package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  91. package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
  92. package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
  93. package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
  94. package/dist/types/nodefony/registerStores.d.ts +52 -0
  95. package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
  96. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
  97. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
  98. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
  99. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
  100. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
  101. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
  102. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
  103. package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
  104. package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
  105. package/dist/types/nodefony/src/likeSql.d.ts +29 -0
  106. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
  107. package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
  108. package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
  109. package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
  110. package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
  111. package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
  112. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
  113. package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
  114. package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
  115. package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
  116. package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
  117. package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
  118. package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
  119. package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
  120. package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
  121. package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
  122. package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
  123. package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
  124. package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
  125. package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
  126. package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
  127. package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
  128. package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
  129. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
  130. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
  131. package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
  132. package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
  133. package/dist/types/nodefony/src/queryKit.d.ts +136 -0
  134. package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
  135. package/docs/index.md +954 -0
  136. package/docs/migrations.md +691 -0
  137. package/migrations/mysql/0000_framework_init.sql +137 -0
  138. package/migrations/mysql/meta/_journal.json +13 -0
  139. package/migrations/postgres/0000_framework_init.sql +128 -0
  140. package/migrations/postgres/meta/_journal.json +13 -0
  141. package/migrations/sqlite/0000_framework_init.sql +127 -0
  142. package/migrations/sqlite/meta/_journal.json +13 -0
  143. package/package.json +126 -0
@@ -0,0 +1,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 };