@nodefony/orm-core 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +131 -0
  3. package/dist/index.js +22 -0
  4. package/dist/nodefony/interfaces/IEntity.js +1 -0
  5. package/dist/nodefony/interfaces/IOrm.js +1 -0
  6. package/dist/nodefony/interfaces/IOrmFlow.js +1 -0
  7. package/dist/nodefony/interfaces/IOrmGraph.js +1 -0
  8. package/dist/nodefony/interfaces/IOrmMigrations.js +12 -0
  9. package/dist/nodefony/interfaces/IOrmProbe.js +1 -0
  10. package/dist/nodefony/interfaces/IPage.js +1 -0
  11. package/dist/nodefony/interfaces/IRepository.js +1 -0
  12. package/dist/nodefony/interfaces/ITransaction.js +1 -0
  13. package/dist/nodefony/interfaces/index.js +1 -0
  14. package/dist/nodefony/src/AbstractCrudService.js +199 -0
  15. package/dist/nodefony/src/ConnectionMonitor.js +181 -0
  16. package/dist/nodefony/src/Entity.js +42 -0
  17. package/dist/nodefony/src/EntityRegistry.js +109 -0
  18. package/dist/nodefony/src/Orm.js +297 -0
  19. package/dist/nodefony/src/OrmAdminApi.js +491 -0
  20. package/dist/nodefony/src/OrmRegistry.js +75 -0
  21. package/dist/nodefony/src/QueryFlowMonitor.js +128 -0
  22. package/dist/nodefony/src/buildOrmLeanHealth.js +54 -0
  23. package/dist/nodefony/src/criteria.js +176 -0
  24. package/dist/nodefony/src/decorators/entitiesDecorator.js +67 -0
  25. package/dist/nodefony/src/decorators/entityDecorator.js +51 -0
  26. package/dist/nodefony/src/decorators/index.js +5 -0
  27. package/dist/nodefony/src/decorators/metadataStore.js +38 -0
  28. package/dist/nodefony/src/decorators/repositoryDecorator.js +35 -0
  29. package/dist/nodefony/src/defineEntity.js +27 -0
  30. package/dist/nodefony/src/errors.js +76 -0
  31. package/dist/nodefony/src/ormWiring.js +78 -0
  32. package/dist/nodefony/src/paginate.js +55 -0
  33. package/dist/nodefony/src/readOptions.js +52 -0
  34. package/dist/nodefony/src/serviceWiring.js +1 -0
  35. package/dist/types/index.d.ts +39 -0
  36. package/dist/types/nodefony/interfaces/IEntity.d.ts +65 -0
  37. package/dist/types/nodefony/interfaces/IOrm.d.ts +136 -0
  38. package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +71 -0
  39. package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +165 -0
  40. package/dist/types/nodefony/interfaces/IOrmMigrations.d.ts +145 -0
  41. package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +70 -0
  42. package/dist/types/nodefony/interfaces/IPage.d.ts +24 -0
  43. package/dist/types/nodefony/interfaces/IRepository.d.ts +369 -0
  44. package/dist/types/nodefony/interfaces/ITransaction.d.ts +31 -0
  45. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  46. package/dist/types/nodefony/src/AbstractCrudService.d.ts +158 -0
  47. package/dist/types/nodefony/src/ConnectionMonitor.d.ts +86 -0
  48. package/dist/types/nodefony/src/Entity.d.ts +44 -0
  49. package/dist/types/nodefony/src/EntityRegistry.d.ts +60 -0
  50. package/dist/types/nodefony/src/Orm.d.ts +197 -0
  51. package/dist/types/nodefony/src/OrmAdminApi.d.ts +84 -0
  52. package/dist/types/nodefony/src/OrmRegistry.d.ts +58 -0
  53. package/dist/types/nodefony/src/QueryFlowMonitor.d.ts +53 -0
  54. package/dist/types/nodefony/src/buildOrmLeanHealth.d.ts +15 -0
  55. package/dist/types/nodefony/src/criteria.d.ts +131 -0
  56. package/dist/types/nodefony/src/decorators/entitiesDecorator.d.ts +48 -0
  57. package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +49 -0
  58. package/dist/types/nodefony/src/decorators/index.d.ts +10 -0
  59. package/dist/types/nodefony/src/decorators/metadataStore.d.ts +54 -0
  60. package/dist/types/nodefony/src/decorators/repositoryDecorator.d.ts +30 -0
  61. package/dist/types/nodefony/src/defineEntity.d.ts +43 -0
  62. package/dist/types/nodefony/src/errors.d.ts +62 -0
  63. package/dist/types/nodefony/src/ormWiring.d.ts +48 -0
  64. package/dist/types/nodefony/src/paginate.d.ts +43 -0
  65. package/dist/types/nodefony/src/readOptions.d.ts +21 -0
  66. package/dist/types/nodefony/src/serviceWiring.d.ts +24 -0
  67. package/docs/index.md +791 -0
  68. package/docs/tutorial-entity.md +577 -0
  69. package/package.json +73 -0
