@nodefony/orm-core 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 (69) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +131 -0
  3. package/dist/index.js +22 -0
  4. package/dist/nodefony/interfaces/IEntity.js +1 -0
  5. package/dist/nodefony/interfaces/IOrm.js +1 -0
  6. package/dist/nodefony/interfaces/IOrmFlow.js +1 -0
  7. package/dist/nodefony/interfaces/IOrmGraph.js +1 -0
  8. package/dist/nodefony/interfaces/IOrmMigrations.js +12 -0
  9. package/dist/nodefony/interfaces/IOrmProbe.js +1 -0
  10. package/dist/nodefony/interfaces/IPage.js +1 -0
  11. package/dist/nodefony/interfaces/IRepository.js +1 -0
  12. package/dist/nodefony/interfaces/ITransaction.js +1 -0
  13. package/dist/nodefony/interfaces/index.js +1 -0
  14. package/dist/nodefony/src/AbstractCrudService.js +199 -0
  15. package/dist/nodefony/src/ConnectionMonitor.js +181 -0
  16. package/dist/nodefony/src/Entity.js +42 -0
  17. package/dist/nodefony/src/EntityRegistry.js +109 -0
  18. package/dist/nodefony/src/Orm.js +297 -0
  19. package/dist/nodefony/src/OrmAdminApi.js +491 -0
  20. package/dist/nodefony/src/OrmRegistry.js +75 -0
  21. package/dist/nodefony/src/QueryFlowMonitor.js +128 -0
  22. package/dist/nodefony/src/buildOrmLeanHealth.js +54 -0
  23. package/dist/nodefony/src/criteria.js +176 -0
  24. package/dist/nodefony/src/decorators/entitiesDecorator.js +67 -0
  25. package/dist/nodefony/src/decorators/entityDecorator.js +51 -0
  26. package/dist/nodefony/src/decorators/index.js +5 -0
  27. package/dist/nodefony/src/decorators/metadataStore.js +38 -0
  28. package/dist/nodefony/src/decorators/repositoryDecorator.js +35 -0
  29. package/dist/nodefony/src/defineEntity.js +27 -0
  30. package/dist/nodefony/src/errors.js +76 -0
  31. package/dist/nodefony/src/ormWiring.js +78 -0
  32. package/dist/nodefony/src/paginate.js +55 -0
  33. package/dist/nodefony/src/readOptions.js +52 -0
  34. package/dist/nodefony/src/serviceWiring.js +1 -0
  35. package/dist/types/index.d.ts +39 -0
  36. package/dist/types/nodefony/interfaces/IEntity.d.ts +65 -0
  37. package/dist/types/nodefony/interfaces/IOrm.d.ts +136 -0
  38. package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +71 -0
  39. package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +165 -0
  40. package/dist/types/nodefony/interfaces/IOrmMigrations.d.ts +145 -0
  41. package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +70 -0
  42. package/dist/types/nodefony/interfaces/IPage.d.ts +24 -0
  43. package/dist/types/nodefony/interfaces/IRepository.d.ts +369 -0
  44. package/dist/types/nodefony/interfaces/ITransaction.d.ts +31 -0
  45. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  46. package/dist/types/nodefony/src/AbstractCrudService.d.ts +158 -0
  47. package/dist/types/nodefony/src/ConnectionMonitor.d.ts +86 -0
  48. package/dist/types/nodefony/src/Entity.d.ts +44 -0
  49. package/dist/types/nodefony/src/EntityRegistry.d.ts +60 -0
  50. package/dist/types/nodefony/src/Orm.d.ts +197 -0
  51. package/dist/types/nodefony/src/OrmAdminApi.d.ts +84 -0
  52. package/dist/types/nodefony/src/OrmRegistry.d.ts +58 -0
  53. package/dist/types/nodefony/src/QueryFlowMonitor.d.ts +53 -0
  54. package/dist/types/nodefony/src/buildOrmLeanHealth.d.ts +15 -0
  55. package/dist/types/nodefony/src/criteria.d.ts +131 -0
  56. package/dist/types/nodefony/src/decorators/entitiesDecorator.d.ts +48 -0
  57. package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +49 -0
  58. package/dist/types/nodefony/src/decorators/index.d.ts +10 -0
  59. package/dist/types/nodefony/src/decorators/metadataStore.d.ts +54 -0
  60. package/dist/types/nodefony/src/decorators/repositoryDecorator.d.ts +30 -0
  61. package/dist/types/nodefony/src/defineEntity.d.ts +43 -0
  62. package/dist/types/nodefony/src/errors.d.ts +62 -0
  63. package/dist/types/nodefony/src/ormWiring.d.ts +48 -0
  64. package/dist/types/nodefony/src/paginate.d.ts +43 -0
  65. package/dist/types/nodefony/src/readOptions.d.ts +21 -0
  66. package/dist/types/nodefony/src/serviceWiring.d.ts +24 -0
  67. package/docs/index.md +791 -0
  68. package/docs/tutorial-entity.md +577 -0
  69. package/package.json +73 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * L'état des migrations d'un connecteur, vu du CŒUR — la forme que tout ORM
