@nodefony/orm-core 10.0.0-alpha.9 → 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/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 +35 -34
- package/docs/tutorial-entity.md +16 -14
- package/package.json +5 -5
|
@@ -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,
|
|
@@ -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:866`) 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:
|
|
623
|
+
| Colonnes pour l'ERD (`describeEntity`) | oui (`DrizzleOrm.ts:1971`) | oui (`MongooseOrm.ts:649`) |
|
|
622
624
|
| Sonde de flux (requêtes/s, lentes) | oui — alimente `queryFlowMonitor` | non câblée |
|
|
623
|
-
| Sonde profonde (`probe`) | oui (`DrizzleOrm.ts:
|
|
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
|
|
|
@@ -740,7 +741,7 @@ deux, et pas de course).
|
|
|
740
741
|
| `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
742
|
| 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
743
|
| 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:
|
|
744
|
+
| `onOrmReady` ne part plus après un ajout dans l'adapter | `connect()` a été surchargé | surcharger `onConnect()` (`Orm.ts:363`), jamais `connect()` |
|
|
744
745
|
| 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
746
|
| 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`) ; le tap n'est câblé que côté Drizzle |
|
|
746
747
|
|
package/docs/tutorial-entity.md
CHANGED
|
@@ -347,20 +347,22 @@ que l'import plus la ligne du décorateur.
|
|
|
347
347
|
Le repository porte **quinze verbes**, pas cinq. Ils se choisissent sur la **garantie** qu'ils
|
|
348
348
|
apportent, jamais sur leur nom.
|
|
349
349
|
|
|
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
|
-
| `
|
|
350
|
+
| Verbe | Ce qu'il garantit | Ancre |
|
|
351
|
+
| --------------------- | ------------------------------------------------------------------------- | ---------------------------- |
|
|
352
|
+
| `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts:230`, `:192` |
|
|
353
|
+
| `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts:240` |
|
|
354
|
+
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts:252` |
|
|
355
|
+
| `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts:269` |
|
|
356
|
+
| `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts:312` |
|
|
357
|
+
| `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts:296` |
|
|
358
|
+
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts:287` |
|
|
359
|
+
| `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts:336` |
|
|
360
|
+
| `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts:345` |
|
|
361
|
+
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts:355` |
|
|
362
|
+
| `count` | compter les entités qui correspondent au critère | `IRepository.ts:362` |
|
|
363
|
+
| `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts:395` |
|
|
364
|
+
| `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts:381` |
|
|
365
|
+
| `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts:406` |
|
|
364
366
|
|
|
365
367
|
> [!IMPORTANT]
|
|
366
368
|
> **Il n'existe pas de méthode `update()`.** Le choix est explicite et il est intentionnel :
|
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.1",
|
|
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.1"
|
|
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.1",
|
|
45
|
+
"vitest": "5.0.2"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|