@nodefony/orm-core 10.0.0-alpha.2 → 10.0.0-alpha.3

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/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { AbstractCrudService } from "./nodefony/src/AbstractCrudService.js";
11
11
  import { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
12
12
  import { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, toDbml, toJsonSchema } from "./nodefony/src/OrmAdminApi.js";
13
13
  import { buildOrmLeanHealth } from "./nodefony/src/buildOrmLeanHealth.js";
14
+ import { diagnoseConnectionFailure, parseConnectionTarget } from "./nodefony/src/connectionDiagnosis.js";
15
+ import { CONNECT_TIMEOUT_MS, withConnectDeadline } from "./nodefony/src/connectDeadline.js";
14
16
  import { reportOrmBootLines, resolveOrmFlowEnabled, wireOrmAdminPlane } from "./nodefony/src/ormWiring.js";
15
17
  import { isMigrationFailure } from "./nodefony/interfaces/IOrmMigrations.js";
16
18
  import { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta } from "./nodefony/src/decorators/metadataStore.js";
@@ -19,4 +21,4 @@ import { DEFAULT_CONNECTOR, entities } from "./nodefony/src/decorators/entitiesD
19
21
  import { repository } from "./nodefony/src/decorators/repositoryDecorator.js";
20
22
  import "./nodefony/src/decorators/index.js";
21
23
  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 };