3
+ * capable de migrer doit rendre, quel que soit son pilote.
4
+ *
5
+ * **Pourquoi cette vue existe ici et pas dans le module qui migre** : l'URL
6
+ * d'administration est générique (`/nodefony/orm/api/migrations?connector=…`),
7
+ * et son écran l'est aussi. Si la forme appartenait au pilote SQL, le premier
8
+ * autre ORM à porter des migrations obligerait à réécrire l'écran — ou pire, à
9
+ * en ajouter un second qui dirait presque la même chose.
10
+ *
11
+ * Ce que la vue fixe : le cœur NEUTRE (verdict, sources, gestes) et rien
12
+ * d'autre. Tout ce qui est propre à un pilote vit sous `driver`, dont la forme
13
+ * n'est pas décrite ici — un consommateur générique n'a pas à la connaître, et
14
+ * un `jq` d'utilisateur ne doit pas avoir gravé un chemin qui passe par le nom
15
+ * d'un pilote.
16
+ *
17
+ * ⚠️ Ce n'est PAS une seconde définition du rapport : c'est sa vue minimale. Le
18
+ * producteur reste le module qui migre, et un test de conformité chez lui
19
+ * prouve que son objet est assignable à cette interface. Recopier ses champs
20
+ * ici en ferait deux contrats à maintenir, dont un mentirait.
21
+ */
22
+ /** Une commande à taper, telle que le produit la propose. */
23
+ export interface IOrmMigrationAction {
24
+ /** La ligne complète, prête à copier. */
25
+ command: string;
26
+ /** Ses arguments, pour qui l'exécute au lieu de l'afficher. */
27
+ args: string[];
28
+ }
29
+ /** Une migration, telle que l'historique et les fichiers la décrivent. */
30
+ export interface IOrmMigrationEntry {
31
+ /** Identifiant immuable une fois publié. */
32
+ tag: string;
33
+ /** Où en est cette migration pour CE connecteur. */
34
+ status: string;
35
+ /** Déploiement qui l'a portée — groupe les migrations d'un même passage. */
36
+ runId?: string | undefined;
37
+ /** Quand elle a été appliquée — absent tant qu'elle ne l'est pas. */
38
+ appliedAt?: number | undefined;
39
+ /** Ce qui l'a appliquée, tel que l'historique l'a retenu. */
40
+ appliedBy?: string | undefined;
41
+ /** Durée d'application, en millisecondes. */
42
+ durationMs?: number | undefined;
43
+ /** Motif de l'échec, quand elle a échoué. */
44
+ error?: string | undefined;
45
+ }
46
+ /** Les migrations d'une origine — le framework, l'application, un module. */
47
+ export interface IOrmMigrationSource {
48
+ /** Nom de l'origine, tel qu'il s'affiche. */
49
+ name: string;
50
+ /** Combien sont passées. */
51
+ applied: number;
52
+ /** Combien restent à appliquer. */
53
+ pending: number;
54
+ /** Combien ont échoué. */
55
+ failed: number;
56
+ /** Ses migrations, dans l'ordre d'application. */
57
+ entries: IOrmMigrationEntry[];
58
+ }
59
+ /** L'état complet, cœur neutre — la charge utile de l'écran et de `--json`. */
60
+ export interface IOrmMigrationStatus {
61
+ /**
62
+ * Version du format, pour qu'un lecteur sache ce qu'il lit — un ENTIER, qui
63
+ * ne s'incrémente que sur une rupture.
64
+ */
65
+ formatVersion: number;
66
+ /** Connecteur observé. */
67
+ connector: string;
68
+ /** Situation d'ensemble (`ok`, `pending`, `divergent`, `failed`…). */
69
+ verdict: string;
70
+ /** Le fait constaté, en une phrase. */
71
+ summary: string;
72
+ /** Ce qu'il faut faire, du plus direct au plus assumé. */
73
+ nextActions: IOrmMigrationAction[];
74
+ /** Une origine par entrée. */
75
+ sources: IOrmMigrationSource[];
76
+ /** Tout ce qui est propre au pilote, et rien d'autre ailleurs. */
77
+ driver: {
78
+ kind: string;
79
+ [key: string]: unknown;
80
+ };
81
+ }
82
+ /**
83
+ * Ce qu'un ORM répond quand il ne PEUT pas rendre d'état.
84
+ *
85
+ * La même forme que l'arrêt publié par la ligne de commande : un écran qui
86
+ * reçoit ceci doit MONTRER l'empêchement, jamais un tableau vide qui ressemble
87
+ * à « tout va bien ».
88
+ */
89
+ export interface IOrmMigrationFailure {
90
+ formatVersion: number;
91
+ connector: string;
92
+ error: {
93
+ /** Code stable — les scripts le lisent. */
94
+ code: string;
95
+ summary: string;
96
+ meaning: string;
97
+ nextActions: IOrmMigrationAction[];
98
+ };
99
+ }
100
+ /** Une migration en attente, avec le SQL qu'elle exécuterait. */
101
+ export interface IOrmPendingMigration {
102
+ /** Origine — le framework, l'application, un module. */
103
+ source: string;
104
+ /** Identité immuable une fois publiée. */
105
+ tag: string;
106
+ /** Les instructions, dans l'ordre d'exécution. */
107
+ statements: string[];
108
+ }
109
+ /**
110
+ * Ce qui S'APPLIQUERAIT — le plan, avec son SQL.
111
+ *
112
+ * Sert la confirmation avant application : un geste qui modifie un schéma ne
113
+ * se confirme pas sur une promesse, il se confirme sur ce qu'il va exécuter.
114
+ */
115
+ export interface IOrmMigrationPlan {
116
+ formatVersion: number;
117
+ connector: string;
118
+ pending: IOrmPendingMigration[];
119
+ }
120
+ /** Ce qu'une application a fait — ou l'empêchement qui l'a arrêtée. */
121
+ export interface IOrmMigrationApplied {
122
+ formatVersion: number;
123
+ connector: string;
124
+ /** Identifiant du passage — groupe les migrations d'un même déploiement. */
125
+ runId: string;
126
+ /** Ce qui a été appliqué, dans l'ordre. */
127
+ applied: {
128
+ source: string;
129
+ tag: string;
130
+ executionMs: number;
131
+ }[];
132
+ }
133
+ /** L'état, ou l'empêchement — jamais les deux. */
134
+ export type IOrmMigrationReply = IOrmMigrationStatus | IOrmMigrationFailure;
135
+ /** Le plan, ou l'empêchement. */
136
+ export type IOrmMigrationPlanReply = IOrmMigrationPlan | IOrmMigrationFailure;
137
+ /** Le compte rendu d'application, ou l'empêchement. */
138
+ export type IOrmMigrationApplyReply = IOrmMigrationApplied | IOrmMigrationFailure;
139
+ /**
140
+ * Y a-t-il un empêchement plutôt qu'un état ?
141
+ *
142
+ * @param reply - ce que l'ORM a rendu.
143
+ * @returns `true` si c'est un empêchement.
144
+ */
145
+ export declare function isMigrationFailure(reply: IOrmMigrationReply | IOrmMigrationPlanReply | IOrmMigrationApplyReply): reply is IOrmMigrationFailure;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Sondes ORM — métriques d'observabilité profonde d'un connecteur, poussées via
3
+ * le hub temps réel (`nodefony:orm:health`) pour un **contrôle total** des ORM.
4
+ *
5
+ * Deux niveaux :
6
+ * - **générique** (porté par {@link ConnectionMonitor}) : latence, cycle de vie,
7
+ * erreurs — calculé pareil pour tout adapter.
8
+ * - **driver-spécifique** ({@link IOrmProbe}, via {@link IOrm.probe}) : stockage,
9
+ * pool… que seul l'adapter sait extraire (SQLite PRAGMA, pool Mongo…).
10
+ */
11
+ /**
12
+ * Fenêtre glissante de latence de ping — `min`/`avg`/`max` sur les N derniers
13
+ * échantillons (révèle les pics, pas seulement l'instantané). `null` tant
14
+ * qu'aucun ping n'a été mesuré.
15
+ */
16
+ export interface ILatencyWindow {
17
+ /** Dernière latence mesurée (ms). */
18
+ last: number | null;
19
+ /** Minimum sur la fenêtre (ms). */
20
+ min: number | null;
21
+ /** Moyenne sur la fenêtre (ms). */
22
+ avg: number | null;
23
+ /** Maximum sur la fenêtre (ms). */
24
+ max: number | null;
25
+ /** Nombre d'échantillons dans la fenêtre. */
26
+ samples: number;
27
+ }
28
+ /**
29
+ * Sonde de stockage — driver-spécifique. Pour SQLite : dérivée des PRAGMA
30
+ * (`page_count`, `page_size`, `journal_mode`, `freelist_count`). Pour un serveur :
31
+ * taille logique de la base si l'adapter sait l'obtenir.
32
+ */
33
+ export interface IOrmStorageProbe {
34
+ /** Taille de la base (octets) — `pages × pageSize` (SQLite). */
35
+ sizeBytes?: number;
36
+ /** Nombre de pages allouées. */
37
+ pages?: number;
38
+ /** Taille d'une page (octets). */
39
+ pageSize?: number;
40
+ /** Mode de journalisation (SQLite : `wal`, `delete`, `memory`…). */
41
+ journalMode?: string;
42
+ /** Pages libres (fragmentation / espace récupérable au VACUUM). */
43
+ freePages?: number;
44
+ }
45
+ /**
46
+ * Sonde de pool de connexions — pertinente pour les bases serveur (Mongo,
47
+ * Postgres, MySQL). SQLite (better-sqlite3) = mono-connexion → non renseigné.
48
+ */
49
+ export interface IOrmPoolProbe {
50
+ /** Taille max configurée du pool. */
51
+ size?: number;
52
+ /** Connexions disponibles (idle). */
53
+ available?: number;
54
+ /** Connexions en cours d'utilisation. */
55
+ borrowed?: number;
56
+ /** Demandes en attente d'une connexion. */
57
+ pending?: number;
58
+ }
59
+ /**
60
+ * Sonde profonde driver-spécifique retournée par {@link IOrm.probe}. Tous les
61
+ * champs sont optionnels : un adapter ne rapporte que ce qu'il sait mesurer.
62
+ */
63
+ export interface IOrmProbe {
64
+ /** Métriques de stockage (taille, pages, journal…). */
65
+ storage?: IOrmStorageProbe;
66
+ /** Métriques de pool de connexions (serveurs). */
67
+ pool?: IOrmPoolProbe;
68
+ /** Métriques libres clé→valeur (extensible par adapter, jamais de credential). */
69
+ extra?: Record<string, string | number | boolean>;
70
+ }
@@ -0,0 +1,24 @@
1
+ import type { Criteria } from "./IRepository.js";
2
+ import type { IPageQuery } from "nodefony";
3
+ export type { IPage, IPageQuery } from "nodefony";
4
+ /**
5
+ * Requête de page pour un {@link IRepository} — le contrat de page core
6
+ * ({@link IPageQuery}) enrichi du `criteria` typé de l'ORM. C'est la forme que
7
+ * {@link paginate} consomme.
8
+ *
9
+ * **Offset-first** : `cursor` reste disponible mais n'est pas lu par le helper
10
+ * offset générique — les stores à curseur ont leur propre implémentation, et
11
+ * `assertPageQuery(query, "offset")` refuse un `cursor` reçu par erreur.
12
+ *
13
+ * `q` **est** honoré, mais seulement si l'appelant déclare où chercher
14
+ * (`paginate(repo, page, { searchable })`) ; sans cette déclaration il est
15
+ * refusé en `400`. Il a longtemps traversé ce type sans être lu une seule fois :
16
+ * un paramètre accepté puis jeté rend la collection entière à qui croit lire un
17
+ * résultat de recherche.
18
+ *
19
+ * @typeParam T - type de l'entité paginée (type le `criteria`).
20
+ */
21
+ export interface PageQuery<T = unknown> extends IPageQuery {
22
+ /** Filtre typé optionnel appliqué avant la pagination (toutes les lignes si omis). */
23
+ criteria?: Criteria<T>;
24
+ }
@@ -0,0 +1,369 @@
1
+ import type { ITransaction } from "./ITransaction.js";
2
+ /**
3
+ * Critère de filtre brut passé aux opérations d'un repository.
4
+ *
5
+ * Volontairement abstrait (`Record<string, unknown>`) : chaque adapter ORM
6
+ * traduit ce critère dans sa syntaxe native (clause `WHERE` SQL, query Mongo,
7
+ * conditions Drizzle...). Le socle reste portable cross-ORM.
8
+ */
9
+ export type OrmCriteria = Record<string, unknown>;
10
+ /**
11
+ * Opérateurs de comparaison riches applicables à **un** champ, façon MongoDB.
12
+ *
13
+ * Forme tranchée en P7.4 (ADR-0003 risque #3) : objet d'opérateurs `$`-préfixés.
14
+ * Raison : (1) familier (convention Mongo) ; (2) mappable par les **trois**
15
+ * drivers — Mongoose en (quasi) identité (`$gt`/`$in` natifs, `$like`→`$regex`),
16
+ * Drizzle via `gt()`/`inArray()`/`like()` (selon l'adapter). Le sous-ensemble
17
+ * est volontairement minimal = intersection portable des 3 ORM.
18
+ *
19
+ * Plusieurs opérateurs sur le même champ se combinent en `AND`
20
+ * (ex. `{ age: { $gte: 18, $lt: 65 } }`).
21
+ *
22
+ * @typeParam V - type de la valeur du champ ciblé.
23
+ */
24
+ export interface FieldOperators<V> {
25
+ /** Égalité stricte (équivalent à passer la valeur nue). */
26
+ $eq?: V;
27
+ /** Différent de. */
28
+ $ne?: V;
29
+ /** Strictement supérieur à. */
30
+ $gt?: V;
31
+ /** Supérieur ou égal à. */
32
+ $gte?: V;
33
+ /** Strictement inférieur à. */
34
+ $lt?: V;
35
+ /** Inférieur ou égal à. */
36
+ $lte?: V;
37
+ /** Appartient à l'ensemble. */
38
+ $in?: readonly V[];
39
+ /** N'appartient pas à l'ensemble. */
40
+ $nin?: readonly V[];
41
+ /** Motif SQL `LIKE` (`%`/`_`) — pertinent pour les champs texte uniquement. */
42
+ $like?: string;
43
+ /**
44
+ * Teste l'**absence de valeur** : `true` → `IS NULL`, `false` → `IS NOT NULL`
45
+ * (Mongo : `$eq`/`$ne null`, qui couvre aussi le champ absent).
46
+ *
47
+ * Indispensable parce qu'une comparaison d'égalité à `NULL` est **fausse** en
48
+ * SQL (`col = NULL` ne matche rien, jamais) : sans cet opérateur, filtrer « la
49
+ * colonne est vide » n'est pas exprimable. Forme équivalente à la valeur nue
50
+ * `{ champ: null }` (cf {@link Criteria}), à préférer quand le `null` vient
51
+ * d'une variable et qu'on veut dire explicitement « IS NULL ».
52
+ *
53
+ * **Exclusif avec `$eq`/`$ne` sur le même champ** : les deux expriment la même
54
+ * comparaison à `null` et l'adapter Mongo les traduit vers la même clé — il
55
+ * lève plutôt que d'en écraser une en silence.
56
+ *
57
+ * @example
58
+ * // les jetons pas encore révoqués
59
+ * repo.find({ revokedAt: { $null: true } });
60
+ * // les PAT qui ont une expiration
61
+ * repo.find({ expiresAt: { $null: false } });
62
+ */
63
+ $null?: boolean;
64
+ }
65
+ /**
66
+ * Opérateurs d'**écriture** applicables à un champ dans le `update` d'un
67
+ * {@link IRepository.upsert} — par opposition aux {@link FieldOperators}, qui
68
+ * filtrent en lecture.
69
+ *
70
+ * Ils existent parce qu'un upsert **ne peut pas porter de condition** : son
71
+ * `DO UPDATE` s'applique dès qu'il y a conflit de clé. Pour une valeur dont la
72
+ * progression est **monotone** (un seuil qui ne doit jamais reculer), la
73
+ * condition doit donc vivre dans la valeur écrite elle-même. Le résultat tient
74
+ * en **une seule instruction atomique** sur les quatre backends (`MAX()` sqlite,
75
+ * `GREATEST()` postgres/mysql, `$max` natif Mongo) — là où une clause `WHERE`
76
+ * sur le `DO UPDATE` n'existe pas en MySQL et imposerait plusieurs requêtes,
77
+ * donc une course entre elles.
78
+ *
79
+ * À l'INSERT, la valeur est posée telle quelle (rien à comparer).
80
+ *
81
+ * Inutiles pour faire progresser une ligne dont on sait qu'elle **existe** :
82
+ * `updateMany({ k, seuil: { $lt: v } }, { seuil: v })` l'exprime déjà, et de
83
+ * façon tout aussi atomique.
84
+ *
85
+ * @typeParam V - type de la valeur du champ ciblé.
86
+ *
87
+ * @example
88
+ * // le seuil de révocation ne recule jamais, même sur deux logouts simultanés
89
+ * repo.upsert({ subjectId }, { invalidBefore: { $max: now } });
90
+ */
91
+ export interface UpdateOperators<V> {
92
+ /** Écrit la valeur seulement si elle est **supérieure** à celle en base. */
93
+ $max?: V;
94
+ /** Écrit la valeur seulement si elle est **inférieure** à celle en base. */
95
+ $min?: V;
96
+ }
97
+ /**
98
+ * Données d'écriture d'un {@link IRepository.upsert} : chaque champ accepte soit
99
+ * sa valeur nue (écriture directe), soit un objet d'{@link UpdateOperators}.
100
+ *
101
+ * @typeParam T - type de l'entité gérée.
102
+ */
103
+ export type UpdateData<T> = {
104
+ [K in keyof T]?: T[K] | UpdateOperators<NonNullable<T[K]>>;
105
+ };
106
+ /**
107
+ * Valeur de critère pour un champ : soit l'**égalité** directe (valeur nue),
108
+ * soit un objet d'{@link FieldOperators} riche.
109
+ *
110
+ * La valeur nue **`null`** signifie `IS NULL` — et non l'égalité `col = NULL`,
111
+ * qui en SQL est toujours fausse et ferait disparaître le filtre en silence
112
+ * (donc `{ revokedAt: null }` ≡ `{ revokedAt: { $null: true } }`). Contrat
113
+ * identique sur tous les adapters.
114
+ *
115
+ * Cette forme n'est ouverte que si le champ est **typé nullable** (`V` contient
116
+ * `null`) : sur un champ non-nullable, chercher `IS NULL` est une erreur de
117
+ * raisonnement, et le typage la refuse. {@link FieldOperators.$null} reste
118
+ * disponible sur tout champ — y compris optionnel (`champ?: T`), où il exprime
119
+ * « pas de valeur » (Mongo : champ absent).
120
+ *
121
+ * @typeParam V - type de la valeur du champ.
122
+ */
123
+ export type FieldCriteria<V> = V | FieldOperators<NonNullable<V>>;
124
+ /**
125
+ * Critère **typé par champ** d'une entité `T`.
126
+ *
127
+ * Chaque champ connu accepte soit son égalité (`{ email }` doit être un `string`
128
+ * si `T.email` l'est), soit un objet d'opérateurs riches typé sur la valeur du
129
+ * champ (`{ age: { $gt: 18 } }`). L'intersection avec {@link OrmCriteria}
130
+ * conserve une échappatoire pour les clés non typées (champ calculé, opérateur
131
+ * natif non couvert). Chaque adapter traduit ce critère dans sa syntaxe native.
132
+ *
133
+ * @typeParam T - type de l'entité gérée.
134
+ */
135
+ export type Criteria<T> = {
136
+ [K in keyof T]?: FieldCriteria<T[K]>;
137
+ } & {
138
+ /**
139
+ * **Disjonction** : au moins une des branches doit être vraie. Les champs
140
+ * posés à côté restent en `ET` avec l'ensemble (`{a: 1, $or: [x, y]}` =
141
+ * `a = 1 AND (x OR y)`).
142
+ *
143
+ * Existe parce que certaines questions du domaine ne sont PAS des
144
+ * conjonctions : « un jeton utilisable » = *sans échéance* **ou** *échéance à
145
+ * venir*. Sans elle, la seule issue était de descendre au SQL/Mongo natif dans
146
+ * chaque store — donc d'écrire la même règle autant de fois qu'il y a de
147
+ * backends, avec la divergence pour seule perspective.
148
+ *
149
+ * Volontairement limitée à `$or` : `$and` est déjà le comportement par défaut
150
+ * d'un critère, et `$not` demanderait de définir la négation d'un `NULL` sur
151
+ * trois dialectes — un piège pour zéro usage démontré.
152
+ *
153
+ * @example
154
+ * // les clés encore utilisables
155
+ * repo.count({ revokedAt: null, $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }] });
156
+ */
157
+ $or?: ReadonlyArray<Criteria<T>>;
158
+ } & OrmCriteria;
159
+ /**
160
+ * Options de lecture (`find`/`findOne`) portables cross-ORM.
161
+ *
162
+ * `relations` charge des associations **déclarées** dans `@entity` (eager-load :
163
+ * `populate` Mongoose / `with` Drizzle) sans descendre au
164
+ * natif pour le cas commun. Les jointures arbitraires restent du ressort de
165
+ * `IOrm.getNativeConnection()`.
166
+ */
167
+ export interface RepositoryReadOptions {
168
+ /** Noms logiques des relations déclarées à charger (eager-load). */
169
+ relations?: string[];
170
+ /** Nombre maximum de lignes. */
171
+ limit?: number;
172
+ /** Décalage (pagination). */
173
+ offset?: number;
174
+ /**
175
+ * Tri : couples `[champ, sens]`, ex. `[["createdAt", "DESC"]]`.
176
+ *
177
+ * **Forme vérifiée à l'exécution** : toute autre forme lève `InvalidOrderOption`
178
+ * au lieu d'être ignorée — un objet (`{ age: "asc" }`) ou une casse basse
179
+ * (`"desc"`) faisait auparavant partir la requête sans tri, ou triait à
180
+ * l'envers, sans un mot. La normalisation d'une entrée utilisateur appartient à
181
+ * la frontière qui la reçoit (`parsePageQuery` accepte `champ:sens` en casse
182
+ * libre), jamais au repository.
183
+ */
184
+ order?: Array<[string, "ASC" | "DESC"]>;
185
+ }
186
+ /**
187
+ * Contrat CRUD minimal exposé par un repository, indépendant de l'ORM sous-jacent.
188
+ *
189
+ * Un repository est obtenu via `IOrm.getRepository(name)` et manipule des entités
190
+ * de type `T`. Les sémantiques fines (cascade, hooks) sont déléguées à
191
+ * l'adapter concret ; ce contrat garantit uniquement le socle portable.
192
+ *
193
+ * @typeParam T - type de l'entité gérée par le repository.
194
+ */
195
+ export interface IRepository<T = unknown> {
196
+ /**
197
+ * Retourne toutes les entités correspondant au critère (toutes si omis).
198
+ *
199
+ * @param criteria - filtre optionnel (partiellement typé).
200
+ * @param options - eager-load / pagination / tri portables.
201
+ * @returns la liste des entités trouvées (vide si aucune).
202
+ * @throws UnknownCriteriaField si un champ du critère est inconnu de l'entité.
203
+ * @throws InvalidOrderOption si `options.order` n'est pas un tableau de couples.
204
+ */
205
+ find(criteria?: Criteria<T>, options?: RepositoryReadOptions): Promise<T[]>;
206
+ /**
207
+ * Retourne la première entité correspondant au critère, ou `null`.
208
+ *
209
+ * Le tri (`options.order`) décide LAQUELLE des lignes revient : il est honoré par
210
+ * tous les adapters, sans quoi le même appel rendrait un document différent selon
211
+ * l'ORM.
212
+ *
213
+ * @param criteria - filtre de sélection (partiellement typé).
214
+ * @param options - eager-load portable (`relations`) et tri (`order`).
215
+ * @throws UnknownCriteriaField si un champ du critère est inconnu de l'entité.
216
+ * @throws InvalidOrderOption si `options.order` n'est pas un tableau de couples.
217
+ */
218
+ findOne(criteria: Criteria<T>, options?: RepositoryReadOptions): Promise<T | null>;
219
+ /**
220
+ * Persiste une nouvelle entité et retourne sa version persistée (id généré, valeurs par défaut).
221
+ *
222
+ * @param data - champs de l'entité à créer.
223
+ */
224
+ create(data: Partial<T>): Promise<T>;
225
+ /**
226
+ * Insère **plusieurs** entités en **une seule** requête (`INSERT … VALUES
227
+ * (…),(…)` SQL / `insertMany` Mongo) et retourne leurs versions persistées
228
+ * (ids générés, défauts appliqués), dans l'ordre. Un tableau vide est un
229
+ * **no-op** (`[]`, aucune requête). Préférer à N appels `create` (N round-trips)
230
+ * pour le seed / l'import / l'ingestion par lots.
231
+ *
232
+ * @param data - entités à créer.
233
+ * @returns les entités persistées, dans le même ordre.
234
+ */
235
+ createMany(data: Partial<T>[]): Promise<T[]>;
236
+ /**
237
+ * Met à jour **au plus une** entité correspondant au critère, de façon
238
+ * **atomique**, et retourne sa version persistée (ou `null` si aucune ne
239
+ * correspond).
240
+ *
241
+ * Atomicité : une **seule** requête (`UPDATE … RETURNING` SQL /
242
+ * `findOneAndUpdate` Mongo), jamais un `UPDATE` suivi d'une relecture séparée
243
+ * — cette dernière renverrait `null` à tort dès que le critère porte sur un
244
+ * champ modifié (ex. `updateOne({ status: "pending" }, { status: "done" })`).
245
+ *
246
+ * @param criteria - filtre de sélection. Un champ inconnu de l'entité lève
247
+ * `UnknownCriteriaField` (mêmes règles que `find`).
248
+ * @param data - champs à modifier.
249
+ * @returns l'entité mise à jour, ou `null` si le critère ne matche rien.
250
+ */
251
+ updateOne(criteria: Criteria<T>, data: Partial<T>): Promise<T | null>;
252
+ /**
253
+ * Insère **ou** met à jour atomiquement l'entité identifiée par `criteria`
254
+ * (clé unique), en **une seule** requête (`INSERT … ON CONFLICT DO UPDATE …
255
+ * RETURNING` SQL / `findOneAndUpdate({ upsert })` Mongo) — jamais un `SELECT`
256
+ * d'existence suivi d'un `INSERT`/`UPDATE` séparé (2 round-trips + une race
257
+ * insert/update entre les deux).
258
+ *
259
+ * - **INSERT** (clé absente) : la ligne créée = `{ ...criteria, ...insertOnly,
260
+ * ...update }`.
261
+ * - **UPDATE** (conflit de clé) : seul `update` est ré-appliqué (`SET`) ;
262
+ * `criteria` et `insertOnly` ne touchent PAS la ligne existante (ex.
263
+ * `createdAt` posé à la création est préservé).
264
+ *
265
+ * Le `DO UPDATE` est **inconditionnel** (aucun `WHERE` : MySQL n'en accepte
266
+ * pas sur son `ON DUPLICATE KEY UPDATE`) — pour une valeur qui ne doit jamais
267
+ * régresser, poser la condition DANS la valeur via {@link UpdateOperators}
268
+ * (`{ seuil: { $max: v } }`), ce qui reste une instruction unique.
269
+ *
270
+ * @param criteria - clé de conflit (colonnes **uniques**, égalité simple — pas
271
+ * d'opérateurs riches `$`). Sert aussi de valeurs d'insertion.
272
+ * @param update - champs posés à l'insertion ET ré-appliqués en cas de conflit.
273
+ * Accepte une valeur nue ou un {@link UpdateOperators} (`$max`/`$min`).
274
+ * @param insertOnly - champs posés **uniquement** à l'insertion (ex. `createdAt`).
275
+ * @returns l'entité persistée (ligne réelle via `RETURNING` / `returnDocument`).
276
+ */
277
+ upsert(criteria: Criteria<T>, update: UpdateData<T>, insertOnly?: Partial<T>): Promise<T>;
278
+ /**
279
+ * Met à jour **toutes** les entités correspondant au critère et retourne le
280
+ * **nombre** de lignes modifiées (parité de signature avec
281
+ * {@link IRepository.delete}).
282
+ *
283
+ * @param criteria - filtre de sélection. Un champ inconnu lève
284
+ * `UnknownCriteriaField`.
285
+ * @param data - champs à modifier.
286
+ * @returns le nombre d'entités mises à jour.
287
+ */
288
+ updateMany(criteria: Criteria<T>, data: Partial<T>): Promise<number>;
289
+ /**
290
+ * Incrémente **atomiquement** des champs numériques d'**au plus une** entité
291
+ * (`SET f = f + ?` SQL / `$inc` Mongo), sans read-modify-write (donc sans race)
292
+ * — pour les compteurs (stats, usage/tokens, rate-limit). Un delta négatif
293
+ * décrémente. Retourne l'entité après modification, ou `null` si le critère ne
294
+ * matche rien.
295
+ *
296
+ * @param criteria - filtre de sélection (au plus une entité affectée).
297
+ * @param changes - deltas par champ (`{ hits: 1, credits: -5 }`).
298
+ * @returns l'entité mise à jour, ou `null`.
299
+ */
300
+ increment(criteria: Criteria<T>, changes: Partial<Record<keyof T, number>>): Promise<T | null>;
301
+ /**
302
+ * Supprime les entités correspondant au critère.
303
+ *
304
+ * @param criteria - filtre de sélection.
305
+ * @returns nombre d'entités supprimées.
306
+ */
307
+ delete(criteria: Criteria<T>): Promise<number>;
308
+ /**
309
+ * Supprime **au plus une** entité de façon **atomique** (symétrique
310
+ * d'{@link IRepository.updateOne} ; `DELETE … LIMIT 1` SQL / `deleteOne` Mongo).
311
+ *
312
+ * @param criteria - filtre de sélection.
313
+ * @returns `true` si une entité a été supprimée, `false` sinon.
314
+ */
315
+ deleteOne(criteria: Criteria<T>): Promise<boolean>;
316
+ /**
317
+ * Supprime **au plus une** entité et **retourne** sa valeur supprimée (ou
318
+ * `null`), de façon atomique (`DELETE … RETURNING` SQL / `findOneAndDelete`
319
+ * Mongo) — claim-and-remove (file de jobs, outbox, pop atomique).
320
+ *
321
+ * @param criteria - filtre de sélection.
322
+ * @returns l'entité supprimée, ou `null` si aucune ne correspond.
323
+ */
324
+ findOneAndDelete(criteria: Criteria<T>): Promise<T | null>;
325
+ /**
326
+ * Compte les entités correspondant au critère (toutes si omis).
327
+ *
328
+ * @param criteria - filtre optionnel.
329
+ */
330
+ count(criteria?: Criteria<T>): Promise<number>;
331
+ /**
332
+ * Compte les **valeurs distinctes** d'un champ parmi les entités qui
333
+ * correspondent au critère (`COUNT(DISTINCT col)` SQL / agrégation Mongo).
334
+ *
335
+ * Répond à une question que `count` ne sait pas poser : « combien de personnes
336
+ * distinctes derrière ces sessions ? », « combien de comptes touchés par ces
337
+ * échecs ? ». La compter côté appelant supposerait de rapatrier la colonne
338
+ * entière pour la dédupliquer en mémoire — soit exactement l'énumération que
339
+ * ces compteurs existent pour éviter.
340
+ *
341
+ * Les valeurs nulles ne sont pas comptées, comme en SQL : l'absence de valeur
342
+ * n'est pas une valeur distincte de plus.
343
+ *
344
+ * @param field - champ dont on compte les valeurs distinctes.
345
+ * @param criteria - filtre optionnel, appliqué avant la déduplication.
346
+ * @returns le nombre de valeurs distinctes et non nulles.
347
+ */
348
+ countDistinct(field: keyof T & string, criteria?: Criteria<T>): Promise<number>;
349
+ /**
350
+ * Indique si **au moins une** entité correspond au critère, sans rapatrier la
351
+ * ligne (`SELECT 1 … LIMIT 1` SQL / `exists` Mongo). Préférer à
352
+ * `findOne(...) !== null` (aucune colonne chargée) et à `count(...) > 0` (pas de
353
+ * comptage complet) pour un simple test d'existence.
354
+ *
355
+ * @param criteria - filtre de sélection.
356
+ * @returns `true` si une entité correspond, `false` sinon.
357
+ */
358
+ exists(criteria: Criteria<T>): Promise<boolean>;
359
+ /**
360
+ * Retourne une **vue de ce repository liée à une transaction** : toutes ses
361
+ * opérations s'exécutent dans `tx` (commit/rollback gérés par
362
+ * `IOrm.transaction()`). Résout la fuite « repository non tx-aware » (ADR-0003
363
+ * risque #4) sans état global ni CLS.
364
+ *
365
+ * @param tx - transaction active (issue du callback de `IOrm.transaction`).
366
+ * @returns un repository équivalent dont les écritures/lectures portent sur `tx`.
367
+ */
368
+ withTransaction(tx: ITransaction): IRepository<T>;
369
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Unité de travail transactionnelle abstraite (commit / rollback / savepoint).
3
+ *
4
+ * Obtenue dans le callback de `IOrm.transaction()`. Les transactions cross-ORM
5
+ * (2PC) ne sont PAS garanties : une transaction porte sur un seul ORM/connexion.
6
+ * La sémantique des savepoints dépend du driver (peut être un no-op).
7
+ */
8
+ export interface ITransaction {
9
+ /** Valide définitivement les opérations de la transaction. */
10
+ commit(): Promise<void>;
11
+ /** Annule toutes les opérations depuis le début de la transaction. */
12
+ rollback(): Promise<void>;
13
+ /**
14
+ * Crée un point de sauvegarde nommé pour un rollback partiel ultérieur.
15
+ *
16
+ * @param name - identifiant du savepoint.
17
+ */
18
+ savepoint(name: string): Promise<void>;
19
+ /**
20
+ * Annule jusqu'à un savepoint sans terminer la transaction.
21
+ *
22
+ * @param name - savepoint cible.
23
+ */
24
+ rollbackTo(name: string): Promise<void>;
25
+ /**
26
+ * Expose l'objet transaction natif du driver (trappe bas niveau).
27
+ *
28
+ * @typeParam C - type natif attendu (ex. `Transaction` de l'ORM).
29
+ */
30
+ getNative<C = unknown>(): C;
31
+ }
@@ -0,0 +1,7 @@
1
+ export type { IOrm } from "./IOrm.js";
2
+ export type { IEntity, IEntityRelation } from "./IEntity.js";
3
+ export type { IRepository, OrmCriteria, Criteria, FieldCriteria, FieldOperators, UpdateData, UpdateOperators, RepositoryReadOptions, } from "./IRepository.js";
4
+ export type { ITransaction } from "./ITransaction.js";
5
+ export type { IPage, IPageQuery, PageQuery } from "./IPage.js";
6
+ export type { IColumnInfo, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionHealth, } from "./IOrmGraph.js";
7
+ export type { ILatencyWindow, IOrmStorageProbe, IOrmPoolProbe, IOrmProbe, } from "./IOrmProbe.js";