@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 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é à "User".
117
+ // `name` vaut par défaut le nom de la classe ; ici forcé à "Member".
106
118
  @entity({
107
119
  connector: "db_principale",
108
- name: "User",
109
- schema: { id: { type: "uuid", primaryKey: true }, email: { type: "string" } },
120
+ name: "Member",
121
+ schema: memberTable,
110
122
  relations: [{ type: "one-to-many", target: "Room", field: "rooms" }],
111
123
  })
112
- class UserEntity {}
124
+ class MemberEntity {}
113
125
 
114
- @repository("repository.user", { entity: "User", connector: "db_principale" })
115
- class UserRepository implements IRepository<UserEntity> {
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 { queryFlowMonitor } from "./nodefony/src/QueryFlowMonitor.js";
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
- * Construit un nœud de graphe pour une entité : relations (toujours) + colonnes
135
- * (via `orm.describeEntity` si l'adapter l'implémente et si l'ORM est connecté).
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 ?? []).map((r) => ({
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 camel = (n) => `${n.charAt(0).toLowerCase()}${n.slice(1)}Id`;
368
+ const key = dbmlId(rel.targetKey ?? "id");
331
369
  if (rel.type === "one-to-many") {
332
- const fk = rel.foreignKey ?? camel(node.name);
333
- refs.add(`Ref: ${dbmlId(rel.target)}.${dbmlId(fk)} > ${dbmlId(node.name)}.id`);
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 ?? camel(rel.target);
336
- refs.add(`Ref: ${dbmlId(node.name)}.${dbmlId(fk)} > ${dbmlId(rel.target)}.id`);
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
  /**
@@ -1,5 +1,5 @@
1
- import { ormRegistry } from "./OrmRegistry.js";
2
1
  import { entityRegistry } from "./EntityRegistry.js";
2
+ import { ormRegistry } from "./OrmRegistry.js";
3
3
  // nodefony/src/orphanEntities.ts
4
4
  /**
5
5
  * Entités inscrites sur un connecteur qu'aucun ORM n'a ouvert.
@@ -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
- /** Relations déclarées. */
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). Un troisième, `indexes` (`IEntityIndex`), déclare les
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 | Cause | Correction |
738
- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
739
- | « 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`) |
740
- | 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`) |
741
- | « 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`) |
742
- | 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`) |
743
- | `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()` |
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#IRepository.updateOne`) |
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#UpdateOperators`) |
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#isFieldOperators`) |
747
- | `onOrmReady` ne part plus après un ajout dans l'adapter | `connect()` a été surchargé | surcharger `onConnect()` (`Orm.ts#Orm.onConnect`), jamais `connect()` |
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 |
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#resolveOrmFlowEnabled`) ; les deux drivers officiels ont un tap |
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
 
@@ -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([...])`. Le schéma vit alors dans son propre fichier, et le point d'entrée ne garde
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.4",
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.4"
40
+ "nodefony": "^10.0.0-beta.6"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "26.6.5",
44
- "nodefony": "^10.0.0-beta.4",
45
- "vitest": "5.0.5"
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",