@@ -0,0 +1,297 @@
1
+ import { ormRegistry } from "./OrmRegistry.js";
2
+ import { connectionMonitor } from "./ConnectionMonitor.js";
3
+ import { Service } from "nodefony";
4
+ import { performance } from "node:perf_hooks";
5
+ //#region nodefony/src/Orm.ts
6
+ /**
7
+ * Période du battement, réglable par l'environnement (`0` le désactive).
8
+ *
9
+ * Lue UNE fois au chargement du module : un connecteur ne la relit pas à
10
+ * chaque construction, et un déploiement qui veut un battement plus serré
11
+ * — ou aucun — n'a pas à toucher au code de l'application.
12
+ */
13
+ const HEARTBEAT_MS_DEFAULT = (() => {
14
+ const brut = process.env.NF_ORM_HEARTBEAT_MS;
15
+ if (brut === void 0) return 3e4;
16
+ const n = Number.parseInt(brut, 10);
17
+ return Number.isFinite(n) && n >= 0 ? n : 3e4;
18
+ })();
19
+ /**
20
+ * Classe de base abstraite de tout ORM Nodefony — câble {@link Service} (DI,
21
+ * Syslog, bus d'événements) et le contrat {@link IOrm}, et s'auto-enregistre
22
+ * dans le {@link ormRegistry} process-wide à la construction.
23
+ *
24
+ * Les drivers concrets (`@nodefony/mongoose`, `@nodefony/drizzle`...)
25
+ * implémentent les opérations bas niveau ({@link Orm.onConnect}, `disconnect`,
26
+ * `getRepository`, `transaction`, `getNativeConnection`). La connexion passe par
27
+ * la template method {@link Orm.connect} qui émet l'événement `onOrmReady` une
28
+ * fois le driver connecté — garantissant que tous les ORM signalent leur
29
+ * disponibilité de façon homogène (avant le `onReady` du Kernel).
30
+ *
31
+ * @typeParam — aucun ; les types natifs du driver transitent via les génériques
32
+ * des méthodes ({@link IRepository}, {@link ITransaction}, native connection).
33
+ */
34
+ var Orm = class extends Service {
35
+ /**
36
+ * État de vie de la connexion — source UNIQUE de `isConnected()`.
37
+ *
38
+ * `protected` et non `#privé` : `disconnect()` est implémenté par chaque
39
+ * adapter et doit pouvoir le remettre à `false`.
40
+ */
41
+ alive = false;
42
+ /**
43
+ * Une perte est-elle EN SOUFFRANCE, c'est-à-dire constatée et pas encore
44
+ * réparée ?
45
+ *
46
+ * Sans ce drapeau, l'idempotence de {@link Orm.connectionRestored} reposait
47
+ * sur le seul `alive` — et `alive` vaut encore `false` PENDANT
48
+ * l'établissement, puisqu'il n'est posé qu'au retour de `onConnect()`. Or un
49
+ * adapter câble ses écoutes AVANT son premier échange (il le doit : ce
50
+ * premier échange peut échouer), si bien que le client initial émettait un
51
+ * signal de « retour » que rien n'arrêtait : **chaque boot comptait une
52
+ * reconnexion et annonçait `onOrmRestored` avant même `onOrmReady`**
53
+ * (mesuré : `reconnectCount = 1` sur un connecteur qui vient de naître).
54
+ * Une reprise n'a de sens que s'il y a eu une perte : c'est ce que ce
55
+ * drapeau exprime, et il rend la garde vraie à tout instant du cycle de vie.
56
+ */
57
+ #lostPending = false;
58
+ /**
59
+ * **Ce que cet adapter sait VRAIMENT de l'état de sa connexion** — déclaré,
60
+ * jamais supposé.
61
+ *
62
+ * `"events"` : le driver signale les pertes et les reprises, l'adapter les
63
+ * traduit ; `isConnected()` est un CONSTAT.
64
+ * `"assumed"` : rien ne signale quoi que ce soit (base embarquée, driver
65
+ * muet) ; `isConnected()` dit seulement « la connexion a été établie et
66
+ * n'a pas été fermée » — une supposition.
67
+ *
68
+ * **Le défaut est `"assumed"`, et il n'est pas abstrait — délibérément.**
69
+ * Le rendre obligatoire forcerait chaque adapter à répondre, mais casserait
70
+ * la compilation de tout adapter existant : ajouter un ORM deviendrait une
71
+ * rupture. Le défaut prudent protège aussi bien de l'oubli, parce qu'il dit
72
+ * la VÉRITÉ sur un adapter qui n'a rien câblé — il ne sait pas. Ce qu'il
73
+ * faut empêcher, ce n'est pas le silence : c'est qu'un silence se fasse
74
+ * passer pour un constat.
75
+ */
76
+ get liveness() {
77
+ return "assumed";
78
+ }
79
+ /**
80
+ * @param name - clé unique de l'ORM dans le {@link ormRegistry}.
81
+ * @param container - container DI hérité (Kernel) ou nouveau si omis.
82
+ * @param notificationsCenter - bus d'événements partagé, `false` pour aucun.
83
+ * @param options - options de service.
84
+ * @throws si un ORM du même `name` est déjà enregistré.
85
+ */
86
+ constructor(name, container, notificationsCenter, options) {
87
+ super(name, container, notificationsCenter, options);
88
+ ormRegistry.register(this.name, this);
89
+ }
90
+ /**
91
+ * Connecte le driver puis émet `onOrmReady`.
92
+ *
93
+ * Template method : ne pas surcharger — implémenter {@link Orm.onConnect}.
94
+ * Instrumente le {@link connectionMonitor} (latence + reconnexion en cas de
95
+ * succès, erreur de connexion en cas d'échec).
96
+ */
97
+ async connect() {
98
+ this.#lostPending = false;
99
+ const t0 = performance.now();
100
+ try {
101
+ await this.onConnect();
102
+ this.alive = true;
103
+ connectionMonitor.recordConnect(this.name, performance.now() - t0);
104
+ this.startHeartbeat();
105
+ } catch (e) {
106
+ this.alive = false;
107
+ connectionMonitor.recordError(this.name, e instanceof Error ? e.message : String(e));
108
+ throw e;
109
+ }
110
+ this.fire("onOrmReady", this);
111
+ }
112
+ /**
113
+ * **Le driver a PERDU la connexion** — à appeler par l'adapter depuis
114
+ * l'événement natif de son driver (`pool.on("error")` côté `pg`,
115
+ * `connection.on("disconnected")` côté Mongoose…).
116
+ *
117
+ * Idempotent : un driver émet souvent plusieurs erreurs pour une seule
118
+ * coupure (une par connexion du pool). Seule la PREMIÈRE bascule l'état,
119
+ * compte l'incident et émet `onOrmLost` — sinon un pool de 10 connexions
120
+ * ferait dix fois le tour du framework pour un seul serveur tombé.
121
+ *
122
+ * @param reason - cause lisible, sans credential (elle est journalisée).
123
+ */
124
+ connectionLost(reason) {
125
+ connectionMonitor.recordError(this.name, reason);
126
+ if (!this.alive) return;
127
+ this.alive = false;
128
+ this.#lostPending = true;
129
+ connectionMonitor.recordLost(this.name);
130
+ this.log(`connexion perdue : ${reason}`, "WARNING");
131
+ this.fire("onOrmLost", this, reason);
132
+ }
133
+ /**
134
+ * **Le driver a RÉTABLI la connexion** — à appeler par l'adapter depuis
135
+ * l'événement natif correspondant (`pool.on("connect")`, `reconnected`…).
136
+ *
137
+ * **Une reprise n'existe que s'il y a eu une perte.** `pg` émet `connect` et
138
+ * `acquire` à chaque client pris au pool, y compris quand rien n'est tombé,
139
+ * et y compris pendant l'établissement initial : sans cette condition, un
140
+ * pool qui grandit sous la charge — ou simplement une application qui
141
+ * démarre — compterait des reconnexions imaginaires.
142
+ */
143
+ connectionRestored() {
144
+ if (this.alive || !this.#lostPending) return;
145
+ this.alive = true;
146
+ this.#lostPending = false;
147
+ connectionMonitor.recordReconnect(this.name);
148
+ this.log("connexion rétablie", "INFO");
149
+ this.fire("onOrmRestored", this);
150
+ }
151
+ /**
152
+ * Période du battement de cœur, en millisecondes. `0` le désactive.
153
+ *
154
+ * **Pourquoi le framework doit fournir ça** : le driver MongoDB surveille ses
155
+ * serveurs en permanence (SDAM) et sait donc qu'une base est tombée même sans
156
+ * le moindre trafic. `pg` et `mysql2` n'ont RIEN de tel — ils n'apprennent
157
+ * l'état du serveur que par leurs requêtes. Mesuré : sur ces deux dialectes,
158
+ * une coupure survenue pendant qu'une requête était en vol, ou un serveur
159
+ * simplement GELÉ (qui ne ferme rien), n'émettent aucun événement : l'état
160
+ * restait « connecté » indéfiniment. Un battement comble cette asymétrie —
161
+ * il ne réinvente rien, il donne aux drivers muets ce que Mongo a déjà.
162
+ *
163
+ * Pourquoi pas une instrumentation des requêtes : son coût croît avec le
164
+ * trafic, pour une information qui ne change qu'aux rares instants de panne ;
165
+ * et il faudrait distinguer une erreur de connexion d'une contrainte violée.
166
+ * Ici le coût est CONSTANT et connu d'avance : une requête légère par période.
167
+ */
168
+ heartbeatMs = HEARTBEAT_MS_DEFAULT;
169
+ /**
170
+ * Délai au-delà duquel un battement sans réponse vaut une PERTE.
171
+ *
172
+ * Sans lui, le battement ne sert à rien contre le cas qui l'a justifié :
173
+ * une base GELÉE ne ferme rien et ne répond pas, donc `ping()` PEND — et
174
+ * le battement pendait avec elle, indéfiniment, sans jamais conclure
175
+ * (mesuré : 30 s de gel, aucune détection). Une sonde doit avoir sa
176
+ * propre montre, sinon elle hérite de la panne qu'elle est censée voir.
177
+ */
178
+ heartbeatTimeoutMs = 5e3;
179
+ /** Minuterie du battement — `null` tant qu'il ne bat pas (aucun coût). */
180
+ #heartbeat = null;
181
+ /** Un battement est-il en vol ? Évite qu'un ping lent en déclenche un autre. */
182
+ #beating = false;
183
+ /**
184
+ * Démarre le battement si l'adapter sait répondre à un `ping()` et que la
185
+ * période n'est pas nulle. Idempotent.
186
+ *
187
+ * La minuterie est `unref()` : elle ne doit JAMAIS retenir le process en vie
188
+ * — un banc, un script CLI ou un test qui se termine ne doit pas attendre le
189
+ * prochain battement pour rendre la main.
190
+ */
191
+ startHeartbeat() {
192
+ if (this.#heartbeat !== null || this.heartbeatMs <= 0) return;
193
+ if (typeof this.ping !== "function") return;
194
+ const timer = setInterval(() => {
195
+ this.#beat();
196
+ }, this.heartbeatMs);
197
+ timer.unref?.();
198
+ this.#heartbeat = timer;
199
+ }
200
+ /** Arrête le battement et libère la minuterie. Idempotent. */
201
+ stopHeartbeat() {
202
+ if (this.#heartbeat !== null) {
203
+ clearInterval(this.#heartbeat);
204
+ this.#heartbeat = null;
205
+ }
206
+ this.#beating = false;
207
+ }
208
+ /**
209
+ * **Bat MAINTENANT**, sans attendre la fin de la période.
210
+ *
211
+ * À appeler par un adapter dont le driver émet un signal SUSPECT mais pas
212
+ * concluant — typiquement la fermeture du socket d'une connexion du pool :
213
+ * elle arrive aussi bien pour un serveur tombé que pour une connexion
214
+ * inactive recyclée, et l'adapter ne peut pas les distinguer. Plutôt que de
215
+ * trancher à sa place — ce qui inscrirait de faux incidents à chaque
216
+ * recyclage — il délègue ici : le battement fait la seule chose qui tranche,
217
+ * une requête, et met l'état à jour selon la réponse.
218
+ *
219
+ * Sans cette porte, un dialecte muet reste marqué connecté jusqu'au battement
220
+ * suivant : 30 s par défaut, là où `pg` bascule en millisecondes. C'est une
221
+ * asymétrie de détection entre deux dialectes de production, pas un réglage.
222
+ *
223
+ * Ne coûte rien quand rien ne va mal : la garde `#beating` écarte les
224
+ * battements concurrents, donc un pool dont dix connexions tombent d'un coup
225
+ * ne sonde qu'une fois. Ne rétablit ni ne perd quoi que ce soit après un
226
+ * `disconnect()` — `#beat` y voit un arrêt volontaire et s'abstient.
227
+ */
228
+ beatNow() {
229
+ this.#beat();
230
+ }
231
+ /**
232
+ * Un battement : sonde la base et met l'état à jour.
233
+ *
234
+ * Ne fait RIEN si un battement précédent n'a pas rendu la main — sur une base
235
+ * gelée, le ping peut pendre longtemps, et empiler les sondes ne ferait
236
+ * qu'empiler des connexions sur un serveur déjà en difficulté.
237
+ */
238
+ async #beat() {
239
+ if (!this.alive && !this.#lostPending) {
240
+ this.stopHeartbeat();
241
+ return;
242
+ }
243
+ if (this.#beating) return;
244
+ this.#beating = true;
245
+ let watchdog = null;
246
+ try {
247
+ await Promise.race([this.ping?.() ?? Promise.resolve(), new Promise((_, rejeter) => {
248
+ watchdog = setTimeout(() => {
249
+ rejeter(/* @__PURE__ */ new Error(`aucune réponse en ${this.heartbeatTimeoutMs} ms`));
250
+ }, this.heartbeatTimeoutMs);
251
+ watchdog.unref?.();
252
+ })]);
253
+ this.connectionRestored();
254
+ } catch (e) {
255
+ this.connectionLost(`battement : ${e instanceof Error ? e.message : String(e)}`);
256
+ } finally {
257
+ if (watchdog !== null) clearTimeout(watchdog);
258
+ this.#beating = false;
259
+ }
260
+ }
261
+ /**
262
+ * Indique si la connexion est active — **implémentation UNIQUE**, portée par
263
+ * la classe de base et non par chaque adapter.
264
+ *
265
+ * C'est délibéré : tant que chaque adapter portait son propre booléen posé à
266
+ * la connexion, aucun ne le remettait à `false` quand le serveur tombait —
267
+ * la santé ORM répondait « connecté » en pleine coupure. L'état vit ici, et
268
+ * il n'a que trois sources : `connect()`, `disconnect()`, et les deux
269
+ * signaux que l'adapter traduit depuis son driver.
270
+ */
271
+ isConnected() {
272
+ return this.alive;
273
+ }
274
+ /**
275
+ * Décrit les colonnes d'une entité pour le graphe canonique. Défaut : `[]`
276
+ * (relations seules dans l'ERD). Les adapters surchargent avec l'introspection
277
+ * native (Drizzle `getTableConfig`, Mongoose paths).
278
+ *
279
+ * @param _name - nom logique de l'entité.
280
+ * @returns colonnes normalisées.
281
+ */
282
+ describeEntity(_name) {
283
+ return [];
284
+ }
285
+ /**
286
+ * Décrit la connexion sous-jacente (driver + cible). Défaut : driver vide
287
+ * (inconnu). Les adapters surchargent (Drizzle → `sqlite` + fichier, etc.).
288
+ * Ne DOIT jamais exposer de credential.
289
+ *
290
+ * @returns infos de connexion (driver vide si non renseigné).
291
+ */
292
+ describeConnection() {
293
+ return { driver: "" };
294
+ }
295
+ };
296
+ //#endregion
297
+ export { Orm };