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

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.
@@ -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 à
@@ -74,7 +94,7 @@ changer de moteur ne touche pas ton code.
74
94
 
75
95
  Une fois l'entité inscrite, tu ne la manipules plus jamais directement : tu demandes son
76
96
  **repository**, et c'est lui qui porte les verbes (`find`, `create`, `updateOne`…). Un repository
77
- est le contrat `IRepository<T>` (`IRepository.ts:176`), identique quel que soit le moteur.
97
+ est le contrat `IRepository<T>` (`IRepository.ts#RepositoryReadOptions.relations`), identique quel que soit le moteur.
78
98
 
79
99
  > [!NOTE]
80
100
  > `@nodefony/orm-core` n'est **pas un module à démarrer** : rien à ajouter dans `modules: [...]`.
@@ -87,16 +107,16 @@ Deux partis pris expliquent la forme exacte des étapes qui suivent. Les connaî
87
107
  battre contre le cadre.
88
108
 
89
109
  **1. Le connecteur est une donnée de configuration, jamais de code.** C'est pourquoi le
90
- descripteur qu'on écrit (`IEntityDefinition`, `defineEntity.ts:15`) est un `IEntity`
91
- (`IEntity.ts:51`) **privé de son `connector`** : celui-ci est résolu au démarrage, avec `"default"`
92
- pour valeur de repli (`DEFAULT_CONNECTOR`, `entitiesDecorator.ts:12`). Figer la connexion dans le
110
+ descripteur qu'on écrit (`IEntityDefinition`, `defineEntity.ts#IEntityDefinition`) est un `IEntity`
111
+ (`IEntity.ts#IEntity`) **privé de son `connector`** : celui-ci est résolu au démarrage, avec `"default"`
112
+ pour valeur de repli (`DEFAULT_CONNECTOR`, `entitiesDecorator.ts#DEFAULT_CONNECTOR`). Figer la connexion dans le
93
113
  fichier d'entité interdirait de servir la même table depuis une base différente selon
94
114
  l'environnement.
95
115
 
96
116
  **2. L'inscription est déclarative, et elle a lieu tôt.** `defineEntity()`
97
- (`defineEntity.ts:48`) n'a **aucun effet de bord** : importer un fichier d'entité n'inscrit rien.
98
- C'est le décorateur `entities()` (`entitiesDecorator.ts:56`), posé sur la classe du module, qui
99
- inscrit la liste — à la phase `onRegister` (`entitiesDecorator.ts:66`), strictement **avant** que
117
+ (`defineEntity.ts#defineEntity`) n'a **aucun effet de bord** : importer un fichier d'entité n'inscrit rien.
118
+ C'est le décorateur `entities()` (`entitiesDecorator.ts#entities`), posé sur la classe du module, qui
119
+ inscrit la liste — à la phase `onRegister` (`entitiesDecorator.ts#entities`), strictement **avant** que
100
120
  le connecteur ne s'ouvre et ne crée les tables.
101
121
 
102
122
  Le bénéfice est concret : une entité oubliée se voit **dans une liste**, pas dans un import à
@@ -114,18 +134,18 @@ Le moteur et sa cible physique vivent ici, et nulle part ailleurs. SQLite ne dem
114
134
  installation : c'est le bon choix pour ce premier tour.
115
135
 
116
136
  ```ts
117
- // nodefony.config.ts — le connecteur "default" est le NOM de la connexion,
118
- // pas celui du moteur : c'est `dialect` qui nomme le moteur.
119
- export default defineConfig(() => ({
120
- modules: [
121
- "@nodefony/framework",
122
- use("@nodefony/drizzle", {
123
- connectors: {
124
- default: { dialect: "sqlite", filename: "nodefony/databases/app.db" },
125
- },
126
- }),
127
- ],
128
- }));
137
+ // nodefony/config/drizzle.ts — le fragment que le générateur a posé, que
138
+ // `nodefony.config.ts` passe à `use("@nodefony/drizzle", drizzleConfig())`.
139
+ // Le connecteur "default" est le NOM de la connexion, pas celui du moteur :
140
+ // c'est `dialect` qui nomme le moteur.
141
+ import type { IDrizzleConfigInput } from "@nodefony/drizzle";
142
+
143
+ export const drizzleConfig = () =>
144
+ ({
145
+ connectors: {
146
+ default: { dialect: "sqlite", filename: "var/databases/app.db" },
147
+ },
148
+ }) satisfies IDrizzleConfigInput;
129
149
  ```
