@nodefony/orm-core 10.0.0-alpha.8 → 10.0.0-beta.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/dist/index.js +4 -3
- package/dist/nodefony/src/AbstractCrudService.js +7 -3
- package/dist/nodefony/src/ConnectionMonitor.js +41 -9
- package/dist/nodefony/src/EntityRegistry.js +9 -3
- package/dist/nodefony/src/Orm.js +19 -3
- package/dist/nodefony/src/OrmAdminApi.js +54 -12
- package/dist/nodefony/src/OrmRegistry.js +1 -1
- package/dist/nodefony/src/QueryFlowMonitor.js +2 -2
- package/dist/nodefony/src/connectDeadline.js +1 -1
- package/dist/nodefony/src/connectionDiagnosis.js +66 -6
- package/dist/nodefony/src/criteria.js +4 -3
- package/dist/nodefony/src/errors.js +59 -1
- package/dist/nodefony/src/omitUndefined.js +30 -0
- package/dist/nodefony/src/orphanEntities.js +63 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/nodefony/interfaces/IEntity.d.ts +23 -4
- package/dist/types/nodefony/interfaces/IOrm.d.ts +10 -1
- package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +58 -13
- package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +9 -9
- package/dist/types/nodefony/interfaces/IPage.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRepository.d.ts +12 -12
- package/dist/types/nodefony/interfaces/ITransaction.d.ts +3 -1
- package/dist/types/nodefony/interfaces/index.d.ts +2 -2
- package/dist/types/nodefony/src/AbstractCrudService.d.ts +9 -5
- package/dist/types/nodefony/src/ConnectionMonitor.d.ts +9 -2
- package/dist/types/nodefony/src/Entity.d.ts +1 -1
- package/dist/types/nodefony/src/Orm.d.ts +9 -1
- package/dist/types/nodefony/src/OrmAdminApi.d.ts +17 -1
- package/dist/types/nodefony/src/connectionDiagnosis.d.ts +29 -0
- package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +1 -1
- package/dist/types/nodefony/src/decorators/metadataStore.d.ts +5 -5
- package/dist/types/nodefony/src/errors.d.ts +48 -0
- package/dist/types/nodefony/src/omitUndefined.d.ts +16 -0
- package/dist/types/nodefony/src/orphanEntities.d.ts +38 -0
- package/docs/index.md +64 -60
- package/docs/tutorial-entity.md +26 -23
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
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";
|
|
2
|
+
import { InvalidOrderOption, ReferencedEntityError, SavepointNotSupportedError, UnknownCriteriaField } from "./nodefony/src/errors.js";
|
|
3
3
|
import { assertOrderOption } from "./nodefony/src/readOptions.js";
|
|
4
4
|
import { OrmRegistry, ormRegistry } from "./nodefony/src/OrmRegistry.js";
|
|
5
5
|
import { EntityRegistry, entityRegistry } from "./nodefony/src/EntityRegistry.js";
|
|
6
|
+
import { reportOrphanEntities } from "./nodefony/src/orphanEntities.js";
|
|
6
7
|
import { connectionMonitor } from "./nodefony/src/ConnectionMonitor.js";
|
|
7
8
|
import { Orm } from "./nodefony/src/Orm.js";
|
|
8
9
|
import { Entity } from "./nodefony/src/Entity.js";
|
|
@@ -11,7 +12,7 @@ import { AbstractCrudService } from "./nodefony/src/AbstractCrudService.js";
|
|
|
11
12
|
import { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
|
|
12
13
|
import { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, toDbml, toJsonSchema } from "./nodefony/src/OrmAdminApi.js";
|
|
13
14
|
import { buildOrmLeanHealth } from "./nodefony/src/buildOrmLeanHealth.js";
|
|
14
|
-
import { diagnoseConnectionFailure, parseConnectionTarget } from "./nodefony/src/connectionDiagnosis.js";
|
|
15
|
+
import { REJECTED_MARKER, describeConnectFailure, diagnoseConnectionFailure, parseConnectionTarget } from "./nodefony/src/connectionDiagnosis.js";
|
|
15
16
|
import { CONNECT_TIMEOUT_MS, withConnectDeadline } from "./nodefony/src/connectDeadline.js";
|
|
16
17
|
import { reportOrmBootLines, resolveOrmFlowEnabled, wireOrmAdminPlane } from "./nodefony/src/ormWiring.js";
|
|
17
18
|
import { isMigrationFailure } from "./nodefony/interfaces/IOrmMigrations.js";
|
|
@@ -21,4 +22,4 @@ import { DEFAULT_CONNECTOR, entities } from "./nodefony/src/decorators/entitiesD
|
|
|
21
22
|
import { repository } from "./nodefony/src/decorators/repositoryDecorator.js";
|
|
22
23
|
import "./nodefony/src/decorators/index.js";
|
|
23
24
|
import { defineEntity } from "./nodefony/src/defineEntity.js";
|
|
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 };
|
|
25
|
+
export { AbstractCrudService, CONNECT_TIMEOUT_MS, DEFAULT_CONNECTOR, Entity, EntityRegistry, InvalidOrderOption, LIKE_ESCAPE_CHAR, OPERATOR_KEYS, Orm, OrmRegistry, REJECTED_MARKER, ReferencedEntityError, SavepointNotSupportedError, UPDATE_OPERATOR_KEYS, UnknownCriteriaField, assertOrderOption, buildConnectionHealth, buildOrmFlow, buildOrmGraph, buildOrmLeanHealth, connectionMonitor, createOrmAdminApi, defineEntity, describeConnectFailure, diagnoseConnectionFailure, entities, entity, entityRegistry, escapeLikeTerm, getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, isFieldOperators, isMigrationFailure, isUpdateOperators, likePatternToRegExp, ormRegistry, paginate, parseConnectionTarget, queryFlowMonitor, registerOrmAdminApi, reportOrmBootLines, reportOrphanEntities, repository, resolveOrmFlowEnabled, searchCriteria, toDbml, toJsonSchema, wireOrmAdminPlane, withConnectDeadline };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { omitUndefined } from "./omitUndefined.js";
|
|
1
2
|
import { paginate } from "./paginate.js";
|
|
2
3
|
import { Service } from "nodefony";
|
|
3
4
|
//#region nodefony/src/AbstractCrudService.ts
|
|
@@ -115,12 +116,15 @@ var AbstractCrudService = class extends Service {
|
|
|
115
116
|
/**
|
|
116
117
|
* Crée une entité : `beforeCreate` → persistance → `afterCreate` → `onCreated`.
|
|
117
118
|
*
|
|
119
|
+
* Une clé qui vaut `undefined` vaut clé absente : elle est retirée avant la
|
|
120
|
+
* persistance (cf `omitUndefined`), quel que soit l'adaptateur.
|
|
121
|
+
*
|
|
118
122
|
* @param data - champs de l'entité à créer.
|
|
119
123
|
* @returns l'entité persistée. Émet `onCreated`.
|
|
120
124
|
*/
|
|
121
125
|
async create(data) {
|
|
122
126
|
const prepared = await this.beforeCreate(data);
|
|
123
|
-
const entity = await this.repository.create(prepared);
|
|
127
|
+
const entity = await this.repository.create(omitUndefined(prepared));
|
|
124
128
|
await this.afterCreate(entity);
|
|
125
129
|
this.fire("onCreated", entity);
|
|
126
130
|
return entity;
|
|
@@ -139,12 +143,12 @@ var AbstractCrudService = class extends Service {
|
|
|
139
143
|
* on la remontera dans un service le jour où un usage métier la réclame.
|
|
140
144
|
*
|
|
141
145
|
* @param criteria - filtre de sélection (champ inconnu → `UnknownCriteriaField`).
|
|
142
|
-
* @param data - champs à modifier.
|
|
146
|
+
* @param data - champs à modifier ; une clé `undefined` n'est pas touchée.
|
|
143
147
|
* @returns l'entité mise à jour, ou `null` si aucune ne correspond (pas d'event).
|
|
144
148
|
*/
|
|
145
149
|
async updateOne(criteria, data) {
|
|
146
150
|
const prepared = await this.beforeUpdate(criteria, data);
|
|
147
|
-
const updated = await this.repository.updateOne(criteria, prepared);
|
|
151
|
+
const updated = await this.repository.updateOne(criteria, omitUndefined(prepared));
|
|
148
152
|
if (updated !== null) {
|
|
149
153
|
await this.afterUpdate(updated);
|
|
150
154
|
this.fire("onUpdated", updated);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region nodefony/src/ConnectionMonitor.ts
|
|
2
2
|
/** Taille max du ring d'erreurs récentes (borne mémoire). */
|
|
3
3
|
const MAX_RECENT_ERRORS = 12;
|
|
4
|
+
/** Taille max de la chronologie pertes/reprises (borne mémoire). */
|
|
5
|
+
const MAX_EVENTS = 20;
|
|
4
6
|
/** Taille de la fenêtre glissante de latence (min/moy/max). */
|
|
5
7
|
const MAX_LATENCY_SAMPLES = 30;
|
|
6
8
|
/** Fenêtre de latence vide. */
|
|
@@ -18,6 +20,9 @@ const EMPTY_CORE = {
|
|
|
18
20
|
connectCount: 0,
|
|
19
21
|
reconnectCount: 0,
|
|
20
22
|
lostCount: 0,
|
|
23
|
+
lastLostAt: null,
|
|
24
|
+
lastRestoredAt: null,
|
|
25
|
+
events: [],
|
|
21
26
|
errorCount: 0,
|
|
22
27
|
lastConnectMs: null,
|
|
23
28
|
lastError: null,
|
|
@@ -46,7 +51,7 @@ var ConnectionMonitor = class {
|
|
|
46
51
|
#stats = null;
|
|
47
52
|
/** Crée/retourne les stats mutables d'un connecteur (alloue à la demande). */
|
|
48
53
|
#ensure(name) {
|
|
49
|
-
|
|
54
|
+
this.#stats ??= /* @__PURE__ */ new Map();
|
|
50
55
|
let s = this.#stats.get(name);
|
|
51
56
|
if (s === void 0) {
|
|
52
57
|
s = {
|
|
@@ -54,6 +59,9 @@ var ConnectionMonitor = class {
|
|
|
54
59
|
connectCount: 0,
|
|
55
60
|
reconnectCount: 0,
|
|
56
61
|
lostCount: 0,
|
|
62
|
+
lastLostAt: null,
|
|
63
|
+
lastRestoredAt: null,
|
|
64
|
+
events: null,
|
|
57
65
|
errorCount: 0,
|
|
58
66
|
lastConnectMs: null,
|
|
59
67
|
lastError: null,
|
|
@@ -72,7 +80,7 @@ var ConnectionMonitor = class {
|
|
|
72
80
|
*/
|
|
73
81
|
recordPing(name, ms) {
|
|
74
82
|
const s = this.#ensure(name);
|
|
75
|
-
|
|
83
|
+
s.latencies ??= [];
|
|
76
84
|
s.latencies.push(ms);
|
|
77
85
|
if (s.latencies.length > MAX_LATENCY_SAMPLES) s.latencies.shift();
|
|
78
86
|
}
|
|
@@ -99,11 +107,19 @@ var ConnectionMonitor = class {
|
|
|
99
107
|
* preuve de bonne santé.
|
|
100
108
|
*
|
|
101
109
|
* @param name - clé du connecteur.
|
|
110
|
+
* @param reason - cause lisible (credential déjà retiré), si connue.
|
|
102
111
|
*/
|
|
103
|
-
recordLost(name) {
|
|
112
|
+
recordLost(name, reason) {
|
|
104
113
|
const s = this.#ensure(name);
|
|
114
|
+
const ts = Date.now();
|
|
105
115
|
s.lostCount += 1;
|
|
106
116
|
s.connectedSince = null;
|
|
117
|
+
s.lastLostAt = ts;
|
|
118
|
+
this.#pushEvent(s, {
|
|
119
|
+
kind: "lost",
|
|
120
|
+
ts,
|
|
121
|
+
reason
|
|
122
|
+
});
|
|
107
123
|
}
|
|
108
124
|
/**
|
|
109
125
|
* Enregistre une **reprise** de connexion constatée par l'adapter.
|
|
@@ -112,8 +128,20 @@ var ConnectionMonitor = class {
|
|
|
112
128
|
*/
|
|
113
129
|
recordReconnect(name) {
|
|
114
130
|
const s = this.#ensure(name);
|
|
131
|
+
const ts = Date.now();
|
|
115
132
|
s.reconnectCount += 1;
|
|
116
|
-
s.connectedSince =
|
|
133
|
+
s.connectedSince = ts;
|
|
134
|
+
s.lastRestoredAt = ts;
|
|
135
|
+
this.#pushEvent(s, {
|
|
136
|
+
kind: "restored",
|
|
137
|
+
ts
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Ajoute un événement en tête de la chronologie (allouée au 1ᵉʳ, bornée). */
|
|
141
|
+
#pushEvent(s, e) {
|
|
142
|
+
s.events ??= [];
|
|
143
|
+
s.events.unshift(e);
|
|
144
|
+
if (s.events.length > MAX_EVENTS) s.events.length = MAX_EVENTS;
|
|
117
145
|
}
|
|
118
146
|
/**
|
|
119
147
|
* Enregistre une erreur (connexion échouée ou ping en échec).
|
|
@@ -129,7 +157,7 @@ var ConnectionMonitor = class {
|
|
|
129
157
|
ts: Date.now()
|
|
130
158
|
};
|
|
131
159
|
s.lastError = err;
|
|
132
|
-
|
|
160
|
+
s.recentErrors ??= [];
|
|
133
161
|
s.recentErrors.unshift(err);
|
|
134
162
|
if (s.recentErrors.length > MAX_RECENT_ERRORS) s.recentErrors.length = MAX_RECENT_ERRORS;
|
|
135
163
|
}
|
|
@@ -144,9 +172,10 @@ var ConnectionMonitor = class {
|
|
|
144
172
|
if (s === void 0) return EMPTY_CORE;
|
|
145
173
|
const lat = s.latencies;
|
|
146
174
|
let latency = EMPTY_LATENCY;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let
|
|
175
|
+
const last = lat?.at(-1);
|
|
176
|
+
if (lat && last !== void 0) {
|
|
177
|
+
let min = last;
|
|
178
|
+
let max = last;
|
|
150
179
|
let sum = 0;
|
|
151
180
|
for (const v of lat) {
|
|
152
181
|
if (v < min) min = v;
|
|
@@ -154,7 +183,7 @@ var ConnectionMonitor = class {
|
|
|
154
183
|
sum += v;
|
|
155
184
|
}
|
|
156
185
|
latency = {
|
|
157
|
-
last
|
|
186
|
+
last,
|
|
158
187
|
min: Math.round(min * 100) / 100,
|
|
159
188
|
avg: Math.round(sum / lat.length * 100) / 100,
|
|
160
189
|
max: Math.round(max * 100) / 100,
|
|
@@ -167,6 +196,9 @@ var ConnectionMonitor = class {
|
|
|
167
196
|
connectCount: s.connectCount,
|
|
168
197
|
reconnectCount: s.reconnectCount,
|
|
169
198
|
lostCount: s.lostCount,
|
|
199
|
+
lastLostAt: s.lastLostAt,
|
|
200
|
+
lastRestoredAt: s.lastRestoredAt,
|
|
201
|
+
events: s.events ?? [],
|
|
170
202
|
errorCount: s.errorCount,
|
|
171
203
|
lastConnectMs: s.lastConnectMs,
|
|
172
204
|
lastError: s.lastError,
|
|
@@ -20,7 +20,7 @@ var EntityRegistry = class {
|
|
|
20
20
|
* @throws si cette entité est déjà enregistrée sur le même connecteur.
|
|
21
21
|
*/
|
|
22
22
|
register(entity) {
|
|
23
|
-
|
|
23
|
+
this.#entities ??= Object.create(null);
|
|
24
24
|
const store = this.#entities;
|
|
25
25
|
let bucket = store[entity.name];
|
|
26
26
|
if (bucket === void 0) {
|
|
@@ -49,7 +49,10 @@ var EntityRegistry = class {
|
|
|
49
49
|
}
|
|
50
50
|
const connectors = Object.keys(bucket);
|
|
51
51
|
if (connectors.length > 1) throw new Error(`EntityRegistry: entity "${name}" exists on multiple connectors (${connectors.join(", ")}); specify one.`);
|
|
52
|
-
|
|
52
|
+
const only = connectors[0];
|
|
53
|
+
const entity = only === void 0 ? void 0 : bucket[only];
|
|
54
|
+
if (entity === void 0) throw new Error(`EntityRegistry: no entity registered under "${name}".`);
|
|
55
|
+
return entity;
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
58
|
* Indique si une entité (optionnellement sur un connecteur précis) est enregistrée.
|
|
@@ -72,7 +75,10 @@ var EntityRegistry = class {
|
|
|
72
75
|
const out = [];
|
|
73
76
|
for (const name in this.#entities) {
|
|
74
77
|
const bucket = this.#entities[name];
|
|
75
|
-
for (const connector in bucket)
|
|
78
|
+
for (const connector in bucket) {
|
|
79
|
+
const entity = bucket[connector];
|
|
80
|
+
if (entity !== void 0) out.push(entity);
|
|
81
|
+
}
|
|
76
82
|
}
|
|
77
83
|
return out;
|
|
78
84
|
}
|
package/dist/nodefony/src/Orm.js
CHANGED
|
@@ -126,7 +126,7 @@ var Orm = class extends Service {
|
|
|
126
126
|
if (!this.alive) return;
|
|
127
127
|
this.alive = false;
|
|
128
128
|
this.#lostPending = true;
|
|
129
|
-
connectionMonitor.recordLost(this.name);
|
|
129
|
+
connectionMonitor.recordLost(this.name, reason);
|
|
130
130
|
this.log(`connexion perdue : ${reason}`, "WARNING");
|
|
131
131
|
this.fire("onOrmLost", this, reason);
|
|
132
132
|
}
|
|
@@ -194,9 +194,25 @@ var Orm = class extends Service {
|
|
|
194
194
|
const timer = setInterval(() => {
|
|
195
195
|
this.#beat();
|
|
196
196
|
}, this.heartbeatMs);
|
|
197
|
-
timer.unref
|
|
197
|
+
timer.unref();
|
|
198
198
|
this.#heartbeat = timer;
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Décrit le mécanisme de résilience TEL QU'IL TOURNE — lecture pure, pour
|
|
202
|
+
* le data plane (`connection/health`) : période et montre du battement,
|
|
203
|
+
* battement actif ou non, perte en souffrance.
|
|
204
|
+
*
|
|
205
|
+
* @returns l'état du mécanisme dans ce process.
|
|
206
|
+
*/
|
|
207
|
+
describeResilience() {
|
|
208
|
+
return {
|
|
209
|
+
heartbeatMs: this.heartbeatMs,
|
|
210
|
+
heartbeatTimeoutMs: this.heartbeatTimeoutMs,
|
|
211
|
+
heartbeatActive: this.#heartbeat !== null,
|
|
212
|
+
pingable: typeof this.ping === "function",
|
|
213
|
+
lostPending: this.#lostPending
|
|
214
|
+
};
|
|
215
|
+
}
|
|
200
216
|
/** Arrête le battement et libère la minuterie. Idempotent. */
|
|
201
217
|
stopHeartbeat() {
|
|
202
218
|
if (this.#heartbeat !== null) {
|
|
@@ -248,7 +264,7 @@ var Orm = class extends Service {
|
|
|
248
264
|
watchdog = setTimeout(() => {
|
|
249
265
|
rejeter(/* @__PURE__ */ new Error(`aucune réponse en ${this.heartbeatTimeoutMs} ms`));
|
|
250
266
|
}, this.heartbeatTimeoutMs);
|
|
251
|
-
watchdog.unref
|
|
267
|
+
watchdog.unref();
|
|
252
268
|
})]);
|
|
253
269
|
this.connectionRestored();
|
|
254
270
|
} catch (e) {
|
|
@@ -2,6 +2,7 @@ import { ormRegistry } from "./OrmRegistry.js";
|
|
|
2
2
|
import { entityRegistry } from "./EntityRegistry.js";
|
|
3
3
|
import { connectionMonitor } from "./ConnectionMonitor.js";
|
|
4
4
|
import { queryFlowMonitor } from "./QueryFlowMonitor.js";
|
|
5
|
+
import { Nodefony } from "nodefony";
|
|
5
6
|
import { performance } from "node:perf_hooks";
|
|
6
7
|
//#region nodefony/src/OrmAdminApi.ts
|
|
7
8
|
/**
|
|
@@ -76,9 +77,38 @@ async function migrationCapability(request, capability, missingMessage) {
|
|
|
76
77
|
return fn.call(orm);
|
|
77
78
|
}
|
|
78
79
|
/** Résumé des ORM enregistrés (statut connexion + nombre d'entités). */
|
|
80
|
+
/**
|
|
81
|
+
* Connecteur qui porte les stores de l'application — le connecteur PAR DÉFAUT.
|
|
82
|
+
*
|
|
83
|
+
* « Défaut » est un RÔLE, pas un nom : sur une infra MongoDB les briques
|
|
84
|
+
* durables vivent sur `nodefony` (Mongoose), et marquer `default` (Drizzle)
|
|
85
|
+
* parce qu'il s'appelle ainsi désignait une base que personne n'utilise. Le
|
|
86
|
+
* rôle se CONSTATE dans le registre des stores (#474) : le connecteur qui porte
|
|
87
|
+
* le plus de briques durables, puis de briques tout court.
|
|
88
|
+
*
|
|
89
|
+
* @param resolutions - registre des stores du kernel (`storeResolutions`).
|
|
90
|
+
* @param names - connecteurs enregistrés ; une brique sur un autre nom est ignorée.
|
|
91
|
+
* @returns le nom du connecteur, ou `null` si aucune brique n'est portée par
|
|
92
|
+
* un ORM (le repli sur le nom `default` s'applique alors).
|
|
93
|
+
*/
|
|
94
|
+
function storeCarrier(resolutions, names) {
|
|
95
|
+
const score = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const r of resolutions) {
|
|
97
|
+
if (!r.connector || !names.includes(r.connector)) continue;
|
|
98
|
+
score.set(r.connector, (score.get(r.connector) ?? 0) + (r.nature === "durable" ? 1e3 : 1));
|
|
99
|
+
}
|
|
100
|
+
let best = null;
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const s = score.get(name) ?? 0;
|
|
103
|
+
if (s > 0 && (best === null || s > (score.get(best) ?? 0))) best = name;
|
|
104
|
+
}
|
|
105
|
+
return best;
|
|
106
|
+
}
|
|
79
107
|
function buildOrmSummaries() {
|
|
80
108
|
const entities = entityRegistry.list();
|
|
81
|
-
|
|
109
|
+
const names = ormRegistry.list();
|
|
110
|
+
const carrier = storeCarrier(Nodefony.getKernel()?.storeResolutions ?? [], names);
|
|
111
|
+
return names.map((name) => {
|
|
82
112
|
let connected = false;
|
|
83
113
|
let vendor = "";
|
|
84
114
|
let connection;
|
|
@@ -93,7 +123,7 @@ function buildOrmSummaries() {
|
|
|
93
123
|
return {
|
|
94
124
|
name,
|
|
95
125
|
vendor,
|
|
96
|
-
default: name === "default",
|
|
126
|
+
default: carrier === null ? name === "default" : name === carrier,
|
|
97
127
|
connected,
|
|
98
128
|
entityCount: entities.filter((e) => e.connector === name).length,
|
|
99
129
|
connection
|
|
@@ -162,8 +192,10 @@ async function buildConnectionHealth(filter) {
|
|
|
162
192
|
let storage;
|
|
163
193
|
let pool;
|
|
164
194
|
let extra;
|
|
195
|
+
let resilience;
|
|
165
196
|
try {
|
|
166
197
|
const inst = ormRegistry.get(name);
|
|
198
|
+
resilience = inst.describeResilience?.();
|
|
167
199
|
connected = inst.isConnected();
|
|
168
200
|
vendor = vendorOf(inst);
|
|
169
201
|
const c = inst.describeConnection?.();
|
|
@@ -211,6 +243,11 @@ async function buildConnectionHealth(filter) {
|
|
|
211
243
|
uptimeMs: core.uptimeMs,
|
|
212
244
|
connectCount: core.connectCount,
|
|
213
245
|
reconnectCount: core.reconnectCount,
|
|
246
|
+
lostCount: core.lostCount,
|
|
247
|
+
lastLostAt: core.lastLostAt,
|
|
248
|
+
lastRestoredAt: core.lastRestoredAt,
|
|
249
|
+
events: core.events,
|
|
250
|
+
resilience,
|
|
214
251
|
errorCount: core.errorCount,
|
|
215
252
|
lastError: core.lastError,
|
|
216
253
|
recentErrors: core.recentErrors,
|
|
@@ -312,8 +349,8 @@ function jsonSchemaType(nativeType) {
|
|
|
312
349
|
const t = nativeType.toLowerCase();
|
|
313
350
|
if (/\b(serial|bigint|smallint|tinyint|mediumint|int|integer)\b/.test(t)) return { type: "integer" };
|
|
314
351
|
if (/(real|float|double|decimal|numeric|number)/.test(t)) return { type: "number" };
|
|
315
|
-
if (
|
|
316
|
-
if (
|
|
352
|
+
if (t.includes("bool")) return { type: "boolean" };
|
|
353
|
+
if (t.includes("json")) return { type: "object" };
|
|
317
354
|
if (/(date|time|timestamp)/.test(t)) return {
|
|
318
355
|
type: "string",
|
|
319
356
|
format: "date-time"
|
|
@@ -385,7 +422,7 @@ function createOrmAdminApi() {
|
|
|
385
422
|
path: "entity/{name}",
|
|
386
423
|
summary: "Une entité (colonnes + relations) — ?connector= si homonymes",
|
|
387
424
|
handler: (request) => {
|
|
388
|
-
const name = request.params.name
|
|
425
|
+
const name = request.params.name;
|
|
389
426
|
const connector = oneParam(request, "connector");
|
|
390
427
|
try {
|
|
391
428
|
const found = entityRegistry.list().find((e) => e.name === name && (!connector || e.connector === connector));
|
|
@@ -409,16 +446,21 @@ function createOrmAdminApi() {
|
|
|
409
446
|
},
|
|
410
447
|
{
|
|
411
448
|
path: "counts",
|
|
412
|
-
summary: "Nombre de lignes par entité (COUNT(*)) — ?connector= pour filtrer. Lazy : 1 COUNT par table.",
|
|
449
|
+
summary: "Nombre de lignes par entité (COUNT(*)) — ?connector= pour filtrer. Lazy : 1 COUNT par table. Clé = nom de l'entité, qualifiée `connecteur:nom` quand plusieurs connecteurs portent ce nom.",
|
|
413
450
|
handler: async (request) => {
|
|
414
451
|
const connector = oneParam(request, "connector");
|
|
415
452
|
const counts = {};
|
|
416
453
|
const entities = entityRegistry.list().filter((e) => !connector || e.connector === connector);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
454
|
+
const homonyms = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const e of entities) homonyms.set(e.name, (homonyms.get(e.name) ?? 0) + 1);
|
|
456
|
+
for (const e of entities) {
|
|
457
|
+
const key = (homonyms.get(e.name) ?? 0) > 1 ? `${e.connector}:${e.name}` : e.name;
|
|
458
|
+
try {
|
|
459
|
+
const inst = ormRegistry.get(e.connector);
|
|
460
|
+
counts[key] = inst.isConnected() ? await inst.getRepository(e.name).count() : -1;
|
|
461
|
+
} catch {
|
|
462
|
+
counts[key] = -1;
|
|
463
|
+
}
|
|
422
464
|
}
|
|
423
465
|
return counts;
|
|
424
466
|
}
|
|
@@ -488,4 +530,4 @@ function registerOrmAdminApi(registry) {
|
|
|
488
530
|
registry.register(createOrmAdminApi());
|
|
489
531
|
}
|
|
490
532
|
//#endregion
|
|
491
|
-
export { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, toDbml, toJsonSchema };
|
|
533
|
+
export { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, storeCarrier, toDbml, toJsonSchema };
|
|
@@ -22,7 +22,7 @@ var OrmRegistry = class {
|
|
|
22
22
|
* @throws si un ORM du même nom est déjà enregistré (erreur de configuration).
|
|
23
23
|
*/
|
|
24
24
|
register(name, orm) {
|
|
25
|
-
|
|
25
|
+
this.#orms ??= /* @__PURE__ */ new Map();
|
|
26
26
|
if (this.#orms.has(name)) throw new Error(`OrmRegistry: an ORM named "${name}" is already registered.`);
|
|
27
27
|
this.#orms.set(name, orm);
|
|
28
28
|
}
|
|
@@ -51,7 +51,7 @@ var QueryFlowMonitor = class {
|
|
|
51
51
|
}
|
|
52
52
|
/** Crée/retourne les stats mutables d'un connecteur (alloue à la demande). */
|
|
53
53
|
#ensure(connector) {
|
|
54
|
-
|
|
54
|
+
this.#stats ??= /* @__PURE__ */ new Map();
|
|
55
55
|
let s = this.#stats.get(connector);
|
|
56
56
|
if (s === void 0) {
|
|
57
57
|
s = {
|
|
@@ -85,7 +85,7 @@ var QueryFlowMonitor = class {
|
|
|
85
85
|
s.ewmaMs = s.ewmaMs === null ? durationMs : EWMA_ALPHA * durationMs + .8 * s.ewmaMs;
|
|
86
86
|
if (durationMs >= this.slowMs) {
|
|
87
87
|
s.slowTotal += 1;
|
|
88
|
-
|
|
88
|
+
s.slow ??= [];
|
|
89
89
|
s.slow.unshift({
|
|
90
90
|
ts: Date.now(),
|
|
91
91
|
durationMs,
|
|
@@ -57,7 +57,7 @@ async function withConnectDeadline(work, what, ms = CONNECT_TIMEOUT_MS) {
|
|
|
57
57
|
timer = setTimeout(() => {
|
|
58
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
59
|
}, ms);
|
|
60
|
-
timer.unref
|
|
60
|
+
timer.unref();
|
|
61
61
|
})]);
|
|
62
62
|
} finally {
|
|
63
63
|
if (timer !== void 0) clearTimeout(timer);
|
|
@@ -31,15 +31,41 @@ const ANSWERED_CODES = /* @__PURE__ */ new Set([
|
|
|
31
31
|
"AuthenticationFailed",
|
|
32
32
|
"Unauthorized"
|
|
33
33
|
]);
|
|
34
|
+
/**
|
|
35
|
+
* Classes SQLSTATE (PostgreSQL) qui décrivent la CONNEXION elle-même, pas une
|
|
36
|
+
* instruction : `08` exception de connexion, `57` intervention de l'opérateur
|
|
37
|
+
* (arrêt, base qui démarre). Tout autre SQLSTATE est rendu par un serveur à qui
|
|
38
|
+
* l'on parlait déjà.
|
|
39
|
+
*/
|
|
40
|
+
const CONNECTION_SQLSTATE_CLASSES = /* @__PURE__ */ new Set(["08", "57"]);
|
|
41
|
+
/**
|
|
42
|
+
* Refus MySQL/MariaDB émis PENDANT l'établissement : le serveur n'a encore
|
|
43
|
+
* exécuté aucune instruction. Tout autre `ER_*` vient d'un serveur connecté.
|
|
44
|
+
*/
|
|
45
|
+
const MYSQL_CONNECTION_PHASE = /* @__PURE__ */ new Set([
|
|
46
|
+
"ER_CON_COUNT_ERROR",
|
|
47
|
+
"ER_TOO_MANY_USER_CONNECTIONS",
|
|
48
|
+
"ER_HOST_IS_BLOCKED",
|
|
49
|
+
"ER_SERVER_SHUTDOWN"
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Le code prouve-t-il que la connexion avait ABOUTI quand le refus est tombé ?
|
|
53
|
+
*
|
|
54
|
+
* Ce n'est vrai que d'un code produit par le SERVEUR en réponse à une
|
|
55
|
+
* instruction : un SQLSTATE PostgreSQL hors des classes de connexion, ou un
|
|
56
|
+
* `ER_*` MySQL hors de la phase d'établissement. Les codes de refus connus
|
|
57
|
+
* ({@link ANSWERED_CODES}) sont tranchés AVANT.
|
|
58
|
+
*/
|
|
59
|
+
function provesEstablished(code) {
|
|
60
|
+
if (/^[0-9A-Z]{5}$/.test(code) && /[0-9]/.test(code)) return !CONNECTION_SQLSTATE_CLASSES.has(code.slice(0, 2));
|
|
61
|
+
return code.startsWith("ER_") && !MYSQL_CONNECTION_PHASE.has(code);
|
|
62
|
+
}
|
|
34
63
|
/** Lit le code que le driver a posé sur l'erreur, sans rien supposer de sa forme. */
|
|
35
64
|
function readCode(error) {
|
|
36
65
|
if (typeof error !== "object" || error === null) return null;
|
|
37
66
|
const carrier = error;
|
|
38
|
-
for (const raw of [carrier.code, carrier.codeName])
|
|
39
|
-
|
|
40
|
-
if (typeof raw === "number") return String(raw);
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
67
|
+
for (const raw of [carrier.code, carrier.codeName]) if (typeof raw === "string" && raw.length > 0) return raw;
|
|
68
|
+
return typeof carrier.code === "number" ? String(carrier.code) : null;
|
|
43
69
|
}
|
|
44
70
|
/**
|
|
45
71
|
* Extrait l'hôte et le port d'une URL de connexion, sans son secret.
|
|
@@ -102,11 +128,45 @@ function diagnoseConnectionFailure(error, target = {
|
|
|
102
128
|
code,
|
|
103
129
|
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
130
|
};
|
|
131
|
+
if (code !== null && provesEstablished(code)) return {
|
|
132
|
+
verdict: "rejected",
|
|
133
|
+
code,
|
|
134
|
+
explanation: `la connexion à ${where} a ABOUTI, puis le serveur a refusé une instruction (${code}) — ce n'est ni l'adresse ni l'état de la base : c'est le schéma envoyé (une table, une contrainte, un type) qu'il faut corriger.`
|
|
135
|
+
};
|
|
105
136
|
return {
|
|
106
137
|
verdict: "unknown",
|
|
107
138
|
code,
|
|
108
139
|
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
140
|
};
|
|
110
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Marqueur de la phrase « connecté, puis refusé » — lu par `create app`
|
|
144
|
+
* (`migrationFailureCause`, cœur) pour NE PAS conclure à une base injoignable.
|
|
145
|
+
* Le cœur ne peut pas importer ce paquet : sa copie est éprouvée contre
|
|
146
|
+
* celle-ci par un test de parité.
|
|
147
|
+
*/
|
|
148
|
+
const REJECTED_MARKER = "s'est connecté, mais le serveur a refusé une instruction — ";
|
|
149
|
+
/**
|
|
150
|
+
* Compose le message d'échec au démarrage d'un connecteur — UNE rédaction pour
|
|
151
|
+
* tous les adapters.
|
|
152
|
+
*
|
|
153
|
+
* 🔴 Un refus d'instruction (verdict `rejected`) ne commence JAMAIS par « n'a
|
|
154
|
+
* pas pu se connecter » : c'était faux, et la phrase envoyait vérifier
|
|
155
|
+
* l'adresse et l'état d'une base qui répondait très bien. La cause du pilote
|
|
156
|
+
* passe alors EN TÊTE — c'est elle qui nomme la table ou la contrainte.
|
|
157
|
+
*
|
|
158
|
+
* @param subject - sujet de la phrase (`Drizzle : le connecteur "x" (…)`).
|
|
159
|
+
* @param diagnosis - verdict de {@link diagnoseConnectionFailure}.
|
|
160
|
+
* @param cause - message brut du pilote.
|
|
161
|
+
* @param advice - conseil propre à l'adapter, pour un échec de CONNEXION.
|
|
162
|
+
* @returns le message complet.
|
|
163
|
+
*/
|
|
164
|
+
function describeConnectFailure(subject, diagnosis, cause, advice = "") {
|
|
165
|
+
if (diagnosis.verdict === "rejected") {
|
|
166
|
+
const explanation = diagnosis.explanation.charAt(0).toUpperCase() + diagnosis.explanation.slice(1);
|
|
167
|
+
return `${subject} ${REJECTED_MARKER}${cause}${diagnosis.code ? ` (${diagnosis.code})` : ""}. ${explanation}`;
|
|
168
|
+
}
|
|
169
|
+
return `${subject} n'a pas pu se connecter — ${diagnosis.explanation}${advice} Cause : ${cause}`;
|
|
170
|
+
}
|
|
111
171
|
//#endregion
|
|
112
|
-
export { diagnoseConnectionFailure, parseConnectionTarget };
|
|
172
|
+
export { REJECTED_MARKER, describeConnectFailure, diagnoseConnectionFailure, parseConnectionTarget };
|
|
@@ -124,8 +124,8 @@ function escapeLikeTerm(text) {
|
|
|
124
124
|
function likePatternToRegExp(pattern) {
|
|
125
125
|
let source = "";
|
|
126
126
|
for (let i = 0; i < pattern.length; i++) {
|
|
127
|
-
const c = pattern
|
|
128
|
-
if (c === "\\" && i + 1 < pattern.length) source += escapeRegExpChar(pattern
|
|
127
|
+
const c = pattern.charAt(i);
|
|
128
|
+
if (c === "\\" && i + 1 < pattern.length) source += escapeRegExpChar(pattern.charAt(++i));
|
|
129
129
|
else if (c === "%") source += ".*";
|
|
130
130
|
else if (c === "_") source += ".";
|
|
131
131
|
else source += escapeRegExpChar(c);
|
|
@@ -169,7 +169,8 @@ function searchCriteria(q, fields) {
|
|
|
169
169
|
const term = q?.trim();
|
|
170
170
|
if (!term || fields.length === 0) return null;
|
|
171
171
|
const likePattern = `${escapeLikeTerm(term)}%`;
|
|
172
|
-
|
|
172
|
+
const only = fields.length === 1 ? fields[0] : void 0;
|
|
173
|
+
if (only !== void 0) return { [only]: { $like: likePattern } };
|
|
173
174
|
return { $or: fields.map((f) => ({ [f]: { $like: likePattern } })) };
|
|
174
175
|
}
|
|
175
176
|
//#endregion
|
|
@@ -72,5 +72,63 @@ var InvalidOrderOption = class extends Error {
|
|
|
72
72
|
this.received = received;
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Levée quand une suppression viserait une entité encore RÉFÉRENCÉE par une
|
|
77
|
+
* relation obligatoire d'une autre entité.
|
|
78
|
+
*
|
|
79
|
+
* C'est le pendant, pour un moteur sans clé étrangère (MongoDB), du `restrict`
|
|
80
|
+
* que le SQL généré pose sur une colonne de référence obligatoire : le parent
|
|
81
|
+
* ne part pas tant qu'un enfant le désigne. Sans cette garde, la suppression
|
|
82
|
+
* passait et `?include=` rendait ensuite `null` à la place du parent — une
|
|
83
|
+
* application passée du SQL à MongoDB perdait en silence une garantie qu'elle
|
|
84
|
+
* croyait avoir.
|
|
85
|
+
*
|
|
86
|
+
* Erreur de DONNÉES : c'est la surface API qui la projette (le rendu d'erreur
|
|
87
|
+
* HTTP la reconnaît par son `name` et répond 409).
|
|
88
|
+
*/
|
|
89
|
+
var ReferencedEntityError = class extends Error {
|
|
90
|
+
/** Entité dont la suppression est refusée (nom logique). */
|
|
91
|
+
entity;
|
|
92
|
+
/** Entité qui la référence encore (nom logique). */
|
|
93
|
+
referencedBy;
|
|
94
|
+
/** Champ de référence de {@link ReferencedEntityError.referencedBy}. */
|
|
95
|
+
field;
|
|
96
|
+
/**
|
|
97
|
+
* @param entity - entité dont la suppression est refusée.
|
|
98
|
+
* @param referencedBy - entité qui la référence encore.
|
|
99
|
+
* @param field - champ de référence obligatoire qui la désigne.
|
|
100
|
+
*/
|
|
101
|
+
constructor(entity, referencedBy, field) {
|
|
102
|
+
super(`Cannot delete "${entity}": still referenced by "${referencedBy}.${field}" (required reference). Delete or reassign the "${referencedBy}" entries first.`);
|
|
103
|
+
this.name = "ReferencedEntityError";
|
|
104
|
+
this.entity = entity;
|
|
105
|
+
this.referencedBy = referencedBy;
|
|
106
|
+
this.field = field;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Levée par `ITransaction.savepoint()` / `rollbackTo()` sur un moteur qui ne porte
|
|
111
|
+
* PAS de savepoints (MongoDB).
|
|
112
|
+
*
|
|
113
|
+
* Refuser vaut mieux que se taire : un `rollbackTo()` rendu en no-op laissait
|
|
114
|
+
* l'appelant croire ses écritures annulées alors qu'elles restaient en base. Le
|
|
115
|
+
* refus tombe dès `savepoint()`, pas au `rollbackTo()` : le chemin d'erreur, où
|
|
116
|
+
* l'on annule, est celui qu'on teste le moins — la faute doit se voir au premier
|
|
117
|
+
* essai. Pour annuler, terminer la transaction entière (`rollback()`, ou laisser
|
|
118
|
+
* le callback de `IOrm.transaction()` lever).
|
|
119
|
+
*/
|
|
120
|
+
var SavepointNotSupportedError = class extends Error {
|
|
121
|
+
/** Moteur qui refuse (valeur de `describeConnection().driver`). */
|
|
122
|
+
driver;
|
|
123
|
+
/**
|
|
124
|
+
* @param driver - moteur qui ne porte pas de savepoints.
|
|
125
|
+
* @param operation - opération refusée (`savepoint` ou `rollbackTo`).
|
|
126
|
+
*/
|
|
127
|
+
constructor(driver, operation) {
|
|
128
|
+
super(`${operation}() is not supported by the "${driver}" driver: it has no savepoints. Roll back the whole transaction instead (rollback(), or throw from the transaction callback).`);
|
|
129
|
+
this.name = "SavepointNotSupportedError";
|
|
130
|
+
this.driver = driver;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
75
133
|
//#endregion
|
|
76
|
-
export { InvalidOrderOption, UnknownCriteriaField };
|
|
134
|
+
export { InvalidOrderOption, ReferencedEntityError, SavepointNotSupportedError, UnknownCriteriaField };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region nodefony/src/omitUndefined.ts
|
|
2
|
+
/**
|
|
3
|
+
* Retire les clés qui valent `undefined` d'une donnée d'écriture partielle.
|
|
4
|
+
*
|
|
5
|
+
* `undefined` et clé absente veulent dire « ne pas toucher » : Drizzle ignore
|
|
6
|
+
* déjà une valeur `undefined`, Mongoose (≥ 6) la retire d'une mise à jour. La
|
|
7
|
+
* règle est posée ICI, une fois, au seuil de la persistance, au lieu d'être
|
|
8
|
+
* un comportement propre à chaque adaptateur.
|
|
9
|
+
*
|
|
10
|
+
* Rend l'objet reçu TEL QUEL quand aucune clé ne vaut `undefined` — le cas
|
|
11
|
+
* courant d'un corps JSON, qui ne peut pas en porter : aucune allocation.
|
|
12
|
+
*
|
|
13
|
+
* @param data - données d'écriture, clés `undefined` admises
|
|
14
|
+
* @returns les mêmes données, sans aucune clé `undefined`
|
|
15
|
+
*/
|
|
16
|
+
function omitUndefined(data) {
|
|
17
|
+
const source = data;
|
|
18
|
+
for (const key of Object.keys(source)) {
|
|
19
|
+
if (source[key] !== void 0) continue;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const k of Object.keys(source)) {
|
|
22
|
+
const value = source[k];
|
|
23
|
+
if (value !== void 0) out[k] = value;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { omitUndefined };
|