@nodefony/orm-core 10.0.0-beta.2 → 10.0.0-beta.4

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.
@@ -74,7 +74,7 @@ changer de moteur ne touche pas ton code.
74
74
 
75
75
  Une fois l'entité inscrite, tu ne la manipules plus jamais directement : tu demandes son
76
76
  **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.
77
+ est le contrat `IRepository<T>` (`IRepository.ts#RepositoryReadOptions.relations`), identique quel que soit le moteur.
78
78
 
79
79
  > [!NOTE]
80
80
  > `@nodefony/orm-core` n'est **pas un module à démarrer** : rien à ajouter dans `modules: [...]`.
@@ -87,16 +87,16 @@ Deux partis pris expliquent la forme exacte des étapes qui suivent. Les connaî
87
87
  battre contre le cadre.
88
88
 
89
89
  **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
90
+ descripteur qu'on écrit (`IEntityDefinition`, `defineEntity.ts#IEntityDefinition`) est un `IEntity`
91
+ (`IEntity.ts#IEntity`) **privé de son `connector`** : celui-ci est résolu au démarrage, avec `"default"`
92
+ pour valeur de repli (`DEFAULT_CONNECTOR`, `entitiesDecorator.ts#DEFAULT_CONNECTOR`). Figer la connexion dans le
93
93
  fichier d'entité interdirait de servir la même table depuis une base différente selon
94
94
  l'environnement.
95
95
 
96
96
  **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
97
+ (`defineEntity.ts#defineEntity`) n'a **aucun effet de bord** : importer un fichier d'entité n'inscrit rien.
98
+ C'est le décorateur `entities()` (`entitiesDecorator.ts#entities`), posé sur la classe du module, qui
99
+ inscrit la liste — à la phase `onRegister` (`entitiesDecorator.ts#entities`), strictement **avant** que
100
100
  le connecteur ne s'ouvre et ne crée les tables.
101
101
 
102
102
  Le bénéfice est concret : une entité oubliée se voit **dans une liste**, pas dans un import à
@@ -114,18 +114,18 @@ Le moteur et sa cible physique vivent ici, et nulle part ailleurs. SQLite ne dem
114
114
  installation : c'est le bon choix pour ce premier tour.
115
115
 
116
116
  ```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
- }));
117
+ // nodefony/config/drizzle.ts — le fragment que le générateur a posé, que
118
+ // `nodefony.config.ts` passe à `use("@nodefony/drizzle", drizzleConfig())`.
119
+ // Le connecteur "default" est le NOM de la connexion, pas celui du moteur :
120
+ // c'est `dialect` qui nomme le moteur.
121
+ import type { IDrizzleConfigInput } from "@nodefony/drizzle";
122
+
123
+ export const drizzleConfig = () =>
124
+ ({
125
+ connectors: {
126
+ default: { dialect: "sqlite", filename: "var/databases/app.db" },
127
+ },
128
+ }) satisfies IDrizzleConfigInput;
129
129
  ```
130
130
 
131
131
  ### 2. Décrire la table et l'inscrire