130
150
 
131
151
  ### 2. Décrire la table et l'inscrire
@@ -234,8 +254,8 @@ export async function demo(): Promise<void> {
234
254
  ### Ce qu'on observe
235
255
 
236
256
  Au démarrage, le driver crée la table et le récap de boot affiche la connexion sous
237
- « Services & ORM » (`reportOrmBootLines()`, `ormWiring.ts:60`). En journal `DEBUG`, chaque entité
238
- inscrite laisse une ligne `ADD ENTITY` (`entitiesDecorator.ts:84`) — c'est la preuve la plus
257
+ « Services & ORM » (`reportOrmBootLines()`, `ormWiring.ts#reportOrmBootLines`). En journal `DEBUG`, chaque entité
258
+ inscrite laisse une ligne `ADD ENTITY` (`entitiesDecorator.ts#entities`) — c'est la preuve la plus
239
259
  directe que le décorateur a bien travaillé.
240
260
 
241
261
  Le data plane confirme depuis l'extérieur, sans ouvrir la base :
@@ -266,12 +286,19 @@ c'est simplement celui que les entités visent quand elles n'en nomment pas d'au
266
286
  Une deuxième base se déclare exactement pareil, sous un autre nom :
267
287
 
268
288
  ```ts ignore
269
- use("@nodefony/drizzle", {
270
- connectors: {
271
- default: { dialect: "sqlite", filename: "nodefony/databases/app.db" },
272
- analytics: { dialect: "postgres", url: process.env.PG_URL },
273
- },
274
- });
289
+ // nodefony/config/drizzle.ts — `NF_ANALYTICS_URL` se déclare dans `env.ts`
290
+ // (`envString`), puis se lit par `ctx.env` : jamais `process.env` ici.
291
+ import type { ConfigContext } from "nodefony";
292
+ import type { IDrizzleConfigInput } from "@nodefony/drizzle";
293
+ import type { env } from "../../env";
294
+
295
+ export const drizzleConfig = (ctx: ConfigContext<typeof env>) =>
296
+ ({
297
+ connectors: {
298
+ default: { dialect: "sqlite", filename: "var/databases/app.db" },
299
+ analytics: { dialect: "postgres", url: ctx.env.NF_ANALYTICS_URL },
300
+ },
301
+ }) satisfies IDrizzleConfigInput;
275
302
  ```
276
303
 
277
304
  Les dialectes disponibles, leurs options et le choix de la cible sont détaillés dans la page du
@@ -297,29 +324,29 @@ Deux conséquences du DDL dérivé au boot, à connaître dès maintenant :
297
324
 
298
325
  ### Étape 3 — déclarer l'entité
299
326
 
300
- `defineEntity()` (`defineEntity.ts:48`) est une fonction d'**identité typée** : elle attache le
327
+ `defineEntity()` (`defineEntity.ts#defineEntity`) est une fonction d'**identité typée** : elle attache le
301
328
  type et rend son argument, rien de plus. Aucun registre n'est touché.
302
329
 
303
330
  Cette absence d'effet de bord est délibérée : elle permet d'importer une entité depuis un test,
304
331
  un script ou un autre module **sans** déclencher son inscription dans un singleton global.
305
332
 
306
333
  Les champs facultatifs qui servent tôt : `module` (qui apporte l'entité) et `domain`
307
- (`IEntity.ts:72`), l'axe de classification qui rend navigable une base de plusieurs centaines de
334
+ (`IEntity.ts#IEntity.domain`), l'axe de classification qui rend navigable une base de plusieurs centaines de
308
335
  tables dans l'ERD Studio.
309
336
 
310
337
  ### Étape 4 — brancher l'entité au module
311
338
 
312
- C'est `entities()` (`entitiesDecorator.ts:56`) qui inscrit, et lui seul. Trois propriétés valent
339
+ C'est `entities()` (`entitiesDecorator.ts#entities`) qui inscrit, et lui seul. Trois propriétés valent
313
340
  d'être connues :
314
341
 
315
- 1. **Phase `onRegister`** (`entitiesDecorator.ts:66`) — strictement avant l'ouverture des
342
+ 1. **Phase `onRegister`** (`entitiesDecorator.ts#entities`) — strictement avant l'ouverture des
316
343
  connexions. C'est ce qui rend l'ordre sûr par construction, contrairement à `@controllers`, qui
317
344
  travaille à `onBoot`.
318
345
  2. **Idempotent** — une entité déjà inscrite sur le même connecteur est ignorée (un module peut
319
346
  être instancié deux fois : tests, rechargement). Une vraie collision, elle, lève
320
- (`EntityRegistry.register()`, `EntityRegistry.ts:24`).
347
+ (`EntityRegistry.register()`, `EntityRegistry.ts#EntityRegistry.register`).
321
348
  3. **Connecteur commun** — `entities([...], { connector: "analytics" })` pose toute la liste sur
322
- une autre base ; une entité qui porte son propre `connector` (`IEntity.ts:56`) garde le sien.
349
+ une autre base ; une entité qui porte son propre `connector` (`IEntity.ts#IEntity.connector`) garde le sien.
323
350
 
324
351
  ### Le raccourci — `nodefony create entity`
325
352
 
@@ -330,11 +357,14 @@ champs se déclarent en positionnels, façon Rails :
330
357
  nodefony create entity Post title:string content:text views:int
331
358
  # --id uuid7|uuid4|serial --soft-delete --no-timestamps
332
359
  # --module <nom> --no-controller --connector <nom>
360
+ # --no-migrate (n'écrit pas la migration : la sortie la conseille)
333
361
  ```
334
362
 
335
363
  Elle écrit le fichier d'entité (`nodefony/entity/Post.ts`), les schémas de validation, un service
336
364
  CRUD, un controller REST + WebSocket et les tests — puis **câble elle-même** `@entities([...])` et
337
- `@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
338
368
  que l'import plus la ligne du décorateur.
339
369
 
340
370
  > [!WARNING]
@@ -348,30 +378,36 @@ que l'import plus la ligne du décorateur.
348
378
  Le repository porte **quinze verbes**, pas cinq. Ils se choisissent sur la **garantie** qu'ils
349
379
  apportent, jamais sur leur nom.
350
380
 
351
- | Verbe | Ce qu'il garantit | Ancre |
352
- | --------------------- | ------------------------------------------------------------------------- | ---------------------------- |
353
- | `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts:230`, `:192` |
354
- | `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts:240` |
355
- | `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts:252` |
356
- | `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts:269` |
357
- | `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts:312` |
358
- | `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts:296` |
359
- | `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts:287` |
360
- | `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts:336` |
361
- | `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts:345` |
362
- | `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts:355` |
363
- | `count` | compter les entités qui correspondent au critère | `IRepository.ts:362` |
364
- | `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts:395` |
365
- | `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts:381` |
366
- | `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts:406` |
381
+ | Verbe | Ce qu'il garantit | Ancre |
382
+ | --------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
383
+ | `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts#IRepository.findOne`, `IRepository.ts#RepositoryReadOptions.order` |
384
+ | `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts#IRepository.create` |
385
+ | `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts#IRepository.createMany` |
386
+ | `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts#IRepository.updateOne` |
387
+ | `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts#IRepository.updateMany` |
388
+ | `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts#IRepository.upsert` |
389
+ | `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts#IRepository.upsert` |
390
+ | `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts#IRepository.delete` |
391
+ | `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts#IRepository.deleteOne` |
392
+ | `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts#IRepository.findOneAndDelete` |
393
+ | `count` | compter les entités qui correspondent au critère | `IRepository.ts#IRepository.count` |
394
+ | `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts#IRepository.exists` |
395
+ | `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts#IRepository.countDistinct` |
396
+ | `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts#IRepository.withTransaction` |
367
397
 
368
398
  > [!IMPORTANT]
369
399
  > **Il n'existe pas de méthode `update()`.** Le choix est explicite et il est intentionnel :
370
- > `IRepository.updateOne()` (`IRepository.ts:269`) pour une ligne — atomique, et elle **rend** la
371
- > ligne modifiée —, `IRepository.updateMany()` (`IRepository.ts:312`) pour un lot — qui rend le
400
+ > `IRepository.updateOne()` (`IRepository.ts#IRepository.updateOne`) pour une ligne — atomique, et elle **rend** la
401
+ > ligne modifiée —, `IRepository.updateMany()` (`IRepository.ts#IRepository.updateMany`) pour un lot — qui rend le
372
402
  > **nombre** de lignes touchées. Un verbe unique masquerait cette différence de garantie, qui est
373
403
  > précisément ce qu'on veut choisir en connaissance de cause.
374
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
+
375
411
  Quelques usages, un par garantie :
376
412
 
377
413
  ```ts ignore
@@ -397,8 +433,8 @@ await orm.transaction(async (tx) => {
397
433
  ```
398
434
 
399
435
  Deux réflexes de performance, dès le premier jour : préférer `IRepository.exists()`
400
- (`IRepository.ts:395`) à `findOne(...) !== null` — aucune colonne n'est chargée —, et
401
- `IRepository.increment()` (`IRepository.ts:325`) à une lecture suivie d'une écriture : une requête
436
+ (`IRepository.ts#IRepository.exists`) à `findOne(...) !== null` — aucune colonne n'est chargée —, et
437
+ `IRepository.increment()` (`IRepository.ts#IRepository.increment`) à une lecture suivie d'une écriture : une requête
402
438
  au lieu de deux, et pas de course.
403
439
 
404
440
  ## 🔎 Filtrer — les opérateurs de critère
@@ -414,7 +450,7 @@ await posts.find({ title: { $like: "Bon%" } }); // motif SQL
414
450
  await posts.find({ publishedAt: { $null: false } }); // IS NOT NULL
415
451
  ```
416
452
 
417
- **Dix** opérateurs sont reconnus, figés dans `OPERATOR_KEYS` (`criteria.ts:13`) — source unique
453
+ **Dix** opérateurs sont reconnus, figés dans `OPERATOR_KEYS` (`criteria.ts#OPERATOR_KEYS`) — source unique
418
454
  partagée par tous les drivers :
419
455
 
420
456
  | Opérateur | Effet | Opérateur | Effet |
@@ -431,21 +467,21 @@ Plusieurs opérateurs sur un même champ se combinent en **ET**.
431
467
  > **`$null` est celui qu'on oublie, et il coûte cher.** En SQL, `colonne = NULL` est **toujours
432
468
  > faux** : un filtre « la colonne est vide » écrit naïvement ne remonte jamais rien — sans la
433
469
  > moindre erreur. Deux formes équivalentes le résolvent : la valeur nue `{ publishedAt: null }`
434
- > et l'opérateur `{ publishedAt: { $null: true } }` (`FieldOperators.$null`, `IRepository.ts:65`).
470
+ > et l'opérateur `{ publishedAt: { $null: true } }` (`FieldOperators.$null`, `IRepository.ts#FieldOperators.$null`).
435
471
  > La valeur nue n'est ouverte par le typage que si le champ est **nullable** (`FieldCriteria`,
436
- > `IRepository.ts:128`) : chercher `IS NULL` sur une colonne non-nullable est une erreur de
472
+ > `IRepository.ts#FieldCriteria`) : chercher `IS NULL` sur une colonne non-nullable est une erreur de
437
473
  > raisonnement, et le compilateur la refuse.
