@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LIKE_ESCAPE_CHAR, OPERATOR_KEYS, UPDATE_OPERATOR_KEYS, escapeLikeTerm, isFieldOperators, isUpdateOperators, likePatternToRegExp, searchCriteria } from "./nodefony/src/criteria.js";
|
|
2
|
+
import { InvalidOrderOption, UnknownCriteriaField } from "./nodefony/src/errors.js";
|
|
3
|
+
import { assertOrderOption } from "./nodefony/src/readOptions.js";
|
|
4
|
+
import { OrmRegistry, ormRegistry } from "./nodefony/src/OrmRegistry.js";
|
|
5
|
+
import { EntityRegistry, entityRegistry } from "./nodefony/src/EntityRegistry.js";
|
|
6
|
+
import { connectionMonitor } from "./nodefony/src/ConnectionMonitor.js";
|
|
7
|
+
import { Orm } from "./nodefony/src/Orm.js";
|
|
8
|
+
import { Entity } from "./nodefony/src/Entity.js";
|
|
9
|
+
import { paginate } from "./nodefony/src/paginate.js";
|
|
10
|
+
import { AbstractCrudService } from "./nodefony/src/AbstractCrudService.js";
|
|
11
|
+
import { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
|
|
12
|
+
import { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, toDbml, toJsonSchema } from "./nodefony/src/OrmAdminApi.js";
|
|
13
|
+
import { buildOrmLeanHealth } from "./nodefony/src/buildOrmLeanHealth.js";
|
|
14
|
+
import { reportOrmBootLines, resolveOrmFlowEnabled, wireOrmAdminPlane } from "./nodefony/src/ormWiring.js";
|
|
15
|
+
import { isMigrationFailure } from "./nodefony/interfaces/IOrmMigrations.js";
|
|
16
|
+
import { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta } from "./nodefony/src/decorators/metadataStore.js";
|
|
17
|
+
import { entity } from "./nodefony/src/decorators/entityDecorator.js";
|
|
18
|
+
import { DEFAULT_CONNECTOR, entities } from "./nodefony/src/decorators/entitiesDecorator.js";
|
|
19
|
+
import { repository } from "./nodefony/src/decorators/repositoryDecorator.js";
|
|
20
|
+
import "./nodefony/src/decorators/index.js";
|
|
21
|
+
import { defineEntity } from "./nodefony/src/defineEntity.js";
|
|
22
|
+
export { AbstractCrudService, DEFAULT_CONNECTOR, Entity, EntityRegistry, InvalidOrderOption, LIKE_ESCAPE_CHAR, OPERATOR_KEYS, Orm, OrmRegistry, UPDATE_OPERATOR_KEYS, UnknownCriteriaField, assertOrderOption, buildConnectionHealth, buildOrmFlow, buildOrmGraph, buildOrmLeanHealth, connectionMonitor, createOrmAdminApi, defineEntity, entities, entity, entityRegistry, escapeLikeTerm, getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, isFieldOperators, isMigrationFailure, isUpdateOperators, likePatternToRegExp, ormRegistry, paginate, queryFlowMonitor, registerOrmAdminApi, reportOrmBootLines, repository, resolveOrmFlowEnabled, searchCriteria, toDbml, toJsonSchema, wireOrmAdminPlane };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region nodefony/interfaces/IOrmMigrations.ts
|
|
2
|
+
/**
|
|
3
|
+
* Y a-t-il un empêchement plutôt qu'un état ?
|
|
4
|
+
*
|
|
5
|
+
* @param reply - ce que l'ORM a rendu.
|
|
6
|
+
* @returns `true` si c'est un empêchement.
|
|
7
|
+
*/
|
|
8
|
+
function isMigrationFailure(reply) {
|
|
9
|
+
return "error" in reply;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { isMigrationFailure };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { paginate } from "./paginate.js";
|
|
2
|
+
import { Service } from "nodefony";
|
|
3
|
+
//#region nodefony/src/AbstractCrudService.ts
|
|
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
|
+
var AbstractCrudService = class extends Service {
|
|
30
|
+
repository;
|
|
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, repository, ...wiring) {
|
|
38
|
+
super(name, ...wiring);
|
|
39
|
+
this.repository = repository;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Liste les entités correspondant au critère (toutes si omis).
|
|
43
|
+
*
|
|
44
|
+
* @param criteria - filtre typé optionnel.
|
|
45
|
+
* @param options - eager-load / pagination / tri portables.
|
|
46
|
+
*/
|
|
47
|
+
find(criteria, options) {
|
|
48
|
+
return this.repository.find(criteria, options);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Première entité correspondant au critère, ou `null`.
|
|
52
|
+
*
|
|
53
|
+
* @param criteria - filtre de sélection.
|
|
54
|
+
* @param options - eager-load portable.
|
|
55
|
+
*/
|
|
56
|
+
findOne(criteria, options) {
|
|
57
|
+
return this.repository.findOne(criteria, options);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Entité par clé primaire `id`, ou `null`.
|
|
61
|
+
*
|
|
62
|
+
* @remarks Suppose une PK nommée `id` (convention Nodefony : UUID `string`).
|
|
63
|
+
* Override dans la sous-classe si la PK diffère.
|
|
64
|
+
* @param id - identifiant primaire.
|
|
65
|
+
* @param options - eager-load portable.
|
|
66
|
+
*/
|
|
67
|
+
findById(id, options) {
|
|
68
|
+
return this.repository.findOne({ id }, options);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compte les entités correspondant au critère (toutes si omis).
|
|
72
|
+
*
|
|
73
|
+
* @param criteria - filtre optionnel.
|
|
74
|
+
*/
|
|
75
|
+
count(criteria) {
|
|
76
|
+
return this.repository.count(criteria);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Liste **une page** d'entités — pagination **native** (jamais matérialiser
|
|
80
|
+
* toute la collection). C'est la primitive à utiliser pour toute liste admin /
|
|
81
|
+
* data plane : elle ne charge qu'une page en mémoire, quelle que soit la taille
|
|
82
|
+
* de la table.
|
|
83
|
+
*
|
|
84
|
+
* @param page - requête de page ({@link PageQuery} : `limit` obligatoire,
|
|
85
|
+
* `offset`/`order`/`criteria`/`withTotal` optionnels).
|
|
86
|
+
* @returns une {@link Page} : au plus `limit` items, `hasNext`, et `total` si demandé.
|
|
87
|
+
* @throws `PageQueryError` (`400`) si un `?q=` est reçu alors que
|
|
88
|
+
* {@link AbstractCrudService.searchableFields} est vide.
|
|
89
|
+
*/
|
|
90
|
+
findPage(page) {
|
|
91
|
+
return paginate(this.repository, page, { searchable: this.searchableFields });
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Champs sur lesquels `?q=` cherche — **vide par défaut, donc `q` est refusé**.
|
|
95
|
+
*
|
|
96
|
+
* Un service CRUD générique ne peut pas deviner ce qui, dans une entité, se
|
|
97
|
+
* cherche : un titre oui, une clé étrangère ou un horodatage non. Le défaut
|
|
98
|
+
* vide n'est donc pas une lacune mais la seule réponse honnête — et il refuse
|
|
99
|
+
* plutôt qu'il n'ignore, sans quoi une recherche non honorée rendrait toute la
|
|
100
|
+
* table sous un `200`.
|
|
101
|
+
*
|
|
102
|
+
* L'activer tient en une ligne dans le service concret, et la recherche
|
|
103
|
+
* devient alors réelle de bout en bout (`?q=` → `LIKE` ancré, indexable) :
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* protected override readonly searchableFields = ["title", "slug"] as const;
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ Le déclarer ici ne suffit PAS à ouvrir la route : le contrat de page
|
|
110
|
+
* refuse `q` en amont tant que le point d'entrée HTTP ne passe pas
|
|
111
|
+
* `searchable: true` à `parsePageQuery`. Les deux se déclarent, pour la même
|
|
112
|
+
* raison — une capacité se constate à chaque étage qu'elle traverse.
|
|
113
|
+
*/
|
|
114
|
+
searchableFields = [];
|
|
115
|
+
/**
|
|
116
|
+
* Crée une entité : `beforeCreate` → persistance → `afterCreate` → `onCreated`.
|
|
117
|
+
*
|
|
118
|
+
* @param data - champs de l'entité à créer.
|
|
119
|
+
* @returns l'entité persistée. Émet `onCreated`.
|
|
120
|
+
*/
|
|
121
|
+
async create(data) {
|
|
122
|
+
const prepared = await this.beforeCreate(data);
|
|
123
|
+
const entity = await this.repository.create(prepared);
|
|
124
|
+
await this.afterCreate(entity);
|
|
125
|
+
this.fire("onCreated", entity);
|
|
126
|
+
return entity;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Met à jour **au plus une** entité : `beforeUpdate` → persistance atomique
|
|
130
|
+
* → `afterUpdate` → `onUpdated`.
|
|
131
|
+
*
|
|
132
|
+
* S'appuie sur {@link IRepository.updateOne} (atomique, `RETURNING` /
|
|
133
|
+
* `findOneAndUpdate`) : l'entité retournée reflète l'écriture, donc l'event
|
|
134
|
+
* `onUpdated` part de façon fiable même quand le critère porte sur un champ
|
|
135
|
+
* modifié (corrige le faux `null` de l'ancien `update` + relecture).
|
|
136
|
+
*
|
|
137
|
+
* `updateMany` (mise à jour en masse) n'est volontairement **pas** exposée
|
|
138
|
+
* ici : c'est une primitive de niveau repository (`IRepository.updateMany`) —
|
|
139
|
+
* on la remontera dans un service le jour où un usage métier la réclame.
|
|
140
|
+
*
|
|
141
|
+
* @param criteria - filtre de sélection (champ inconnu → `UnknownCriteriaField`).
|
|
142
|
+
* @param data - champs à modifier.
|
|
143
|
+
* @returns l'entité mise à jour, ou `null` si aucune ne correspond (pas d'event).
|
|
144
|
+
*/
|
|
145
|
+
async updateOne(criteria, data) {
|
|
146
|
+
const prepared = await this.beforeUpdate(criteria, data);
|
|
147
|
+
const updated = await this.repository.updateOne(criteria, prepared);
|
|
148
|
+
if (updated !== null) {
|
|
149
|
+
await this.afterUpdate(updated);
|
|
150
|
+
this.fire("onUpdated", updated);
|
|
151
|
+
}
|
|
152
|
+
return updated;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Supprime : `beforeDelete` → persistance → `afterDelete` → `onDeleted`.
|
|
156
|
+
*
|
|
157
|
+
* @param criteria - filtre de sélection.
|
|
158
|
+
* @returns le nombre d'entités supprimées. Émet `onDeleted` (criteria, count)
|
|
159
|
+
* uniquement si au moins une ligne a été supprimée.
|
|
160
|
+
*/
|
|
161
|
+
async delete(criteria) {
|
|
162
|
+
await this.beforeDelete(criteria);
|
|
163
|
+
const removed = await this.repository.delete(criteria);
|
|
164
|
+
if (removed > 0) {
|
|
165
|
+
await this.afterDelete(criteria, removed);
|
|
166
|
+
this.fire("onDeleted", criteria, removed);
|
|
167
|
+
}
|
|
168
|
+
return removed;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Transforme/valide les données avant création (ex. hacher un mot de passe).
|
|
172
|
+
*
|
|
173
|
+
* @param data - données entrantes.
|
|
174
|
+
* @returns les données préparées à persister.
|
|
175
|
+
*/
|
|
176
|
+
beforeCreate(data) {
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
/** Effet de bord après création (ex. provisionner une ressource liée). */
|
|
180
|
+
afterCreate(_entity) {}
|
|
181
|
+
/**
|
|
182
|
+
* Transforme/valide les données avant mise à jour.
|
|
183
|
+
*
|
|
184
|
+
* @param criteria - cible de la mise à jour.
|
|
185
|
+
* @param data - données entrantes.
|
|
186
|
+
* @returns les données préparées à persister.
|
|
187
|
+
*/
|
|
188
|
+
beforeUpdate(_criteria, data) {
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
191
|
+
/** Effet de bord après mise à jour. */
|
|
192
|
+
afterUpdate(_entity) {}
|
|
193
|
+
/** Garde/validation avant suppression (ex. interdire la suppression du dernier admin). */
|
|
194
|
+
beforeDelete(_criteria) {}
|
|
195
|
+
/** Effet de bord après suppression (ex. nettoyer un cache). */
|
|
196
|
+
afterDelete(_criteria, _removed) {}
|
|
197
|
+
};
|
|
198
|
+
//#endregion
|
|
199
|
+
export { AbstractCrudService };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//#region nodefony/src/ConnectionMonitor.ts
|
|
2
|
+
/** Taille max du ring d'erreurs récentes (borne mémoire). */
|
|
3
|
+
const MAX_RECENT_ERRORS = 12;
|
|
4
|
+
/** Taille de la fenêtre glissante de latence (min/moy/max). */
|
|
5
|
+
const MAX_LATENCY_SAMPLES = 30;
|
|
6
|
+
/** Fenêtre de latence vide. */
|
|
7
|
+
const EMPTY_LATENCY = {
|
|
8
|
+
last: null,
|
|
9
|
+
min: null,
|
|
10
|
+
avg: null,
|
|
11
|
+
max: null,
|
|
12
|
+
samples: 0
|
|
13
|
+
};
|
|
14
|
+
/** Compteurs neutres d'un connecteur jamais observé. */
|
|
15
|
+
const EMPTY_CORE = {
|
|
16
|
+
connectedSince: null,
|
|
17
|
+
uptimeMs: null,
|
|
18
|
+
connectCount: 0,
|
|
19
|
+
reconnectCount: 0,
|
|
20
|
+
lostCount: 0,
|
|
21
|
+
errorCount: 0,
|
|
22
|
+
lastConnectMs: null,
|
|
23
|
+
lastError: null,
|
|
24
|
+
recentErrors: [],
|
|
25
|
+
latency: EMPTY_LATENCY
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* **ConnectionMonitor** — observabilité **per-instance** (process-local) du
|
|
29
|
+
* cycle de vie des connexions ORM : connexions, **reconnexions**, **erreurs**
|
|
30
|
+
* (connexion + ping).
|
|
31
|
+
*
|
|
32
|
+
* Alimenté par la template method {@link Orm.connect} (capture latence + erreur
|
|
33
|
+
* de connexion) et par le ping live de l'endpoint `connection/health`. Lecture
|
|
34
|
+
* via {@link snapshot} (data plane ORM / dashboard Studio).
|
|
35
|
+
*
|
|
36
|
+
* **Cloud-native** : l'état d'une connexion DB est local au process (pool par
|
|
37
|
+
* pod) → le moniteur est volontairement per-instance. La vue multi-pod relève
|
|
38
|
+
* de l'agrégation (Prometheus par pod / fan-out Redis P13), pas de cette classe.
|
|
39
|
+
*
|
|
40
|
+
* **Perf** : structure lazy (`Map` allouée au 1ᵉʳ enregistrement) ; ring
|
|
41
|
+
* d'erreurs alloué seulement au 1ᵉʳ incident ; aucun coût tant qu'aucun ORM ne
|
|
42
|
+
* se connecte ou n'échoue. Reset au restart (diagnostic, pas d'état durable).
|
|
43
|
+
*/
|
|
44
|
+
var ConnectionMonitor = class {
|
|
45
|
+
/** `null` tant qu'aucun connecteur n'a été observé (lazy). */
|
|
46
|
+
#stats = null;
|
|
47
|
+
/** Crée/retourne les stats mutables d'un connecteur (alloue à la demande). */
|
|
48
|
+
#ensure(name) {
|
|
49
|
+
if (this.#stats === null) this.#stats = /* @__PURE__ */ new Map();
|
|
50
|
+
let s = this.#stats.get(name);
|
|
51
|
+
if (s === void 0) {
|
|
52
|
+
s = {
|
|
53
|
+
connectedSince: null,
|
|
54
|
+
connectCount: 0,
|
|
55
|
+
reconnectCount: 0,
|
|
56
|
+
lostCount: 0,
|
|
57
|
+
errorCount: 0,
|
|
58
|
+
lastConnectMs: null,
|
|
59
|
+
lastError: null,
|
|
60
|
+
recentErrors: null,
|
|
61
|
+
latencies: null
|
|
62
|
+
};
|
|
63
|
+
this.#stats.set(name, s);
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Enregistre une latence de ping live dans la fenêtre glissante (ring borné).
|
|
69
|
+
*
|
|
70
|
+
* @param name - clé du connecteur.
|
|
71
|
+
* @param ms - latence du ping (ms).
|
|
72
|
+
*/
|
|
73
|
+
recordPing(name, ms) {
|
|
74
|
+
const s = this.#ensure(name);
|
|
75
|
+
if (s.latencies === null) s.latencies = [];
|
|
76
|
+
s.latencies.push(ms);
|
|
77
|
+
if (s.latencies.length > MAX_LATENCY_SAMPLES) s.latencies.shift();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Enregistre une connexion réussie + sa latence.
|
|
81
|
+
*
|
|
82
|
+
* Ne compte PAS une reconnexion : une reprise du driver ne repasse jamais
|
|
83
|
+
* par `Orm.connect()`, elle se signale par {@link ConnectionMonitor.recordReconnect}.
|
|
84
|
+
*
|
|
85
|
+
* @param name - clé du connecteur.
|
|
86
|
+
* @param latencyMs - durée de l'établissement (ms).
|
|
87
|
+
*/
|
|
88
|
+
recordConnect(name, latencyMs) {
|
|
89
|
+
const s = this.#ensure(name);
|
|
90
|
+
s.connectCount += 1;
|
|
91
|
+
s.connectedSince = Date.now();
|
|
92
|
+
s.lastConnectMs = Math.round(latencyMs * 100) / 100;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Enregistre une **perte** de connexion constatée par l'adapter.
|
|
96
|
+
*
|
|
97
|
+
* `connectedSince` retombe à `null` : un uptime qui continue de croître
|
|
98
|
+
* pendant que le serveur est tombé est pire qu'absent — il se lit comme une
|
|
99
|
+
* preuve de bonne santé.
|
|
100
|
+
*
|
|
101
|
+
* @param name - clé du connecteur.
|
|
102
|
+
*/
|
|
103
|
+
recordLost(name) {
|
|
104
|
+
const s = this.#ensure(name);
|
|
105
|
+
s.lostCount += 1;
|
|
106
|
+
s.connectedSince = null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Enregistre une **reprise** de connexion constatée par l'adapter.
|
|
110
|
+
*
|
|
111
|
+
* @param name - clé du connecteur.
|
|
112
|
+
*/
|
|
113
|
+
recordReconnect(name) {
|
|
114
|
+
const s = this.#ensure(name);
|
|
115
|
+
s.reconnectCount += 1;
|
|
116
|
+
s.connectedSince = Date.now();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Enregistre une erreur (connexion échouée ou ping en échec).
|
|
120
|
+
*
|
|
121
|
+
* @param name - clé du connecteur.
|
|
122
|
+
* @param message - message d'erreur (credential déjà retiré par l'appelant).
|
|
123
|
+
*/
|
|
124
|
+
recordError(name, message) {
|
|
125
|
+
const s = this.#ensure(name);
|
|
126
|
+
s.errorCount += 1;
|
|
127
|
+
const err = {
|
|
128
|
+
message,
|
|
129
|
+
ts: Date.now()
|
|
130
|
+
};
|
|
131
|
+
s.lastError = err;
|
|
132
|
+
if (s.recentErrors === null) s.recentErrors = [];
|
|
133
|
+
s.recentErrors.unshift(err);
|
|
134
|
+
if (s.recentErrors.length > MAX_RECENT_ERRORS) s.recentErrors.length = MAX_RECENT_ERRORS;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Vue figée des compteurs d'un connecteur (ou compteurs neutres si jamais
|
|
138
|
+
* observé).
|
|
139
|
+
*
|
|
140
|
+
* @param name - clé du connecteur.
|
|
141
|
+
*/
|
|
142
|
+
snapshot(name) {
|
|
143
|
+
const s = this.#stats?.get(name);
|
|
144
|
+
if (s === void 0) return EMPTY_CORE;
|
|
145
|
+
const lat = s.latencies;
|
|
146
|
+
let latency = EMPTY_LATENCY;
|
|
147
|
+
if (lat && lat.length) {
|
|
148
|
+
let min = lat[0];
|
|
149
|
+
let max = lat[0];
|
|
150
|
+
let sum = 0;
|
|
151
|
+
for (const v of lat) {
|
|
152
|
+
if (v < min) min = v;
|
|
153
|
+
if (v > max) max = v;
|
|
154
|
+
sum += v;
|
|
155
|
+
}
|
|
156
|
+
latency = {
|
|
157
|
+
last: lat[lat.length - 1],
|
|
158
|
+
min: Math.round(min * 100) / 100,
|
|
159
|
+
avg: Math.round(sum / lat.length * 100) / 100,
|
|
160
|
+
max: Math.round(max * 100) / 100,
|
|
161
|
+
samples: lat.length
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
connectedSince: s.connectedSince,
|
|
166
|
+
uptimeMs: s.connectedSince === null ? null : Date.now() - s.connectedSince,
|
|
167
|
+
connectCount: s.connectCount,
|
|
168
|
+
reconnectCount: s.reconnectCount,
|
|
169
|
+
lostCount: s.lostCount,
|
|
170
|
+
errorCount: s.errorCount,
|
|
171
|
+
lastConnectMs: s.lastConnectMs,
|
|
172
|
+
lastError: s.lastError,
|
|
173
|
+
recentErrors: s.recentErrors ?? [],
|
|
174
|
+
latency
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/** Singleton process-wide du moniteur de connexions. */
|
|
179
|
+
const connectionMonitor = new ConnectionMonitor();
|
|
180
|
+
//#endregion
|
|
181
|
+
export { connectionMonitor };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { entityRegistry } from "./EntityRegistry.js";
|
|
2
|
+
//#region nodefony/src/Entity.ts
|
|
3
|
+
/**
|
|
4
|
+
* Classe de base abstraite d'une entité, indépendante du driver ORM.
|
|
5
|
+
*
|
|
6
|
+
* Porte le contrat {@link IEntity} : nom logique, connecteur cible, schéma natif
|
|
7
|
+
* (calculé via {@link Entity.getSchema}), modèle compilé (post-connexion) et
|
|
8
|
+
* relations déclaratives. {@link Entity.register} insère l'entité dans le
|
|
9
|
+
* {@link entityRegistry} process-wide.
|
|
10
|
+
*
|
|
11
|
+
* **Pas d'auto-enregistrement dans le constructeur** : en TypeScript, le
|
|
12
|
+
* constructeur de la classe de base s'exécute AVANT les initialiseurs de champs
|
|
13
|
+
* de la sous-classe — `name`/`connector` seraient encore `undefined`. L'enregistrement
|
|
14
|
+
* automatique est donc porté par le décorateur `@entity` (P5.3), qui lit les
|
|
15
|
+
* métadonnées de classe ; en attendant, appeler explicitement
|
|
16
|
+
* {@link Entity.register} après instanciation.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam S - type du schéma natif du driver.
|
|
19
|
+
* @typeParam M - type du modèle compilé natif du driver.
|
|
20
|
+
*/
|
|
21
|
+
var Entity = class {
|
|
22
|
+
/** Modèle compilé natif, renseigné après connexion de l'ORM. */
|
|
23
|
+
model;
|
|
24
|
+
/** Relations déclarées vers d'autres entités (par nom logique). */
|
|
25
|
+
relations;
|
|
26
|
+
/** Définition de schéma propre au driver (délègue à {@link Entity.getSchema}). */
|
|
27
|
+
get schema() {
|
|
28
|
+
return this.getSchema();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Enregistre cette entité dans le {@link entityRegistry} process-wide.
|
|
32
|
+
*
|
|
33
|
+
* @returns `this` (chaînable).
|
|
34
|
+
* @throws si l'entité est déjà enregistrée sur le même connecteur.
|
|
35
|
+
*/
|
|
36
|
+
register() {
|
|
37
|
+
entityRegistry.register(this);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { Entity };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//#region nodefony/src/EntityRegistry.ts
|
|
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
|
+
var EntityRegistry = class {
|
|
14
|
+
/** `entities[name][connector] = entity` — allouée au premier enregistrement. */
|
|
15
|
+
#entities = null;
|
|
16
|
+
/**
|
|
17
|
+
* Enregistre une entité pour son connecteur cible.
|
|
18
|
+
*
|
|
19
|
+
* @param entity - entité implémentant {@link IEntity} (utilise `name` + `connector`).
|
|
20
|
+
* @throws si cette entité est déjà enregistrée sur le même connecteur.
|
|
21
|
+
*/
|
|
22
|
+
register(entity) {
|
|
23
|
+
if (this.#entities === null) this.#entities = Object.create(null);
|
|
24
|
+
const store = this.#entities;
|
|
25
|
+
let bucket = store[entity.name];
|
|
26
|
+
if (bucket === void 0) {
|
|
27
|
+
bucket = Object.create(null);
|
|
28
|
+
store[entity.name] = bucket;
|
|
29
|
+
}
|
|
30
|
+
if (bucket[entity.connector] !== void 0) throw new Error(`EntityRegistry: entity "${entity.name}" already registered for connector "${entity.connector}".`);
|
|
31
|
+
bucket[entity.connector] = entity;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Résout une entité par nom (et connecteur si plusieurs candidats).
|
|
35
|
+
*
|
|
36
|
+
* @param name - nom logique de l'entité (ex. `"User"`).
|
|
37
|
+
* @param connector - connecteur cible ; obligatoire si l'entité existe sur plusieurs.
|
|
38
|
+
* @returns l'entité correspondante.
|
|
39
|
+
* @throws si l'entité est inconnue, absente du connecteur demandé, ou ambiguë
|
|
40
|
+
* (plusieurs connecteurs) sans `connector` précisé.
|
|
41
|
+
*/
|
|
42
|
+
get(name, connector) {
|
|
43
|
+
const bucket = this.#entities?.[name];
|
|
44
|
+
if (bucket === void 0) throw new Error(`EntityRegistry: no entity registered under "${name}".`);
|
|
45
|
+
if (connector !== void 0) {
|
|
46
|
+
const entity = bucket[connector];
|
|
47
|
+
if (entity === void 0) throw new Error(`EntityRegistry: entity "${name}" is not registered for connector "${connector}".`);
|
|
48
|
+
return entity;
|
|
49
|
+
}
|
|
50
|
+
const connectors = Object.keys(bucket);
|
|
51
|
+
if (connectors.length > 1) throw new Error(`EntityRegistry: entity "${name}" exists on multiple connectors (${connectors.join(", ")}); specify one.`);
|
|
52
|
+
return bucket[connectors[0]];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Indique si une entité (optionnellement sur un connecteur précis) est enregistrée.
|
|
56
|
+
*
|
|
57
|
+
* @param name - nom logique de l'entité.
|
|
58
|
+
* @param connector - connecteur cible facultatif.
|
|
59
|
+
*/
|
|
60
|
+
has(name, connector) {
|
|
61
|
+
const bucket = this.#entities?.[name];
|
|
62
|
+
if (bucket === void 0) return false;
|
|
63
|
+
return connector === void 0 ? true : bucket[connector] !== void 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Liste toutes les entités enregistrées, tous connecteurs confondus.
|
|
67
|
+
*
|
|
68
|
+
* @returns tableau d'entités (vide si registre vide).
|
|
69
|
+
*/
|
|
70
|
+
list() {
|
|
71
|
+
if (this.#entities === null) return [];
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const name in this.#entities) {
|
|
74
|
+
const bucket = this.#entities[name];
|
|
75
|
+
for (const connector in bucket) out.push(bucket[connector]);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Retire une entité du registre (teardown tests / hot-reload).
|
|
81
|
+
*
|
|
82
|
+
* @param name - nom logique de l'entité.
|
|
83
|
+
* @param connector - si fourni, retire seulement cette variante ; sinon toutes.
|
|
84
|
+
* @returns `true` si quelque chose a été retiré, `false` sinon.
|
|
85
|
+
*/
|
|
86
|
+
unregister(name, connector) {
|
|
87
|
+
const store = this.#entities;
|
|
88
|
+
if (store === null) return false;
|
|
89
|
+
const bucket = store[name];
|
|
90
|
+
if (bucket === void 0) return false;
|
|
91
|
+
if (connector === void 0) {
|
|
92
|
+
delete store[name];
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (bucket[connector] === void 0) return false;
|
|
96
|
+
delete bucket[connector];
|
|
97
|
+
if (Object.keys(bucket).length === 0) delete store[name];
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Singleton process-wide partagé. Les entités s'y enregistrent (via le
|
|
103
|
+
* décorateur `entities([...])` posé sur le Module, ou {@link Entity.register}).
|
|
104
|
+
* La classe {@link EntityRegistry} reste instanciable pour des registres isolés
|
|
105
|
+
* (tests).
|
|
106
|
+
*/
|
|
107
|
+
const entityRegistry = new EntityRegistry();
|
|
108
|
+
//#endregion
|
|
109
|
+
export { EntityRegistry, entityRegistry };
|