@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.
Files changed (38) hide show
  1. package/dist/index.js +4 -3
  2. package/dist/nodefony/src/AbstractCrudService.js +7 -3
  3. package/dist/nodefony/src/ConnectionMonitor.js +41 -9
  4. package/dist/nodefony/src/EntityRegistry.js +9 -3
  5. package/dist/nodefony/src/Orm.js +19 -3
  6. package/dist/nodefony/src/OrmAdminApi.js +54 -12
  7. package/dist/nodefony/src/OrmRegistry.js +1 -1
  8. package/dist/nodefony/src/QueryFlowMonitor.js +2 -2
  9. package/dist/nodefony/src/connectDeadline.js +1 -1
  10. package/dist/nodefony/src/connectionDiagnosis.js +66 -6
  11. package/dist/nodefony/src/criteria.js +4 -3
  12. package/dist/nodefony/src/errors.js +59 -1
  13. package/dist/nodefony/src/omitUndefined.js +30 -0
  14. package/dist/nodefony/src/orphanEntities.js +63 -0
  15. package/dist/types/index.d.ts +5 -4
  16. package/dist/types/nodefony/interfaces/IEntity.d.ts +23 -4
  17. package/dist/types/nodefony/interfaces/IOrm.d.ts +10 -1
  18. package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +1 -1
  19. package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +58 -13
  20. package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +9 -9
  21. package/dist/types/nodefony/interfaces/IPage.d.ts +1 -1
  22. package/dist/types/nodefony/interfaces/IRepository.d.ts +12 -12
  23. package/dist/types/nodefony/interfaces/ITransaction.d.ts +3 -1
  24. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  25. package/dist/types/nodefony/src/AbstractCrudService.d.ts +9 -5
  26. package/dist/types/nodefony/src/ConnectionMonitor.d.ts +9 -2
  27. package/dist/types/nodefony/src/Entity.d.ts +1 -1
  28. package/dist/types/nodefony/src/Orm.d.ts +9 -1
  29. package/dist/types/nodefony/src/OrmAdminApi.d.ts +17 -1
  30. package/dist/types/nodefony/src/connectionDiagnosis.d.ts +29 -0
  31. package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +1 -1
  32. package/dist/types/nodefony/src/decorators/metadataStore.d.ts +5 -5
  33. package/dist/types/nodefony/src/errors.d.ts +48 -0
  34. package/dist/types/nodefony/src/omitUndefined.d.ts +16 -0
  35. package/dist/types/nodefony/src/orphanEntities.d.ts +38 -0
  36. package/docs/index.md +64 -60
  37. package/docs/tutorial-entity.md +26 -23
  38. 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
- if (this.#stats === null) this.#stats = /* @__PURE__ */ new Map();
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
- if (s.latencies === null) s.latencies = [];
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 = Date.now();
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
- if (s.recentErrors === null) s.recentErrors = [];
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
- if (lat && lat.length) {
148
- let min = lat[0];
149
- let max = lat[0];
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: lat[lat.length - 1],
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
- if (this.#entities === null) this.#entities = Object.create(null);
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
- return bucket[connectors[0]];
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) out.push(bucket[connector]);
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
  }
@@ -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
- return ormRegistry.list().map((name) => {
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 (/bool/.test(t)) return { type: "boolean" };
316
- if (/json/.test(t)) return { type: "object" };
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
- for (const e of entities) try {
418
- const inst = ormRegistry.get(e.connector);
419
- counts[e.name] = inst.isConnected() ? await inst.getRepository(e.name).count() : -1;
420
- } catch {
421
- counts[e.name] = -1;
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
- if (this.#orms === null) this.#orms = /* @__PURE__ */ new Map();
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
- if (this.#stats === null) this.#stats = /* @__PURE__ */ new Map();
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
- if (s.slow === null) s.slow = [];
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
- if (typeof raw === "string" && raw.length > 0) return raw;
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[i];
128
- if (c === "\\" && i + 1 < pattern.length) source += escapeRegExpChar(pattern[++i]);
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
- if (fields.length === 1) return { [fields[0]]: { $like: likePattern } };
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 };