438
474
 
439
475
  **Comment un objet est reconnu comme filtre plutôt que comme valeur** : `isFieldOperators()`
440
- (`criteria.ts:42`) ne l'interprète que si **toutes** ses clés sont des opérateurs connus. Une
476
+ (`criteria.ts#isFieldOperators`) ne l'interprète que si **toutes** ses clés sont des opérateurs connus. Une
441
477
  colonne JSON ou un sous-document (`{ meta: { auteur: "…" } }`) reste donc une égalité — c'est ce
442
478
  qui évite qu'une donnée métier soit prise pour une requête.
443
479
 
444
480
  ### Les opérateurs d'écriture — `$max` et `$min`
445
481
 
446
482
  Une seconde famille, distincte, s'applique **en écriture** dans un `upsert` :
447
- `UpdateOperators` (`IRepository.ts:94`), dont les clés reconnues sont listées par
448
- `UPDATE_OPERATOR_KEYS` (`criteria.ts:67`).
483
+ `UpdateOperators` (`IRepository.ts#UpdateOperators`), dont les clés reconnues sont listées par
484
+ `UPDATE_OPERATOR_KEYS` (`criteria.ts#UPDATE_OPERATOR_KEYS`).
449
485
 
450
486
  Ils existent parce qu'un upsert **ne peut pas porter de condition** : son `DO UPDATE` s'applique
