@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
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 };