@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,52 @@
1
+ import type { SqlDialect } from "./config/config.js";
2
+ /**
3
+ * AUTO-ENREGISTREMENT des backends framework portés par Drizzle — « charger le
4
+ * module = ses backends deviennent sélectionnables par simple nom ».
5
+ *
6
+ * Appelé par `Drizzle.onKernelRegister` (config validée → dialecte du connecteur
7
+ * `default` connu, AVANT le connect de `onBoot` → les tables sont créées au
8
+ * connect). Remplace l'« approche B » historique où chaque application devait
9
+ * câbler `registerXStore(...)` + `registerXEntities(...)` à la main.
10
+ *
11
+ * Deux garde-fous préservent la main de l'application (customisation) :
12
+ * - **entité** : `entityRegistry.has(name, orm)` → une entité déjà enregistrée
13
+ * par l'app est respectée (jamais de doublon-throw) ;
14
+ * - **fabrique** : `getXStoreFactory("drizzle")` → une fabrique déjà posée par
15
+ * l'app garde la main (le registre est premier-arrivé-premier-servi ici).
16
+ *
17
+ * Une brique dont l'entité n'est PAS portée sur le dialecte configuré n'est NI
18
+ * déclarée NI fabricable : les registres reflètent le RÉEL (`listXStores()` ne
19
+ * promet jamais un backend qui échouerait), et la sélectionner échoue franc au
20
+ * boot (« store inconnu ») — jamais de table fantôme ni d'erreur SQL différée.
21
+ */
22
+ /**
23
+ * Connecteur conventionnel qui héberge le schéma framework (même convention que
24
+ * `SESSION_CONNECTOR` : `"default"` pour Drizzle, `"nodefony"` pour Mongoose).
25
+ */
26
+ export declare const FRAMEWORK_CONNECTOR = "default";
27
+ /** Bilan de l'auto-enregistrement (loggé par le module — jamais silencieux). */
28
+ export interface IFrameworkStoresReport {
29
+ /** Entités déclarées par l'auto-register (tables créées au connect). */
30
+ registered: string[];
31
+ /** Entités déjà enregistrées par l'app (customisation respectée). */
32
+ appOwned: string[];
33
+ /** Entités non portées sur le dialecte configuré (stores indisponibles). */
34
+ unported: string[];
35
+ }
36
+ /**
37
+ * Cette entité a-t-elle été posée par le repli du framework ?
38
+ *
39
+ * @param entityName - nom de l'entité (`"User"`…).
40
+ * @param connector - connecteur porteur.
41
+ * @returns `true` si le framework l'a enregistrée lui-même, faute d'entité d'app.
42
+ */
43
+ export declare function isFrameworkFallbackEntity(entityName: string, connector?: string): boolean;
44
+ /**
45
+ * Déclare les entités framework portées sur `dialect` (connecteur `default`) et
46
+ * enregistre leurs fabriques de stores dans les registres de `@nodefony/security`
47
+ * et `@nodefony/framework`. Idempotent (guards) — rejouable sans effet.
48
+ *
49
+ * @param dialect - dialecte du connecteur `default` (config validée du module)
50
+ * @returns bilan à logger (registered / appOwned / unported)
51
+ */
52
+ export declare function registerDrizzleFrameworkStores(dialect: SqlDialect): IFrameworkStoresReport;
@@ -0,0 +1,27 @@
1
+ import { Service } from "nodefony";
2
+ import type { Module } from "nodefony";
3
+ import { DrizzleOrm } from "../src/orm-core/index.js";
4
+ /**
5
+ * Service bootable du module `@nodefony/drizzle`.
6
+ *
7
+ * Au boot du kernel (`onBoot`), instancie un {@link DrizzleOrm} (adapter
8
+ * orm-core) **par connecteur** déclaré dans la config et le connecte ; chaque
9
+ * ORM s'auto-enregistre dans le `ormRegistry` (accessible ensuite via DI ou
10
+ * `OrmRegistry.get(name)`). Ferme proprement les connexions à `onTerminate`.
11
+ *
12
+ * C'est le point d'entrée « ORM par défaut » de l'app : il rend Drizzle utilisable
13
+ * sans logique métier — les entités (`@entity`) ciblant un connecteur sont
14
+ * compilées à la connexion (aucune au départ = base connectée mais vide).
15
+ */
16
+ declare class DrizzleService extends Service {
17
+ #private;
18
+ module: Module;
19
+ constructor(module: Module);
20
+ /** Connecte tous les connecteurs déclarés en config (validée Zod). */
21
+ connectAll(): Promise<void>;
22
+ /** Ferme toutes les connexions. */
23
+ disconnectAll(): Promise<void>;
24
+ /** Retourne l'ORM Drizzle d'un connecteur (défaut : `"default"`). */
25
+ getOrm(name?: string): DrizzleOrm | undefined;
26
+ }
27
+ export default DrizzleService;
@@ -0,0 +1,65 @@
1
+ import type { IPage } from "nodefony";
2
+ import type { IAuditEvent, IAuditListQuery, IAuditStore } from "@nodefony/security";
3
+ import { type DrizzleDb, type DrizzleTable } from "./orm-core/DrizzleRepository.js";
4
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
5
+ /**
6
+ * Journal d'audit **Drizzle** (driver `better-sqlite3` en test, Postgres/MySQL en
7
+ * prod) — implémentation SQL d'{@link IAuditStore} (append-only, tamper-evident)
8
+ * pour la **rétention longue** et le **partage cross-pod**, là où le store mémoire
9
+ * par défaut reste affine à un pod et volatile.
10
+ *
11
+ * **Append-only** : `append` est la seule écriture (INSERT) ; aucune mutation ni
12
+ * suppression ciblée d'un événement — seul {@link DrizzleAuditStore.gc} (rétention)
13
+ * retire des lignes. L'immuabilité EST la garantie d'audit.
14
+ *
15
+ * **Pagination curseur EXACTE (`listPage`)** — via le query builder Drizzle
16
+ * (dialect-agnostique), l'ordre total est `(ts DESC, id DESC)` : `ts` porte l'ordre
17
+ * chronologique, `id` casse les collisions à la milliseconde (rafales de login). Le
18
+ * curseur transporte **les deux** (`<ts>:<id>`) et se compare en **composite**
19
+ * `(ts, id) < (cursorTs, cursorId)` — non exprimable en critère `IRepository`
20
+ * AND-only, d'où la trappe native (ADR-0003, comme `DrizzleIdempotencyStore`). Une
21
+ * ligne de garde (`limit + 1`) détermine `hasNext` sans page vide parasite.
22
+ *
23
+ * **Résolution LAZY + dégradation gracieuse** (calqué sur les stores frères) : le
24
+ * handle Drizzle est résolu à CHAQUE appel, pas capturé à la construction — l'ordre
25
+ * de boot n'est pas garanti (l'app câble à `onKernelBoot`, l'ORM peut n'être pas
26
+ * encore connecté) et l'ORM se déconnecte au shutdown avant le drain des serveurs.
27
+ * Si le handle est `null` : `append` est un no-op **best-effort** (l'audit ne
28
+ * bloque ni ne fait échouer le flux métier — un événement au boot/shutdown est
29
+ * perdu plutôt que de crasher un login), `listPage` rend une page vide et `gc` rend 0.
30
+ *
31
+ * Horloge injectable (`now`) pour des tests déterministes.
32
+ */
33
+ export declare class DrizzleAuditStore implements IAuditStore {
34
+ #private;
35
+ /**
36
+ * @param resolveDb - résolveur **lazy** du handle Drizzle (`null` = ORM non
37
+ * connecté → dégradation gracieuse).
38
+ * @param now - horloge (epoch ms) injectable pour des tests déterministes.
39
+ * @param retentionMs - fenêtre de rétention (ms) avant purge par `gc`.
40
+ * @param table - variante de table à utiliser (dialecte). Défaut = SQLite.
41
+ * @param location - emplacement physique de la base (fichier SQLite) pour Studio
42
+ * ({@link DrizzleOrm.location}) ; `undefined` pour un backend réseau/`:memory:`.
43
+ */
44
+ constructor(resolveDb: () => DrizzleDb | null, now?: () => number, retentionMs?: number, table?: DrizzleTable, location?: string);
45
+ /**
46
+ * Emplacement physique de la base (fichier SQLite) pour l'écran Studio « Stores »
47
+ * — lu par `readStoreLocation`. `undefined` = backend réseau ou `:memory:`.
48
+ */
49
+ get location(): string | undefined;
50
+ /**
51
+ * Construit le store depuis un {@link DrizzleOrm}. Le handle est résolu **lazy**
52
+ * (gardé par `isConnected()` → `null` tant que l'ORM n'est pas/plus connecté).
53
+ * L'entité (`registerAuditEntities`) doit avoir été enregistrée **avant**
54
+ * `orm.connect()` (la table est créée au connect) — sur la **variante de table
55
+ * du dialecte de l'ORM** (S3 multi-dialecte).
56
+ *
57
+ * @param orm - ORM Drizzle hébergeant la table `audit_event`.
58
+ * @param now - horloge injectable (tests).
59
+ * @param retentionMs - fenêtre de rétention (ms).
60
+ */
61
+ static from(orm: DrizzleOrm, now?: () => number, retentionMs?: number): DrizzleAuditStore;
62
+ append(event: IAuditEvent): Promise<void>;
63
+ listPage(query: IAuditListQuery): Promise<IPage<IAuditEvent>>;
64
+ gc(now?: number): Promise<number>;
65
+ }
@@ -0,0 +1,129 @@
1
+ import type { IIdempotencyKeyEntry, IIdempotencyListQuery, IIdempotencyStore, IdempotencyOutcome, IdempotentResponse, IPage } from "nodefony";
2
+ import { type DrizzleDb, type DrizzleTable } from "./orm-core/DrizzleRepository.js";
3
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
4
+ /**
5
+ * Store d'idempotence **Drizzle** (driver `better-sqlite3` en test, Postgres/MySQL
6
+ * en prod) — implémentation SQL d'{@link IIdempotencyStore} (contrat au CORE) pour
7
+ * dédoublonner les mutations rejouées PARTAGÉ cross-pod, là où le store mémoire par
8
+ * défaut reste affine à un pod.
9
+ *
10
+ * **Pourquoi SQL plutôt que Redis** : un cluster qui possède déjà une base
11
+ * (Postgres) mais pas de Redis obtient la dédup cross-pod sans nouvelle infra.
12
+ *
13
+ * **Réservation atomique (`begin`)** — clé de voûte. Un `INSERT … ON CONFLICT(key)
14
+ * DO UPDATE SET … WHERE expiresAt < now RETURNING` = l'équivalent SQL du `SET … NX
15
+ * PX` Redis, en **une seule instruction atomique** côté serveur :
16
+ * - clé absente → l'`INSERT` passe → 1 ligne retournée → `fresh` ;
17
+ * - clé présente mais **morte** (bail/rétention expirés) → le `DO UPDATE … WHERE
18
+ * expirée` la **vole** atomiquement → 1 ligne retournée → `fresh` ;
19
+ * - clé présente et **vivante** → le `WHERE` du `DO UPDATE` échoue → 0 ligne
20
+ * retournée → on lit l'état (`in-flight` / `replayed` / `mismatch`).
21
+ *
22
+ * Cette atomicité au niveau de l'INSTRUCTION est ce qui rend la dédup correcte
23
+ * sous concurrence inter-pods (deux `begin` simultanés sur deux pods : un seul
24
+ * gagne la réservation). Le store ne renvoie JAMAIS `fresh` sur le chemin de
25
+ * contention → anti double-effet garanti (l'invariant capital d'un store
26
+ * d'idempotence).
27
+ *
28
+ * **Pas de TTL natif** (≠ Redis `PX`) → un {@link DrizzleIdempotencyStore.gc}
29
+ * applicatif purge les entrées expirées. À mutualiser avec la maintenance du store
30
+ * de session (chantier « GC moderne »).
31
+ *
32
+ * **Empreinte préservée à la complétion** : `complete()` ne touche pas la colonne
33
+ * `fingerprint` (UPDATE ciblé) → un rejeu de la clé avec un AUTRE payload après
34
+ * complétion est toujours détecté (`mismatch` 422, draft §2.7).
35
+ *
36
+ * ⚠️ **SQLite = banc de test de la sémantique** : un fichier SQLite est
37
+ * mono-machine (lock d'écriture) → aucun intérêt multi-pod. La cible RÉELLE est
38
+ * **Postgres/MySQL** (changement de driver). La preuve cross-pod réelle passe par
39
+ * un e2e Postgres (≠ test SQLite, qui valide la sémantique séquentielle).
40
+ *
41
+ * **Résolution LAZY + dégradation gracieuse** (calqué sur `RedisIdempotencyStore`,
42
+ * le frère idempotence) : le store ne capture PAS le handle Drizzle à la
43
+ * construction mais le résout à CHAQUE appel — l'ordre de boot n'est pas garanti
44
+ * (le framework résout ce store à `onKernelBoot`, quand l'ORM peut ne pas être
45
+ * encore connecté), et l'ORM se déconnecte au shutdown avant le drain des
46
+ * serveurs (cf le gotcha `SessionStorage`). Si le handle est `null` (ORM non
47
+ * connecté), `begin` renvoie `fresh` (la mutation s'exécute SANS dédup) et
48
+ * `complete`/`abort`/`gc` sont des no-op — l'idempotence est temporairement
49
+ * inactive plutôt que de crasher une mutation en vol. Trade-off identique à Redis
50
+ * (un rejeu pendant la fenêtre peut ré-exécuter) ; en pratique la base
51
+ * d'idempotence = la base applicative → « connectée » est vrai sur tout le service.
52
+ *
53
+ * **Câblage** : la classe reste PURE (`import type` du contrat core), mais le
54
+ * module drizzle s'enregistre LUI-MÊME au boot — `registerStores.ts:311-316` pose
55
+ * l'entité (`registerIdempotencyEntities`) puis la fabrique
56
+ * (`registerIdempotencyStore("drizzle", …)`, registre `@nodefony/framework`), sans
57
+ * rien demander à l'application. Celle-ci choisit seulement le store à employer
58
+ * (`idempotency.store`). L'enregistrement est idempotent : une fabrique déjà
59
+ * posée n'est pas écrasée, donc une app peut fournir la sienne avant le boot.
60
+ */
61
+ export declare class DrizzleIdempotencyStore implements IIdempotencyStore {
62
+ #private;
63
+ /**
64
+ * @param resolveDb - résolveur **lazy** du handle Drizzle (`null` = ORM non
65
+ * connecté → dégradation gracieuse). Lazy car l'ordre de boot/shutdown n'est
66
+ * pas garanti à la construction.
67
+ * @param now - horloge (epoch ms) injectable pour des tests déterministes.
68
+ * @param leaseMs - bail d'une entrée *in-flight* (ms).
69
+ * @param ttlMs - rétention d'une réponse mémorisée (ms).
70
+ * @param table - variante de table à utiliser (dialecte). Défaut = variante
71
+ * SQLite ; `from()` injecte la variante du dialecte de l'ORM.
72
+ * @param resolveLocation - résolveur **lazy** de l'emplacement physique de la
73
+ * base ({@link DrizzleOrm.location}) pour Studio. Lazy (comme `resolveDb`) car
74
+ * le store est fabriqué AVANT le connect de l'ORM → l'emplacement n'est lisible
75
+ * qu'une fois l'ORM enregistré (lu au `onReady`, pas à la construction).
76
+ */
77
+ constructor(resolveDb: () => DrizzleDb | null, now?: () => number, leaseMs?: number, ttlMs?: number, table?: DrizzleTable, resolveLocation?: () => string | undefined);
78
+ /**
79
+ * Emplacement physique de la base (fichier SQLite) pour l'écran Studio « Stores »
80
+ * — lu par `readStoreLocation`. Résolu **lazy** (l'ORM n'existe pas à la
81
+ * construction) : `undefined` tant que l'ORM n'est pas enregistré, en `:memory:`,
82
+ * ou sur un backend réseau (pg/mysql → voir l'infra déclarée).
83
+ */
84
+ get location(): string | undefined;
85
+ /**
86
+ * Construit le store depuis un {@link DrizzleOrm}. Le handle est résolu **lazy**
87
+ * (gardé par `isConnected()` → `null` tant que l'ORM n'est pas/plus connecté).
88
+ * L'entité (`registerIdempotencyEntities`) doit avoir été enregistrée **avant**
89
+ * `orm.connect()` (la table est créée au connect).
90
+ *
91
+ * @param orm - ORM Drizzle hébergeant la table `idempotency_key` (dialecte
92
+ * sélectionne la variante de table — `orm.dialect`).
93
+ * @param now - horloge injectable (tests).
94
+ * @param leaseMs - bail *in-flight* (ms).
95
+ * @param ttlMs - rétention d'une réponse mémorisée (ms).
96
+ */
97
+ static from(orm: DrizzleOrm, now?: () => number, leaseMs?: number, ttlMs?: number): DrizzleIdempotencyStore;
98
+ /**
99
+ * Approximation **per-pod, best-effort** : compteur local des réservations
100
+ * faites par CE pod (incrémenté au `fresh`, décrémenté au `complete`/`abort`),
101
+ * non décrémenté si le bail expire sans complétion, et désaligné cross-pod. La
102
+ * vérité cluster passe par un `COUNT(*)`, jamais ce getter (sync). Borné à ≥ 0.
103
+ */
104
+ get size(): number;
105
+ begin(key: string, fingerprint: string): Promise<IdempotencyOutcome>;
106
+ complete(key: string, response: IdempotentResponse): Promise<void>;
107
+ abort(key: string): Promise<void>;
108
+ /**
109
+ * Purge les entrées mortes (`expiresAt <= now`) — supplée l'absence de TTL natif
110
+ * SQL (≠ Redis `PX`). À déclencher périodiquement (timer de maintenance, à
111
+ * mutualiser avec le GC du store de session).
112
+ *
113
+ * @param now - horloge de purge (défaut : horloge injectée).
114
+ * @returns le nombre d'entrées supprimées.
115
+ */
116
+ gc(now?: number): Promise<number>;
117
+ /**
118
+ * {@inheritDoc IIdempotencyStore.listPage}
119
+ *
120
+ * Query builder dialect-agnostique (comme `gc`) : `LIMIT/OFFSET` + `COUNT`
121
+ * filtré. On ne SELECTe QUE les colonnes exposées — la réponse mémorisée
122
+ * (`response`) ne quitte jamais la base par ce chemin, quelle que soit la
123
+ * taille de la page.
124
+ *
125
+ * Les entrées expirées sont exclues (`expiresAt > now`) : le GC applicatif
126
+ * passe plus tard, mais une clé échue n'est déjà plus opposable.
127
+ */
128
+ listPage(query: IIdempotencyListQuery): Promise<IPage<IIdempotencyKeyEntry>>;
129
+ }
@@ -0,0 +1,146 @@
1
+ import type { IRepository } from "@nodefony/orm-core";
2
+ import type { IPage } from "nodefony";
3
+ import type { IAccessTokenRecord, ITokenListQuery, ITokenStore, ITokenUsage, TokenRevokeReason } from "@nodefony/security";
4
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
5
+ import { type DeniedJtiRow, type SubjectRevocationRow } from "../entity/tokenEntity.js";
6
+ /**
7
+ * Store de jetons **Drizzle** (driver `better-sqlite3`) — implémentation SQL
8
+ * d'{@link ITokenStore} au-dessus de trois repositories `@nodefony/orm-core`
9
+ * (`access_token`, `denied_jti`, `subject_revocation`).
10
+ *
11
+ * **Approche B** (validée 2026-06-14) : l'ORM ne connaît `@nodefony/security`
12
+ * qu'en `import type` → 0 dépendance runtime. C'est l'application qui enregistre
13
+ * la fabrique (`registerTokenStore("drizzle", ({ container }) =>
14
+ * DrizzleTokenStore.from(container.get("…orm…")))`) et les entités
15
+ * (`registerTokenEntities(orm)` avant `orm.connect()`).
16
+ *
17
+ * **100 % portable** (aucun SQL natif) — toutes les opérations passent par le
18
+ * contrat `IRepository`, donc le code se transpose tel quel aux autres drivers.
19
+ * Les écritures conditionnelles portent leur condition dans le `WHERE` plutôt
20
+ * que dans un `if` JS après lecture (`{ revokedAt: { $null: true } }`) : chacune
21
+ * est une instruction unique, donc atomique — un `findOne` suivi d'un `update`
22
+ * laisse deux appels concurrents agir sur un état déjà périmé.
23
+ *
24
+ * Horloge injectable (`now`) pour des tests déterministes.
25
+ */
26
+ export declare class DrizzleTokenStore implements ITokenStore {
27
+ #private;
28
+ /**
29
+ * {@inheritDoc ITokenStore.sortableFields}
30
+ *
31
+ * Le moteur SQL trie sur n'importe laquelle de ces colonnes : capacité pleine.
32
+ */
33
+ readonly sortableFields: readonly ["createdAt", "name", "subjectId", "id"];
34
+ /**
35
+ * @param records - repository de la table `access_token` (PAT + refresh).
36
+ * @param denied - repository de la denylist `denied_jti`.
37
+ * @param revocations - repository des seuils `subject_revocation`.
38
+ * @param now - horloge (epoch ms) injectable pour les tests.
39
+ * @param retentionRevokedMs - rétention d'un PAT révoqué sans `exp` avant purge.
40
+ * @param location - emplacement physique de la base (fichier SQLite) pour Studio
41
+ * ({@link DrizzleOrm.location}) ; `undefined` pour un backend réseau/`:memory:`.
42
+ */
43
+ constructor(records: IRepository<IAccessTokenRecord>, denied: IRepository<DeniedJtiRow>, revocations: IRepository<SubjectRevocationRow>, now?: () => number, retentionRevokedMs?: number, location?: string);
44
+ /**
45
+ * Emplacement physique de la base (fichier SQLite) pour l'écran Studio « Stores »
46
+ * — lu par `readStoreLocation`. `undefined` = backend réseau ou `:memory:`.
47
+ */
48
+ get location(): string | undefined;
49
+ /**
50
+ * Construit le store depuis un {@link DrizzleOrm} connecté. Les entités
51
+ * (`registerTokenEntities`) doivent avoir été enregistrées **avant**
52
+ * `orm.connect()`.
53
+ *
54
+ * @param orm - ORM Drizzle connecté hébergeant les tables du store.
55
+ * @param now - horloge injectable (tests).
56
+ * @param retentionRevokedMs - rétention des PAT révoqués sans `exp`.
57
+ */
58
+ static from(orm: DrizzleOrm, now?: () => number, retentionRevokedMs?: number): DrizzleTokenStore;
59
+ /**
60
+ * Insère ou remplace un record (PAT / refresh) — 1 requête, `upsert` atomique
61
+ * sur la PK `id` plutôt qu'un `findOne` d'existence + `create`/`updateOne`
62
+ * (2 round-trips). `put` pose le record COMPLET (`createdAt` inclus) → tout
63
+ * hors `id` est ré-appliqué en cas de conflit ; pas de champ insert-only.
64
+ *
65
+ * ⚠️ **Limite `ON CONFLICT`, propre à cette table** : `access_token` porte
66
+ * DEUX contraintes uniques (`id` PK + `secretHash`), or un upsert n'arbitre
67
+ * qu'UN index. Deux INSERT **concurrents** d'un record **absent** partageant
68
+ * le même `secretHash` feraient donc lever le perdant (PG : `23505` sur
69
+ * `access_token_secretHash_unique`) — l'arbitre `id` ne couvre pas la seconde
70
+ * unique. Ce n'est pas atteignable : les trois appelants (`tokenService`
71
+ * émission + rotation, `apiKeys`) posent un `id` **généré** (`randomUUID` /
72
+ * `#randomId`), donc jamais deux `put` du même id neuf ; le seul `put`
73
+ * concurrent d'un même id porte sur une ligne **existante** (rotation
74
+ * rejouée), qui tombe sur le chemin DO UPDATE et passe. Une entité à deux
75
+ * uniques dont les DEUX seraient réellement disputées demanderait un autre
76
+ * remède (réservation en deux instructions, cf `reserveIdempotencyKeyMysql`).
77
+ *
78
+ * @param record - le record complet à persister.
79
+ */
80
+ put(record: IAccessTokenRecord): Promise<void>;
81
+ findById(id: string): Promise<IAccessTokenRecord | null>;
82
+ findByHash(secretHash: string): Promise<IAccessTokenRecord | null>;
83
+ findBySubject(subjectId: string): Promise<IAccessTokenRecord[]>;
84
+ /** Tous les jetons (PAT + refresh) — vue d'administration cross-porteur. */
85
+ listAll(): Promise<IAccessTokenRecord[]>;
86
+ /**
87
+ * {@inheritDoc ITokenStore.listPage}
88
+ *
89
+ * Portable à 100 % : le helper `paginate()` d'orm-core (LIMIT/OFFSET + COUNT
90
+ * optionnel) sur un `Criteria` simple — ne matérialise qu'une page. Le tri
91
+ * demandé descend dans le `ORDER BY` (jamais de tri après découpe : la 2ᵉ page
92
+ * doit continuer la 1ʳᵉ) ; à défaut, l'ordre contractuel `createdAt DESC, id
93
+ * DESC` rend l'offset déterministe. Les noms de colonnes SQL sont ceux du
94
+ * vocabulaire public — aucune traduction n'est nécessaire ici.
95
+ */
96
+ listPage(query: ITokenListQuery): Promise<IPage<IAccessTokenRecord>>;
97
+ /** {@inheritDoc ITokenStore.countTokens} */
98
+ countTokens(query: ITokenListQuery): Promise<number>;
99
+ markUsed(id: string, usage: ITokenUsage): Promise<void>;
100
+ /**
101
+ * Révoque un jeton — **idempotent** : la 1ʳᵉ date/raison de révocation est
102
+ * conservée (l'audit ne se réécrit pas).
103
+ *
104
+ * Le « pas encore révoqué » vit dans le `WHERE` (`revokedAt IS NULL`), pas
105
+ * dans un `if` JS après lecture : une seule instruction, donc deux révocations
106
+ * concurrentes ne peuvent plus se recouvrir (la seconde n'affecte 0 ligne au
107
+ * lieu d'écraser la date de la première).
108
+ *
109
+ * @param id - identifiant du jeton.
110
+ * @param reason - motif de révocation, posé seulement à la 1ʳᵉ.
111
+ */
112
+ revoke(id: string, reason: TokenRevokeReason): Promise<void>;
113
+ /**
114
+ * Coupe toute une famille de refresh (détection de rejeu, RFC 9700) — les
115
+ * membres déjà révoqués (ex. `rotated`) gardent leur raison d'origine.
116
+ *
117
+ * Un seul `UPDATE … WHERE family = ? AND revokedAt IS NULL` : atomique, et
118
+ * N+1 requêtes (1 SELECT + 1 UPDATE par membre actif) tombent à 1.
119
+ *
120
+ * @param family - famille de refresh à couper.
121
+ * @param reason - motif appliqué aux membres encore actifs.
122
+ */
123
+ revokeFamily(family: string, reason: TokenRevokeReason): Promise<void>;
124
+ denyJti(jti: string, expiresAt: number): Promise<void>;
125
+ isJtiDenied(jti: string): Promise<boolean>;
126
+ /**
127
+ * Pose le seuil de révocation en masse d'un porteur (« déconnecte-moi de
128
+ * partout ») : tout jeton émis avant `invalidBefore` est mort.
129
+ *
130
+ * **Monotone — le seuil ne recule JAMAIS**, y compris sous deux logouts
131
+ * simultanés : la comparaison vit dans la valeur écrite (`$max`), pas dans un
132
+ * `if` JS après lecture. Une lecture suivie d'une écriture laisserait les deux
133
+ * appels voir le même état et écrire tous les deux — c'est le dernier qui
134
+ * resterait, même porteur d'un seuil plus ANCIEN, et les jetons que le logout
135
+ * le plus récent venait d'invalider repasseraient sous le seuil : **des jetons
136
+ * révoqués redeviendraient valides**. Une instruction unique sur les 4
137
+ * backends (`MAX()` sqlite, `GREATEST()` pg/mysql, `$max` Mongo) — un `WHERE`
138
+ * sur le `DO UPDATE` n'existe pas en MySQL.
139
+ *
140
+ * @param subjectId - porteur visé.
141
+ * @param invalidBefore - seuil (epoch ms) ; ignoré s'il est antérieur au seuil courant.
142
+ */
143
+ revokeAllForSubject(subjectId: string, invalidBefore: number): Promise<void>;
144
+ getInvalidBefore(subjectId: string): Promise<number | null>;
145
+ gc(now?: number): Promise<number>;
146
+ }
@@ -0,0 +1,60 @@
1
+ import { type IRepository } from "@nodefony/orm-core";
2
+ import type { IPage } from "nodefony";
3
+ import type { ITotpEnrollmentSummary, ITotpListQuery, ITotpSecret, ITotpSecretStore, TotpSecretUpdate } from "@nodefony/security";
4
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
5
+ import { type TotpSecretRow } from "../entity/totpSecretEntity.js";
6
+ /**
7
+ * Store de secrets TOTP **Drizzle** (driver `better-sqlite3`) — implémentation SQL
8
+ * d'{@link ITotpSecretStore} au-dessus d'un unique repository `@nodefony/orm-core`
9
+ * (`totp_secret`). Comble le gap « 2FA persistant sans fichier » : là où
10
+ * `MemoryTotpSecretStore` est volatile, ce store survit au redémarrage et se
11
+ * partage entre pods (base durable).
12
+ *
13
+ * **Modèle 1 secret / utilisateur** (clé = `userId`) → `save` est un upsert par PK.
14
+ *
15
+ * **Approche B** : l'ORM ne connaît `@nodefony/security` qu'en `import type` → 0
16
+ * dépendance runtime. L'entité (`registerTotpSecretEntity(orm)`) doit être
17
+ * enregistrée **avant** `orm.connect()`.
18
+ *
19
+ * **100 % portable** (aucun SQL natif) — toutes les opérations passent par le
20
+ * contrat `IRepository`, donc le code se transpose tel quel aux autres drivers.
21
+ *
22
+ * **`secretEnc` opaque** : le store persiste le secret DÉJÀ chiffré (AES-256-GCM
23
+ * côté service) — il ne déchiffre jamais, ne voit que des octets.
24
+ */
25
+ export declare class DrizzleTotpSecretStore implements ITotpSecretStore {
26
+ #private;
27
+ /**
28
+ * @param repo - repository de la table `totp_secret`.
29
+ * @param location - emplacement physique de la base (fichier SQLite) pour Studio
30
+ * ({@link DrizzleOrm.location}) ; `undefined` pour un backend réseau/`:memory:`.
31
+ */
32
+ constructor(repo: IRepository<TotpSecretRow>, location?: string);
33
+ /**
34
+ * Emplacement physique de la base (fichier SQLite) pour l'écran Studio « Stores »
35
+ * — lu par `readStoreLocation`. `undefined` = backend réseau ou `:memory:`.
36
+ */
37
+ get location(): string | undefined;
38
+ /**
39
+ * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
40
+ * (`registerTotpSecretEntity`) doit avoir été enregistrée **avant** `orm.connect()`.
41
+ *
42
+ * @param orm - ORM Drizzle connecté hébergeant la table du store.
43
+ */
44
+ static from(orm: DrizzleOrm): DrizzleTotpSecretStore;
45
+ findByUser(userId: string): Promise<ITotpSecret | null>;
46
+ save(secret: ITotpSecret): Promise<void>;
47
+ update(userId: string, patch: TotpSecretUpdate): Promise<void>;
48
+ delete(userId: string): Promise<void>;
49
+ /**
50
+ * {@inheritDoc ITotpSecretStore.listPage}
51
+ *
52
+ * 100 % portable : le helper `paginate()` d'orm-core (LIMIT/OFFSET + COUNT
53
+ * optionnel) sur un critère simple. La projection en vue d'enrôlement retire
54
+ * `secretEnc` et les condensats — ils ne franchissent jamais la frontière du
55
+ * store, quel que soit l'appelant.
56
+ */
57
+ listPage(query: ITotpListQuery): Promise<IPage<ITotpEnrollmentSummary>>;
58
+ /** {@inheritDoc ITotpSecretStore.countEnrollments} */
59
+ countEnrollments(query: ITotpListQuery): Promise<number>;
60
+ }
@@ -0,0 +1,67 @@
1
+ import type { Criteria, IRepository, ITransaction, RepositoryReadOptions } from "@nodefony/orm-core";
2
+ import type { IPasswordAuthenticatedUser, IUserListQuery, IUserRepository } from "@nodefony/user";
3
+ import type { IPage } from "nodefony";
4
+ import type { DrizzleDb } from "./orm-core/DrizzleRepository.js";
5
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
6
+ import type { SqlDialect } from "../interfaces/IDrizzleConfig.js";
7
+ import type { UserRow } from "../entity/userTable.js";
8
+ export declare class DrizzleUserRepository implements IUserRepository {
9
+ #private;
10
+ /**
11
+ * Même vocabulaire public que les autres repositories — ici le tri part
12
+ * dans la requête, où il ne coûte qu'un index.
13
+ */
14
+ readonly sortableFields: readonly ["identifier", "enabled", "createdAt", "updatedAt", "id"];
15
+ /**
16
+ * @param base - repository portable sur la table `User` (CRUD + criteria).
17
+ * @param db - handle Drizzle (racine ou transaction) pour les requêtes JSON brutes.
18
+ * @param dialect - dialecte SQL du connecteur (route les requêtes du queryKit).
19
+ */
20
+ constructor(base: IRepository<UserRow>, db: DrizzleDb, dialect?: SqlDialect);
21
+ /**
22
+ * Construit le repository utilisateur depuis un {@link DrizzleOrm} connecté.
23
+ * L'entité `User` doit avoir été enregistrée (cf `registerUserEntity`) avant
24
+ * `orm.connect()` — sur la variante de table du dialecte de l'ORM.
25
+ *
26
+ * @param orm - ORM Drizzle connecté.
27
+ * @returns le repository utilisateur prêt à l'emploi.
28
+ */
29
+ static from(orm: DrizzleOrm): DrizzleUserRepository;
30
+ find(criteria?: Criteria<IPasswordAuthenticatedUser>, options?: RepositoryReadOptions): Promise<IPasswordAuthenticatedUser[]>;
31
+ findOne(criteria: Criteria<IPasswordAuthenticatedUser>, options?: RepositoryReadOptions): Promise<IPasswordAuthenticatedUser | null>;
32
+ create(data: Partial<IPasswordAuthenticatedUser>): Promise<IPasswordAuthenticatedUser>;
33
+ updateOne(criteria: Criteria<IPasswordAuthenticatedUser>, data: Partial<IPasswordAuthenticatedUser>): Promise<IPasswordAuthenticatedUser | null>;
34
+ upsert(criteria: Criteria<IPasswordAuthenticatedUser>, update: Partial<IPasswordAuthenticatedUser>, insertOnly?: Partial<IPasswordAuthenticatedUser>): Promise<IPasswordAuthenticatedUser>;
35
+ createMany(data: Partial<IPasswordAuthenticatedUser>[]): Promise<IPasswordAuthenticatedUser[]>;
36
+ exists(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<boolean>;
37
+ deleteOne(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<boolean>;
38
+ findOneAndDelete(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<IPasswordAuthenticatedUser | null>;
39
+ increment(criteria: Criteria<IPasswordAuthenticatedUser>, changes: Partial<Record<keyof IPasswordAuthenticatedUser, number>>): Promise<IPasswordAuthenticatedUser | null>;
40
+ updateMany(criteria: Criteria<IPasswordAuthenticatedUser>, data: Partial<IPasswordAuthenticatedUser>): Promise<number>;
41
+ delete(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
42
+ count(criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
43
+ countDistinct(field: keyof IPasswordAuthenticatedUser & string, criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
44
+ withTransaction(tx: ITransaction): IUserRepository;
45
+ findByIdentifier(identifier: string): Promise<IPasswordAuthenticatedUser | null>;
46
+ /**
47
+ * Recherche par compte externe lié — cherche dans le JSON `socialProviders`
48
+ * via le queryKit (forme native du dialecte : `json_each` SQLite / `@>`
49
+ * jsonb PG, 1 requête), récupère l'`id`, puis recharge par le chemin typé
50
+ * (parsing JSON/booléens cohérent). `null` si aucun lien.
51
+ */
52
+ findBySocialProvider(provider: string, providerId: string): Promise<IPasswordAuthenticatedUser | null>;
53
+ /**
54
+ * {@inheritDoc IUserRepository.listPage}
55
+ *
56
+ * SQL natif (queryKit, routé par dialecte) → **uniquement les `id`** de la page
57
+ * (containment de rôle + `LIKE` insensible casse non exprimables par le query
58
+ * builder portable), puis rechargement des lignes complètes par le chemin typé
59
+ * (`find({ id: $in })`, parsing JSON/booléens cohérent), **ré-ordonnées** selon
60
+ * le tri SQL. Jamais plus d'une page matérialisée.
61
+ */
62
+ listPage(query: IUserListQuery): Promise<IPage<IPasswordAuthenticatedUser>>;
63
+ /** {@inheritDoc IUserRepository.countUsers} */
64
+ countUsers(query: IUserListQuery): Promise<number>;
65
+ /** {@inheritDoc IUserRepository.countActiveAdmins} */
66
+ countActiveAdmins(adminRole: string): Promise<number>;
67
+ }
@@ -0,0 +1,62 @@
1
+ import { type IRepository } from "@nodefony/orm-core";
2
+ import type { IPage } from "nodefony";
3
+ import type { IWebAuthnCredential, IWebAuthnCredentialStore, IWebAuthnCredentialSummary, IWebAuthnListQuery, WebAuthnAuthUpdate } from "@nodefony/security";
4
+ import type { DrizzleOrm } from "./orm-core/DrizzleOrm.js";
5
+ import { type WebAuthnCredentialRow } from "../entity/webAuthnCredentialEntity.js";
6
+ /**
7
+ * Store de credentials WebAuthn **Drizzle** (driver `better-sqlite3`) —
8
+ * implémentation SQL d'{@link IWebAuthnCredentialStore} au-dessus d'un unique
9
+ * repository `@nodefony/orm-core` (`webauthn_credential`).
10
+ *
11
+ * **Approche B** : l'ORM ne connaît `@nodefony/security` qu'en `import type` → 0
12
+ * dépendance runtime. C'est l'application qui enregistre la fabrique
13
+ * (`registerWebAuthnStore("drizzle", …)`) et l'entité
14
+ * (`registerWebAuthnCredentialEntity(orm)` avant `orm.connect()`).
15
+ *
16
+ * **100 % portable** (aucun SQL natif) — toutes les opérations passent par le
17
+ * contrat `IRepository`, donc le code se transpose tel quel aux autres drivers.
18
+ *
19
+ * **Mapping Row ↔ contrat** : le repository renvoie une {@link WebAuthnCredentialRow}
20
+ * plate (`nickname: string | null`, `transports` mutable) ; le store la normalise
21
+ * en `IWebAuthnCredential` (`nickname?` omis si `null`). Le store de jetons n'a pas
22
+ * ce mapping car `IAccessTokenRecord` est déjà la forme repository (tout `| null`).
23
+ */
24
+ export declare class DrizzleWebAuthnCredentialStore implements IWebAuthnCredentialStore {
25
+ #private;
26
+ /**
27
+ * @param repo - repository de la table `webauthn_credential`.
28
+ * @param location - emplacement physique de la base (fichier SQLite) pour Studio
29
+ * ({@link DrizzleOrm.location}) ; `undefined` pour un backend réseau/`:memory:`.
30
+ */
31
+ constructor(repo: IRepository<WebAuthnCredentialRow>, location?: string);
32
+ /**
33
+ * Emplacement physique de la base (fichier SQLite) pour l'écran Studio « Stores »
34
+ * — lu par `readStoreLocation`. `undefined` = backend réseau ou `:memory:`.
35
+ */
36
+ get location(): string | undefined;
37
+ /**
38
+ * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
39
+ * (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
40
+ * `orm.connect()`.
41
+ *
42
+ * @param orm - ORM Drizzle connecté hébergeant la table du store.
43
+ */
44
+ static from(orm: DrizzleOrm): DrizzleWebAuthnCredentialStore;
45
+ findById(credentialId: string): Promise<IWebAuthnCredential | null>;
46
+ findByUser(userId: string): Promise<IWebAuthnCredential[]>;
47
+ /** `COUNT(*)` natif — jamais un `find().length` (le plafond ne charge rien). */
48
+ countByUser(userId: string): Promise<number>;
49
+ save(credential: IWebAuthnCredential): Promise<void>;
50
+ update(credentialId: string, patch: WebAuthnAuthUpdate): Promise<void>;
51
+ delete(credentialId: string): Promise<void>;
52
+ /**
53
+ * {@inheritDoc IWebAuthnCredentialStore.listPage}
54
+ *
55
+ * 100 % portable : le helper `paginate()` d'orm-core (LIMIT/OFFSET + COUNT
56
+ * optionnel). La projection en vue admin retire `publicKey` — elle ne franchit
57
+ * jamais la frontière du store, quel que soit l'appelant.
58
+ */
59
+ listPage(query: IWebAuthnListQuery): Promise<IPage<IWebAuthnCredentialSummary>>;
60
+ /** {@inheritDoc IWebAuthnCredentialStore.countCredentials} */
61
+ countCredentials(query: IWebAuthnListQuery): Promise<number>;
62
+ }