451
487
  dès qu'il y a conflit de clé (MySQL n'accepte pas de `WHERE` sur `ON DUPLICATE KEY UPDATE`). Pour
@@ -468,16 +504,16 @@ Sur une ligne dont on sait qu'elle **existe**, ils sont inutiles :
468
504
 
469
505
  Les sous-requêtes, les agrégats et les jointures arbitraires n'en font pas partie (la disjonction,
470
506
  elle, s'exprime par `$or: [...]`). Ce n'est pas un oubli : c'est la limite de ce qui se porte d'un moteur SQL à MongoDB. La
471
- sortie est `IOrm.getNativeConnection()` (`IOrm.ts:51`), qui rend la connexion brute du driver.
507
+ sortie est `IOrm.getNativeConnection()` (`IOrm.ts#IOrm.getNativeConnection`), qui rend la connexion brute du driver.
472
508
 
473
- Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts:23`), et le message liste les
509
+ Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts#UnknownCriteriaField`), et le message liste les
474
510
  champs connus — le diagnostic d'une faute de frappe est immédiat.
475
511
 
476
512
  ## 📄 Pager les résultats
477
513
 
478
- `find()` avec `limit`/`offset`/`order` (`RepositoryReadOptions`, `IRepository.ts:174`) suffit pour
514
+ `find()` avec `limit`/`offset`/`order` (`RepositoryReadOptions`, `IRepository.ts#RepositoryReadOptions`) suffit pour
479
515
  une tranche. Pour une vraie page — celle qui sait s'il y a une suite — utilise `paginate()`
