@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.
- package/LICENSE +544 -0
- package/README.md +131 -0
- package/dist/index.js +22 -0
- package/dist/nodefony/interfaces/IEntity.js +1 -0
- package/dist/nodefony/interfaces/IOrm.js +1 -0
- package/dist/nodefony/interfaces/IOrmFlow.js +1 -0
- package/dist/nodefony/interfaces/IOrmGraph.js +1 -0
- package/dist/nodefony/interfaces/IOrmMigrations.js +12 -0
- package/dist/nodefony/interfaces/IOrmProbe.js +1 -0
- package/dist/nodefony/interfaces/IPage.js +1 -0
- package/dist/nodefony/interfaces/IRepository.js +1 -0
- package/dist/nodefony/interfaces/ITransaction.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/AbstractCrudService.js +199 -0
- package/dist/nodefony/src/ConnectionMonitor.js +181 -0
- package/dist/nodefony/src/Entity.js +42 -0
- package/dist/nodefony/src/EntityRegistry.js +109 -0
- package/dist/nodefony/src/Orm.js +297 -0
- package/dist/nodefony/src/OrmAdminApi.js +491 -0
- package/dist/nodefony/src/OrmRegistry.js +75 -0
- package/dist/nodefony/src/QueryFlowMonitor.js +128 -0
- package/dist/nodefony/src/buildOrmLeanHealth.js +54 -0
- package/dist/nodefony/src/criteria.js +176 -0
- package/dist/nodefony/src/decorators/entitiesDecorator.js +67 -0
- package/dist/nodefony/src/decorators/entityDecorator.js +51 -0
- package/dist/nodefony/src/decorators/index.js +5 -0
- package/dist/nodefony/src/decorators/metadataStore.js +38 -0
- package/dist/nodefony/src/decorators/repositoryDecorator.js +35 -0
- package/dist/nodefony/src/defineEntity.js +27 -0
- package/dist/nodefony/src/errors.js +76 -0
- package/dist/nodefony/src/ormWiring.js +78 -0
- package/dist/nodefony/src/paginate.js +55 -0
- package/dist/nodefony/src/readOptions.js +52 -0
- package/dist/nodefony/src/serviceWiring.js +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IEntity.d.ts +65 -0
- package/dist/types/nodefony/interfaces/IOrm.d.ts +136 -0
- package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +71 -0
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +165 -0
- package/dist/types/nodefony/interfaces/IOrmMigrations.d.ts +145 -0
- package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +70 -0
- package/dist/types/nodefony/interfaces/IPage.d.ts +24 -0
- package/dist/types/nodefony/interfaces/IRepository.d.ts +369 -0
- package/dist/types/nodefony/interfaces/ITransaction.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/src/AbstractCrudService.d.ts +158 -0
- package/dist/types/nodefony/src/ConnectionMonitor.d.ts +86 -0
- package/dist/types/nodefony/src/Entity.d.ts +44 -0
- package/dist/types/nodefony/src/EntityRegistry.d.ts +60 -0
- package/dist/types/nodefony/src/Orm.d.ts +197 -0
- package/dist/types/nodefony/src/OrmAdminApi.d.ts +84 -0
- package/dist/types/nodefony/src/OrmRegistry.d.ts +58 -0
- package/dist/types/nodefony/src/QueryFlowMonitor.d.ts +53 -0
- package/dist/types/nodefony/src/buildOrmLeanHealth.d.ts +15 -0
- package/dist/types/nodefony/src/criteria.d.ts +131 -0
- package/dist/types/nodefony/src/decorators/entitiesDecorator.d.ts +48 -0
- package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +49 -0
- package/dist/types/nodefony/src/decorators/index.d.ts +10 -0
- package/dist/types/nodefony/src/decorators/metadataStore.d.ts +54 -0
- package/dist/types/nodefony/src/decorators/repositoryDecorator.d.ts +30 -0
- package/dist/types/nodefony/src/defineEntity.d.ts +43 -0
- package/dist/types/nodefony/src/errors.d.ts +62 -0
- package/dist/types/nodefony/src/ormWiring.d.ts +48 -0
- package/dist/types/nodefony/src/paginate.d.ts +43 -0
- package/dist/types/nodefony/src/readOptions.d.ts +21 -0
- package/dist/types/nodefony/src/serviceWiring.d.ts +24 -0
- package/docs/index.md +791 -0
- package/docs/tutorial-entity.md +577 -0
- package/package.json +73 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Service } from "nodefony";
|
|
2
|
+
import type { IRepository, Criteria, RepositoryReadOptions, IPage, PageQuery } from "../interfaces/index.js";
|
|
3
|
+
import type { ServiceWiring } from "./serviceWiring.js";
|
|
4
|
+
/**
|
|
5
|
+
* Service CRUD **générique** — socle réutilisable au-dessus d'un {@link IRepository}.
|
|
6
|
+
*
|
|
7
|
+
* Toute entité (User, Article, Room...) expose son CRUD « toujours de la même
|
|
8
|
+
* manière » en étendant cette classe : `class UserService extends
|
|
9
|
+
* AbstractCrudService<IUser, IUserRepository>`. Le service est la **source de
|
|
10
|
+
* vérité métier**, transport-agnostique : les adaptateurs (REST controller, WS
|
|
11
|
+
* RPC, resolver GraphQL, commande CLI) l'appellent sans dupliquer la logique.
|
|
12
|
+
*
|
|
13
|
+
* Étend {@link Service} (DI, Syslog, bus d'événements) → instancié **une seule
|
|
14
|
+
* fois** (singleton DI). C'est légitime car la classe est **stateless** : son
|
|
15
|
+
* état d'instance ne porte que des invariants partagés (le `repository` injecté).
|
|
16
|
+
* L'état par requête (utilisateur courant, tenant, transaction) vit dans le
|
|
17
|
+
* `Context`/ALS, **jamais** dans un champ du service.
|
|
18
|
+
*
|
|
19
|
+
* **Perf** : les lectures (`find`/`findOne`/`findById`/`count`) délèguent
|
|
20
|
+
* directement au repository — aucun hook ni event sur le chemin chaud. Les
|
|
21
|
+
* mutations (`create`/`update`/`delete`) passent par les hooks template-method et
|
|
22
|
+
* émettent un événement de cycle de vie (`onCreated`/`onUpdated`/`onDeleted`)
|
|
23
|
+
* consommable pour l'audit, le cache, ou Studio.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam T - type de l'entité gérée.
|
|
26
|
+
* @typeParam R - type concret du repository (conserve les finders métier dans la
|
|
27
|
+
* sous-classe, ex. `IUserRepository`). Défaut : `IRepository<T>`.
|
|
28
|
+
*/
|
|
29
|
+
export declare abstract class AbstractCrudService<T, R extends IRepository<T> = IRepository<T>> extends Service {
|
|
30
|
+
protected readonly repository: R;
|
|
31
|
+
/**
|
|
32
|
+
* @param name - identifiant logique du service (msgid des logs, clé d'abonnement events).
|
|
33
|
+
* @param repository - source de persistance injectée (DI).
|
|
34
|
+
* @param wiring - câblage Service ({@link ServiceWiring} : `container?`,
|
|
35
|
+
* `notificationsCenter?`, `options?`) — quasi toujours omis (fourni par le container DI).
|
|
36
|
+
*/
|
|
37
|
+
constructor(name: string, repository: R, ...wiring: ServiceWiring);
|
|
38
|
+
/**
|
|
39
|
+
* Liste les entités correspondant au critère (toutes si omis).
|
|
40
|
+
*
|
|
41
|
+
* @param criteria - filtre typé optionnel.
|
|
42
|
+
* @param options - eager-load / pagination / tri portables.
|
|
43
|
+
*/
|
|
44
|
+
find(criteria?: Criteria<T>, options?: RepositoryReadOptions): Promise<T[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Première entité correspondant au critère, ou `null`.
|
|
47
|
+
*
|
|
48
|
+
* @param criteria - filtre de sélection.
|
|
49
|
+
* @param options - eager-load portable.
|
|
50
|
+
*/
|
|
51
|
+
findOne(criteria: Criteria<T>, options?: RepositoryReadOptions): Promise<T | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Entité par clé primaire `id`, ou `null`.
|
|
54
|
+
*
|
|
55
|
+
* @remarks Suppose une PK nommée `id` (convention Nodefony : UUID `string`).
|
|
56
|
+
* Override dans la sous-classe si la PK diffère.
|
|
57
|
+
* @param id - identifiant primaire.
|
|
58
|
+
* @param options - eager-load portable.
|
|
59
|
+
*/
|
|
60
|
+
findById(id: string, options?: RepositoryReadOptions): Promise<T | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Compte les entités correspondant au critère (toutes si omis).
|
|
63
|
+
*
|
|
64
|
+
* @param criteria - filtre optionnel.
|
|
65
|
+
*/
|
|
66
|
+
count(criteria?: Criteria<T>): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* Liste **une page** d'entités — pagination **native** (jamais matérialiser
|
|
69
|
+
* toute la collection). C'est la primitive à utiliser pour toute liste admin /
|
|
70
|
+
* data plane : elle ne charge qu'une page en mémoire, quelle que soit la taille
|
|
71
|
+
* de la table.
|
|
72
|
+
*
|
|
73
|
+
* @param page - requête de page ({@link PageQuery} : `limit` obligatoire,
|
|
74
|
+
* `offset`/`order`/`criteria`/`withTotal` optionnels).
|
|
75
|
+
* @returns une {@link Page} : au plus `limit` items, `hasNext`, et `total` si demandé.
|
|
76
|
+
* @throws `PageQueryError` (`400`) si un `?q=` est reçu alors que
|
|
77
|
+
* {@link AbstractCrudService.searchableFields} est vide.
|
|
78
|
+
*/
|
|
79
|
+
findPage(page: PageQuery<T>): Promise<IPage<T>>;
|
|
80
|
+
/**
|
|
81
|
+
* Champs sur lesquels `?q=` cherche — **vide par défaut, donc `q` est refusé**.
|
|
82
|
+
*
|
|
83
|
+
* Un service CRUD générique ne peut pas deviner ce qui, dans une entité, se
|
|
84
|
+
* cherche : un titre oui, une clé étrangère ou un horodatage non. Le défaut
|
|
85
|
+
* vide n'est donc pas une lacune mais la seule réponse honnête — et il refuse
|
|
86
|
+
* plutôt qu'il n'ignore, sans quoi une recherche non honorée rendrait toute la
|
|
87
|
+
* table sous un `200`.
|
|
88
|
+
*
|
|
89
|
+
* L'activer tient en une ligne dans le service concret, et la recherche
|
|
90
|
+
* devient alors réelle de bout en bout (`?q=` → `LIKE` ancré, indexable) :
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* protected override readonly searchableFields = ["title", "slug"] as const;
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ Le déclarer ici ne suffit PAS à ouvrir la route : le contrat de page
|
|
97
|
+
* refuse `q` en amont tant que le point d'entrée HTTP ne passe pas
|
|
98
|
+
* `searchable: true` à `parsePageQuery`. Les deux se déclarent, pour la même
|
|
99
|
+
* raison — une capacité se constate à chaque étage qu'elle traverse.
|
|
100
|
+
*/
|
|
101
|
+
protected readonly searchableFields: ReadonlyArray<keyof T & string>;
|
|
102
|
+
/**
|
|
103
|
+
* Crée une entité : `beforeCreate` → persistance → `afterCreate` → `onCreated`.
|
|
104
|
+
*
|
|
105
|
+
* @param data - champs de l'entité à créer.
|
|
106
|
+
* @returns l'entité persistée. Émet `onCreated`.
|
|
107
|
+
*/
|
|
108
|
+
create(data: Partial<T>): Promise<T>;
|
|
109
|
+
/**
|
|
110
|
+
* Met à jour **au plus une** entité : `beforeUpdate` → persistance atomique
|
|
111
|
+
* → `afterUpdate` → `onUpdated`.
|
|
112
|
+
*
|
|
113
|
+
* S'appuie sur {@link IRepository.updateOne} (atomique, `RETURNING` /
|
|
114
|
+
* `findOneAndUpdate`) : l'entité retournée reflète l'écriture, donc l'event
|
|
115
|
+
* `onUpdated` part de façon fiable même quand le critère porte sur un champ
|
|
116
|
+
* modifié (corrige le faux `null` de l'ancien `update` + relecture).
|
|
117
|
+
*
|
|
118
|
+
* `updateMany` (mise à jour en masse) n'est volontairement **pas** exposée
|
|
119
|
+
* ici : c'est une primitive de niveau repository (`IRepository.updateMany`) —
|
|
120
|
+
* on la remontera dans un service le jour où un usage métier la réclame.
|
|
121
|
+
*
|
|
122
|
+
* @param criteria - filtre de sélection (champ inconnu → `UnknownCriteriaField`).
|
|
123
|
+
* @param data - champs à modifier.
|
|
124
|
+
* @returns l'entité mise à jour, ou `null` si aucune ne correspond (pas d'event).
|
|
125
|
+
*/
|
|
126
|
+
updateOne(criteria: Criteria<T>, data: Partial<T>): Promise<T | null>;
|
|
127
|
+
/**
|
|
128
|
+
* Supprime : `beforeDelete` → persistance → `afterDelete` → `onDeleted`.
|
|
129
|
+
*
|
|
130
|
+
* @param criteria - filtre de sélection.
|
|
131
|
+
* @returns le nombre d'entités supprimées. Émet `onDeleted` (criteria, count)
|
|
132
|
+
* uniquement si au moins une ligne a été supprimée.
|
|
133
|
+
*/
|
|
134
|
+
delete(criteria: Criteria<T>): Promise<number>;
|
|
135
|
+
/**
|
|
136
|
+
* Transforme/valide les données avant création (ex. hacher un mot de passe).
|
|
137
|
+
*
|
|
138
|
+
* @param data - données entrantes.
|
|
139
|
+
* @returns les données préparées à persister.
|
|
140
|
+
*/
|
|
141
|
+
protected beforeCreate(data: Partial<T>): Partial<T> | Promise<Partial<T>>;
|
|
142
|
+
/** Effet de bord après création (ex. provisionner une ressource liée). */
|
|
143
|
+
protected afterCreate(_entity: T): void | Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Transforme/valide les données avant mise à jour.
|
|
146
|
+
*
|
|
147
|
+
* @param criteria - cible de la mise à jour.
|
|
148
|
+
* @param data - données entrantes.
|
|
149
|
+
* @returns les données préparées à persister.
|
|
150
|
+
*/
|
|
151
|
+
protected beforeUpdate(_criteria: Criteria<T>, data: Partial<T>): Partial<T> | Promise<Partial<T>>;
|
|
152
|
+
/** Effet de bord après mise à jour. */
|
|
153
|
+
protected afterUpdate(_entity: T): void | Promise<void>;
|
|
154
|
+
/** Garde/validation avant suppression (ex. interdire la suppression du dernier admin). */
|
|
155
|
+
protected beforeDelete(_criteria: Criteria<T>): void | Promise<void>;
|
|
156
|
+
/** Effet de bord après suppression (ex. nettoyer un cache). */
|
|
157
|
+
protected afterDelete(_criteria: Criteria<T>, _removed: number): void | Promise<void>;
|
|
158
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { IConnectionError } from "../interfaces/IOrmGraph.js";
|
|
2
|
+
import type { ILatencyWindow } from "../interfaces/IOrmProbe.js";
|
|
3
|
+
/** Vue figée des compteurs du moniteur pour un connecteur. */
|
|
4
|
+
export interface IConnectionMonitorCore {
|
|
5
|
+
connectedSince: number | null;
|
|
6
|
+
uptimeMs: number | null;
|
|
7
|
+
connectCount: number;
|
|
8
|
+
reconnectCount: number;
|
|
9
|
+
/** Pertes de connexion constatées depuis le démarrage du process. */
|
|
10
|
+
lostCount: number;
|
|
11
|
+
errorCount: number;
|
|
12
|
+
lastConnectMs: number | null;
|
|
13
|
+
lastError: IConnectionError | null;
|
|
14
|
+
recentErrors: IConnectionError[];
|
|
15
|
+
latency: ILatencyWindow;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* **ConnectionMonitor** — observabilité **per-instance** (process-local) du
|
|
19
|
+
* cycle de vie des connexions ORM : connexions, **reconnexions**, **erreurs**
|
|
20
|
+
* (connexion + ping).
|
|
21
|
+
*
|
|
22
|
+
* Alimenté par la template method {@link Orm.connect} (capture latence + erreur
|
|
23
|
+
* de connexion) et par le ping live de l'endpoint `connection/health`. Lecture
|
|
24
|
+
* via {@link snapshot} (data plane ORM / dashboard Studio).
|
|
25
|
+
*
|
|
26
|
+
* **Cloud-native** : l'état d'une connexion DB est local au process (pool par
|
|
27
|
+
* pod) → le moniteur est volontairement per-instance. La vue multi-pod relève
|
|
28
|
+
* de l'agrégation (Prometheus par pod / fan-out Redis P13), pas de cette classe.
|
|
29
|
+
*
|
|
30
|
+
* **Perf** : structure lazy (`Map` allouée au 1ᵉʳ enregistrement) ; ring
|
|
31
|
+
* d'erreurs alloué seulement au 1ᵉʳ incident ; aucun coût tant qu'aucun ORM ne
|
|
32
|
+
* se connecte ou n'échoue. Reset au restart (diagnostic, pas d'état durable).
|
|
33
|
+
*/
|
|
34
|
+
declare class ConnectionMonitor {
|
|
35
|
+
#private;
|
|
36
|
+
/**
|
|
37
|
+
* Enregistre une latence de ping live dans la fenêtre glissante (ring borné).
|
|
38
|
+
*
|
|
39
|
+
* @param name - clé du connecteur.
|
|
40
|
+
* @param ms - latence du ping (ms).
|
|
41
|
+
*/
|
|
42
|
+
recordPing(name: string, ms: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Enregistre une connexion réussie + sa latence.
|
|
45
|
+
*
|
|
46
|
+
* Ne compte PAS une reconnexion : une reprise du driver ne repasse jamais
|
|
47
|
+
* par `Orm.connect()`, elle se signale par {@link ConnectionMonitor.recordReconnect}.
|
|
48
|
+
*
|
|
49
|
+
* @param name - clé du connecteur.
|
|
50
|
+
* @param latencyMs - durée de l'établissement (ms).
|
|
51
|
+
*/
|
|
52
|
+
recordConnect(name: string, latencyMs: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Enregistre une **perte** de connexion constatée par l'adapter.
|
|
55
|
+
*
|
|
56
|
+
* `connectedSince` retombe à `null` : un uptime qui continue de croître
|
|
57
|
+
* pendant que le serveur est tombé est pire qu'absent — il se lit comme une
|
|
58
|
+
* preuve de bonne santé.
|
|
59
|
+
*
|
|
60
|
+
* @param name - clé du connecteur.
|
|
61
|
+
*/
|
|
62
|
+
recordLost(name: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Enregistre une **reprise** de connexion constatée par l'adapter.
|
|
65
|
+
*
|
|
66
|
+
* @param name - clé du connecteur.
|
|
67
|
+
*/
|
|
68
|
+
recordReconnect(name: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Enregistre une erreur (connexion échouée ou ping en échec).
|
|
71
|
+
*
|
|
72
|
+
* @param name - clé du connecteur.
|
|
73
|
+
* @param message - message d'erreur (credential déjà retiré par l'appelant).
|
|
74
|
+
*/
|
|
75
|
+
recordError(name: string, message: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Vue figée des compteurs d'un connecteur (ou compteurs neutres si jamais
|
|
78
|
+
* observé).
|
|
79
|
+
*
|
|
80
|
+
* @param name - clé du connecteur.
|
|
81
|
+
*/
|
|
82
|
+
snapshot(name: string): IConnectionMonitorCore;
|
|
83
|
+
}
|
|
84
|
+
/** Singleton process-wide du moniteur de connexions. */
|
|
85
|
+
export declare const connectionMonitor: ConnectionMonitor;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { IEntity, IEntityRelation } from "../interfaces/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Classe de base abstraite d'une entité, indépendante du driver ORM.
|
|
4
|
+
*
|
|
5
|
+
* Porte le contrat {@link IEntity} : nom logique, connecteur cible, schéma natif
|
|
6
|
+
* (calculé via {@link Entity.getSchema}), modèle compilé (post-connexion) et
|
|
7
|
+
* relations déclaratives. {@link Entity.register} insère l'entité dans le
|
|
8
|
+
* {@link entityRegistry} process-wide.
|
|
9
|
+
*
|
|
10
|
+
* **Pas d'auto-enregistrement dans le constructeur** : en TypeScript, le
|
|
11
|
+
* constructeur de la classe de base s'exécute AVANT les initialiseurs de champs
|
|
12
|
+
* de la sous-classe — `name`/`connector` seraient encore `undefined`. L'enregistrement
|
|
13
|
+
* automatique est donc porté par le décorateur `@entity` (P5.3), qui lit les
|
|
14
|
+
* métadonnées de classe ; en attendant, appeler explicitement
|
|
15
|
+
* {@link Entity.register} après instanciation.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam S - type du schéma natif du driver.
|
|
18
|
+
* @typeParam M - type du modèle compilé natif du driver.
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class Entity<S = unknown, M = unknown> implements IEntity<S, M> {
|
|
21
|
+
/** Nom logique de l'entité (clé de lookup, ex. `"User"`). */
|
|
22
|
+
abstract readonly name: string;
|
|
23
|
+
/** Connexion nommée cible, telle que déclarée en config (clé du `ormRegistry`). */
|
|
24
|
+
abstract readonly connector: string;
|
|
25
|
+
/** Modèle compilé natif, renseigné après connexion de l'ORM. */
|
|
26
|
+
model?: M;
|
|
27
|
+
/** Relations déclarées vers d'autres entités (par nom logique). */
|
|
28
|
+
readonly relations?: ReadonlyArray<IEntityRelation>;
|
|
29
|
+
/**
|
|
30
|
+
* Construit la définition de schéma propre au driver.
|
|
31
|
+
*
|
|
32
|
+
* @returns le schéma natif (forme libre selon l'ORM).
|
|
33
|
+
*/
|
|
34
|
+
abstract getSchema(): S;
|
|
35
|
+
/** Définition de schéma propre au driver (délègue à {@link Entity.getSchema}). */
|
|
36
|
+
get schema(): S;
|
|
37
|
+
/**
|
|
38
|
+
* Enregistre cette entité dans le {@link entityRegistry} process-wide.
|
|
39
|
+
*
|
|
40
|
+
* @returns `this` (chaînable).
|
|
41
|
+
* @throws si l'entité est déjà enregistrée sur le même connecteur.
|
|
42
|
+
*/
|
|
43
|
+
register(): this;
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { IEntity } from "../interfaces/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Registre cross-ORM des entités, indexé par nom logique puis par connecteur.
|
|
4
|
+
*
|
|
5
|
+
* Une même entité logique (ex. `User`) peut exister sur plusieurs connexions :
|
|
6
|
+
* l'index interne est `entities[entityName][connectorName] = IEntity`. Le lookup
|
|
7
|
+
* {@link EntityRegistry.get} accepte un nom seul (résolu si l'entité n'existe
|
|
8
|
+
* que sur un connecteur) ou un couple nom + connecteur pour lever l'ambiguïté.
|
|
9
|
+
*
|
|
10
|
+
* Structure pure (`Object.create(null)`) et lazy : rien n'est alloué tant
|
|
11
|
+
* qu'aucune entité n'est enregistrée. Aucun coupling à `nodefony`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class EntityRegistry {
|
|
14
|
+
#private;
|
|
15
|
+
/**
|
|
16
|
+
* Enregistre une entité pour son connecteur cible.
|
|
17
|
+
*
|
|
18
|
+
* @param entity - entité implémentant {@link IEntity} (utilise `name` + `connector`).
|
|
19
|
+
* @throws si cette entité est déjà enregistrée sur le même connecteur.
|
|
20
|
+
*/
|
|
21
|
+
register(entity: IEntity): void;
|
|
22
|
+
/**
|
|
23
|
+
* Résout une entité par nom (et connecteur si plusieurs candidats).
|
|
24
|
+
*
|
|
25
|
+
* @param name - nom logique de l'entité (ex. `"User"`).
|
|
26
|
+
* @param connector - connecteur cible ; obligatoire si l'entité existe sur plusieurs.
|
|
27
|
+
* @returns l'entité correspondante.
|
|
28
|
+
* @throws si l'entité est inconnue, absente du connecteur demandé, ou ambiguë
|
|
29
|
+
* (plusieurs connecteurs) sans `connector` précisé.
|
|
30
|
+
*/
|
|
31
|
+
get(name: string, connector?: string): IEntity;
|
|
32
|
+
/**
|
|
33
|
+
* Indique si une entité (optionnellement sur un connecteur précis) est enregistrée.
|
|
34
|
+
*
|
|
35
|
+
* @param name - nom logique de l'entité.
|
|
36
|
+
* @param connector - connecteur cible facultatif.
|
|
37
|
+
*/
|
|
38
|
+
has(name: string, connector?: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Liste toutes les entités enregistrées, tous connecteurs confondus.
|
|
41
|
+
*
|
|
42
|
+
* @returns tableau d'entités (vide si registre vide).
|
|
43
|
+
*/
|
|
44
|
+
list(): IEntity[];
|
|
45
|
+
/**
|
|
46
|
+
* Retire une entité du registre (teardown tests / hot-reload).
|
|
47
|
+
*
|
|
48
|
+
* @param name - nom logique de l'entité.
|
|
49
|
+
* @param connector - si fourni, retire seulement cette variante ; sinon toutes.
|
|
50
|
+
* @returns `true` si quelque chose a été retiré, `false` sinon.
|
|
51
|
+
*/
|
|
52
|
+
unregister(name: string, connector?: string): boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Singleton process-wide partagé. Les entités s'y enregistrent (via le
|
|
56
|
+
* décorateur `entities([...])` posé sur le Module, ou {@link Entity.register}).
|
|
57
|
+
* La classe {@link EntityRegistry} reste instanciable pour des registres isolés
|
|
58
|
+
* (tests).
|
|
59
|
+
*/
|
|
60
|
+
export declare const entityRegistry: EntityRegistry;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Service } from "nodefony";
|
|
2
|
+
import type { Container, Event, DefaultOptionsService } from "nodefony";
|
|
3
|
+
import type { IColumnInfo, IConnectionInfo, IOrm, IRepository, ITransaction } from "../interfaces/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Classe de base abstraite de tout ORM Nodefony — câble {@link Service} (DI,
|
|
6
|
+
* Syslog, bus d'événements) et le contrat {@link IOrm}, et s'auto-enregistre
|
|
7
|
+
* dans le {@link ormRegistry} process-wide à la construction.
|
|
8
|
+
*
|
|
9
|
+
* Les drivers concrets (`@nodefony/mongoose`, `@nodefony/drizzle`...)
|
|
10
|
+
* implémentent les opérations bas niveau ({@link Orm.onConnect}, `disconnect`,
|
|
11
|
+
* `getRepository`, `transaction`, `getNativeConnection`). La connexion passe par
|
|
12
|
+
* la template method {@link Orm.connect} qui émet l'événement `onOrmReady` une
|
|
13
|
+
* fois le driver connecté — garantissant que tous les ORM signalent leur
|
|
14
|
+
* disponibilité de façon homogène (avant le `onReady` du Kernel).
|
|
15
|
+
*
|
|
16
|
+
* @typeParam — aucun ; les types natifs du driver transitent via les génériques
|
|
17
|
+
* des méthodes ({@link IRepository}, {@link ITransaction}, native connection).
|
|
18
|
+
*/
|
|
19
|
+
export declare abstract class Orm extends Service implements IOrm {
|
|
20
|
+
#private;
|
|
21
|
+
/**
|
|
22
|
+
* État de vie de la connexion — source UNIQUE de `isConnected()`.
|
|
23
|
+
*
|
|
24
|
+
* `protected` et non `#privé` : `disconnect()` est implémenté par chaque
|
|
25
|
+
* adapter et doit pouvoir le remettre à `false`.
|
|
26
|
+
*/
|
|
27
|
+
protected alive: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* **Ce que cet adapter sait VRAIMENT de l'état de sa connexion** — déclaré,
|
|
30
|
+
* jamais supposé.
|
|
31
|
+
*
|
|
32
|
+
* `"events"` : le driver signale les pertes et les reprises, l'adapter les
|
|
33
|
+
* traduit ; `isConnected()` est un CONSTAT.
|
|
34
|
+
* `"assumed"` : rien ne signale quoi que ce soit (base embarquée, driver
|
|
35
|
+
* muet) ; `isConnected()` dit seulement « la connexion a été établie et
|
|
36
|
+
* n'a pas été fermée » — une supposition.
|
|
37
|
+
*
|
|
38
|
+
* **Le défaut est `"assumed"`, et il n'est pas abstrait — délibérément.**
|
|
39
|
+
* Le rendre obligatoire forcerait chaque adapter à répondre, mais casserait
|
|
40
|
+
* la compilation de tout adapter existant : ajouter un ORM deviendrait une
|
|
41
|
+
* rupture. Le défaut prudent protège aussi bien de l'oubli, parce qu'il dit
|
|
42
|
+
* la VÉRITÉ sur un adapter qui n'a rien câblé — il ne sait pas. Ce qu'il
|
|
43
|
+
* faut empêcher, ce n'est pas le silence : c'est qu'un silence se fasse
|
|
44
|
+
* passer pour un constat.
|
|
45
|
+
*/
|
|
46
|
+
get liveness(): "events" | "assumed";
|
|
47
|
+
/**
|
|
48
|
+
* @param name - clé unique de l'ORM dans le {@link ormRegistry}.
|
|
49
|
+
* @param container - container DI hérité (Kernel) ou nouveau si omis.
|
|
50
|
+
* @param notificationsCenter - bus d'événements partagé, `false` pour aucun.
|
|
51
|
+
* @param options - options de service.
|
|
52
|
+
* @throws si un ORM du même `name` est déjà enregistré.
|
|
53
|
+
*/
|
|
54
|
+
constructor(name: string, container?: Container, notificationsCenter?: Event | false | null, options?: DefaultOptionsService);
|
|
55
|
+
/**
|
|
56
|
+
* Connecte le driver puis émet `onOrmReady`.
|
|
57
|
+
*
|
|
58
|
+
* Template method : ne pas surcharger — implémenter {@link Orm.onConnect}.
|
|
59
|
+
* Instrumente le {@link connectionMonitor} (latence + reconnexion en cas de
|
|
60
|
+
* succès, erreur de connexion en cas d'échec).
|
|
61
|
+
*/
|
|
62
|
+
connect(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* **Le driver a PERDU la connexion** — à appeler par l'adapter depuis
|
|
65
|
+
* l'événement natif de son driver (`pool.on("error")` côté `pg`,
|
|
66
|
+
* `connection.on("disconnected")` côté Mongoose…).
|
|
67
|
+
*
|
|
68
|
+
* Idempotent : un driver émet souvent plusieurs erreurs pour une seule
|
|
69
|
+
* coupure (une par connexion du pool). Seule la PREMIÈRE bascule l'état,
|
|
70
|
+
* compte l'incident et émet `onOrmLost` — sinon un pool de 10 connexions
|
|
71
|
+
* ferait dix fois le tour du framework pour un seul serveur tombé.
|
|
72
|
+
*
|
|
73
|
+
* @param reason - cause lisible, sans credential (elle est journalisée).
|
|
74
|
+
*/
|
|
75
|
+
protected connectionLost(reason: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* **Le driver a RÉTABLI la connexion** — à appeler par l'adapter depuis
|
|
78
|
+
* l'événement natif correspondant (`pool.on("connect")`, `reconnected`…).
|
|
79
|
+
*
|
|
80
|
+
* **Une reprise n'existe que s'il y a eu une perte.** `pg` émet `connect` et
|
|
81
|
+
* `acquire` à chaque client pris au pool, y compris quand rien n'est tombé,
|
|
82
|
+
* et y compris pendant l'établissement initial : sans cette condition, un
|
|
83
|
+
* pool qui grandit sous la charge — ou simplement une application qui
|
|
84
|
+
* démarre — compterait des reconnexions imaginaires.
|
|
85
|
+
*/
|
|
86
|
+
protected connectionRestored(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Période du battement de cœur, en millisecondes. `0` le désactive.
|
|
89
|
+
*
|
|
90
|
+
* **Pourquoi le framework doit fournir ça** : le driver MongoDB surveille ses
|
|
91
|
+
* serveurs en permanence (SDAM) et sait donc qu'une base est tombée même sans
|
|
92
|
+
* le moindre trafic. `pg` et `mysql2` n'ont RIEN de tel — ils n'apprennent
|
|
93
|
+
* l'état du serveur que par leurs requêtes. Mesuré : sur ces deux dialectes,
|
|
94
|
+
* une coupure survenue pendant qu'une requête était en vol, ou un serveur
|
|
95
|
+
* simplement GELÉ (qui ne ferme rien), n'émettent aucun événement : l'état
|
|
96
|
+
* restait « connecté » indéfiniment. Un battement comble cette asymétrie —
|
|
97
|
+
* il ne réinvente rien, il donne aux drivers muets ce que Mongo a déjà.
|
|
98
|
+
*
|
|
99
|
+
* Pourquoi pas une instrumentation des requêtes : son coût croît avec le
|
|
100
|
+
* trafic, pour une information qui ne change qu'aux rares instants de panne ;
|
|
101
|
+
* et il faudrait distinguer une erreur de connexion d'une contrainte violée.
|
|
102
|
+
* Ici le coût est CONSTANT et connu d'avance : une requête légère par période.
|
|
103
|
+
*/
|
|
104
|
+
protected heartbeatMs: number;
|
|
105
|
+
/**
|
|
106
|
+
* Délai au-delà duquel un battement sans réponse vaut une PERTE.
|
|
107
|
+
*
|
|
108
|
+
* Sans lui, le battement ne sert à rien contre le cas qui l'a justifié :
|
|
109
|
+
* une base GELÉE ne ferme rien et ne répond pas, donc `ping()` PEND — et
|
|
110
|
+
* le battement pendait avec elle, indéfiniment, sans jamais conclure
|
|
111
|
+
* (mesuré : 30 s de gel, aucune détection). Une sonde doit avoir sa
|
|
112
|
+
* propre montre, sinon elle hérite de la panne qu'elle est censée voir.
|
|
113
|
+
*/
|
|
114
|
+
protected heartbeatTimeoutMs: number;
|
|
115
|
+
/**
|
|
116
|
+
* Démarre le battement si l'adapter sait répondre à un `ping()` et que la
|
|
117
|
+
* période n'est pas nulle. Idempotent.
|
|
118
|
+
*
|
|
119
|
+
* La minuterie est `unref()` : elle ne doit JAMAIS retenir le process en vie
|
|
120
|
+
* — un banc, un script CLI ou un test qui se termine ne doit pas attendre le
|
|
121
|
+
* prochain battement pour rendre la main.
|
|
122
|
+
*/
|
|
123
|
+
protected startHeartbeat(): void;
|
|
124
|
+
/** Arrête le battement et libère la minuterie. Idempotent. */
|
|
125
|
+
protected stopHeartbeat(): void;
|
|
126
|
+
/**
|
|
127
|
+
* **Bat MAINTENANT**, sans attendre la fin de la période.
|
|
128
|
+
*
|
|
129
|
+
* À appeler par un adapter dont le driver émet un signal SUSPECT mais pas
|
|
130
|
+
* concluant — typiquement la fermeture du socket d'une connexion du pool :
|
|
131
|
+
* elle arrive aussi bien pour un serveur tombé que pour une connexion
|
|
132
|
+
* inactive recyclée, et l'adapter ne peut pas les distinguer. Plutôt que de
|
|
133
|
+
* trancher à sa place — ce qui inscrirait de faux incidents à chaque
|
|
134
|
+
* recyclage — il délègue ici : le battement fait la seule chose qui tranche,
|
|
135
|
+
* une requête, et met l'état à jour selon la réponse.
|
|
136
|
+
*
|
|
137
|
+
* Sans cette porte, un dialecte muet reste marqué connecté jusqu'au battement
|
|
138
|
+
* suivant : 30 s par défaut, là où `pg` bascule en millisecondes. C'est une
|
|
139
|
+
* asymétrie de détection entre deux dialectes de production, pas un réglage.
|
|
140
|
+
*
|
|
141
|
+
* Ne coûte rien quand rien ne va mal : la garde `#beating` écarte les
|
|
142
|
+
* battements concurrents, donc un pool dont dix connexions tombent d'un coup
|
|
143
|
+
* ne sonde qu'une fois. Ne rétablit ni ne perd quoi que ce soit après un
|
|
144
|
+
* `disconnect()` — `#beat` y voit un arrêt volontaire et s'abstient.
|
|
145
|
+
*/
|
|
146
|
+
protected beatNow(): void;
|
|
147
|
+
/**
|
|
148
|
+
* Indique si la connexion est active — **implémentation UNIQUE**, portée par
|
|
149
|
+
* la classe de base et non par chaque adapter.
|
|
150
|
+
*
|
|
151
|
+
* C'est délibéré : tant que chaque adapter portait son propre booléen posé à
|
|
152
|
+
* la connexion, aucun ne le remettait à `false` quand le serveur tombait —
|
|
153
|
+
* la santé ORM répondait « connecté » en pleine coupure. L'état vit ici, et
|
|
154
|
+
* il n'a que trois sources : `connect()`, `disconnect()`, et les deux
|
|
155
|
+
* signaux que l'adapter traduit depuis son driver.
|
|
156
|
+
*/
|
|
157
|
+
isConnected(): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Établit la connexion native du driver (compilation des entités, pool...).
|
|
160
|
+
*
|
|
161
|
+
* Appelé par {@link Orm.connect} ; ne pas émettre `onOrmReady` ici.
|
|
162
|
+
*/
|
|
163
|
+
protected abstract onConnect(): Promise<void>;
|
|
164
|
+
/** Ferme la connexion et libère le pool. */
|
|
165
|
+
abstract disconnect(): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Repository typé d'une entité enregistrée.
|
|
168
|
+
*
|
|
169
|
+
* @param name - nom logique de l'entité.
|
|
170
|
+
*/
|
|
171
|
+
abstract getRepository<T = unknown>(name: string): IRepository<T>;
|
|
172
|
+
/**
|
|
173
|
+
* Exécute un travail transactionnel (commit si résolu, rollback si rejeté).
|
|
174
|
+
*
|
|
175
|
+
* @param work - callback recevant la transaction active.
|
|
176
|
+
*/
|
|
177
|
+
abstract transaction<R>(work: (tx: ITransaction) => Promise<R>): Promise<R>;
|
|
178
|
+
/** Expose la connexion native du driver (trappe SQL/commandes brutes). */
|
|
179
|
+
abstract getNativeConnection<C = unknown>(): C;
|
|
180
|
+
/**
|
|
181
|
+
* Décrit les colonnes d'une entité pour le graphe canonique. Défaut : `[]`
|
|
182
|
+
* (relations seules dans l'ERD). Les adapters surchargent avec l'introspection
|
|
183
|
+
* native (Drizzle `getTableConfig`, Mongoose paths).
|
|
184
|
+
*
|
|
185
|
+
* @param _name - nom logique de l'entité.
|
|
186
|
+
* @returns colonnes normalisées.
|
|
187
|
+
*/
|
|
188
|
+
describeEntity(_name: string): IColumnInfo[];
|
|
189
|
+
/**
|
|
190
|
+
* Décrit la connexion sous-jacente (driver + cible). Défaut : driver vide
|
|
191
|
+
* (inconnu). Les adapters surchargent (Drizzle → `sqlite` + fichier, etc.).
|
|
192
|
+
* Ne DOIT jamais exposer de credential.
|
|
193
|
+
*
|
|
194
|
+
* @returns infos de connexion (driver vide si non renseigné).
|
|
195
|
+
*/
|
|
196
|
+
describeConnection(): IConnectionInfo;
|
|
197
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { IAdminApi, IAdminRegistry } from "nodefony";
|
|
2
|
+
import type { IConnectionHealth, IOrmGraph } from "../interfaces/IOrmGraph.js";
|
|
3
|
+
import type { IOrmFlowReport } from "../interfaces/IOrmFlow.js";
|
|
4
|
+
/** Construit le graphe canonique complet (optionnellement filtré par connecteur). */
|
|
5
|
+
export declare function buildOrmGraph(connectorFilter?: string): IOrmGraph;
|
|
6
|
+
/**
|
|
7
|
+
* Construit le **diagnostic complet des connexions** (per-instance) : ping live
|
|
8
|
+
* (latence enregistrée dans la fenêtre glissante), sonde profonde driver
|
|
9
|
+
* ({@link IOrm.probe} — stockage/pool), et compteurs de cycle de vie du
|
|
10
|
+
* {@link connectionMonitor}. Réutilisé par l'endpoint `connection/health` ET par
|
|
11
|
+
* le ticker hub realtime de Studio (« contrôle total des ORM »).
|
|
12
|
+
*
|
|
13
|
+
* @param filter - nom de connecteur (optionnel) pour ne sonder que celui-ci.
|
|
14
|
+
* @returns un {@link IConnectionHealth} par connecteur.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildConnectionHealth(filter?: string): Promise<IConnectionHealth[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Construit le rapport de **flux ORM** (per-instance) : débit (via `total`),
|
|
19
|
+
* latence (moyenne + EWMA), pire latence et requêtes lentes récentes, par
|
|
20
|
+
* connecteur enregistré. Lecture pure du {@link queryFlowMonitor} (aucune
|
|
21
|
+
* requête émise — contrairement à `buildConnectionHealth` qui ping) → bon marché.
|
|
22
|
+
* Réutilisé par l'endpoint `flow` ET le ticker hub realtime de Studio.
|
|
23
|
+
*
|
|
24
|
+
* @param filter - nom de connecteur (optionnel) pour ne rapporter que celui-ci.
|
|
25
|
+
* @returns le rapport (`enabled=false` en prod → connecteurs à 0).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildOrmFlow(filter?: string): IOrmFlowReport;
|
|
28
|
+
/**
|
|
29
|
+
* Sérialise un graphe en **DBML** (Database Markup Language) — format pivot lu
|
|
30
|
+
* par dbdiagram.io, les outils IA et convertible en SQL. Les `Ref:` sont dérivés
|
|
31
|
+
* des relations selon la convention FK des adapters (`<source>Id` sur la cible
|
|
32
|
+
* pour 1-N ; `<target>Id` sur la source pour N-1/1-1). Le many-to-many est
|
|
33
|
+
* annoté en commentaire (table de jonction non portable).
|
|
34
|
+
*
|
|
35
|
+
* @param graph - graphe canonique.
|
|
36
|
+
* @returns texte DBML.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toDbml(graph: IOrmGraph): string;
|
|
39
|
+
/** Propriété d'un schéma JSON (scalaire, réf relation, ou tableau de réfs). */
|
|
40
|
+
interface IJsonSchemaProperty {
|
|
41
|
+
type?: string;
|
|
42
|
+
format?: string;
|
|
43
|
+
$ref?: string;
|
|
44
|
+
items?: {
|
|
45
|
+
$ref: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Définition d'objet JSON Schema pour une entité. */
|
|
49
|
+
interface IJsonSchemaObject {
|
|
50
|
+
type: "object";
|
|
51
|
+
title: string;
|
|
52
|
+
properties: Record<string, IJsonSchemaProperty>;
|
|
53
|
+
required?: string[];
|
|
54
|
+
additionalProperties: false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Sérialise un graphe en **JSON Schema** (draft 2020-12) — un `$defs` par
|
|
58
|
+
* entité. Format « IA-first » : un agent (text-to-SQL, génération de formulaire,
|
|
59
|
+
* validation de payload) consomme directement ce schéma. Les colonnes
|
|
60
|
+
* non-nullables alimentent `required` ; les relations deviennent des `$ref`
|
|
61
|
+
* (N→1/1→1) ou des tableaux de `$ref` (1→N/N→N) vers les autres `$defs`.
|
|
62
|
+
*
|
|
63
|
+
* @param graph - graphe canonique.
|
|
64
|
+
* @returns document JSON Schema ({@link IJsonSchemaObject} par entité).
|
|
65
|
+
*/
|
|
66
|
+
export declare function toJsonSchema(graph: IOrmGraph): {
|
|
67
|
+
$schema: string;
|
|
68
|
+
$defs: Record<string, IJsonSchemaObject>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Construit le producteur `IAdminApi` du data plane ORM.
|
|
72
|
+
*
|
|
73
|
+
* @returns le `IAdminApi` (namespace `"orm"`).
|
|
74
|
+
*/
|
|
75
|
+
export declare function createOrmAdminApi(): IAdminApi;
|
|
76
|
+
/**
|
|
77
|
+
* Enregistre l'`OrmAdminApi` sur le broker admin, **idempotent** (no-op si déjà
|
|
78
|
+
* monté). Appelé par un module driver à son `onKernelBoot` (orm-core est une lib
|
|
79
|
+
* pure et ne peut pas s'auto-monter).
|
|
80
|
+
*
|
|
81
|
+
* @param registry - broker admin (`container.get("adminBroker")`).
|
|
82
|
+
*/
|
|
83
|
+
export declare function registerOrmAdminApi(registry: IAdminRegistry): void;
|
|
84
|
+
export {};
|