@nodefony/orm-core 10.0.0-beta.5 → 10.0.0-beta.7

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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/orm-core",
3
- "version": "10.0.0-beta.5",
3
+ "version": "10.0.0-beta.7",
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",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "private": false,
27
27
  "engines": {
28
- "node": ">=24.14.0"
28
+ "node": "^24.16.0 || >=26.0.0"
29
29
  },
30
30
  "keywords": [
31
31
  "nodefony",
@@ -37,12 +37,12 @@
37
37
  "esm"
38
38
  ],
39
39
  "peerDependencies": {
40
- "nodefony": "^10.0.0-beta.5"
40
+ "nodefony": "^10.0.0-beta.7"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "26.6.5",
44
- "nodefony": "^10.0.0-beta.5",
45
- "vitest": "5.0.5"
43
+ "@types/node": "26.6.6",
44
+ "nodefony": "^10.0.0-beta.7",
45
+ "vitest": "5.1.0"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",
@@ -66,7 +66,7 @@
66
66
  "access": "public"
67
67
  },
68
68
  "dependencies": {},
69
- "homepage": "https://nodefony.github.io/nodefony-core/",
69
+ "homepage": "https://nodefony.net/",
70
70
  "bugs": {
71
71
  "url": "https://github.com/nodefony/nodefony-core/issues"
72
72
  }