@nodefony/orm-core 10.0.0-beta.4 → 10.0.0-beta.6
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 +21 -6
- package/dist/index.js +3 -3
- package/dist/nodefony/src/OrmAdminApi.js +52 -14
- package/dist/nodefony/src/decorators/index.js +1 -1
- package/dist/nodefony/src/ormWiring.js +1 -1
- package/dist/nodefony/src/orphanEntities.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IEntity.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +20 -1
- package/dist/types/nodefony/interfaces/index.d.ts +1 -1
- package/dist/types/nodefony/src/OrmAdminApi.d.ts +8 -0
- package/docs/index.md +19 -14
- package/docs/tutorial-entity.md +30 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -99,20 +99,35 @@ du module**, sur la classe et non sur une instance : il connaît donc son nom et
|
|
|
99
99
|
instancier, et enregistre l'entité directement.
|
|
100
100
|
|
|
101
101
|
```typescript
|
|
102
|
+
import { randomUUID } from "node:crypto";
|
|
102
103
|
import { entity, repository } from "@nodefony/orm-core";
|
|
103
104
|
import type { IRepository } from "@nodefony/orm-core";
|
|
105
|
+
import { defineTable } from "@nodefony/drizzle";
|
|
106
|
+
|
|
107
|
+
// Le `schema` est la forme de l'ADAPTATEUR : ici la description portable du kit
|
|
108
|
+
// Drizzle, qui suit le dialecte du connecteur.
|
|
109
|
+
const memberTable = defineTable({
|
|
110
|
+
name: "Member",
|
|
111
|
+
columns: {
|
|
112
|
+
id: { kind: "uuid", primaryKey: true, defaultFn: () => randomUUID() },
|
|
113
|
+
email: { kind: "string", length: 255, notNull: true },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
104
116
|
|
|
105
|
-
// `name` vaut par défaut le nom de la classe ; ici forcé à "
|
|
117
|
+
// `name` vaut par défaut le nom de la classe ; ici forcé à "Member".
|
|
106
118
|
@entity({
|
|
107
119
|
connector: "db_principale",
|
|
108
|
-
name: "
|
|
109
|
-
schema:
|
|
120
|
+
name: "Member",
|
|
121
|
+
schema: memberTable,
|
|
110
122
|
relations: [{ type: "one-to-many", target: "Room", field: "rooms" }],
|
|
111
123
|
})
|
|
112
|
-
class
|
|
124
|
+
class MemberEntity {}
|
|
113
125
|
|
|
114
|
-
@repository("repository.
|
|
115
|
-
|
|
126
|
+
@repository("repository.member", {
|
|
127
|
+
entity: "Member",
|
|
128
|
+
connector: "db_principale",
|
|
129
|
+
})
|
|
130
|
+
class MemberRepository implements IRepository<MemberEntity> {
|
|
116
131
|
/* find, findOne, create… sont fournis par l'adaptateur ORM */
|
|
117
132
|
}
|
|
118
133
|
```
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,13 @@ import { assertOrderOption } from "./nodefony/src/readOptions.js";
|
|
|
4
4
|
import { OrmRegistry, ormRegistry } from "./nodefony/src/OrmRegistry.js";
|
|
5
5
|
import { EntityRegistry, entityRegistry } from "./nodefony/src/EntityRegistry.js";
|
|
6
6
|
import { reportOrphanEntities } from "./nodefony/src/orphanEntities.js";
|
|
7
|
-
import { connectionMonitor } from "./nodefony/src/ConnectionMonitor.js";
|
|
8
7
|
import { Orm } from "./nodefony/src/Orm.js";
|
|
9
8
|
import { Entity } from "./nodefony/src/Entity.js";
|
|
10
|
-
import { paginate } from "./nodefony/src/paginate.js";
|
|
11
9
|
import { AbstractCrudService } from "./nodefony/src/AbstractCrudService.js";
|
|
12
|
-
import {
|
|
10
|
+
import { paginate } from "./nodefony/src/paginate.js";
|
|
13
11
|
import { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, toDbml, toJsonSchema } from "./nodefony/src/OrmAdminApi.js";
|
|
12
|
+
import { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
|
|
13
|
+
import { connectionMonitor } from "./nodefony/src/ConnectionMonitor.js";
|
|
14
14
|
import { buildOrmLeanHealth } from "./nodefony/src/buildOrmLeanHealth.js";
|
|
15
15
|
import { REJECTED_MARKER, describeConnectFailure, diagnoseConnectionFailure, parseConnectionTarget } from "./nodefony/src/connectionDiagnosis.js";
|
|
16
16
|
import { CONNECT_TIMEOUT_MS, withConnectDeadline } from "./nodefony/src/connectDeadline.js";
|
|
@@ -131,8 +131,51 @@ function buildOrmSummaries() {
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
134
|
+
* Clé étrangère d'une relation qui n'en nomme aucune : `<entité>Id`, l'entité
|
|
135
|
+
* en minuscule initiale — la convention des adapters.
|
|
136
|
+
*
|
|
137
|
+
* @param entity - nom logique de l'entité qui donne son nom à la clé.
|
|
138
|
+
* @returns le nom de colonne conventionnel.
|
|
139
|
+
*/
|
|
140
|
+
function conventionalForeignKey(entity) {
|
|
141
|
+
return `${entity.charAt(0).toLowerCase()}${entity.slice(1)}Id`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Relations d'un nœud : celles que l'entité DÉCLARE, puis une « plusieurs vers
|
|
145
|
+
* un » par clé étrangère que la base porte et qu'aucune déclaration ne couvre.
|
|
146
|
+
*
|
|
147
|
+
* Sans la seconde moitié, une entité qui référence par ses colonnes (`ref` du
|
|
148
|
+
* kit, table écrite à la main) sans répéter le lien dans `relations` n'avait
|
|
149
|
+
* AUCUNE arête dans le graphe, alors que la contrainte existe en base.
|
|
150
|
+
* Une déclaration couvre une colonne quand sa clé — explicite, ou
|
|
151
|
+
* conventionnelle — est cette colonne.
|
|
152
|
+
*/
|
|
153
|
+
function nodeRelations(declared, columns) {
|
|
154
|
+
const relations = declared.map((r) => ({
|
|
155
|
+
type: r.type,
|
|
156
|
+
target: r.target,
|
|
157
|
+
field: r.field,
|
|
158
|
+
foreignKey: r.foreignKey
|
|
159
|
+
}));
|
|
160
|
+
let covered = null;
|
|
161
|
+
for (const col of columns) {
|
|
162
|
+
if (col.references === void 0) continue;
|
|
163
|
+
covered ??= new Set(declared.filter((r) => r.type === "many-to-one" || r.type === "one-to-one").map((r) => r.foreignKey ?? conventionalForeignKey(r.target)));
|
|
164
|
+
if (covered.has(col.name)) continue;
|
|
165
|
+
relations.push({
|
|
166
|
+
type: "many-to-one",
|
|
167
|
+
target: col.references.entity,
|
|
168
|
+
field: col.name,
|
|
169
|
+
foreignKey: col.name,
|
|
170
|
+
targetKey: col.references.column
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return relations;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Construit un nœud de graphe pour une entité : colonnes (via
|
|
177
|
+
* `orm.describeEntity` si l'adapter l'implémente et si l'ORM est connecté) et
|
|
178
|
+
* relations, déclarées puis imposées par les clés étrangères.
|
|
136
179
|
*/
|
|
137
180
|
function buildEntityNode(connector, name) {
|
|
138
181
|
const entity = entityRegistry.get(name, connector);
|
|
@@ -148,12 +191,7 @@ function buildEntityNode(connector, name) {
|
|
|
148
191
|
module: entity.module ?? "",
|
|
149
192
|
domain: entity.domain ?? "",
|
|
150
193
|
columns,
|
|
151
|
-
relations: (entity.relations ?? [])
|
|
152
|
-
type: r.type,
|
|
153
|
-
target: r.target,
|
|
154
|
-
field: r.field,
|
|
155
|
-
foreignKey: r.foreignKey
|
|
156
|
-
}))
|
|
194
|
+
relations: nodeRelations(entity.relations ?? [], columns)
|
|
157
195
|
};
|
|
158
196
|
}
|
|
159
197
|
/** Construit le graphe canonique complet (optionnellement filtré par connecteur). */
|
|
@@ -327,13 +365,13 @@ function toDbml(graph) {
|
|
|
327
365
|
refs.add(`// many-to-many ${node.name}.${rel.field} <> ${rel.target} (table de jonction)`);
|
|
328
366
|
continue;
|
|
329
367
|
}
|
|
330
|
-
const
|
|
368
|
+
const key = dbmlId(rel.targetKey ?? "id");
|
|
331
369
|
if (rel.type === "one-to-many") {
|
|
332
|
-
const fk = rel.foreignKey ??
|
|
333
|
-
refs.add(`Ref: ${dbmlId(rel.target)}.${dbmlId(fk)} > ${dbmlId(node.name)}
|
|
370
|
+
const fk = rel.foreignKey ?? conventionalForeignKey(node.name);
|
|
371
|
+
refs.add(`Ref: ${dbmlId(rel.target)}.${dbmlId(fk)} > ${dbmlId(node.name)}.${key}`);
|
|
334
372
|
} else {
|
|
335
|
-
const fk = rel.foreignKey ??
|
|
336
|
-
refs.add(`Ref: ${dbmlId(node.name)}.${dbmlId(fk)} > ${dbmlId(rel.target)}
|
|
373
|
+
const fk = rel.foreignKey ?? conventionalForeignKey(rel.target);
|
|
374
|
+
refs.add(`Ref: ${dbmlId(node.name)}.${dbmlId(fk)} > ${dbmlId(rel.target)}.${key}`);
|
|
337
375
|
}
|
|
338
376
|
}
|
|
339
377
|
lines.push(...refs);
|
|
@@ -529,4 +567,4 @@ function registerOrmAdminApi(registry) {
|
|
|
529
567
|
if (registry.has("orm")) return;
|
|
530
568
|
registry.register(createOrmAdminApi());
|
|
531
569
|
}
|
|
532
|
-
export { buildConnectionHealth, buildOrmFlow, buildOrmGraph, createOrmAdminApi, registerOrmAdminApi, storeCarrier, toDbml, toJsonSchema };
|
|
570
|
+
export { buildConnectionHealth, buildOrmFlow, buildOrmGraph, conventionalForeignKey, createOrmAdminApi, registerOrmAdminApi, storeCarrier, toDbml, toJsonSchema };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta } from "./metadataStore.js";
|
|
2
1
|
import { entity } from "./entityDecorator.js";
|
|
3
2
|
import { DEFAULT_CONNECTOR, entities } from "./entitiesDecorator.js";
|
|
4
3
|
import { repository } from "./repositoryDecorator.js";
|
|
4
|
+
import { getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta } from "./metadataStore.js";
|
|
5
5
|
export { DEFAULT_CONNECTOR, entities, entity, getEntityMeta, getRepositoryMeta, hasEntityMeta, hasRepositoryMeta, repository };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ormRegistry } from "./OrmRegistry.js";
|
|
2
1
|
import { buildConnectionHealth, buildOrmFlow, registerOrmAdminApi } from "./OrmAdminApi.js";
|
|
3
2
|
import { buildOrmLeanHealth } from "./buildOrmLeanHealth.js";
|
|
3
|
+
import { ormRegistry } from "./OrmRegistry.js";
|
|
4
4
|
import { setOrmHealthProvider, setOrmRichProvider } from "nodefony";
|
|
5
5
|
// nodefony/src/ormWiring.ts
|
|
6
6
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type { ConnectionVerdict, IConnectionDiagnosis, IConnectionTarget, } from
|
|
|
31
31
|
export { CONNECT_TIMEOUT_MS, withConnectDeadline, } from "./nodefony/src/connectDeadline.js";
|
|
32
32
|
export { wireOrmAdminPlane, resolveOrmFlowEnabled, reportOrmBootLines, } from "./nodefony/src/ormWiring.js";
|
|
33
33
|
export type { ISlowQuery, IQueryFlow, IOrmFlowReport, } from "./nodefony/interfaces/IOrmFlow.js";
|
|
34
|
-
export type { IColumnInfo, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionEvent, IOrmResilience, IConnectionHealth, } from "./nodefony/interfaces/IOrmGraph.js";
|
|
34
|
+
export type { IColumnInfo, IColumnReference, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionEvent, IOrmResilience, IConnectionHealth, } from "./nodefony/interfaces/IOrmGraph.js";
|
|
35
35
|
export type { ILatencyWindow, IOrmStorageProbe, IOrmPoolProbe, IOrmProbe, } from "./nodefony/interfaces/IOrmProbe.js";
|
|
36
36
|
export type { IOrmMigrationAction, IOrmMigrationEntry, IOrmMigrationSource, IOrmMigrationStatus, IOrmMigrationFailure, IOrmMigrationReply, IOrmPendingMigration, IOrmMigrationPlan, IOrmMigrationPlanReply, IOrmMigrationApplied, IOrmMigrationApplyReply, } from "./nodefony/interfaces/IOrmMigrations.js";
|
|
37
37
|
export { isMigrationFailure } from "./nodefony/interfaces/IOrmMigrations.js";
|
|
@@ -81,4 +81,16 @@ export interface IEntity<S = unknown, M = unknown> {
|
|
|
81
81
|
* dans leur table → ce champ y est sans effet. **Optionnel**.
|
|
82
82
|
*/
|
|
83
83
|
readonly indexes?: ReadonlyArray<IEntityIndex> | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Champs qui ne sortent JAMAIS par une relation : quand une autre entité
|
|
86
|
+
* charge celle-ci (`relations: ["owner"]`, `?include=owner`), l'adapter ne les
|
|
87
|
+
* lit pas. Le dépôt de l'entité elle-même les rend toujours — c'est la porte
|
|
88
|
+
* de qui en a besoin (l'authentification lit `password` par là).
|
|
89
|
+
*
|
|
90
|
+
* Sans cette déclaration, référencer un `User` exposait son empreinte de mot
|
|
91
|
+
* de passe à quiconque lisait la ressource qui le référence. Un nom qui ne
|
|
92
|
+
* désigne aucun champ fait échouer la connexion en le nommant : une faute de
|
|
93
|
+
* frappe ici rouvrirait la fuite sans un mot. **Optionnel**.
|
|
94
|
+
*/
|
|
95
|
+
readonly privateFields?: ReadonlyArray<string> | undefined;
|
|
84
96
|
}
|
|
@@ -22,6 +22,20 @@ export interface IColumnInfo {
|
|
|
22
22
|
nullable: boolean;
|
|
23
23
|
/** `true` si contrainte d'unicité. */
|
|
24
24
|
unique: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Cible de la clé étrangère que la BASE porte sur cette colonne — absente si
|
|
27
|
+
* la colonne n'en porte pas, ou si l'adapter ne sait pas la lire. Le graphe
|
|
28
|
+
* en dérive une relation « plusieurs vers un » quand aucune relation déclarée
|
|
29
|
+
* ne couvre déjà la colonne.
|
|
30
|
+
*/
|
|
31
|
+
references?: IColumnReference | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Cible d'une clé étrangère : l'entité visée et la colonne qu'elle y désigne. */
|
|
34
|
+
export interface IColumnReference {
|
|
35
|
+
/** Nom logique de l'entité visée (nom de table si aucune entité ne la porte). */
|
|
36
|
+
entity: string;
|
|
37
|
+
/** Colonne visée (en général la clé primaire). */
|
|
38
|
+
column: string;
|
|
25
39
|
}
|
|
26
40
|
/** Relation projetée pour le graphe (sous-ensemble sérialisable de `IEntityRelation`). */
|
|
27
41
|
export interface IRelationInfo {
|
|
@@ -33,6 +47,8 @@ export interface IRelationInfo {
|
|
|
33
47
|
field: string;
|
|
34
48
|
/** Clé étrangère (explicite ou dérivée déterministe). */
|
|
35
49
|
foreignKey?: string | undefined;
|
|
50
|
+
/** Colonne visée sur la cible — `id` par convention quand elle est absente. */
|
|
51
|
+
targetKey?: string | undefined;
|
|
36
52
|
}
|
|
37
53
|
/** Nœud du graphe : une entité avec ses colonnes et ses relations. */
|
|
38
54
|
export interface IEntityGraphNode {
|
|
@@ -51,7 +67,10 @@ export interface IEntityGraphNode {
|
|
|
51
67
|
domain: string;
|
|
52
68
|
/** Colonnes normalisées (vide si l'adapter n'implémente pas `describeEntity`). */
|
|
53
69
|
columns: IColumnInfo[];
|
|
54
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Relations : celles que l'entité DÉCLARE, puis celles que ses clés
|
|
72
|
+
* étrangères imposent et qu'aucune déclaration ne couvre.
|
|
73
|
+
*/
|
|
55
74
|
relations: IRelationInfo[];
|
|
56
75
|
}
|
|
57
76
|
/** Connexion sous-jacente d'un ORM (extrait via {@link IOrm.describeConnection}). */
|
|
@@ -3,5 +3,5 @@ export type { IEntity, IEntityIndex, IEntityRelation } from "./IEntity.js";
|
|
|
3
3
|
export type { IRepository, OrmCriteria, Criteria, FieldCriteria, FieldOperators, UpdateData, UpdateOperators, RepositoryReadOptions, } from "./IRepository.js";
|
|
4
4
|
export type { ITransaction } from "./ITransaction.js";
|
|
5
5
|
export type { IPage, IPageQuery, PageQuery } from "./IPage.js";
|
|
6
|
-
export type { IColumnInfo, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionEvent, IOrmResilience, IConnectionHealth, } from "./IOrmGraph.js";
|
|
6
|
+
export type { IColumnInfo, IColumnReference, IConnectionInfo, IRelationInfo, IEntityGraphNode, IOrmSummary, IOrmGraph, IConnectionError, IConnectionEvent, IOrmResilience, IConnectionHealth, } from "./IOrmGraph.js";
|
|
7
7
|
export type { ILatencyWindow, IOrmStorageProbe, IOrmPoolProbe, IOrmProbe, } from "./IOrmProbe.js";
|
|
@@ -17,6 +17,14 @@ import type { IOrmFlowReport } from "../interfaces/IOrmFlow.js";
|
|
|
17
17
|
* un ORM (le repli sur le nom `default` s'applique alors).
|
|
18
18
|
*/
|
|
19
19
|
export declare function storeCarrier(resolutions: readonly IStoreResolution[], names: readonly string[]): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Clé étrangère d'une relation qui n'en nomme aucune : `<entité>Id`, l'entité
|
|
22
|
+
* en minuscule initiale — la convention des adapters.
|
|
23
|
+
*
|
|
24
|
+
* @param entity - nom logique de l'entité qui donne son nom à la clé.
|
|
25
|
+
* @returns le nom de colonne conventionnel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function conventionalForeignKey(entity: string): string;
|
|
20
28
|
/** Construit le graphe canonique complet (optionnellement filtré par connecteur). */
|
|
21
29
|
export declare function buildOrmGraph(connectorFilter?: string): IOrmGraph;
|
|
22
30
|
/**
|
package/docs/index.md
CHANGED
|
@@ -478,7 +478,11 @@ atomique sur les quatre backends.
|
|
|
478
478
|
d'un moteur), un `schema` natif du driver, et des `relations` déclaratives (`IEntityRelation`,
|
|
479
479
|
`IEntity.ts#IEntityRelation`). Deux champs facultatifs servent la lisibilité d'un gros modèle : `module` (qui
|
|
480
480
|
apporte l'entité) et `domain` (`IEntity.ts#IEntity.domain`, la classification métier — l'axe qui rend navigable
|
|
481
|
-
une base de plusieurs centaines de tables
|
|
481
|
+
une base de plusieurs centaines de tables ; `create entity --domain` le pose). Le graphe d'entités ne
|
|
482
|
+
se limite pas aux `relations` déclarées : une colonne qui porte une clé étrangère en base en rend la
|
|
483
|
+
cible (`IOrmGraph.ts#IColumnReference`, lue par `describeEntity`), et le graphe en tire une
|
|
484
|
+
relation « plusieurs vers un » quand aucune déclaration ne couvre déjà la colonne
|
|
485
|
+
(`OrmAdminApi.ts#conventionalForeignKey` dit quelle colonne une déclaration couvre). Un troisième, `indexes` (`IEntityIndex`), déclare les
|
|
482
486
|
index COMPOSITES qu'un adaptateur ne sait pas exprimer dans son schéma : Mongoose les applique
|
|
483
487
|
(`schema.index()`), Drizzle les ignore, puisqu'il les déclare dans sa table.
|
|
484
488
|
|
|
@@ -734,19 +738,20 @@ deux, et pas de course).
|
|
|
734
738
|
|
|
735
739
|
## ⚠️ Pièges
|
|
736
740
|
|
|
737
|
-
| Symptôme
|
|
738
|
-
|
|
|
739
|
-
| « no entity registered under "X" » au premier appel
|
|
740
|
-
| La table n'existe pas alors que l'entité est déclarée
|
|
741
|
-
| « entity "User" exists on multiple connectors … specify one »
|
|
742
|
-
| Un filtre « champ vide » ne remonte jamais rien
|
|
743
|
-
| `UnknownCriteriaField` sur un champ qui existe « pourtant »
|
|
744
|
-
| `updateOne` rend `null` alors que la ligne a bien changé
|
|
745
|
-
| Un `upsert` écrase une valeur qui devait progresser
|
|
746
|
-
| Un objet de critère est pris pour une égalité (colonne JSON)
|
|
747
|
-
| `onOrmReady` ne part plus après un ajout dans l'adapter
|
|
748
|
-
| Une entité déclarée dans un module reste invisible
|
|
749
|
-
| Rien dans `flow` alors que la base travaille
|
|
741
|
+
| Symptôme | Cause | Correction |
|
|
742
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
743
|
+
| « no entity registered under "X" » au premier appel | l'entité n'a jamais été inscrite (fichier importé mais `defineEntity` est sans effet de bord) | l'ajouter à `@entities([...])` sur le module (`entitiesDecorator.ts#entities`) |
|
|
744
|
+
| La table n'existe pas alors que l'entité est déclarée | inscription faite à `onBoot` → course avec `connect()` | inscrire à `onRegister` — c'est ce que fait `entities()` (`entitiesDecorator.ts#entities`) |
|
|
745
|
+
| « entity "User" exists on multiple connectors … specify one » | la même entité vit sur plusieurs connecteurs | préciser le connecteur : `entityRegistry.get("User", "analytics")` (`EntityRegistry.ts#EntityRegistry.get`) |
|
|
746
|
+
| 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#FieldOperators.$null`) |
|
|
747
|
+
| `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()` |
|
|
748
|
+
| `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#IRepository.updateOne`) |
|
|
749
|
+
| 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#UpdateOperators`) |
|
|
750
|
+
| 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#isFieldOperators`) |
|
|
751
|
+
| `onOrmReady` ne part plus après un ajout dans l'adapter | `connect()` a été surchargé | surcharger `onConnect()` (`Orm.ts#Orm.onConnect`), jamais `connect()` |
|
|
752
|
+
| 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 |
|
|
753
|
+
| 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#resolveOrmFlowEnabled`) ; les deux drivers officiels ont un tap |
|
|
754
|
+
| Un champ vaut `null` dans la réponse alors que la colonne est remplie | repository maison : la fonction qui transforme une ligne en entité ne recopie pas cette colonne — rien ne lève, le champ optionnel reste vide | faire porter au mapping **chaque** champ exposé, et un test qui écrit une ligne complète puis compare tous les champs relus (`IRepository.ts#IRepository`) |
|
|
750
755
|
|
|
751
756
|
## 🧪 Tests & couverture
|
|
752
757
|
|
package/docs/tutorial-entity.md
CHANGED
|
@@ -23,6 +23,26 @@ source: "src/packages/@nodefony/orm-core/docs/tutorial-entity.md"
|
|
|
23
23
|
|
|
24
24
|
📍 [Documentation](../../../../../docs/index.md) › [ORM — le socle](index.md) › **Créer une entité**
|
|
25
25
|
|
|
26
|
+
## ⚡ Le geste — une commande
|
|
27
|
+
|
|
28
|
+
Dans une application, une entité ne s'écrit pas à la main : `create entity` pose tout ce que cette
|
|
29
|
+
page construit pas à pas, câblé et testé.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx nodefony create entity Post title:string content:text views:int --route /api/posts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Elle écrit la table (`nodefony/entity/Post.ts`), ses schémas de validation, un service CRUD, un
|
|
36
|
+
contrôleur REST dont les lectures répondent aussi par la socket, un test unitaire et un test e2e,
|
|
37
|
+
puis inscrit l'entité et le contrôleur dans `index.ts`. Si l'application versionne ses migrations
|
|
38
|
+
(toute application complète), elle écrit puis applique celle de la table à la base de
|
|
39
|
+
développement ; sinon, la table naît au prochain démarrage. Si la base ne répond pas, sa sortie donne la commande à
|
|
40
|
+
relancer (`npx nodefony orm:generate --name posts --apply`). Les options :
|
|
41
|
+
[le raccourci](#le-raccourci--nodefony-create-entity).
|
|
42
|
+
|
|
43
|
+
La suite de la page construit la même entité à la main : c'est ce qui permet de comprendre ce que
|
|
44
|
+
la commande écrit, et de le retoucher.
|
|
45
|
+
|
|
26
46
|
## 🧠 Schéma général
|
|
27
47
|
|
|
28
48
|
Trois objets s'emboîtent, toujours dans le même ordre. Le tutoriel les parcourt de gauche à
|
|
@@ -337,11 +357,14 @@ champs se déclarent en positionnels, façon Rails :
|
|
|
337
357
|
nodefony create entity Post title:string content:text views:int
|
|
338
358
|
# --id uuid7|uuid4|serial --soft-delete --no-timestamps
|
|
339
359
|
# --module <nom> --no-controller --connector <nom>
|
|
360
|
+
# --no-migrate (n'écrit pas la migration : la sortie la conseille)
|
|
340
361
|
```
|
|
341
362
|
|
|
342
363
|
Elle écrit le fichier d'entité (`nodefony/entity/Post.ts`), les schémas de validation, un service
|
|
343
364
|
CRUD, un controller REST + WebSocket et les tests — puis **câble elle-même** `@entities([...])` et
|
|
344
|
-
`@controllers([...])
|
|
365
|
+
`@controllers([...])`, et migre la table neuve quand l'application versionne son schéma — mais seulement si la migration ne touche
|
|
366
|
+
qu'elle : une autre entité modifiée en attente laisse le fichier écrit, NON appliqué, et la sortie
|
|
367
|
+
nomme les tables en trop. Le schéma vit alors dans son propre fichier, et le point d'entrée ne garde
|
|
345
368
|
que l'import plus la ligne du décorateur.
|
|
346
369
|
|
|
347
370
|
> [!WARNING]
|
|
@@ -379,6 +402,12 @@ apportent, jamais sur leur nom.
|
|
|
379
402
|
> **nombre** de lignes touchées. Un verbe unique masquerait cette différence de garantie, qui est
|
|
380
403
|
> précisément ce qu'on veut choisir en connaissance de cause.
|
|
381
404
|
|
|
405
|
+
**Une clé qui vaut `undefined` vaut une clé absente.** Les écritures du service CRUD acceptent
|
|
406
|
+
`PartialInput<T>` (`types/PartialInput.ts#PartialInput`) et retirent ces clés avant la persistance
|
|
407
|
+
(`AbstractCrudService.updateOne()`, `AbstractCrudService.ts#AbstractCrudService.updateOne`) : un champ
|
|
408
|
+
facultatif d'un schéma Zod (`.optional()`, `.partial()`) qui arrive `undefined` ne touche pas la
|
|
409
|
+
colonne. Pour la **vider**, écrire `null`.
|
|
410
|
+
|
|
382
411
|
Quelques usages, un par garantie :
|
|
383
412
|
|
|
384
413
|
```ts ignore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/orm-core",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.6",
|
|
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-beta.
|
|
40
|
+
"nodefony": "^10.0.0-beta.6"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "26.6.
|
|
44
|
-
"nodefony": "^10.0.0-beta.
|
|
45
|
-
"vitest": "5.0
|
|
43
|
+
"@types/node": "26.6.6",
|
|
44
|
+
"nodefony": "^10.0.0-beta.6",
|
|
45
|
+
"vitest": "5.1.0"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|