24
+ export { AbstractCrudService, CONNECT_TIMEOUT_MS, DEFAULT_CONNECTOR, Entity, EntityRegistry, InvalidOrderOption, LIKE_ESCAPE_CHAR, OPERATOR_KEYS, Orm, OrmRegistry, UPDATE_OPERATOR_KEYS, UnknownCriteriaField, assertOrderOption, buildConnectionHealth, buildOrmFlow, buildOrmGraph, buildOrmLeanHealth, connectionMonitor, createOrmAdminApi, defineEntity, diagnoseConnectionFailure, entities, entity, entityRegistry, escapeLikeTerm, getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, isFieldOperators, isMigrationFailure, isUpdateOperators, likePatternToRegExp, ormRegistry, paginate, parseConnectionTarget, queryFlowMonitor, registerOrmAdminApi, reportOrmBootLines, repository, resolveOrmFlowEnabled, searchCriteria, toDbml, toJsonSchema, wireOrmAdminPlane, withConnectDeadline };
@@ -0,0 +1,67 @@
1
+ //#region nodefony/src/connectDeadline.ts
2
+ /**
3
+ * Le DÉLAI DE GARDE d'une connexion à une base — une commande n'attend jamais
4
+ * sans fin.
5
+ *
6
+ * 🔴 Le cas est vécu, et il ne ressemble pas à une panne : `nodefony inspect
7
+ * config` restait suspendu, sans un mot, sans erreur, sans reprendre la main.
8
+ * Aucun driver ne borne l'établissement par défaut — `pg-pool` traite son
9
+ * `connectionTimeoutMillis` à `0` comme « attendre indéfiniment » — et un
10
+ * serveur qui accepte la connexion TCP sans jamais répondre (conteneur en
11
+ * train de mourir, port relayé vers rien, réseau qui avale les paquets) laisse
12
+ * l'appelant pendu.
13
+ *
14
+ * Une commande de diagnostic qui se bloque est pire qu'une commande qui
15
+ * échoue : l'échec nomme une cause, le blocage n'apprend rien et fait douter
16
+ * de l'outil plutôt que de l'infrastructure.
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * Le temps qu'on accorde à une base pour ACCEPTER une connexion, en
22
+ * millisecondes.
23
+ *
24
+ * Dix secondes, et ce n'est pas un réglage de performance : c'est la frontière
25
+ * entre « lent » et « ne répondra pas ». Un serveur local répond en quelques
26
+ * millisecondes, un serveur infonuagique qui sort de veille en quelques
27
+ * secondes ; au-delà, ce qui se passe n'est plus une lenteur, et le dire vaut
28
+ * mieux que l'attendre.
29
+ *
30
+ * Volontairement NON configurable : le besoin n'est pas de choisir une durée,
31
+ * c'est de n'attendre jamais sans fin. Une clé de configuration serait une
32
+ * surface publique à porter pour toute une série majeure, au service d'un
33
+ * réglage que personne n'a demandé.
34
+ */
35
+ const CONNECT_TIMEOUT_MS = 1e4;
36
+ /**
37
+ * Borne l'attente d'une promesse, et rejette avec une cause NOMMÉE au-delà.
38
+ *
39
+ * Le message importe autant que la borne : « délai dépassé » sans sujet
40
+ * envoie chercher partout. Celui-ci dit ce qu'on attendait, de qui, et
41
+ * combien de temps — c'est ce qui distingue un diagnostic d'un abandon.
42
+ *
43
+ * Le minuteur est `unref`é : il ne doit pas, à lui seul, retenir le processus
44
+ * en vie une fois la réponse arrivée.
45
+ *
46
+ * @param work - la promesse à borner (établissement, ping…).
47
+ * @param what - ce qu'on attend, à la première personne du sujet : « la
48
+ * réponse de postgres 127.0.0.1:5432 ».
49
+ * @param ms - la borne, en millisecondes.
50
+ * @returns la valeur de `work` si elle arrive à temps.
51
+ * @throws Error quand la borne est atteinte — jamais de résolution silencieuse.
52
+ */
53
+ async function withConnectDeadline(work, what, ms = CONNECT_TIMEOUT_MS) {
54
+ let timer;
55
+ try {
56
+ return await Promise.race([work, new Promise((_, reject) => {
57
+ timer = setTimeout(() => {
58
+ reject(/* @__PURE__ */ new Error(`délai dépassé (${ms} ms) en attendant ${what} — le serveur a accepté la connexion sans répondre, ou n'a jamais répondu. Vérifier que le service visé est bien celui qui écoute, et qu'il est en état de servir.`));
59
+ }, ms);
60
+ timer.unref?.();
61
+ })]);
62
+ } finally {
63
+ if (timer !== void 0) clearTimeout(timer);
64
+ }
65
+ }
66
+ //#endregion
67
+ export { CONNECT_TIMEOUT_MS, withConnectDeadline };
@@ -0,0 +1,112 @@
1
+ //#region nodefony/src/connectionDiagnosis.ts
2
+ /**
3
+ * Personne n'écoute : la couche transport a refusé ou n'a trouvé personne.
4
+ * Ces codes viennent du système, pas du serveur de base de données.
5
+ */
6
+ const UNREACHABLE_CODES = /* @__PURE__ */ new Set([
7
+ "ECONNREFUSED",
8
+ "ENOTFOUND",
9
+ "EHOSTUNREACH",
10
+ "ENETUNREACH",
11
+ "EAI_AGAIN"
12
+ ]);
13
+ /**
14
+ * Un serveur a répondu ET refusé. Chacun de ces codes est produit par le
15
+ * serveur lui-même — donc il a parlé, donc quelque chose tient le port.
16
+ *
17
+ * PostgreSQL rend des SQLSTATE : classe `28` = autorisation invalide,
18
+ * `3D000` = base inconnue, `53300` = trop de connexions. MySQL et MariaDB
19
+ * rendent des noms `ER_*`. MongoDB nomme ses refus (`AuthenticationFailed`).
20
+ */
21
+ const ANSWERED_CODES = /* @__PURE__ */ new Set([
22
+ "28P01",
23
+ "28000",
24
+ "3D000",
25
+ "53300",
26
+ "ER_ACCESS_DENIED_ERROR",
27
+ "ER_DBACCESS_DENIED_ERROR",
28
+ "ER_BAD_DB_ERROR",
29
+ "ER_NOT_SUPPORTED_AUTH_MODE",
30
+ "ER_HOST_NOT_PRIVILEGED",
31
+ "AuthenticationFailed",
32
+ "Unauthorized"
33
+ ]);
34
+ /** Lit le code que le driver a posé sur l'erreur, sans rien supposer de sa forme. */
35
+ function readCode(error) {
36
+ if (typeof error !== "object" || error === null) return null;
37
+ const carrier = error;
38
+ for (const raw of [carrier.code, carrier.codeName]) {
39
+ if (typeof raw === "string" && raw.length > 0) return raw;
40
+ if (typeof raw === "number") return String(raw);
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Extrait l'hôte et le port d'une URL de connexion, sans son secret.
46
+ *
47
+ * @param url - URL de connexion (`postgres://…`, `mysql://…`, `mongodb://…`).
48
+ * @returns l'adresse visée ; champs à `null` si l'URL est absente ou illisible.
49
+ */
50
+ function parseConnectionTarget(url) {
51
+ if (!url) return {
52
+ host: null,
53
+ port: null
54
+ };
55
+ try {
56
+ const parsed = new URL(url);
57
+ const port = parsed.port ? Number(parsed.port) : null;
58
+ return {
59
+ host: parsed.hostname || null,
60
+ port: Number.isFinite(port) ? port : null
61
+ };
62
+ } catch {
63
+ return {
64
+ host: null,
65
+ port: null
66
+ };
67
+ }
68
+ }
69
+ /**
70
+ * La commande qui nomme QUI tient un port, sur la plateforme visée.
71
+ *
72
+ * La plateforme est un PARAMÈTRE et non une lecture de `process.platform` : la
73
+ * règle se vérifie alors pour les trois systèmes depuis n'importe lequel, sans
74
+ * machine Windows. `netstat` parce que `lsof` n'existe pas sous Windows, et
75
+ * qu'une commande introuvable est un second mystère à résoudre.
76
+ */
77
+ function whoHoldsPort(port, platform) {
78
+ return platform === "win32" ? `netstat -ano | findstr :${port}` : `lsof -nP -iTCP:${port} -sTCP:LISTEN`;
79
+ }
80
+ /**
81
+ * Explique un échec de connexion en distinguant « personne n'écoute » de
82
+ * « quelqu'un a répondu et refuse ».
83
+ *
84
+ * @param error - l'erreur rendue par le driver.
85
+ * @param target - hôte et port visés (cf {@link parseConnectionTarget}).
86
+ * @param platform - plateforme pour laquelle rédiger le geste ; défaut : la courante.
87
+ * @returns le verdict, le code constaté et une explication déjà rédigée.
88
+ */
89
+ function diagnoseConnectionFailure(error, target = {
90
+ host: null,
91
+ port: null
92
+ }, platform = process.platform) {
93
+ const code = readCode(error);
94
+ const where = target.host && target.port ? `${target.host}:${target.port}` : target.host ?? "l'adresse configurée";
95
+ if (code !== null && UNREACHABLE_CODES.has(code)) return {
96
+ verdict: "unreachable",
97
+ code,
98
+ explanation: `personne n'écoute sur ${where} (${code}) — la base n'est pas démarrée, ou l'adresse configurée n'est pas la sienne.`
99
+ };
100
+ if (code !== null && ANSWERED_CODES.has(code)) return {
101
+ verdict: "answered",
102
+ code,
103
+ explanation: `un serveur a RÉPONDU sur ${where} puis a refusé (${code}) — donc quelque chose tient bien ce port. Ce peut être la base attendue avec de mauvais identifiants, mais aussi UN AUTRE SERVEUR (le conteneur d'un autre projet, par exemple) : le refus vient alors d'une base qui n'est pas la vôtre, et vérifier les identifiants ne mène nulle part.${target.port !== null ? ` Pour savoir qui tient ce port : « docker ps --filter publish=${target.port} », ou « ${whoHoldsPort(target.port, platform)} ».` : ""}`
104
+ };
105
+ return {
106
+ verdict: "unknown",
107
+ code,
108
+ explanation: `échec de connexion à ${where}${code ? ` (${code})` : ""} — vérifier que l'infrastructure déclarée (NF_DATABASE_URL / connectors) désigne bien la base attendue, et qu'elle est démarrée.`
109
+ };
110
+ }
111
+ //#endregion
112
+ export { diagnoseConnectionFailure, parseConnectionTarget };
@@ -25,6 +25,9 @@ export { buildOrmGraph, buildConnectionHealth, buildOrmFlow, toDbml, toJsonSchem
25
25
  export { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
26
26
  export { connectionMonitor } from "./nodefony/src/ConnectionMonitor.js";
27
27
  export { buildOrmLeanHealth } from "./nodefony/src/buildOrmLeanHealth.js";
28
+ export { diagnoseConnectionFailure, parseConnectionTarget, } from "./nodefony/src/connectionDiagnosis.js";
29
+ export type { ConnectionVerdict, IConnectionDiagnosis, IConnectionTarget, } from "./nodefony/src/connectionDiagnosis.js";
30
+ export { CONNECT_TIMEOUT_MS, withConnectDeadline, } from "./nodefony/src/connectDeadline.js";
28
31
  export { wireOrmAdminPlane, resolveOrmFlowEnabled, reportOrmBootLines, } from "./nodefony/src/ormWiring.js";
29
32
  export type { ISlowQuery, IQueryFlow, IOrmFlowReport, } from "./nodefony/interfaces/IOrmFlow.js";
30
33
  export type { IColumnInfo, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionHealth, } from "./nodefony/interfaces/IOrmGraph.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Le DÉLAI DE GARDE d'une connexion à une base — une commande n'attend jamais
3
+ * sans fin.
4
+ *
5
+ * 🔴 Le cas est vécu, et il ne ressemble pas à une panne : `nodefony inspect
6
+ * config` restait suspendu, sans un mot, sans erreur, sans reprendre la main.
7
+ * Aucun driver ne borne l'établissement par défaut — `pg-pool` traite son
8
+ * `connectionTimeoutMillis` à `0` comme « attendre indéfiniment » — et un
9
+ * serveur qui accepte la connexion TCP sans jamais répondre (conteneur en
10
+ * train de mourir, port relayé vers rien, réseau qui avale les paquets) laisse
11
+ * l'appelant pendu.
12
+ *
13
+ * Une commande de diagnostic qui se bloque est pire qu'une commande qui
14
+ * échoue : l'échec nomme une cause, le blocage n'apprend rien et fait douter
15
+ * de l'outil plutôt que de l'infrastructure.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * Le temps qu'on accorde à une base pour ACCEPTER une connexion, en
21
+ * millisecondes.
22
+ *
23
+ * Dix secondes, et ce n'est pas un réglage de performance : c'est la frontière
24
+ * entre « lent » et « ne répondra pas ». Un serveur local répond en quelques
25
+ * millisecondes, un serveur infonuagique qui sort de veille en quelques
26
+ * secondes ; au-delà, ce qui se passe n'est plus une lenteur, et le dire vaut
27
+ * mieux que l'attendre.
28
+ *
29
+ * Volontairement NON configurable : le besoin n'est pas de choisir une durée,
30
+ * c'est de n'attendre jamais sans fin. Une clé de configuration serait une
31
+ * surface publique à porter pour toute une série majeure, au service d'un
32
+ * réglage que personne n'a demandé.
33
+ */
34
+ export declare const CONNECT_TIMEOUT_MS = 10000;
35
+ /**
36
+ * Borne l'attente d'une promesse, et rejette avec une cause NOMMÉE au-delà.
37
+ *
38
+ * Le message importe autant que la borne : « délai dépassé » sans sujet
39
+ * envoie chercher partout. Celui-ci dit ce qu'on attendait, de qui, et
40
+ * combien de temps — c'est ce qui distingue un diagnostic d'un abandon.
41
+ *
42
+ * Le minuteur est `unref`é : il ne doit pas, à lui seul, retenir le processus
43
+ * en vie une fois la réponse arrivée.
44
+ *
45
+ * @param work - la promesse à borner (établissement, ping…).
46
+ * @param what - ce qu'on attend, à la première personne du sujet : « la
47
+ * réponse de postgres 127.0.0.1:5432 ».
48
+ * @param ms - la borne, en millisecondes.
49
+ * @returns la valeur de `work` si elle arrive à temps.
50
+ * @throws Error quand la borne est atteinte — jamais de résolution silencieuse.
51
+ */
52
+ export declare function withConnectDeadline<T>(work: Promise<T>, what: string, ms?: number): Promise<T>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Diagnostic d'un échec de connexion à une base — **ce qui a été CONSTATÉ**,
3
+ * jamais seulement ce qu'on en déduit.
4
+ *
5
+ * Le message d'échec énumérait trois causes (infrastructure déclarée, base
6
+ * démarrée, entités portées) et aucune n'était la bonne dans un cas fréquent :
7
+ * **un AUTRE serveur occupe déjà le port**. Vécu sur une application fraîchement
8
+ * générée — son PostgreSQL n'était pas démarré, mais le conteneur d'un autre
9
+ * projet écoutait sur `127.0.0.1:5432`. L'application s'y est connectée et a reçu
10
+ * « password authentication failed ». Le message était EXACT, et c'est ce qui le
11
+ * rendait trompeur : il est cru, et il envoie vérifier des identifiants justes.
12
+ *
13
+ * La distinction qui tranche tient en une question : **quelqu'un a-t-il
14
+ * répondu ?** `ECONNREFUSED` dit que non — personne n'écoute, la base n'est pas
15
+ * démarrée. Un refus d'authentification dit que si : un serveur a parlé, donc le
16
+ * port est tenu, et la question devient « par QUI ? ».
17
+ *
18
+ * Vit dans `orm-core` parce que les deux adapters posent la même question : deux
19
+ * implémentations parallèles diraient deux choses différentes du même symptôme.
20
+ */
21
+ /** Ce que l'échec permet d'affirmer sur l'autre bout du socket. */
22
+ export type ConnectionVerdict =
23
+ /** Personne n'a répondu — rien n'écoute à cette adresse. */
24
+ "unreachable"
25
+ /** Un serveur a répondu, et il refuse — le port est bien tenu par quelqu'un. */
26
+ | "answered"
27
+ /** L'erreur ne permet de trancher ni dans un sens ni dans l'autre. */
28
+ | "unknown";
29
+ /** Diagnostic rendu à l'appelant, qui compose le message final. */
30
+ export interface IConnectionDiagnosis {
31
+ verdict: ConnectionVerdict;
32
+ /** Code CONSTATÉ, tel que le driver l'a rendu (`ECONNREFUSED`, `28P01`, …). */
33
+ code: string | null;
34
+ /** Ce qu'on peut affirmer, et le geste qui tranche. Déjà rédigé. */
35
+ explanation: string;
36
+ }
37
+ /** Adresse visée, telle qu'on peut la dire sans divulguer de secret. */
38
+ export interface IConnectionTarget {
39
+ host: string | null;
40
+ port: number | null;
41
+ }
42
+ /**
43
+ * Extrait l'hôte et le port d'une URL de connexion, sans son secret.
44
+ *
45
+ * @param url - URL de connexion (`postgres://…`, `mysql://…`, `mongodb://…`).
46
+ * @returns l'adresse visée ; champs à `null` si l'URL est absente ou illisible.
47
+ */
48
+ export declare function parseConnectionTarget(url?: string | null): IConnectionTarget;
49
+ /**
50
+ * Explique un échec de connexion en distinguant « personne n'écoute » de
51
+ * « quelqu'un a répondu et refuse ».
52
+ *
53
+ * @param error - l'erreur rendue par le driver.
54
+ * @param target - hôte et port visés (cf {@link parseConnectionTarget}).
55
+ * @param platform - plateforme pour laquelle rédiger le geste ; défaut : la courante.
56
+ * @returns le verdict, le code constaté et une explication déjà rédigée.
57
+ */
58
+ export declare function diagnoseConnectionFailure(error: unknown, target?: IConnectionTarget, platform?: string): IConnectionDiagnosis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/orm-core",
3
- "version": "10.0.0-alpha.2",
3
+ "version": "10.0.0-alpha.3",
4
4
  "description": "Un contrat de dépôt de données pour Nodefony, plusieurs moteurs : interfaces, registre et classes de base pour le support multi-ORM",
5
5
  "contributors": [],
6
6
  "type": "module",
@@ -36,11 +36,11 @@
36
36
  "esm"
37
37
  ],
38
38
  "peerDependencies": {
39
- "nodefony": "^10.0.0-alpha.2"
39
+ "nodefony": "^10.0.0-alpha.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "26.4.1",
43
- "nodefony": "^10.0.0-alpha.2",
43
+ "nodefony": "^10.0.0-alpha.3",
44
44
  "rimraf": "6.1.3",
45
45
  "vitest": "5.0.0"
46
46
  },