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