480
- (`paginate.ts:47`) :
516
+ (`paginate.ts#paginate`) :
481
517
 
482
518
  ```ts ignore
483
519
  const page = await paginate(posts, {
@@ -497,7 +533,7 @@ sur une grosse table.
497
533
  ## ⚙️ Ranger le métier dans un service
498
534
 
499
535
  Dès que la logique dépasse l'appel direct, elle quitte le controller. `AbstractCrudService`
500
- (`AbstractCrudService.ts:37`) donne le socle : les lectures sont une **délégation pure** (aucun
536
+ (`AbstractCrudService.ts#AbstractCrudService`) donne le socle : les lectures sont une **délégation pure** (aucun
501
537
  surcoût sur le chemin chaud), les mutations sont encadrées par des points d'extension puis un
502
538
  événement de cycle de vie.
503
539
 
@@ -516,7 +552,7 @@ export class PostService extends AbstractCrudService<PostRow> {
516
552
 
517
553
  L'intérêt est de n'avoir **qu'un seul endroit** à modifier quand la règle change : la même méthode
518
554
  sert la route REST, l'appel WebSocket, un résolveur GraphQL et une commande CLI. Pour toute liste
519
- d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCrudService.ts:110`)
555
+ d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCrudService.ts#AbstractCrudService.findPage`)
520
556
  — elle ne charge qu'une page, quelle que soit la taille de la table.
521
557
 
522
558
  > [!WARNING]
@@ -526,20 +562,20 @@ d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCr
526
562
 
527
563
  ## ⚠️ Pièges
528
564
 