@@ -234,8 +234,8 @@ export async function demo(): Promise<void> {
234
234
  ### Ce qu'on observe
235
235
 
236
236
  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
237
+ « Services & ORM » (`reportOrmBootLines()`, `ormWiring.ts#reportOrmBootLines`). En journal `DEBUG`, chaque entité
238
+ inscrite laisse une ligne `ADD ENTITY` (`entitiesDecorator.ts#entities`) — c'est la preuve la plus
239
239
  directe que le décorateur a bien travaillé.
240
240
 
241
241
  Le data plane confirme depuis l'extérieur, sans ouvrir la base :
@@ -266,12 +266,19 @@ c'est simplement celui que les entités visent quand elles n'en nomment pas d'au
266
266
  Une deuxième base se déclare exactement pareil, sous un autre nom :
267
267
 
268
268
  ```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
- });
269
+ // nodefony/config/drizzle.ts — `NF_ANALYTICS_URL` se déclare dans `env.ts`
270
+ // (`envString`), puis se lit par `ctx.env` : jamais `process.env` ici.
271
+ import type { ConfigContext } from "nodefony";
272
+ import type { IDrizzleConfigInput } from "@nodefony/drizzle";
273
+ import type { env } from "../../env";
274
+
275
+ export const drizzleConfig = (ctx: ConfigContext<typeof env>) =>
276
+ ({
277
+ connectors: {
278
+ default: { dialect: "sqlite", filename: "var/databases/app.db" },
279
+ analytics: { dialect: "postgres", url: ctx.env.NF_ANALYTICS_URL },
280
+ },
281
+ }) satisfies IDrizzleConfigInput;
275
282
  ```
276
283
 
277
284
  Les dialectes disponibles, leurs options et le choix de la cible sont détaillés dans la page du
@@ -297,29 +304,29 @@ Deux conséquences du DDL dérivé au boot, à connaître dès maintenant :
297
304
 
298
305
  ### Étape 3 — déclarer l'entité
299
306
 
300
- `defineEntity()` (`defineEntity.ts:48`) est une fonction d'**identité typée** : elle attache le
307
+ `defineEntity()` (`defineEntity.ts#defineEntity`) est une fonction d'**identité typée** : elle attache le
301
308
  type et rend son argument, rien de plus. Aucun registre n'est touché.
302
309
 
303
310
  Cette absence d'effet de bord est délibérée : elle permet d'importer une entité depuis un test,
304
311
  un script ou un autre module **sans** déclencher son inscription dans un singleton global.
305
312
 
306
313
  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
314
+ (`IEntity.ts#IEntity.domain`), l'axe de classification qui rend navigable une base de plusieurs centaines de
308
315
  tables dans l'ERD Studio.
309
316
 
310
317
  ### Étape 4 — brancher l'entité au module
311
318
 
312
- C'est `entities()` (`entitiesDecorator.ts:56`) qui inscrit, et lui seul. Trois propriétés valent
319
+ C'est `entities()` (`entitiesDecorator.ts#entities`) qui inscrit, et lui seul. Trois propriétés valent
313
320
  d'être connues :
314
321
 
315
- 1. **Phase `onRegister`** (`entitiesDecorator.ts:66`) — strictement avant l'ouverture des
322
+ 1. **Phase `onRegister`** (`entitiesDecorator.ts#entities`) — strictement avant l'ouverture des
316
323
  connexions. C'est ce qui rend l'ordre sûr par construction, contrairement à `@controllers`, qui
317
324
  travaille à `onBoot`.
318
325
  2. **Idempotent** — une entité déjà inscrite sur le même connecteur est ignorée (un module peut
319
326
  être instancié deux fois : tests, rechargement). Une vraie collision, elle, lève
320
- (`EntityRegistry.register()`, `EntityRegistry.ts:24`).
327
+ (`EntityRegistry.register()`, `EntityRegistry.ts#EntityRegistry.register`).
321
328
  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.
329
+ une autre base ; une entité qui porte son propre `connector` (`IEntity.ts#IEntity.connector`) garde le sien.
323
330
 
324
331
  ### Le raccourci — `nodefony create entity`
325
332
 
@@ -348,27 +355,27 @@ que l'import plus la ligne du décorateur.
348
355
  Le repository porte **quinze verbes**, pas cinq. Ils se choisissent sur la **garantie** qu'ils
349
356
  apportent, jamais sur leur nom.
350
357
 
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` |
358
+ | Verbe | Ce qu'il garantit | Ancre |
359
+ | --------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
360
+ | `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts#IRepository.findOne`, `IRepository.ts#RepositoryReadOptions.order` |
361
+ | `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts#IRepository.create` |
362
+ | `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts#IRepository.createMany` |
363
+ | `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts#IRepository.updateOne` |
364
+ | `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts#IRepository.updateMany` |
365
+ | `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts#IRepository.upsert` |
366
+ | `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts#IRepository.upsert` |
367
+ | `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts#IRepository.delete` |
368
+ | `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts#IRepository.deleteOne` |
369
+ | `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts#IRepository.findOneAndDelete` |
370
+ | `count` | compter les entités qui correspondent au critère | `IRepository.ts#IRepository.count` |
371
+ | `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts#IRepository.exists` |
372
+ | `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts#IRepository.countDistinct` |
373
+ | `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts#IRepository.withTransaction` |
367
374
 
368
375
  > [!IMPORTANT]
369
376
  > **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
377
+ > `IRepository.updateOne()` (`IRepository.ts#IRepository.updateOne`) pour une ligne — atomique, et elle **rend** la
378
+ > ligne modifiée —, `IRepository.updateMany()` (`IRepository.ts#IRepository.updateMany`) pour un lot — qui rend le
372
379
  > **nombre** de lignes touchées. Un verbe unique masquerait cette différence de garantie, qui est
373
380
  > précisément ce qu'on veut choisir en connaissance de cause.
374
381
 
@@ -397,8 +404,8 @@ await orm.transaction(async (tx) => {
397
404
  ```
398
405
 
399
406
  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
407
+ (`IRepository.ts#IRepository.exists`) à `findOne(...) !== null` — aucune colonne n'est chargée —, et
408
+ `IRepository.increment()` (`IRepository.ts#IRepository.increment`) à une lecture suivie d'une écriture : une requête
402
409
  au lieu de deux, et pas de course.
403
410
 
404
411
  ## 🔎 Filtrer — les opérateurs de critère
@@ -414,7 +421,7 @@ await posts.find({ title: { $like: "Bon%" } }); // motif SQL
414
421
  await posts.find({ publishedAt: { $null: false } }); // IS NOT NULL
415
422
  ```
416
423
 
417
- **Dix** opérateurs sont reconnus, figés dans `OPERATOR_KEYS` (`criteria.ts:13`) — source unique
424
+ **Dix** opérateurs sont reconnus, figés dans `OPERATOR_KEYS` (`criteria.ts#OPERATOR_KEYS`) — source unique
418
425
  partagée par tous les drivers :
419
426
 
420
427
  | Opérateur | Effet | Opérateur | Effet |
@@ -431,21 +438,21 @@ Plusieurs opérateurs sur un même champ se combinent en **ET**.
431
438
  > **`$null` est celui qu'on oublie, et il coûte cher.** En SQL, `colonne = NULL` est **toujours
432
439
  > faux** : un filtre « la colonne est vide » écrit naïvement ne remonte jamais rien — sans la
433
440
  > 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`).
441
+ > et l'opérateur `{ publishedAt: { $null: true } }` (`FieldOperators.$null`, `IRepository.ts#FieldOperators.$null`).
435
442
  > 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
443
+ > `IRepository.ts#FieldCriteria`) : chercher `IS NULL` sur une colonne non-nullable est une erreur de
437
444
  > raisonnement, et le compilateur la refuse.
438
445
 
439
446
  **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
447
+ (`criteria.ts#isFieldOperators`) ne l'interprète que si **toutes** ses clés sont des opérateurs connus. Une
441
448
  colonne JSON ou un sous-document (`{ meta: { auteur: "…" } }`) reste donc une égalité — c'est ce
442
449
  qui évite qu'une donnée métier soit prise pour une requête.
443
450
 
444
451
  ### Les opérateurs d'écriture — `$max` et `$min`
445
452
 
446
453
  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`).
454
+ `UpdateOperators` (`IRepository.ts#UpdateOperators`), dont les clés reconnues sont listées par
455
+ `UPDATE_OPERATOR_KEYS` (`criteria.ts#UPDATE_OPERATOR_KEYS`).
449
456
 
450
457
  Ils existent parce qu'un upsert **ne peut pas porter de condition** : son `DO UPDATE` s'applique
451
458
  dès qu'il y a conflit de clé (MySQL n'accepte pas de `WHERE` sur `ON DUPLICATE KEY UPDATE`). Pour
@@ -468,16 +475,16 @@ Sur une ligne dont on sait qu'elle **existe**, ils sont inutiles :
468
475
 
469
476
  Les sous-requêtes, les agrégats et les jointures arbitraires n'en font pas partie (la disjonction,
470
477
  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.
478
+ sortie est `IOrm.getNativeConnection()` (`IOrm.ts#IOrm.getNativeConnection`), qui rend la connexion brute du driver.
472
479
 
473
- Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts:23`), et le message liste les
480
+ Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts#UnknownCriteriaField`), et le message liste les
474
481
  champs connus — le diagnostic d'une faute de frappe est immédiat.
