@nodefony/orm-core 10.0.0-alpha.9 → 10.0.0-beta.2
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/README.md +14 -12
- package/dist/nodefony/src/AbstractCrudService.js +7 -3
- package/dist/nodefony/src/ConnectionMonitor.js +9 -8
- package/dist/nodefony/src/EntityRegistry.js +9 -3
- package/dist/nodefony/src/Orm.js +2 -2
- package/dist/nodefony/src/OrmAdminApi.js +3 -3
- 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/criteria.js +4 -3
- package/dist/nodefony/src/omitUndefined.js +30 -0
- package/dist/types/nodefony/interfaces/IEntity.d.ts +5 -5
- package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +13 -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/src/AbstractCrudService.d.ts +9 -5
- package/dist/types/nodefony/src/Entity.d.ts +1 -1
- 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/omitUndefined.d.ts +16 -0
- package/docs/index.md +53 -49
- package/docs/tutorial-entity.md +27 -24
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ plusieurs ORM dans le même processus — par exemple Drizzle pour SQL et Mongoo
|
|
|
5
5
|
derrière une API portable, avec une trappe vers le driver natif pour les cas que l'abstraction ne
|
|
6
6
|
couvre pas.
|
|
7
7
|
|
|
8
|
-
> Bibliothèque pure : ce module n'expose pas de classe `Module` et n'est pas listé dans
|
|
9
|
-
>
|
|
8
|
+
> Bibliothèque pure : ce module n'expose pas de classe `Module` et n'est pas listé dans le
|
|
9
|
+
> manifeste `modules` de `nodefony.config.ts`. Ce sont les **drivers** (`@nodefony/drizzle`, `@nodefony/mongoose`…) qui sont des
|
|
10
10
|
> modules Nodefony et qui s'enregistrent eux-mêmes dans l'`OrmRegistry`.
|
|
11
11
|
|
|
12
12
|
## Quel ORM par défaut ?
|
|
@@ -36,13 +36,13 @@ d'imposer plusieurs ORM simultanés.
|
|
|
36
36
|
|
|
37
37
|
## Contrats exposés
|
|
38
38
|
|
|
39
|
-
| Interface | Rôle
|
|
40
|
-
| ---------------- |
|
|
41
|
-
| `IOrm` | Instance ORM (connexion logique) : `connect`, `
|
|
42
|
-
| `IEntity<S,M>` | Entité enregistrée : nom logique, connecteur cible, schéma, modèle natif
|
|
43
|
-
| `IRepository<T>` | CRUD portable : `find`/`findOne`/`create`/`
|
|
44
|
-
| `ITransaction` | Unité de travail : `commit`/`rollback`/`savepoint`/`rollbackTo`
|
|
45
|
-
| `Criteria<T>` | Filtre typé par champ et opérateurs riches ; `OrmCriteria` = échappatoire
|
|
39
|
+
| Interface | Rôle |
|
|
40
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `IOrm` | Instance ORM (connexion logique) : `connect`, `getRepository`, `transaction` |
|
|
42
|
+
| `IEntity<S,M>` | Entité enregistrée : nom logique, connecteur cible, schéma, modèle natif |
|
|
43
|
+
| `IRepository<T>` | CRUD portable (15 verbes) : `find`/`findOne`/`create`/`updateOne`/`updateMany`/`upsert`/`delete`/`count`… |
|
|
44
|
+
| `ITransaction` | Unité de travail : `commit`/`rollback`/`savepoint`/`rollbackTo` |
|
|
45
|
+
| `Criteria<T>` | Filtre typé par champ et opérateurs riches ; `OrmCriteria` = échappatoire |
|
|
46
46
|
|
|
47
47
|
### Critères riches (opérateurs typés)
|
|
48
48
|
|
|
@@ -70,7 +70,7 @@ fenêtrage. C'est le garde-fou qui empêche l'abstraction de devenir un plafond.
|
|
|
70
70
|
## Exemple
|
|
71
71
|
|
|
72
72
|
```typescript
|
|
73
|
-
import {
|
|
73
|
+
import { ormRegistry } from "@nodefony/orm-core";
|
|
74
74
|
import type { IRepository } from "@nodefony/orm-core";
|
|
75
75
|
|
|
76
76
|
interface User {
|
|
@@ -78,14 +78,16 @@ interface User {
|
|
|
78
78
|
email: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const orm =
|
|
81
|
+
const orm = ormRegistry.get("db_principale");
|
|
82
82
|
const users: IRepository<User> = orm.getRepository<User>("User");
|
|
83
83
|
|
|
84
84
|
const created = await users.create({ email: "a@b.c" });
|
|
85
85
|
const found = await users.findOne({ email: "a@b.c" });
|
|
86
86
|
|
|
87
87
|
await orm.transaction(async (tx) => {
|
|
88
|
-
await users
|
|
88
|
+
await users
|
|
89
|
+
.withTransaction(tx)
|
|
90
|
+
.updateOne({ id: created.id }, { email: "x@y.z" });
|
|
89
91
|
// validation automatique si la fonction résout, annulation si elle rejette
|
|
90
92
|
});
|
|
91
93
|
```
|
|
@@ -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);
|
|
@@ -51,7 +51,7 @@ var ConnectionMonitor = class {
|
|
|
51
51
|
#stats = null;
|
|
52
52
|
/** Crée/retourne les stats mutables d'un connecteur (alloue à la demande). */
|
|
53
53
|
#ensure(name) {
|
|
54
|
-
|
|
54
|
+
this.#stats ??= /* @__PURE__ */ new Map();
|
|
55
55
|
let s = this.#stats.get(name);
|
|
56
56
|
if (s === void 0) {
|
|
57
57
|
s = {
|
|
@@ -80,7 +80,7 @@ var ConnectionMonitor = class {
|
|
|
80
80
|
*/
|
|
81
81
|
recordPing(name, ms) {
|
|
82
82
|
const s = this.#ensure(name);
|
|
83
|
-
|
|
83
|
+
s.latencies ??= [];
|
|
84
84
|
s.latencies.push(ms);
|
|
85
85
|
if (s.latencies.length > MAX_LATENCY_SAMPLES) s.latencies.shift();
|
|
86
86
|
}
|
|
@@ -139,7 +139,7 @@ var ConnectionMonitor = class {
|
|
|
139
139
|
}
|
|
140
140
|
/** Ajoute un événement en tête de la chronologie (allouée au 1ᵉʳ, bornée). */
|
|
141
141
|
#pushEvent(s, e) {
|
|
142
|
-
|
|
142
|
+
s.events ??= [];
|
|
143
143
|
s.events.unshift(e);
|
|
144
144
|
if (s.events.length > MAX_EVENTS) s.events.length = MAX_EVENTS;
|
|
145
145
|
}
|
|
@@ -157,7 +157,7 @@ var ConnectionMonitor = class {
|
|
|
157
157
|
ts: Date.now()
|
|
158
158
|
};
|
|
159
159
|
s.lastError = err;
|
|
160
|
-
|
|
160
|
+
s.recentErrors ??= [];
|
|
161
161
|
s.recentErrors.unshift(err);
|
|
162
162
|
if (s.recentErrors.length > MAX_RECENT_ERRORS) s.recentErrors.length = MAX_RECENT_ERRORS;
|
|
163
163
|
}
|
|
@@ -172,9 +172,10 @@ var ConnectionMonitor = class {
|
|
|
172
172
|
if (s === void 0) return EMPTY_CORE;
|
|
173
173
|
const lat = s.latencies;
|
|
174
174
|
let latency = EMPTY_LATENCY;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let
|
|
175
|
+
const last = lat?.at(-1);
|
|
176
|
+
if (lat && last !== void 0) {
|
|
177
|
+
let min = last;
|
|
178
|
+
let max = last;
|
|
178
179
|
let sum = 0;
|
|
179
180
|
for (const v of lat) {
|
|
180
181
|
if (v < min) min = v;
|
|
@@ -182,7 +183,7 @@ var ConnectionMonitor = class {
|
|
|
182
183
|
sum += v;
|
|
183
184
|
}
|
|
184
185
|
latency = {
|
|
185
|
-
last
|
|
186
|
+
last,
|
|
186
187
|
min: Math.round(min * 100) / 100,
|
|
187
188
|
avg: Math.round(sum / lat.length * 100) / 100,
|
|
188
189
|
max: Math.round(max * 100) / 100,
|
|
@@ -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
|
@@ -194,7 +194,7 @@ 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
200
|
/**
|
|
@@ -264,7 +264,7 @@ var Orm = class extends Service {
|
|
|
264
264
|
watchdog = setTimeout(() => {
|
|
265
265
|
rejeter(/* @__PURE__ */ new Error(`aucune réponse en ${this.heartbeatTimeoutMs} ms`));
|
|
266
266
|
}, this.heartbeatTimeoutMs);
|
|
267
|
-
watchdog.unref
|
|
267
|
+
watchdog.unref();
|
|
268
268
|
})]);
|
|
269
269
|
this.connectionRestored();
|
|
270
270
|
} catch (e) {
|
|
@@ -349,8 +349,8 @@ function jsonSchemaType(nativeType) {
|
|
|
349
349
|
const t = nativeType.toLowerCase();
|
|
350
350
|
if (/\b(serial|bigint|smallint|tinyint|mediumint|int|integer)\b/.test(t)) return { type: "integer" };
|
|
351
351
|
if (/(real|float|double|decimal|numeric|number)/.test(t)) return { type: "number" };
|
|
352
|
-
if (
|
|
353
|
-
if (
|
|
352
|
+
if (t.includes("bool")) return { type: "boolean" };
|
|
353
|
+
if (t.includes("json")) return { type: "object" };
|
|
354
354
|
if (/(date|time|timestamp)/.test(t)) return {
|
|
355
355
|
type: "string",
|
|
356
356
|
format: "date-time"
|
|
@@ -422,7 +422,7 @@ function createOrmAdminApi() {
|
|
|
422
422
|
path: "entity/{name}",
|
|
423
423
|
summary: "Une entité (colonnes + relations) — ?connector= si homonymes",
|
|
424
424
|
handler: (request) => {
|
|
425
|
-
const name = request.params.name
|
|
425
|
+
const name = request.params.name;
|
|
426
426
|
const connector = oneParam(request, "connector");
|
|
427
427
|
try {
|
|
428
428
|
const found = entityRegistry.list().find((e) => e.name === name && (!connector || e.connector === connector));
|
|
@@ -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);
|
|
@@ -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
|
|
@@ -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 };
|
|
@@ -53,7 +53,7 @@ export interface IEntity<S = unknown, M = unknown> {
|
|
|
53
53
|
* **Optionnel** : sert au regroupement dans le graphe canonique / ERD Studio.
|
|
54
54
|
* Non renseigné → entité non rattachée (groupe « — » côté UI).
|
|
55
55
|
*/
|
|
56
|
-
readonly module?: string;
|
|
56
|
+
readonly module?: string | undefined;
|
|
57
57
|
/**
|
|
58
58
|
* **Classification** de l'entité (ex. domaine fonctionnel `"facturation"`,
|
|
59
59
|
* `"comptabilité"`) — axe de regroupement **distinct** du `module`
|
|
@@ -61,7 +61,7 @@ export interface IEntity<S = unknown, M = unknown> {
|
|
|
61
61
|
* base** (centaines de tables d'un même module) : l'ERD groupe/filtre par
|
|
62
62
|
* `domain`. **Optionnel** ; non renseigné → retombe sur `module`.
|
|
63
63
|
*/
|
|
64
|
-
readonly domain?: string;
|
|
64
|
+
readonly domain?: string | undefined;
|
|
65
65
|
/** Définition de schéma propre au driver (forme libre). */
|
|
66
66
|
readonly schema: S;
|
|
67
67
|
/** Modèle compilé natif, renseigné après connexion de l'ORM. */
|
|
@@ -72,13 +72,13 @@ export interface IEntity<S = unknown, M = unknown> {
|
|
|
72
72
|
* schema-as-code (Drizzle) les expriment en colonnes → ce flag y est sans effet.
|
|
73
73
|
* **Optionnel**, défaut `false`.
|
|
74
74
|
*/
|
|
75
|
-
readonly timestamps?: boolean;
|
|
75
|
+
readonly timestamps?: boolean | undefined;
|
|
76
76
|
/** Relations déclarées vers d'autres entités (par nom logique). */
|
|
77
|
-
readonly relations?: ReadonlyArray<IEntityRelation
|
|
77
|
+
readonly relations?: ReadonlyArray<IEntityRelation> | undefined;
|
|
78
78
|
/**
|
|
79
79
|
* Index composites, appliqués par l'adapter qui les exprime hors schéma
|
|
80
80
|
* (Mongoose `schema.index()`). Les ORM schema-as-code (Drizzle) les déclarent
|
|
81
81
|
* dans leur table → ce champ y est sans effet. **Optionnel**.
|
|
82
82
|
*/
|
|
83
|
-
readonly indexes?: ReadonlyArray<IEntityIndex
|
|
83
|
+
readonly indexes?: ReadonlyArray<IEntityIndex> | undefined;
|
|
84
84
|
}
|
|
@@ -26,7 +26,7 @@ export interface ISlowQuery {
|
|
|
26
26
|
/** Connecteur ORM (clé du registre). */
|
|
27
27
|
connector: string;
|
|
28
28
|
/** SQL paramétré et redacté (absent si l'adapter ne sait pas l'extraire). */
|
|
29
|
-
sql?: string;
|
|
29
|
+
sql?: string | undefined;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Compteurs de flux d'UN connecteur ORM. Cumuls depuis le boot (le débit/s se
|
|
@@ -32,7 +32,7 @@ export interface IRelationInfo {
|
|
|
32
32
|
/** Champ portant la relation côté entité courante. */
|
|
33
33
|
field: string;
|
|
34
34
|
/** Clé étrangère (explicite ou dérivée déterministe). */
|
|
35
|
-
foreignKey?: string;
|
|
35
|
+
foreignKey?: string | undefined;
|
|
36
36
|
}
|
|
37
37
|
/** Nœud du graphe : une entité avec ses colonnes et ses relations. */
|
|
38
38
|
export interface IEntityGraphNode {
|
|
@@ -66,11 +66,11 @@ export interface IConnectionInfo {
|
|
|
66
66
|
* `host:port/base` (serveur) ou URI redactée. **Jamais de credential** (mot de
|
|
67
67
|
* passe retiré côté adapter).
|
|
68
68
|
*/
|
|
69
|
-
target?: string;
|
|
69
|
+
target?: string | undefined;
|
|
70
70
|
/** Version du moteur/base (ex. SQLite `3.45.1`), si l'adapter peut l'obtenir. */
|
|
71
|
-
version?: string;
|
|
71
|
+
version?: string | undefined;
|
|
72
72
|
/** Version de la lib ORM elle-même (ex. drizzle-orm `0.44.x`), si connue. */
|
|
73
|
-
ormVersion?: string;
|
|
73
|
+
ormVersion?: string | undefined;
|
|
74
74
|
}
|
|
75
75
|
/** Résumé d'un ORM/connecteur enregistré. */
|
|
76
76
|
export interface IOrmSummary {
|
|
@@ -93,7 +93,7 @@ export interface IOrmSummary {
|
|
|
93
93
|
/** Nombre d'entités rattachées à cet ORM. */
|
|
94
94
|
entityCount: number;
|
|
95
95
|
/** Connexion sous-jacente (driver + cible), si l'adapter l'expose. */
|
|
96
|
-
connection?: IConnectionInfo;
|
|
96
|
+
connection?: IConnectionInfo | undefined;
|
|
97
97
|
}
|
|
98
98
|
/** Erreur de connexion horodatée (message **redacté** — jamais de credential). */
|
|
99
99
|
export interface IConnectionError {
|
|
@@ -112,7 +112,7 @@ export interface IConnectionEvent {
|
|
|
112
112
|
/** Horodatage epoch ms. */
|
|
113
113
|
ts: number;
|
|
114
114
|
/** Cause de la perte (credential déjà retiré) — absente pour une reprise. */
|
|
115
|
-
reason?: string;
|
|
115
|
+
reason?: string | undefined;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* Le mécanisme de résilience d'un connecteur, TEL QU'IL TOURNE dans ce process.
|
|
@@ -153,11 +153,11 @@ export interface IConnectionHealth {
|
|
|
153
153
|
/** Base/driver (`sqlite`, `mongodb`…). */
|
|
154
154
|
driver: string;
|
|
155
155
|
/** Cible lisible (chemin relatif / host:port), jamais de credential. */
|
|
156
|
-
target?: string;
|
|
156
|
+
target?: string | undefined;
|
|
157
157
|
/** Version moteur. */
|
|
158
|
-
version?: string;
|
|
158
|
+
version?: string | undefined;
|
|
159
159
|
/** Version lib ORM. */
|
|
160
|
-
ormVersion?: string;
|
|
160
|
+
ormVersion?: string | undefined;
|
|
161
161
|
/** État courant (`isConnected`). */
|
|
162
162
|
connected: boolean;
|
|
163
163
|
/** Connecté depuis (epoch ms), `null` si jamais connecté. */
|
|
@@ -177,7 +177,7 @@ export interface IConnectionHealth {
|
|
|
177
177
|
/** Pertes et reprises récentes (ring borné, plus récentes d'abord). */
|
|
178
178
|
events: IConnectionEvent[];
|
|
179
179
|
/** Mécanisme de détection et de reprise, si l'ORM le décrit. */
|
|
180
|
-
resilience?: IOrmResilience;
|
|
180
|
+
resilience?: IOrmResilience | undefined;
|
|
181
181
|
/** Nombre total d'erreurs enregistrées (connexion + ping). */
|
|
182
182
|
errorCount: number;
|
|
183
183
|
/** Dernière erreur, `null` si aucune. */
|
|
@@ -195,11 +195,11 @@ export interface IConnectionHealth {
|
|
|
195
195
|
/** Fenêtre glissante de latence (min/moy/max sur les N derniers pings). */
|
|
196
196
|
latency: ILatencyWindow;
|
|
197
197
|
/** Sonde de stockage (driver), si l'adapter l'expose. */
|
|
198
|
-
storage?: IOrmStorageProbe;
|
|
198
|
+
storage?: IOrmStorageProbe | undefined;
|
|
199
199
|
/** Sonde de pool de connexions (driver), si l'adapter l'expose. */
|
|
200
|
-
pool?: IOrmPoolProbe;
|
|
200
|
+
pool?: IOrmPoolProbe | undefined;
|
|
201
201
|
/** Métriques driver libres (clé→valeur). */
|
|
202
|
-
extra?: Record<string, string | number | boolean
|
|
202
|
+
extra?: Record<string, string | number | boolean> | undefined;
|
|
203
203
|
}
|
|
204
204
|
/** Graphe complet du modèle de données — réponse de `/nodefony/orm/api/graph`. */
|
|
205
205
|
export interface IOrmGraph {
|
|
@@ -32,15 +32,15 @@ export interface ILatencyWindow {
|
|
|
32
32
|
*/
|
|
33
33
|
export interface IOrmStorageProbe {
|
|
34
34
|
/** Taille de la base (octets) — `pages × pageSize` (SQLite). */
|
|
35
|
-
sizeBytes?: number;
|
|
35
|
+
sizeBytes?: number | undefined;
|
|
36
36
|
/** Nombre de pages allouées. */
|
|
37
|
-
pages?: number;
|
|
37
|
+
pages?: number | undefined;
|
|
38
38
|
/** Taille d'une page (octets). */
|
|
39
|
-
pageSize?: number;
|
|
39
|
+
pageSize?: number | undefined;
|
|
40
40
|
/** Mode de journalisation (SQLite : `wal`, `delete`, `memory`…). */
|
|
41
|
-
journalMode?: string;
|
|
41
|
+
journalMode?: string | undefined;
|
|
42
42
|
/** Pages libres (fragmentation / espace récupérable au VACUUM). */
|
|
43
|
-
freePages?: number;
|
|
43
|
+
freePages?: number | undefined;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Sonde de pool de connexions — pertinente pour les bases serveur (Mongo,
|
|
@@ -48,13 +48,13 @@ export interface IOrmStorageProbe {
|
|
|
48
48
|
*/
|
|
49
49
|
export interface IOrmPoolProbe {
|
|
50
50
|
/** Taille max configurée du pool. */
|
|
51
|
-
size?: number;
|
|
51
|
+
size?: number | undefined;
|
|
52
52
|
/** Connexions disponibles (idle). */
|
|
53
|
-
available?: number;
|
|
53
|
+
available?: number | undefined;
|
|
54
54
|
/** Connexions en cours d'utilisation. */
|
|
55
|
-
borrowed?: number;
|
|
55
|
+
borrowed?: number | undefined;
|
|
56
56
|
/** Demandes en attente d'une connexion. */
|
|
57
|
-
pending?: number;
|
|
57
|
+
pending?: number | undefined;
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Sonde profonde driver-spécifique retournée par {@link IOrm.probe}. Tous les
|
|
@@ -20,5 +20,5 @@ export type { IPage, IPageQuery } from "nodefony";
|
|
|
20
20
|
*/
|
|
21
21
|
export interface PageQuery<T = unknown> extends IPageQuery {
|
|
22
22
|
/** Filtre typé optionnel appliqué avant la pagination (toutes les lignes si omis). */
|
|
23
|
-
criteria?: Criteria<T
|
|
23
|
+
criteria?: Criteria<T> | undefined;
|
|
24
24
|
}
|
|
@@ -23,23 +23,23 @@ export type OrmCriteria = Record<string, unknown>;
|
|
|
23
23
|
*/
|
|
24
24
|
export interface FieldOperators<V> {
|
|
25
25
|
/** Égalité stricte (équivalent à passer la valeur nue). */
|
|
26
|
-
$eq?: V;
|
|
26
|
+
$eq?: V | undefined;
|
|
27
27
|
/** Différent de. */
|
|
28
|
-
$ne?: V;
|
|
28
|
+
$ne?: V | undefined;
|
|
29
29
|
/** Strictement supérieur à. */
|
|
30
|
-
$gt?: V;
|
|
30
|
+
$gt?: V | undefined;
|
|
31
31
|
/** Supérieur ou égal à. */
|
|
32
|
-
$gte?: V;
|
|
32
|
+
$gte?: V | undefined;
|
|
33
33
|
/** Strictement inférieur à. */
|
|
34
|
-
$lt?: V;
|
|
34
|
+
$lt?: V | undefined;
|
|
35
35
|
/** Inférieur ou égal à. */
|
|
36
|
-
$lte?: V;
|
|
36
|
+
$lte?: V | undefined;
|
|
37
37
|
/** Appartient à l'ensemble. */
|
|
38
|
-
$in?: readonly V[];
|
|
38
|
+
$in?: readonly V[] | undefined;
|
|
39
39
|
/** N'appartient pas à l'ensemble. */
|
|
40
|
-
$nin?: readonly V[];
|
|
40
|
+
$nin?: readonly V[] | undefined;
|
|
41
41
|
/** Motif SQL `LIKE` (`%`/`_`) — pertinent pour les champs texte uniquement. */
|
|
42
|
-
$like?: string;
|
|
42
|
+
$like?: string | undefined;
|
|
43
43
|
/**
|
|
44
44
|
* Teste l'**absence de valeur** : `true` → `IS NULL`, `false` → `IS NOT NULL`
|
|
45
45
|
* (Mongo : `$eq`/`$ne null`, qui couvre aussi le champ absent).
|
|
@@ -60,7 +60,7 @@ export interface FieldOperators<V> {
|
|
|
60
60
|
* // les PAT qui ont une expiration
|
|
61
61
|
* repo.find({ expiresAt: { $null: false } });
|
|
62
62
|
*/
|
|
63
|
-
$null?: boolean;
|
|
63
|
+
$null?: boolean | undefined;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Opérateurs d'**écriture** applicables à un champ dans le `update` d'un
|
|
@@ -154,7 +154,7 @@ export type Criteria<T> = {
|
|
|
154
154
|
* // les clés encore utilisables
|
|
155
155
|
* repo.count({ revokedAt: null, $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }] });
|
|
156
156
|
*/
|
|
157
|
-
$or?: ReadonlyArray<Criteria<T
|
|
157
|
+
$or?: ReadonlyArray<Criteria<T>> | undefined;
|
|
158
158
|
} & OrmCriteria;
|
|
159
159
|
/**
|
|
160
160
|
* Options de lecture (`find`/`findOne`) portables cross-ORM.
|
|
@@ -181,7 +181,7 @@ export interface RepositoryReadOptions {
|
|
|
181
181
|
* la frontière qui la reçoit (`parsePageQuery` accepte `champ:sens` en casse
|
|
182
182
|
* libre), jamais au repository.
|
|
183
183
|
*/
|
|
184
|
-
order?: Array<[string, "ASC" | "DESC"]
|
|
184
|
+
order?: Array<[string, "ASC" | "DESC"]> | undefined;
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
187
|
* Contrat CRUD minimal exposé par un repository, indépendant de l'ORM sous-jacent.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Service } from "nodefony";
|
|
2
|
+
import type { PartialInput } from "nodefony";
|
|
2
3
|
import type { IRepository, Criteria, RepositoryReadOptions, IPage, PageQuery } from "../interfaces/index.js";
|
|
3
4
|
import type { ServiceWiring } from "./serviceWiring.js";
|
|
4
5
|
/**
|
|
@@ -102,10 +103,13 @@ export declare abstract class AbstractCrudService<T, R extends IRepository<T> =
|
|
|
102
103
|
/**
|
|
103
104
|
* Crée une entité : `beforeCreate` → persistance → `afterCreate` → `onCreated`.
|
|
104
105
|
*
|
|
106
|
+
* Une clé qui vaut `undefined` vaut clé absente : elle est retirée avant la
|
|
107
|
+
* persistance (cf `omitUndefined`), quel que soit l'adaptateur.
|
|
108
|
+
*
|
|
105
109
|
* @param data - champs de l'entité à créer.
|
|
106
110
|
* @returns l'entité persistée. Émet `onCreated`.
|
|
107
111
|
*/
|
|
108
|
-
create(data:
|
|
112
|
+
create(data: PartialInput<T>): Promise<T>;
|
|
109
113
|
/**
|
|
110
114
|
* Met à jour **au plus une** entité : `beforeUpdate` → persistance atomique
|
|
111
115
|
* → `afterUpdate` → `onUpdated`.
|
|
@@ -120,10 +124,10 @@ export declare abstract class AbstractCrudService<T, R extends IRepository<T> =
|
|
|
120
124
|
* on la remontera dans un service le jour où un usage métier la réclame.
|
|
121
125
|
*
|
|
122
126
|
* @param criteria - filtre de sélection (champ inconnu → `UnknownCriteriaField`).
|
|
123
|
-
* @param data - champs à modifier.
|
|
127
|
+
* @param data - champs à modifier ; une clé `undefined` n'est pas touchée.
|
|
124
128
|
* @returns l'entité mise à jour, ou `null` si aucune ne correspond (pas d'event).
|
|
125
129
|
*/
|
|
126
|
-
updateOne(criteria: Criteria<T>, data:
|
|
130
|
+
updateOne(criteria: Criteria<T>, data: PartialInput<T>): Promise<T | null>;
|
|
127
131
|
/**
|
|
128
132
|
* Supprime : `beforeDelete` → persistance → `afterDelete` → `onDeleted`.
|
|
129
133
|
*
|
|
@@ -138,7 +142,7 @@ export declare abstract class AbstractCrudService<T, R extends IRepository<T> =
|
|
|
138
142
|
* @param data - données entrantes.
|
|
139
143
|
* @returns les données préparées à persister.
|
|
140
144
|
*/
|
|
141
|
-
protected beforeCreate(data:
|
|
145
|
+
protected beforeCreate(data: PartialInput<T>): PartialInput<T> | Promise<PartialInput<T>>;
|
|
142
146
|
/** Effet de bord après création (ex. provisionner une ressource liée). */
|
|
143
147
|
protected afterCreate(_entity: T): void | Promise<void>;
|
|
144
148
|
/**
|
|
@@ -148,7 +152,7 @@ export declare abstract class AbstractCrudService<T, R extends IRepository<T> =
|
|
|
148
152
|
* @param data - données entrantes.
|
|
149
153
|
* @returns les données préparées à persister.
|
|
150
154
|
*/
|
|
151
|
-
protected beforeUpdate(_criteria: Criteria<T>, data:
|
|
155
|
+
protected beforeUpdate(_criteria: Criteria<T>, data: PartialInput<T>): PartialInput<T> | Promise<PartialInput<T>>;
|
|
152
156
|
/** Effet de bord après mise à jour. */
|
|
153
157
|
protected afterUpdate(_entity: T): void | Promise<void>;
|
|
154
158
|
/** Garde/validation avant suppression (ex. interdire la suppression du dernier admin). */
|
|
@@ -25,7 +25,7 @@ export declare abstract class Entity<S = unknown, M = unknown> implements IEntit
|
|
|
25
25
|
/** Modèle compilé natif, renseigné après connexion de l'ORM. */
|
|
26
26
|
model?: M;
|
|
27
27
|
/** Relations déclarées vers d'autres entités (par nom logique). */
|
|
28
|
-
readonly relations?: ReadonlyArray<IEntityRelation
|
|
28
|
+
readonly relations?: ReadonlyArray<IEntityRelation> | undefined;
|
|
29
29
|
/**
|
|
30
30
|
* Construit la définition de schéma propre au driver.
|
|
31
31
|
*
|
|
@@ -21,7 +21,7 @@ export interface EntityOptions<S = unknown> {
|
|
|
21
21
|
*/
|
|
22
22
|
domain?: string;
|
|
23
23
|
/** Schéma natif du driver (schéma Mongoose, schéma Drizzle...). */
|
|
24
|
-
schema?: S;
|
|
24
|
+
schema?: S | undefined;
|
|
25
25
|
/** Relations déclaratives vers d'autres entités. */
|
|
26
26
|
relations?: ReadonlyArray<IEntityRelation>;
|
|
27
27
|
}
|
|
@@ -17,13 +17,13 @@ export interface EntityMetadata<S = unknown> {
|
|
|
17
17
|
/** Connexion nommée cible (clé du `ormRegistry`). */
|
|
18
18
|
readonly connector: string;
|
|
19
19
|
/** Module Nodefony propriétaire (regroupement graphe/ERD), si fourni. */
|
|
20
|
-
readonly module?: string;
|
|
20
|
+
readonly module?: string | undefined;
|
|
21
21
|
/** Classification (domaine fonctionnel), axe de regroupement distinct du module. */
|
|
22
|
-
readonly domain?: string;
|
|
22
|
+
readonly domain?: string | undefined;
|
|
23
23
|
/** Schéma natif du driver (forme libre), si fourni au décorateur. */
|
|
24
|
-
readonly schema?: S;
|
|
24
|
+
readonly schema?: S | undefined;
|
|
25
25
|
/** Relations déclaratives vers d'autres entités. */
|
|
26
|
-
readonly relations?: ReadonlyArray<IEntityRelation
|
|
26
|
+
readonly relations?: ReadonlyArray<IEntityRelation> | undefined;
|
|
27
27
|
/** Constructeur décoré (pour introspection / résolution lazy par le driver). */
|
|
28
28
|
readonly target: DecoratedClass;
|
|
29
29
|
}
|
|
@@ -36,7 +36,7 @@ export interface RepositoryMetadata {
|
|
|
36
36
|
/** Nom logique de l'entité gérée par ce repository. */
|
|
37
37
|
readonly entity: string;
|
|
38
38
|
/** Connecteur cible (lève l'ambiguïté si l'entité existe sur plusieurs connexions). */
|
|
39
|
-
readonly connector?: string;
|
|
39
|
+
readonly connector?: string | undefined;
|
|
40
40
|
/** Constructeur décoré. */
|
|
41
41
|
readonly target: DecoratedClass;
|
|
42
42
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PartialInput } from "nodefony";
|
|
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
|
+
export declare function omitUndefined<T>(data: PartialInput<T>): Partial<T>;
|
package/docs/index.md
CHANGED
|
@@ -177,7 +177,7 @@ garantit que la dépendance va bien du concret vers l'abstrait, et jamais l'inve
|
|
|
177
177
|
> [!NOTE]
|
|
178
178
|
> `orm-core` n'est **pas un module bootable** : pas de classe `Module`, rien à mettre dans
|
|
179
179
|
> `modules: [...]`. C'est une bibliothèque pure. Les modules, ce sont les **drivers** — ce sont eux
|
|
180
|
-
> qui s'enregistrent dans `ormRegistry` (`OrmRegistry.ts:
|
|
180
|
+
> qui s'enregistrent dans `ormRegistry` (`OrmRegistry.ts:86`) à leur démarrage.
|
|
181
181
|
|
|
182
182
|
## La vision Nodefony
|
|
183
183
|
|
|
@@ -377,13 +377,13 @@ sequenceDiagram
|
|
|
377
377
|
Note over K: onServersReady → récap de boot
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
-
**`entityRegistry`** (`EntityRegistry.ts:
|
|
380
|
+
**`entityRegistry`** (`EntityRegistry.ts:145`) indexe les entités à **deux** niveaux — nom puis
|
|
381
381
|
connecteur — parce qu'une même entité logique (`User`) peut vivre sur plusieurs connexions. Demander
|
|
382
382
|
`get("User")` sans préciser le connecteur alors que deux le portent **lève** plutôt que de deviner
|
|
383
383
|
(`EntityRegistry.get()`, `EntityRegistry.ts:54`). Le stockage est un `Object.create(null)` alloué
|
|
384
384
|
**au premier enregistrement** : une application sans base ne paie rien.
|
|
385
385
|
|
|
386
|
-
**`ormRegistry`** (`OrmRegistry.ts:
|
|
386
|
+
**`ormRegistry`** (`OrmRegistry.ts:86`) associe un nom de connexion à son instance `IOrm`. Un doublon
|
|
387
387
|
de nom **lève** (`OrmRegistry.register()`, `OrmRegistry.ts:26`) : deux connexions homonymes seraient
|
|
388
388
|
un bug silencieux, jamais une intention.
|
|
389
389
|
|
|
@@ -394,7 +394,7 @@ l'ordre des écouteurs, la table existe ou non. `entities()` s'accroche à `onRe
|
|
|
394
394
|
comportement avec `@controllers`, qui lui reste à `onBoot`.
|
|
395
395
|
|
|
396
396
|
**`Orm.connect()`** (`Orm.ts:118`) est une **template method** : elle mesure la latence, alimente le
|
|
397
|
-
moniteur de connexion, puis émet `onOrmReady`. Un adapter surcharge `onConnect()` (`Orm.ts:
|
|
397
|
+
moniteur de connexion, puis émet `onOrmReady`. Un adapter surcharge `onConnect()` (`Orm.ts:363`), et
|
|
398
398
|
**jamais** `connect()` — sinon l'événement et l'instrumentation disparaissent.
|
|
399
399
|
|
|
400
400
|
## 🧰 Les contrats — la surface publique
|
|
@@ -420,20 +420,22 @@ lieu d'échouer : `describeEntity()` (`IOrm.ts:78`, colonnes pour l'ERD), `descr
|
|
|
420
420
|
`IRepository<T>` (`IRepository.ts:197`) est la seule surface que ton métier devrait connaître. Les
|
|
421
421
|
verbes se choisissent sur **la garantie** qu'ils apportent, pas sur leur nom.
|
|
422
422
|
|
|
423
|
-
| Verbe | Ce qu'il garantit
|
|
424
|
-
| --------------------- |
|
|
425
|
-
| `find` / `findOne` | lecture filtrée + eager-load + tri + bornes
|
|
426
|
-
| `count`
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
431
|
-
| `
|
|
432
|
-
| `
|
|
433
|
-
| `
|
|
434
|
-
| `
|
|
435
|
-
| `
|
|
436
|
-
| `
|
|
423
|
+
| Verbe | Ce qu'il garantit | Ancre |
|
|
424
|
+
| --------------------- | ------------------------------------------------------------------------- | -------------------- |
|
|
425
|
+
| `find` / `findOne` | lecture filtrée + eager-load + tri + bornes | `IRepository.ts:230` |
|
|
426
|
+
| `count` | compter les entités qui correspondent au critère | `IRepository.ts:362` |
|
|
427
|
+
| `exists` | savoir s'il y en a au moins un, sans charger de colonne | `IRepository.ts:395` |
|
|
428
|
+
| `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts:381` |
|
|
429
|
+
| `create` | insertion d'une ligne, rend la version persistée (id, défauts) | `IRepository.ts:240` |
|
|
430
|
+
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts:252` |
|
|
431
|
+
| `updateOne` | met à jour **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts:269` |
|
|
432
|
+
| `updateMany` | met à jour toutes les lignes du critère, rend le **nombre** | `IRepository.ts:312` |
|
|
433
|
+
| `upsert` | insère **ou** met à jour sur conflit de clé, en **une** instruction | `IRepository.ts:296` |
|
|
434
|
+
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, rate-limit | `IRepository.ts:287` |
|
|
435
|
+
| `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts:336` |
|
|
436
|
+
| `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts:345` |
|
|
437
|
+
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, outbox, `pop` atomique | `IRepository.ts:355` |
|
|
438
|
+
| `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts:406` |
|
|
437
439
|
|
|
438
440
|
> [!IMPORTANT]
|
|
439
441
|
> `updateOne` est atomique **par construction** : une seule requête (`UPDATE … RETURNING` en SQL,
|
|
@@ -538,8 +540,8 @@ raisonnement, et le compilateur la refuse.
|
|
|
538
540
|
JSON ou un sous-document (`{ meta: { auteur: "…" } }`) reste donc une égalité — c'est ce qui évite
|
|
539
541
|
qu'une donnée métier soit prise pour une requête.
|
|
540
542
|
|
|
541
|
-
**Ce que le critère ne couvre pas** : les
|
|
542
|
-
|
|
543
|
+
**Ce que le critère ne couvre pas** : les sous-requêtes, les agrégats, les jointures arbitraires
|
|
544
|
+
(la disjonction, elle, s'exprime par `$or: [...]`, portable). Ce n'est pas un oubli — c'est la limite du portable, et la sortie est
|
|
543
545
|
`getNativeConnection()`. L'erreur `UnknownCriteriaField` (`errors.ts:23`) le dit d'ailleurs
|
|
544
546
|
explicitement dans son message, avec la liste des champs connus de l'entité (diagnostic d'une faute
|
|
545
547
|
de frappe).
|
|
@@ -608,8 +610,8 @@ d'administration : elle ne charge qu'une page, quelle que soit la taille de la t
|
|
|
608
610
|
|
|
609
611
|
Deux drivers implémentent les contrats. Le contrat `IRepository` est tenu **en entier** par les
|
|
610
612
|
deux : les quinze verbes existent des deux côtés — par exemple l'upsert, avec
|
|
611
|
-
`DrizzleRepository.upsert()` (`DrizzleRepository.ts:
|
|
612
|
-
(`MongooseRepository.ts:
|
|
613
|
+
`DrizzleRepository.upsert()` (`DrizzleRepository.ts:986`) et `MongooseRepository.upsert()`
|
|
614
|
+
(`MongooseRepository.ts:422`).
|
|
613
615
|
|
|
614
616
|
| Capacité | `@nodefony/drizzle` | `@nodefony/mongoose` |
|
|
615
617
|
| -------------------------------------- | ----------------------------------- | -------------------------------------------- |
|
|
@@ -618,12 +620,12 @@ deux : les quinze verbes existent des deux côtés — par exemple l'upsert, ave
|
|
|
618
620
|
| Eager-load `{ relations }` | oui | oui (`populate`) |
|
|
619
621
|
| Transactions | oui | oui (replica set requis par Mongo) |
|
|
620
622
|
| Savepoints (rollback partiel) | oui | non — refusés (`SavepointNotSupportedError`) |
|
|
621
|
-
| Colonnes pour l'ERD (`describeEntity`) | oui (`DrizzleOrm.ts:
|
|
622
|
-
| Sonde de flux (requêtes/s, lentes) | oui — alimente `queryFlowMonitor` |
|
|
623
|
-
| Sonde profonde (`probe`) | oui (`DrizzleOrm.ts:
|
|
623
|
+
| Colonnes pour l'ERD (`describeEntity`) | oui (`DrizzleOrm.ts:1982`) | oui (`MongooseOrm.ts:649`) |
|
|
624
|
+
| Sonde de flux (requêtes/s, lentes) | oui — alimente `queryFlowMonitor` | oui — alimente `queryFlowMonitor` |
|
|
625
|
+
| Sonde profonde (`probe`) | oui (`DrizzleOrm.ts:1873`) | oui (`MongooseOrm.ts:617`) |
|
|
624
626
|
|
|
625
|
-
**Les « stores » du framework
|
|
626
|
-
|
|
627
|
+
**Les « stores » du framework** : chaque adapter déclare ce qu'il porte dans son `package.json`, clé
|
|
628
|
+
`nodefony.stores`. Les deux backends durables portent les huit :
|
|
627
629
|
|
|
628
630
|
| Store | drizzle | mongoose |
|
|
629
631
|
| ------------- | ------- | -------- |
|
|
@@ -632,16 +634,15 @@ qu'il porte dans son `package.json`, clé `nodefony.stores` :
|
|
|
632
634
|
| `tokens` | ✅ | ✅ |
|
|
633
635
|
| `passkeys` | ✅ | ✅ |
|
|
634
636
|
| `webhooks` | ✅ | ✅ |
|
|
635
|
-
| `totp` | ✅ |
|
|
636
|
-
| `audit` | ✅ |
|
|
637
|
-
| `idempotency` | ✅ |
|
|
637
|
+
| `totp` | ✅ | ✅ |
|
|
638
|
+
| `audit` | ✅ | ✅ |
|
|
639
|
+
| `idempotency` | ✅ | ✅ |
|
|
638
640
|
|
|
639
641
|
> [!NOTE]
|
|
640
|
-
> La couverture est **adaptée à la nature de chaque backend
|
|
641
|
-
>
|
|
642
|
-
>
|
|
643
|
-
>
|
|
644
|
-
> (`@nodefony/drizzle/package.json`, clé `nodefony.stores`) — elle n'est pas un commentaire.
|
|
642
|
+
> La couverture est **adaptée à la nature de chaque backend** : c'est `@nodefony/redis`, un cache,
|
|
643
|
+
> qui s'arrête à quatre stores (`session`, `tokens`, `passkeys`, `idempotency`) — il ne garde ni
|
|
644
|
+
> comptes ni journal. La liste fait autorité côté code (`nodefony.stores` du `package.json` de chaque
|
|
645
|
+
> adapter) — elle n'est pas un commentaire.
|
|
645
646
|
|
|
646
647
|
## 🧩 Extension — brancher son propre driver
|
|
647
648
|
|
|
@@ -678,7 +679,7 @@ première connexion, reconnexions, erreurs récentes, et une fenêtre de latence
|
|
|
678
679
|
(`ConnectionMonitor.recordPing()`, `ConnectionMonitor.ts:136`). Il est alimenté par `Orm.connect()`
|
|
679
680
|
sans que l'adapter ait à y penser.
|
|
680
681
|
|
|
681
|
-
**`queryFlowMonitor`** (`QueryFlowMonitor.ts:
|
|
682
|
+
**`queryFlowMonitor`** (`QueryFlowMonitor.ts:144`) suit le **débit** : total de requêtes, latence
|
|
682
683
|
moyenne et EWMA, pire latence, et un anneau borné à vingt requêtes lentes. Trois propriétés le
|
|
683
684
|
rendent sûr en production :
|
|
684
685
|
|
|
@@ -690,19 +691,22 @@ rendent sûr en production :
|
|
|
690
691
|
observe, et le débit par seconde est **dérivé côté lecteur** (delta entre deux relevés), donc rien
|
|
691
692
|
n'est muté à la lecture.
|
|
692
693
|
|
|
693
|
-
Le data plane `/nodefony/orm/api/*` (`createOrmAdminApi()`, `OrmAdminApi.ts:541`) expose
|
|
694
|
+
Le data plane `/nodefony/orm/api/*` (`createOrmAdminApi()`, `OrmAdminApi.ts:541`) expose onze points
|
|
694
695
|
d'entrée, tous filtrables par `?connector=` :
|
|
695
696
|
|
|
696
|
-
| Point d'entrée | Ce qu'il rend
|
|
697
|
-
| ------------------- |
|
|
698
|
-
| `orms` | les connecteurs, leur état, leur nombre d'entités
|
|
699
|
-
| `entities` | le modèle complet : colonnes + relations
|
|
700
|
-
| `entity/{name}` | une entité (404 si inconnue)
|
|
701
|
-
| `graph` | le graphe canonique (`buildOrmGraph()`, `OrmAdminApi.ts:227`)
|
|
702
|
-
| `counts` | le nombre de lignes par entité — un `COUNT(*)` par table
|
|
703
|
-
| `connection/health` | état, latence, erreurs, reconnexions, sondes
|
|
704
|
-
| `flow` | débit et requêtes lentes (`buildOrmFlow()`, `OrmAdminApi.ts:360`)
|
|
705
|
-
| `export/{format}` | `dbml` (`toDbml()`, `OrmAdminApi.ts:395`) ou `jsonschema`
|
|
697
|
+
| Point d'entrée | Ce qu'il rend |
|
|
698
|
+
| ------------------- | ------------------------------------------------------------------ |
|
|
699
|
+
| `orms` | les connecteurs, leur état, leur nombre d'entités |
|
|
700
|
+
| `entities` | le modèle complet : colonnes + relations |
|
|
701
|
+
| `entity/{name}` | une entité (404 si inconnue) |
|
|
702
|
+
| `graph` | le graphe canonique (`buildOrmGraph()`, `OrmAdminApi.ts:227`) |
|
|
703
|
+
| `counts` | le nombre de lignes par entité — un `COUNT(*)` par table |
|
|
704
|
+
| `connection/health` | état, latence, erreurs, reconnexions, sondes |
|
|
705
|
+
| `flow` | débit et requêtes lentes (`buildOrmFlow()`, `OrmAdminApi.ts:360`) |
|
|
706
|
+
| `export/{format}` | `dbml` (`toDbml()`, `OrmAdminApi.ts:395`) ou `jsonschema` |
|
|
707
|
+
| `migrations` | l'état des migrations (même objet que `orm:migrate:status --json`) |
|
|
708
|
+
| `migrations/plan` | le SQL en attente |
|
|
709
|
+
| `migrations/apply` | applique les migrations (développement seulement) |
|
|
706
710
|
|
|
707
711
|
Ce graphe canonique est **la pièce maîtresse**, pas le diagramme : c'est une donnée sérialisable qui
|
|
708
712
|
sert à la fois l'ERD de Studio, un export vers un outil tiers, et le contexte d'un agent IA
|
|
@@ -740,9 +744,9 @@ deux, et pas de course).
|
|
|
740
744
|
| `updateOne` rend `null` alors que la ligne a bien changé | ancien réflexe `UPDATE` + relecture (le critère porte sur le champ modifié) | utiliser `updateOne`, atomique par construction (`IRepository.ts:269`) |
|
|
741
745
|
| Un `upsert` écrase une valeur qui devait progresser | le `DO UPDATE` est inconditionnel (contrainte MySQL) | poser la condition **dans** la valeur : `{ seuil: { $max: v } }` (`IRepository.ts:94`) |
|
|
742
746
|
| Un objet de critère est pris pour une égalité (colonne JSON) | comportement **voulu** : une valeur n'est un filtre que si **toutes** ses clés sont des opérateurs | c'est la protection ; pour filtrer dedans, passer au natif (`criteria.ts:42`) |
|
|
743
|
-
| `onOrmReady` ne part plus après un ajout dans l'adapter | `connect()` a été surchargé | surcharger `onConnect()` (`Orm.ts:
|
|
747
|
+
| `onOrmReady` ne part plus après un ajout dans l'adapter | `connect()` a été surchargé | surcharger `onConnect()` (`Orm.ts:363`), jamais `connect()` |
|
|
744
748
|
| Une entité déclarée dans un module reste invisible | le module embarque sa **propre copie** du registre (singleton dédoublé) | externaliser `@nodefony/orm-core` dans le `rolldown.config.ts` du module |
|
|
745
|
-
| Rien dans `flow` alors que la base travaille | la sonde est éteinte hors développement, ou le driver n'a pas de tap | `NF_ORM_FLOW=1` (`ormWiring.ts:96`) ;
|
|
749
|
+
| Rien dans `flow` alors que la base travaille | la sonde est éteinte hors développement, ou le driver n'a pas de tap | `NF_ORM_FLOW=1` (`ormWiring.ts:96`) ; les deux drivers officiels ont un tap |
|
|
746
750
|
|
|
747
751
|
## 🧪 Tests & couverture
|
|
748
752
|
|
package/docs/tutorial-entity.md
CHANGED
|
@@ -285,14 +285,15 @@ fichier, sans rien contourner.
|
|
|
285
285
|
|
|
286
286
|
Deux conséquences du DDL dérivé au boot, à connaître dès maintenant :
|
|
287
287
|
|
|
288
|
-
- la table est créée par un `CREATE TABLE IF NOT EXISTS
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
- la table est créée par un `CREATE TABLE IF NOT EXISTS`, avec ses index et ses clés étrangères ;
|
|
289
|
+
au démarrage suivant, seule une colonne ajoutée qui **accepte le vide** est rattrapée — une colonne
|
|
290
|
+
obligatoire ne l'est jamais ;
|
|
291
|
+
- les `DEFAULT` **SQL** ne sont pas émis. D'où les défauts posés côté JavaScript (`$defaultFn`), qui
|
|
292
|
+
s'appliquent quoi qu'il arrive.
|
|
292
293
|
|
|
293
294
|
> [!TIP]
|
|
294
|
-
> En développement,
|
|
295
|
-
>
|
|
295
|
+
> En développement, une colonne obligatoire ajoutée se prend en compte par `nodefony orm:reset`. En
|
|
296
|
+
> production, cela relève d'une migration (`orm:generate`, puis `orm:migrate`).
|
|
296
297
|
|
|
297
298
|
### Étape 3 — déclarer l'entité
|
|
298
299
|
|
|
@@ -347,20 +348,22 @@ que l'import plus la ligne du décorateur.
|
|
|
347
348
|
Le repository porte **quinze verbes**, pas cinq. Ils se choisissent sur la **garantie** qu'ils
|
|
348
349
|
apportent, jamais sur leur nom.
|
|
349
350
|
|
|
350
|
-
| Verbe | Ce qu'il garantit
|
|
351
|
-
| --------------------- |
|
|
352
|
-
| `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load
|
|
353
|
-
| `create` | insère une ligne et rend sa version persistée (id, défauts)
|
|
354
|
-
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots
|
|
355
|
-
| `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend
|
|
356
|
-
| `updateMany` | modifie toutes les lignes du critère, rend le **nombre**
|
|
357
|
-
| `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction
|
|
358
|
-
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit
|
|
359
|
-
| `delete` | supprime tout ce qui matche, rend le nombre
|
|
360
|
-
| `deleteOne` | supprime **au plus une** ligne, rend un booléen
|
|
361
|
-
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique
|
|
362
|
-
| `count`
|
|
363
|
-
| `
|
|
351
|
+
| Verbe | Ce qu'il garantit | Ancre |
|
|
352
|
+
| --------------------- | ------------------------------------------------------------------------- | ---------------------------- |
|
|
353
|
+
| `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts:230`, `:192` |
|
|
354
|
+
| `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts:240` |
|
|
355
|
+
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts:252` |
|
|
356
|
+
| `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts:269` |
|
|
357
|
+
| `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts:312` |
|
|
358
|
+
| `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts:296` |
|
|
359
|
+
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts:287` |
|
|
360
|
+
| `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts:336` |
|
|
361
|
+
| `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts:345` |
|
|
362
|
+
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts:355` |
|
|
363
|
+
| `count` | compter les entités qui correspondent au critère | `IRepository.ts:362` |
|
|
364
|
+
| `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts:395` |
|
|
365
|
+
| `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts:381` |
|
|
366
|
+
| `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts:406` |
|
|
364
367
|
|
|
365
368
|
> [!IMPORTANT]
|
|
366
369
|
> **Il n'existe pas de méthode `update()`.** Le choix est explicite et il est intentionnel :
|
|
@@ -463,8 +466,8 @@ Sur une ligne dont on sait qu'elle **existe**, ils sont inutiles :
|
|
|
463
466
|
|
|
464
467
|
### Ce que le critère ne couvre pas
|
|
465
468
|
|
|
466
|
-
Les
|
|
467
|
-
|
|
469
|
+
Les sous-requêtes, les agrégats et les jointures arbitraires n'en font pas partie (la disjonction,
|
|
470
|
+
elle, s'exprime par `$or: [...]`). Ce n'est pas un oubli : c'est la limite de ce qui se porte d'un moteur SQL à MongoDB. La
|
|
468
471
|
sortie est `IOrm.getNativeConnection()` (`IOrm.ts:51`), qui rend la connexion brute du driver.
|
|
469
472
|
|
|
470
473
|
Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts:23`), et le message liste les
|
|
@@ -528,8 +531,8 @@ d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCr
|
|
|
528
531
|
| `posts.update is not a function` | la méthode `update()` n'existe pas dans le contrat | `updateOne` pour une ligne (`IRepository.ts:269`), `updateMany` pour un lot (`IRepository.ts:312`) |
|
|
529
532
|
| « no entity registered under "Post" » au premier appel | le fichier d'entité est importé, mais `defineEntity()` est **sans effet de bord** | ajouter l'entité à `@entities([...])` sur le module (`entitiesDecorator.ts:56`) |
|
|
530
533
|
| La table n'existe pas alors que l'entité est déclarée | inscription faite à `onBoot` → course avec l'ouverture du connecteur | inscrire à `onRegister` — c'est ce que fait `entities()` (`entitiesDecorator.ts:56`) |
|
|
531
|
-
| Une colonne ajoutée au schéma reste absente de la table | le
|
|
532
|
-
| Un `DEFAULT` SQL
|
|
534
|
+
| Une colonne ajoutée au schéma reste absente de la table | colonne **obligatoire** : le rattrapage de développement n'ajoute que les colonnes facultatives | `nodefony orm:reset` en développement, ou `orm:generate` puis `orm:migrate` |
|
|
535
|
+
| Un `DEFAULT` SQL déclaré n'apparaît pas | le DDL dérivé ne l'émet pas | poser le défaut côté JavaScript (`$defaultFn`) |
|
|
533
536
|
| Un filtre « champ vide » ne remonte jamais rien | `colonne = NULL` est toujours faux en SQL | `{ champ: { $null: true } }` ou la valeur nue `{ champ: null }` (`IRepository.ts:65`) |
|
|
534
537
|
| `UnknownCriteriaField` sur un champ qui « existe pourtant » | faute de frappe, ou champ calculé absent du schéma | lire les champs connus dans le message ; pour du natif, passer par `getNativeConnection()` |
|
|
535
538
|
| `connector: "sqlite"` ne trouve aucune connexion | `connector` nomme une **connexion**, pas un moteur | mettre la clé de `connectors` (`"default"`) ; le moteur est le `dialect` de la config |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/orm-core",
|
|
3
|
-
"version": "10.0.0-
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
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",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"esm"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"nodefony": "^10.0.0-
|
|
40
|
+
"nodefony": "^10.0.0-beta.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "26.6.
|
|
44
|
-
"nodefony": "^10.0.0-
|
|
45
|
-
"vitest": "5.0.
|
|
43
|
+
"@types/node": "26.6.3",
|
|
44
|
+
"nodefony": "^10.0.0-beta.2",
|
|
45
|
+
"vitest": "5.0.3"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|