529
- | Symptôme | Cause | Correction |
530
- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
531
- | `posts.update is not a function` | la méthode `update()` n'existe pas dans le contrat | `updateOne` pour une ligne (`IRepository.ts:269`), `updateMany` pour un lot (`IRepository.ts:312`) |
532
- | « no entity registered under "Post" » au premier appel | le fichier d'entité est importé, mais `defineEntity()` est **sans effet de bord** | ajouter l'entité à `@entities([...])` sur le module (`entitiesDecorator.ts:56`) |
533
- | La table n'existe pas alors que l'entité est déclarée | inscription faite à `onBoot` → course avec l'ouverture du connecteur | inscrire à `onRegister` — c'est ce que fait `entities()` (`entitiesDecorator.ts:56`) |
534
- | Une colonne ajoutée au schéma reste absente de la table | colonne **obligatoire** : le rattrapage de développement n'ajoute que les colonnes facultatives | `nodefony orm:reset` en développement, ou `orm:generate` puis `orm:migrate` |
535
- | Un `DEFAULT` SQL déclaré n'apparaît pas | le DDL dérivé ne l'émet pas | poser le défaut côté JavaScript (`$defaultFn`) |
536
- | Un filtre « champ vide » ne remonte jamais rien | `colonne = NULL` est toujours faux en SQL | `{ champ: { $null: true } }` ou la valeur nue `{ champ: null }` (`IRepository.ts:65`) |
537
- | `UnknownCriteriaField` sur un champ qui « existe pourtant » | faute de frappe, ou champ calculé absent du schéma | lire les champs connus dans le message ; pour du natif, passer par `getNativeConnection()` |
538
- | `connector: "sqlite"` ne trouve aucune connexion | `connector` nomme une **connexion**, pas un moteur | mettre la clé de `connectors` (`"default"`) ; le moteur est le `dialect` de la config |
539
- | « entity "Post" exists on multiple connectors … specify one » | la même entité est inscrite sur deux connexions | préciser laquelle : `entityRegistry.get("Post", "analytics")` (`EntityRegistry.ts:54`) |
540
- | « no ORM registered under "default" » au premier appel | le repository est demandé avant l'ouverture du connecteur, ou le driver n'est pas dans `modules` | construire le service **au premier usage**, pas au chargement (`ormRegistry.get()`, `OrmRegistry.ts:45`) |
541
- | 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 |
542
- | Une transaction ne couvre pas les deux bases | une transaction porte sur **un seul** connecteur (2PC non garanti) | rassembler les écritures atomiques sur une seule connexion |
565
+ | Symptôme | Cause | Correction |
566
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
567
+ | `posts.update is not a function` | la méthode `update()` n'existe pas dans le contrat | `updateOne` pour une ligne (`IRepository.ts#IRepository.updateOne`), `updateMany` pour un lot (`IRepository.ts#IRepository.updateMany`) |
568
+ | « no entity registered under "Post" » au premier appel | le fichier d'entité est importé, mais `defineEntity()` est **sans effet de bord** | ajouter l'entité à `@entities([...])` sur le module (`entitiesDecorator.ts#entities`) |
569
+ | La table n'existe pas alors que l'entité est déclarée | inscription faite à `onBoot` → course avec l'ouverture du connecteur | inscrire à `onRegister` — c'est ce que fait `entities()` (`entitiesDecorator.ts#entities`) |
570
+ | Une colonne ajoutée au schéma reste absente de la table | colonne **obligatoire** : le rattrapage de développement n'ajoute que les colonnes facultatives | `nodefony orm:reset` en développement, ou `orm:generate` puis `orm:migrate` |
571
+ | Un `DEFAULT` SQL déclaré n'apparaît pas | le DDL dérivé ne l'émet pas | poser le défaut côté JavaScript (`$defaultFn`) |
572
+ | 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`) |
573
+ | `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()` |
574
+ | `connector: "sqlite"` ne trouve aucune connexion | `connector` nomme une **connexion**, pas un moteur | mettre la clé de `connectors` (`"default"`) ; le moteur est le `dialect` de la config |
575
+ | « entity "Post" exists on multiple connectors … specify one » | la même entité est inscrite sur deux connexions | préciser laquelle : `entityRegistry.get("Post", "analytics")` (`EntityRegistry.ts#EntityRegistry.get`) |
576
+ | « no ORM registered under "default" » au premier appel | le repository est demandé avant l'ouverture du connecteur, ou le driver n'est pas dans `modules` | construire le service **au premier usage**, pas au chargement (`ormRegistry.get()`, `OrmRegistry.ts#OrmRegistry.get`) |
577
+ | 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 |
578
+ | Une transaction ne couvre pas les deux bases | une transaction porte sur **un seul** connecteur (2PC non garanti) | rassembler les écritures atomiques sur une seule connexion |
543
579
 
544
580
  ## 🧪 Tests & couverture
545
581
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/orm-core",
3
- "version": "10.0.0-beta.3",
3
+ "version": "10.0.0-beta.5",
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.0.0"
28
+ "node": ">=24.14.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.3"
40
+ "nodefony": "^10.0.0-beta.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "26.6.5",
44
- "nodefony": "^10.0.0-beta.3",
45
- "vitest": "5.0.4"
44
+ "nodefony": "^10.0.0-beta.5",
45
+ "vitest": "5.0.5"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",