475
482
 
476
483
  ## 📄 Pager les résultats
477
484
 
478
- `find()` avec `limit`/`offset`/`order` (`RepositoryReadOptions`, `IRepository.ts:174`) suffit pour
485
+ `find()` avec `limit`/`offset`/`order` (`RepositoryReadOptions`, `IRepository.ts#RepositoryReadOptions`) suffit pour
479
486
  une tranche. Pour une vraie page — celle qui sait s'il y a une suite — utilise `paginate()`
480
- (`paginate.ts:47`) :
487
+ (`paginate.ts#paginate`) :
481
488
 
482
489
  ```ts ignore
483
490
  const page = await paginate(posts, {
@@ -497,7 +504,7 @@ sur une grosse table.
497
504
  ## ⚙️ Ranger le métier dans un service
498
505
 
499
506
  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
507
+ (`AbstractCrudService.ts#AbstractCrudService`) donne le socle : les lectures sont une **délégation pure** (aucun
501
508
  surcoût sur le chemin chaud), les mutations sont encadrées par des points d'extension puis un
502
509
  événement de cycle de vie.
503
510
 
@@ -516,7 +523,7 @@ export class PostService extends AbstractCrudService<PostRow> {
516
523
 
517
524
  L'intérêt est de n'avoir **qu'un seul endroit** à modifier quand la règle change : la même méthode
518
525
  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`)
526
+ d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCrudService.ts#AbstractCrudService.findPage`)
520
527
  — elle ne charge qu'une page, quelle que soit la taille de la table.
