@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.
- package/dist/nodefony/interfaces/IOrmMigrations.js +1 -2
- package/dist/nodefony/src/AbstractCrudService.js +1 -2
- package/dist/nodefony/src/ConnectionMonitor.js +1 -2
- package/dist/nodefony/src/Entity.js +1 -2
- package/dist/nodefony/src/EntityRegistry.js +1 -2
- package/dist/nodefony/src/Orm.js +1 -2
- package/dist/nodefony/src/OrmAdminApi.js +4 -5
- package/dist/nodefony/src/OrmRegistry.js +1 -2
- package/dist/nodefony/src/QueryFlowMonitor.js +1 -2
- package/dist/nodefony/src/buildOrmLeanHealth.js +1 -2
- package/dist/nodefony/src/connectDeadline.js +1 -2
- package/dist/nodefony/src/connectionDiagnosis.js +72 -15
- package/dist/nodefony/src/criteria.js +1 -2
- package/dist/nodefony/src/decorators/entitiesDecorator.js +1 -2
- package/dist/nodefony/src/decorators/entityDecorator.js +1 -2
- package/dist/nodefony/src/decorators/metadataStore.js +3 -4
- package/dist/nodefony/src/decorators/repositoryDecorator.js +1 -2
- package/dist/nodefony/src/defineEntity.js +1 -2
- package/dist/nodefony/src/errors.js +1 -2
- package/dist/nodefony/src/omitUndefined.js +1 -2
- package/dist/nodefony/src/ormWiring.js +1 -2
- package/dist/nodefony/src/orphanEntities.js +1 -2
- package/dist/nodefony/src/paginate.js +1 -2
- package/dist/nodefony/src/readOptions.js +1 -2
- package/dist/types/nodefony/src/connectionDiagnosis.d.ts +9 -1
- package/docs/index.md +133 -132
- package/docs/tutorial-entity.md +116 -80
- package/package.json +5 -5
package/docs/tutorial-entity.md
CHANGED
|
@@ -23,6 +23,26 @@ source: "src/packages/@nodefony/orm-core/docs/tutorial-entity.md"
|
|
|
23
23
|
|
|
24
24
|
📍 [Documentation](../../../../../docs/index.md) › [ORM — le socle](index.md) › **Créer une entité**
|
|
25
25
|
|
|
26
|
+
## ⚡ Le geste — une commande
|
|
27
|
+
|
|
28
|
+
Dans une application, une entité ne s'écrit pas à la main : `create entity` pose tout ce que cette
|
|
29
|
+
page construit pas à pas, câblé et testé.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx nodefony create entity Post title:string content:text views:int --route /api/posts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Elle écrit la table (`nodefony/entity/Post.ts`), ses schémas de validation, un service CRUD, un
|
|
36
|
+
contrôleur REST dont les lectures répondent aussi par la socket, un test unitaire et un test e2e,
|
|
37
|
+
puis inscrit l'entité et le contrôleur dans `index.ts`. Si l'application versionne ses migrations
|
|
38
|
+
(toute application complète), elle écrit puis applique celle de la table à la base de
|
|
39
|
+
développement ; sinon, la table naît au prochain démarrage. Si la base ne répond pas, sa sortie donne la commande à
|
|
40
|
+
relancer (`npx nodefony orm:generate --name posts --apply`). Les options :
|
|
41
|
+
[le raccourci](#le-raccourci--nodefony-create-entity).
|
|
42
|
+
|
|
43
|
+
La suite de la page construit la même entité à la main : c'est ce qui permet de comprendre ce que
|
|
44
|
+
la commande écrit, et de le retoucher.
|
|
45
|
+
|
|
26
46
|
## 🧠 Schéma général
|
|
27
47
|
|
|
28
48
|
Trois objets s'emboîtent, toujours dans le même ordre. Le tutoriel les parcourt de gauche à
|
|
@@ -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
|
|
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
|
|
91
|
-
(`IEntity.ts
|
|
92
|
-
pour valeur de repli (`DEFAULT_CONNECTOR`, `entitiesDecorator.ts
|
|
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
|
|
98
|
-
C'est le décorateur `entities()` (`entitiesDecorator.ts
|
|
99
|
-
inscrit la liste — à la phase `onRegister` (`entitiesDecorator.ts
|
|
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
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
238
|
-
inscrite laisse une ligne `ADD ENTITY` (`entitiesDecorator.ts
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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([...])
|
|
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
|
|
354
|
-
| `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts
|
|
355
|
-
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts
|
|
356
|
-
| `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts
|
|
357
|
-
| `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts
|
|
358
|
-
| `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts
|
|
359
|
-
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts
|
|
360
|
-
| `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts
|
|
361
|
-
| `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts
|
|
362
|
-
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts
|
|
363
|
-
| `count` | compter les entités qui correspondent au critère | `IRepository.ts
|
|
364
|
-
| `exists` | savoir s'il y en a au moins une, sans charger de colonne | `IRepository.ts
|
|
365
|
-
| `countDistinct` | compter les valeurs DISTINCTES et non nulles d'un champ, sans les charger | `IRepository.ts
|
|
366
|
-
| `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts
|
|
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
|
|
371
|
-
> ligne modifiée —, `IRepository.updateMany()` (`IRepository.ts
|
|
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
|
|
401
|
-
`IRepository.increment()` (`IRepository.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
448
|
-
`UPDATE_OPERATOR_KEYS` (`criteria.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
"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.
|
|
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.
|
|
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.
|
|
45
|
-
"vitest": "5.0.
|
|
44
|
+
"nodefony": "^10.0.0-beta.5",
|
|
45
|
+
"vitest": "5.0.5"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|