521
528
 
522
529
  > [!WARNING]
@@ -526,20 +533,20 @@ d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCr
526
533
 
527
534
  ## ⚠️ Pièges
528
535
 
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 |
536
+ | Symptôme | Cause | Correction |
537
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
538
+ | `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`) |
539
+ | « 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`) |
540
+ | 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`) |
541
+ | 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` |
542
+ | 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`) |
543
+ | 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`) |
544
+ | `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()` |
545
+ | `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 |
546
+ | « 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`) |
547
+ | « 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`) |
548
+ | 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 |
549
+ | 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
550
 
544
551
  ## 🧪 Tests & couverture
545
552
 
@@ -560,7 +567,7 @@ dialecte — `@nodefony/drizzle` (sqlite en mémoire, PostgreSQL et MySQL en end
560
567
  > [!WARNING]
561
568
  > Un compteur vert ne prouve pas qu'une base a été touchée : les bancs sur serveur réel se
562
569
  > **skippent** faute de leur variable d'infra, et un test skippé compte comme vert. La source
563
- > unique de ces variables et des commandes Docker correspondantes est `vitest.gates.ts` à la
570
+ > unique de ces variables et des commandes Docker correspondantes est `scripts/test/vitest/gates.ts` à la
564
571
  > racine du dépôt ; les suites concernées affichent leur récapitulatif en fin d'exécution.
565
572
 
566
573
  ## 🔗 Pour aller plus loin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/orm-core",
3
- "version": "10.0.0-beta.2",
3
+ "version": "10.0.0-beta.4",
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.2"
40
+ "nodefony": "^10.0.0-beta.4"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "26.6.3",
44
- "nodefony": "^10.0.0-beta.2",
45
- "vitest": "5.0.3"
43
+ "@types/node": "26.6.5",
44
+ "nodefony": "^10.0.0-beta.4",
45
+ "vitest": "5.0.5"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",