@nodefony/orm-core 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +131 -0
- package/dist/index.js +22 -0
- package/dist/nodefony/interfaces/IEntity.js +1 -0
- package/dist/nodefony/interfaces/IOrm.js +1 -0
- package/dist/nodefony/interfaces/IOrmFlow.js +1 -0
- package/dist/nodefony/interfaces/IOrmGraph.js +1 -0
- package/dist/nodefony/interfaces/IOrmMigrations.js +12 -0
- package/dist/nodefony/interfaces/IOrmProbe.js +1 -0
- package/dist/nodefony/interfaces/IPage.js +1 -0
- package/dist/nodefony/interfaces/IRepository.js +1 -0
- package/dist/nodefony/interfaces/ITransaction.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/AbstractCrudService.js +199 -0
- package/dist/nodefony/src/ConnectionMonitor.js +181 -0
- package/dist/nodefony/src/Entity.js +42 -0
- package/dist/nodefony/src/EntityRegistry.js +109 -0
- package/dist/nodefony/src/Orm.js +297 -0
- package/dist/nodefony/src/OrmAdminApi.js +491 -0
- package/dist/nodefony/src/OrmRegistry.js +75 -0
- package/dist/nodefony/src/QueryFlowMonitor.js +128 -0
- package/dist/nodefony/src/buildOrmLeanHealth.js +54 -0
- package/dist/nodefony/src/criteria.js +176 -0
- package/dist/nodefony/src/decorators/entitiesDecorator.js +67 -0
- package/dist/nodefony/src/decorators/entityDecorator.js +51 -0
- package/dist/nodefony/src/decorators/index.js +5 -0
- package/dist/nodefony/src/decorators/metadataStore.js +38 -0
- package/dist/nodefony/src/decorators/repositoryDecorator.js +35 -0
- package/dist/nodefony/src/defineEntity.js +27 -0
- package/dist/nodefony/src/errors.js +76 -0
- package/dist/nodefony/src/ormWiring.js +78 -0
- package/dist/nodefony/src/paginate.js +55 -0
- package/dist/nodefony/src/readOptions.js +52 -0
- package/dist/nodefony/src/serviceWiring.js +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IEntity.d.ts +65 -0
- package/dist/types/nodefony/interfaces/IOrm.d.ts +136 -0
- package/dist/types/nodefony/interfaces/IOrmFlow.d.ts +71 -0
- package/dist/types/nodefony/interfaces/IOrmGraph.d.ts +165 -0
- package/dist/types/nodefony/interfaces/IOrmMigrations.d.ts +145 -0
- package/dist/types/nodefony/interfaces/IOrmProbe.d.ts +70 -0
- package/dist/types/nodefony/interfaces/IPage.d.ts +24 -0
- package/dist/types/nodefony/interfaces/IRepository.d.ts +369 -0
- package/dist/types/nodefony/interfaces/ITransaction.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/src/AbstractCrudService.d.ts +158 -0
- package/dist/types/nodefony/src/ConnectionMonitor.d.ts +86 -0
- package/dist/types/nodefony/src/Entity.d.ts +44 -0
- package/dist/types/nodefony/src/EntityRegistry.d.ts +60 -0
- package/dist/types/nodefony/src/Orm.d.ts +197 -0
- package/dist/types/nodefony/src/OrmAdminApi.d.ts +84 -0
- package/dist/types/nodefony/src/OrmRegistry.d.ts +58 -0
- package/dist/types/nodefony/src/QueryFlowMonitor.d.ts +53 -0
- package/dist/types/nodefony/src/buildOrmLeanHealth.d.ts +15 -0
- package/dist/types/nodefony/src/criteria.d.ts +131 -0
- package/dist/types/nodefony/src/decorators/entitiesDecorator.d.ts +48 -0
- package/dist/types/nodefony/src/decorators/entityDecorator.d.ts +49 -0
- package/dist/types/nodefony/src/decorators/index.d.ts +10 -0
- package/dist/types/nodefony/src/decorators/metadataStore.d.ts +54 -0
- package/dist/types/nodefony/src/decorators/repositoryDecorator.d.ts +30 -0
- package/dist/types/nodefony/src/defineEntity.d.ts +43 -0
- package/dist/types/nodefony/src/errors.d.ts +62 -0
- package/dist/types/nodefony/src/ormWiring.d.ts +48 -0
- package/dist/types/nodefony/src/paginate.d.ts +43 -0
- package/dist/types/nodefony/src/readOptions.d.ts +21 -0
- package/dist/types/nodefony/src/serviceWiring.d.ts +24 -0
- package/docs/index.md +791 -0
- package/docs/tutorial-entity.md +577 -0
- package/package.json +73 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Créer une entité — du premier fichier à la première requête"
|
|
3
|
+
navTitle: Créer une entité
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/orm-core"
|
|
6
|
+
topic: orm-core
|
|
7
|
+
section: "Données"
|
|
8
|
+
audience: [developer]
|
|
9
|
+
tags:
|
|
10
|
+
[orm, entite, repository, tutoriel, crud, criteres, operateurs, drizzle, page]
|
|
11
|
+
version: "doc"
|
|
12
|
+
status: stable
|
|
13
|
+
updated: 2026-07-19
|
|
14
|
+
source: "src/packages/@nodefony/orm-core/docs/tutorial-entity.md"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Créer une entité — du premier fichier à la première requête
|
|
18
|
+
|
|
19
|
+
> Le pas-à-pas d'une première table : déclarer la connexion, décrire le schéma, inscrire
|
|
20
|
+
> l'entité, obtenir le repository, lire et écrire. Rien n'est supposé connu, et le code de
|
|
21
|
+
> chaque étape est complet. À la fin, une table existe en base et ton code la lit sans jamais
|
|
22
|
+
> nommer Drizzle.
|
|
23
|
+
|
|
24
|
+
📍 [Documentation](../../../../../docs/index.md) › [ORM — le socle](index.md) › **Créer une entité**
|
|
25
|
+
|
|
26
|
+
## 🧠 Schéma général
|
|
27
|
+
|
|
28
|
+
Trois objets s'emboîtent, toujours dans le même ordre. Le tutoriel les parcourt de gauche à
|
|
29
|
+
droite.
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
CFG["1 · nodefony.config.ts<br/><b>le connecteur</b><br/>« default » = sqlite/postgres/mysql"]
|
|
34
|
+
ENT["2 · nodefony/entity/Post.ts<br/><b>l'entité</b><br/>nom logique + schéma natif"]
|
|
35
|
+
MOD["3 · index.ts<br/><b>@entities([PostEntity])</b><br/>inscription au boot"]
|
|
36
|
+
REPO["4 · le repository<br/><b>IRepository<PostRow></b><br/>find · create · updateOne…"]
|
|
37
|
+
DB[("Base")]
|
|
38
|
+
|
|
39
|
+
CFG -->|"nomme le moteur"| REPO
|
|
40
|
+
ENT -->|"vise le NOM du connecteur"| MOD
|
|
41
|
+
MOD -->|"crée la table au boot"| REPO
|
|
42
|
+
REPO --> DB
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Le point à retenir dès maintenant : **l'entité ne connaît que le nom de la connexion**, jamais le
|
|
46
|
+
moteur. Passer de SQLite à PostgreSQL ne touche que le fichier de configuration.
|
|
47
|
+
|
|
48
|
+
## 📖 Lexique
|
|
49
|
+
|
|
50
|
+
| Terme | Sens |
|
|
51
|
+
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| ORM | _Object-Relational Mapping_ : la bibliothèque qui traduit tes objets en lignes de base (Drizzle pour le SQL, Mongoose pour Mongo). |
|
|
53
|
+
| Connecteur | Le **nom** d'une connexion déclarée en configuration (`"default"`, `"analytics"`) — jamais le nom d'un moteur. |
|
|
54
|
+
| Dialecte | La variante SQL servie par un même driver : `sqlite`, `postgres`, `mysql`. |
|
|
55
|
+
| Entité | La description d'une table : un nom logique, un schéma natif, un connecteur cible. |
|
|
56
|
+
| Schéma natif | La table écrite dans la syntaxe du driver (`sqliteTable(...)` en Drizzle). Aucune couche à contourner. |
|
|
57
|
+
| Repository | L'objet avec lequel on lit et écrit : `IRepository<T>`, la seule surface que voit ton métier. |
|
|
58
|
+
| Registre | Le singleton qui relie les noms aux objets : `entityRegistry` pour les entités, `ormRegistry` pour les connexions. |
|
|
59
|
+
| Critère | Le filtre d'une requête : `{ title: "Bonjour" }` (égalité) ou `{ views: { $gte: 100 } }` (opérateurs). |
|
|
60
|
+
| DDL | _Data Definition Language_ : le SQL qui crée ou modifie les tables (`CREATE TABLE`, `ALTER`). |
|
|
61
|
+
| Atomique | Qui tient en **une** requête, donc que rien ne peut interrompre à mi-chemin (pas de course entre deux appels). |
|
|
62
|
+
| Upsert | « insère **ou** met à jour » en une seule instruction, sur conflit de clé unique. |
|
|
63
|
+
| Page / Slice | Une tranche de résultats : avec le total compté (Page) ou sans (Slice, meilleur marché). |
|
|
64
|
+
|
|
65
|
+
## Qu'est-ce que c'est ?
|
|
66
|
+
|
|
67
|
+
Une **entité**, c'est la fiche d'identité d'une table : un nom que ton code utilisera
|
|
68
|
+
(`"Post"`), un schéma qui décrit les colonnes, et le nom de la connexion où elle vit.
|
|
69
|
+
|
|
70
|
+
Compare avec une prise électrique. Le **schéma** dit la forme de la fiche (les colonnes). Le
|
|
71
|
+
**connecteur** dit dans quelle prise on la branche — mais pas si le courant vient d'un barrage ou
|
|
72
|
+
d'un panneau solaire. Ça, c'est le rôle de la configuration, et c'est pour cette raison que
|
|
73
|
+
changer de moteur ne touche pas ton code.
|
|
74
|
+
|
|
75
|
+
Une fois l'entité inscrite, tu ne la manipules plus jamais directement : tu demandes son
|
|
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.
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> `@nodefony/orm-core` n'est **pas un module à démarrer** : rien à ajouter dans `modules: [...]`.
|
|
81
|
+
> C'est une bibliothèque de contrats. Le module, c'est le **driver** — `@nodefony/drizzle` pour
|
|
82
|
+
> le SQL, `@nodefony/mongoose` pour MongoDB.
|
|
83
|
+
|
|
84
|
+
## La vision Nodefony
|
|
85
|
+
|
|
86
|
+
Deux partis pris expliquent la forme exacte des étapes qui suivent. Les connaître évite de se
|
|
87
|
+
battre contre le cadre.
|
|
88
|
+
|
|
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:37`) **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
|
|
93
|
+
fichier d'entité interdirait de servir la même table depuis une base différente selon
|
|
94
|
+
l'environnement.
|
|
95
|
+
|
|
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
|
|
100
|
+
le connecteur ne s'ouvre et ne crée les tables.
|
|
101
|
+
|
|
102
|
+
Le bénéfice est concret : une entité oubliée se voit **dans une liste**, pas dans un import à
|
|
103
|
+
effet de bord qu'on a omis. Et l'ordre des phases écarte par construction la course « la table
|
|
104
|
+
n'existait pas encore », qui ne se reproduit jamais en test.
|
|
105
|
+
|
|
106
|
+
## 🚀 Démarrage rapide
|
|
107
|
+
|
|
108
|
+
Vu d'une application générée par `nodefony create app`. Trois fichiers, puis une table qui
|
|
109
|
+
répond.
|
|
110
|
+
|
|
111
|
+
### 1. Déclarer la connexion
|
|
112
|
+
|
|
113
|
+
Le moteur et sa cible physique vivent ici, et nulle part ailleurs. SQLite ne demande aucune
|
|
114
|
+
installation : c'est le bon choix pour ce premier tour.
|
|
115
|
+
|
|
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
|
+
}));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Décrire la table et l'inscrire
|
|
132
|
+
|
|
133
|
+
Le schéma est du **Drizzle natif** : tous les types et options du moteur restent disponibles.
|
|
134
|
+
Le décorateur `@entities([...])` est ce qui rend l'entité réelle.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// index.ts — pour une première table, tout tient dans le fichier d'entrée de l'app.
|
|
138
|
+
import { randomUUID } from "node:crypto";
|
|
139
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
140
|
+
import { Kernel, Module } from "nodefony";
|
|
141
|
+
import type { DefaultOptionsService } from "nodefony";
|
|
142
|
+
import { defineEntity, entities } from "@nodefony/orm-core";
|
|
143
|
+
|
|
144
|
+
export const postTable = sqliteTable("Post", {
|
|
145
|
+
id: text("id")
|
|
146
|
+
.primaryKey()
|
|
147
|
+
.$defaultFn(() => randomUUID()),
|
|
148
|
+
title: text("title").notNull(),
|
|
149
|
+
// Défaut posé côté JS : le DDL dérivé au boot n'émet pas les DEFAULT SQL.
|
|
150
|
+
views: integer("views")
|
|
151
|
+
.notNull()
|
|
152
|
+
.$defaultFn(() => 0),
|
|
153
|
+
publishedAt: integer("publishedAt"), // null = brouillon
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** Une ligne de `Post`, telle que la rend le repository. */
|
|
157
|
+
export interface PostRow {
|
|
158
|
+
id: string;
|
|
159
|
+
title: string;
|
|
160
|
+
views: number;
|
|
161
|
+
publishedAt: number | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Pas de `connector` ici : il est résolu au démarrage (défaut `"default"`).
|
|
165
|
+
export const PostEntity = defineEntity({
|
|
166
|
+
name: "Post",
|
|
167
|
+
module: "app",
|
|
168
|
+
schema: postTable,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// `config` est déjà importé par ton index.ts (`./nodefony.config.js`) — inchangé.
|
|
172
|
+
declare const config: DefaultOptionsService;
|
|
173
|
+
|
|
174
|
+
@entities([PostEntity])
|
|
175
|
+
class App extends Module {
|
|
176
|
+
constructor(kernel: Kernel) {
|
|
177
|
+
super("app", kernel, import.meta.url, config);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default App;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 3. Lire et écrire
|
|
185
|
+
|
|
186
|
+
Le repository s'obtient par le registre des connexions. À partir de cette ligne, plus rien dans
|
|
187
|
+
ton code ne nomme Drizzle.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// nodefony/service/postDemo.ts
|
|
191
|
+
import { ormRegistry, paginate } from "@nodefony/orm-core";
|
|
192
|
+
|
|
193
|
+
/** Le type de ligne exporté à côté de l'entité (rappelé ici pour l'extrait). */
|
|
194
|
+
interface PostRow {
|
|
195
|
+
id: string;
|
|
196
|
+
title: string;
|
|
197
|
+
views: number;
|
|
198
|
+
publishedAt: number | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function demo(): Promise<void> {
|
|
202
|
+
const posts = ormRegistry.get("default").getRepository<PostRow>("Post");
|
|
203
|
+
|
|
204
|
+
// CRÉER — rend la ligne persistée (id généré, défauts appliqués).
|
|
205
|
+
const post = await posts.create({ title: "Bonjour" });
|
|
206
|
+
|
|
207
|
+
// LIRE — sans critère : toute la table.
|
|
208
|
+
const tous = await posts.find();
|
|
209
|
+
|
|
210
|
+
// LIRE filtré — `$null: true` produit un IS NULL ; `= NULL` serait toujours faux en SQL.
|
|
211
|
+
const brouillons = await posts.find({ publishedAt: { $null: true } });
|
|
212
|
+
|
|
213
|
+
// MODIFIER — au plus une ligne, atomiquement, et la rend.
|
|
214
|
+
const renomme = await posts.updateOne({ id: post.id }, { title: "Salut" });
|
|
215
|
+
|
|
216
|
+
// COMPTER, puis SUPPRIMER (rend le nombre de lignes supprimées).
|
|
217
|
+
const total = await posts.count();
|
|
218
|
+
const supprimees = await posts.delete({ id: post.id });
|
|
219
|
+
|
|
220
|
+
// PAGINER — une page, sans jamais matérialiser toute la table.
|
|
221
|
+
const page = await paginate(posts, { limit: 20, order: [["views", "DESC"]] });
|
|
222
|
+
|
|
223
|
+
console.log(
|
|
224
|
+
tous.length,
|
|
225
|
+
brouillons.length,
|
|
226
|
+
renomme,
|
|
227
|
+
total,
|
|
228
|
+
supprimees,
|
|
229
|
+
page.hasNext,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Ce qu'on observe
|
|
235
|
+
|
|
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
|
|
239
|
+
directe que le décorateur a bien travaillé.
|
|
240
|
+
|
|
241
|
+
Le data plane confirme depuis l'extérieur, sans ouvrir la base :
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Les connecteurs enregistrés, leur état, leur nombre d'entités
|
|
245
|
+
curl -s http://localhost:5151/nodefony/orm/api/orms
|
|
246
|
+
# [{"name":"default","default":true,"connected":true,"entityCount":1}]
|
|
247
|
+
|
|
248
|
+
# Le modèle canonique de l'entité : colonnes + relations
|
|
249
|
+
curl -s http://localhost:5151/nodefony/orm/api/entity/Post
|
|
250
|
+
|
|
251
|
+
# Le nombre de lignes par entité
|
|
252
|
+
curl -s http://localhost:5151/nodefony/orm/api/counts
|
|
253
|
+
# {"Post":1}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## 🧩 Le pas-à-pas commenté
|
|
257
|
+
|
|
258
|
+
Les quatre gestes du démarrage rapide, cette fois expliqués — c'est ici qu'on comprend
|
|
259
|
+
**pourquoi** chacun est nécessaire.
|
|
260
|
+
|
|
261
|
+
### Étape 1 — nommer la connexion
|
|
262
|
+
|
|
263
|
+
`connectors` associe un **nom** à un moteur et à sa cible. Le nom `"default"` n'a rien de magique :
|
|
264
|
+
c'est simplement celui que les entités visent quand elles n'en nomment pas d'autre.
|
|
265
|
+
|
|
266
|
+
Une deuxième base se déclare exactement pareil, sous un autre nom :
|
|
267
|
+
|
|
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
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Les dialectes disponibles, leurs options et le choix de la cible sont détaillés dans la page du
|
|
278
|
+
driver : [`@nodefony/drizzle`](../../drizzle/docs/index.md).
|
|
279
|
+
|
|
280
|
+
### Étape 2 — écrire le schéma
|
|
281
|
+
|
|
282
|
+
Le schéma est du Drizzle ordinaire. Un besoin non couvert par les exemples — colonne
|
|
283
|
+
`numeric(12,4)`, index composite, contrainte de clé étrangère — s'écrit directement dans ce
|
|
284
|
+
fichier, sans rien contourner.
|
|
285
|
+
|
|
286
|
+
Deux conséquences du DDL dérivé au boot, à connaître dès maintenant :
|
|
287
|
+
|
|
288
|
+
- la table est créée par un `CREATE TABLE IF NOT EXISTS` — **modifier le schéma n'altère pas une
|
|
289
|
+
table déjà créée** (aucun `ALTER` n'est émis) ;
|
|
290
|
+
- les **index** déclarés et les `DEFAULT` **SQL** ne sont pas émis. D'où les défauts posés côté
|
|
291
|
+
JavaScript (`$defaultFn`), qui s'appliquent quoi qu'il arrive.
|
|
292
|
+
|
|
293
|
+
> [!TIP]
|
|
294
|
+
> En développement, la façon la plus rapide de prendre en compte une colonne ajoutée est de
|
|
295
|
+
> supprimer le fichier SQLite et de redémarrer. En production, cela relève d'une migration.
|
|
296
|
+
|
|
297
|
+
### Étape 3 — déclarer l'entité
|
|
298
|
+
|
|
299
|
+
`defineEntity()` (`defineEntity.ts:48`) est une fonction d'**identité typée** : elle attache le
|
|
300
|
+
type et rend son argument, rien de plus. Aucun registre n'est touché.
|
|
301
|
+
|
|
302
|
+
Cette absence d'effet de bord est délibérée : elle permet d'importer une entité depuis un test,
|
|
303
|
+
un script ou un autre module **sans** déclencher son inscription dans un singleton global.
|
|
304
|
+
|
|
305
|
+
Les champs facultatifs qui servent tôt : `module` (qui apporte l'entité) et `domain`
|
|
306
|
+
(`IEntity.ts:58`), l'axe de classification qui rend navigable une base de plusieurs centaines de
|
|
307
|
+
tables dans l'ERD Studio.
|
|
308
|
+
|
|
309
|
+
### Étape 4 — brancher l'entité au module
|
|
310
|
+
|
|
311
|
+
C'est `entities()` (`entitiesDecorator.ts:56`) qui inscrit, et lui seul. Trois propriétés valent
|
|
312
|
+
d'être connues :
|
|
313
|
+
|
|
314
|
+
1. **Phase `onRegister`** (`entitiesDecorator.ts:66`) — strictement avant l'ouverture des
|
|
315
|
+
connexions. C'est ce qui rend l'ordre sûr par construction, contrairement à `@controllers`, qui
|
|
316
|
+
travaille à `onBoot`.
|
|
317
|
+
2. **Idempotent** — une entité déjà inscrite sur le même connecteur est ignorée (un module peut
|
|
318
|
+
être instancié deux fois : tests, rechargement). Une vraie collision, elle, lève
|
|
319
|
+
(`EntityRegistry.register()`, `EntityRegistry.ts:24`).
|
|
320
|
+
3. **Connecteur commun** — `entities([...], { connector: "analytics" })` pose toute la liste sur
|
|
321
|
+
une autre base ; une entité qui porte son propre `connector` (`IEntity.ts:42`) garde le sien.
|
|
322
|
+
|
|
323
|
+
### Le raccourci — `nodefony create entity`
|
|
324
|
+
|
|
325
|
+
Tout ce qui précède est scaffoldé par une commande, dans l'application ou dans un module. Les
|
|
326
|
+
champs se déclarent en positionnels, façon Rails :
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
nodefony create entity Post title:string content:text views:int
|
|
330
|
+
# --id uuid7|uuid4|serial --soft-delete --no-timestamps
|
|
331
|
+
# --module <nom> --no-controller --connector <nom>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Elle écrit le fichier d'entité (`nodefony/entity/Post.ts`), les schémas de validation, un service
|
|
335
|
+
CRUD, un controller REST + WebSocket et les tests — puis **câble elle-même** `@entities([...])` et
|
|
336
|
+
`@controllers([...])`. Le schéma vit alors dans son propre fichier, et le point d'entrée ne garde
|
|
337
|
+
que l'import plus la ligne du décorateur.
|
|
338
|
+
|
|
339
|
+
> [!WARNING]
|
|
340
|
+
> La commande refuse de générer si aucun module ORM n'est déclaré dans l'application. C'est
|
|
341
|
+
> volontaire : une entité sans driver produit du code mort qui ne compile même pas. Ajoute
|
|
342
|
+
> `use("@nodefony/drizzle", …)` au manifeste `modules`, puis relance.
|
|
343
|
+
|
|
344
|
+
## 🧰 Lire et écrire — le contrat complet
|
|
345
|
+
|
|
346
|
+
Le repository porte **quinze verbes**, pas cinq. Ils se choisissent sur la **garantie** qu'ils
|
|
347
|
+
apportent, jamais sur leur nom.
|
|
348
|
+
|
|
349
|
+
| Verbe | Ce qu'il garantit | Ancre |
|
|
350
|
+
| --------------------- | ------------------------------------------------------------------ | ---------------------------- |
|
|
351
|
+
| `find` / `findOne` | lecture filtrée, avec tri, bornes et eager-load | `IRepository.ts:213`, `:192` |
|
|
352
|
+
| `create` | insère une ligne et rend sa version persistée (id, défauts) | `IRepository.ts:240` |
|
|
353
|
+
| `createMany` | N lignes en **une** requête — seed, import, ingestion par lots | `IRepository.ts:252` |
|
|
354
|
+
| `updateOne` | modifie **au plus une** ligne, **atomiquement**, et la rend | `IRepository.ts:269` |
|
|
355
|
+
| `updateMany` | modifie toutes les lignes du critère, rend le **nombre** | `IRepository.ts:312` |
|
|
356
|
+
| `upsert` | insère **ou** met à jour sur conflit de clé, en une instruction | `IRepository.ts:296` |
|
|
357
|
+
| `increment` | `SET f = f + ?` atomique — compteurs, quotas, limitation de débit | `IRepository.ts:287` |
|
|
358
|
+
| `delete` | supprime tout ce qui matche, rend le nombre | `IRepository.ts:298` |
|
|
359
|
+
| `deleteOne` | supprime **au plus une** ligne, rend un booléen | `IRepository.ts:328` |
|
|
360
|
+
| `findOneAndDelete` | supprime **et rend** la ligne — file de jobs, `pop` atomique | `IRepository.ts:355` |
|
|
361
|
+
| `count` / `exists` | compter, ou juste savoir s'il y en a une (sans charger de colonne) | `IRepository.ts:378`, `:335` |
|
|
362
|
+
| `withTransaction(tx)` | une **vue** du repository liée à une transaction | `IRepository.ts:406` |
|
|
363
|
+
|
|
364
|
+
> [!IMPORTANT]
|
|
365
|
+
> **Il n'existe pas de méthode `update()`.** Le choix est explicite et il est intentionnel :
|
|
366
|
+
> `IRepository.updateOne()` (`IRepository.ts:269`) pour une ligne — atomique, et elle **rend** la
|
|
367
|
+
> ligne modifiée —, `IRepository.updateMany()` (`IRepository.ts:274`) pour un lot — qui rend le
|
|
368
|
+
> **nombre** de lignes touchées. Un verbe unique masquerait cette différence de garantie, qui est
|
|
369
|
+
> précisément ce qu'on veut choisir en connaissance de cause.
|
|
370
|
+
|
|
371
|
+
Quelques usages, un par garantie :
|
|
372
|
+
|
|
373
|
+
```ts ignore
|
|
374
|
+
// Insertion par lots : une seule requête, l'ordre est conservé.
|
|
375
|
+
await posts.createMany([{ title: "A" }, { title: "B" }]);
|
|
376
|
+
|
|
377
|
+
// Existence sans charger la ligne (ni compter la table).
|
|
378
|
+
if (await posts.exists({ title: "A" })) {
|
|
379
|
+
/* … */
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Compteur atomique : jamais de lecture-modification-écriture, donc jamais de course.
|
|
383
|
+
await posts.increment({ id }, { views: 1 });
|
|
384
|
+
|
|
385
|
+
// Claim-and-remove : on prend le job ET on le retire, sans que deux workers l'obtiennent.
|
|
386
|
+
const job = await jobs.findOneAndDelete({ status: "queued" });
|
|
387
|
+
|
|
388
|
+
// Tout ou rien : une exception dans le bloc annule les DEUX écritures.
|
|
389
|
+
await orm.transaction(async (tx) => {
|
|
390
|
+
const auteur = await users.withTransaction(tx).create({ email: "x@y.z" });
|
|
391
|
+
await posts.withTransaction(tx).create({ title: "Hello", userId: auteur.id });
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Deux réflexes de performance, dès le premier jour : préférer `IRepository.exists()`
|
|
396
|
+
(`IRepository.ts:335`) à `findOne(...) !== null` — aucune colonne n'est chargée —, et
|
|
397
|
+
`IRepository.increment()` (`IRepository.ts:325`) à une lecture suivie d'une écriture : une requête
|
|
398
|
+
au lieu de deux, et pas de course.
|
|
399
|
+
|
|
400
|
+
## 🔎 Filtrer — les opérateurs de critère
|
|
401
|
+
|
|
402
|
+
Un critère est un objet. Chaque clé est un champ ; chaque valeur est soit une **égalité**, soit
|
|
403
|
+
un objet d'**opérateurs**.
|
|
404
|
+
|
|
405
|
+
```ts ignore
|
|
406
|
+
await posts.find({ title: "Bonjour" }); // égalité
|
|
407
|
+
await posts.find({ views: { $gte: 100, $lt: 1000 } }); // deux opérateurs = ET
|
|
408
|
+
await posts.find({ id: { $in: ids } }); // appartenance
|
|
409
|
+
await posts.find({ title: { $like: "Bon%" } }); // motif SQL
|
|
410
|
+
await posts.find({ publishedAt: { $null: false } }); // IS NOT NULL
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Dix** opérateurs sont reconnus, figés dans `OPERATOR_KEYS` (`criteria.ts:13`) — source unique
|
|
414
|
+
partagée par tous les drivers :
|
|
415
|
+
|
|
416
|
+
| Opérateur | Effet | Opérateur | Effet |
|
|
417
|
+
| --------- | -------------------------------- | --------- | ---------------------------------- |
|
|
418
|
+
| `$eq` | égal (identique à la valeur nue) | `$lte` | inférieur ou égal |
|
|
419
|
+
| `$ne` | différent | `$in` | appartient à la liste |
|
|
420
|
+
| `$gt` | strictement supérieur | `$nin` | n'appartient pas à la liste |
|
|
421
|
+
| `$gte` | supérieur ou égal | `$like` | motif SQL (`%`, `_`), champs texte |
|
|
422
|
+
| `$lt` | strictement inférieur | `$null` | `IS NULL` (`true`) / `IS NOT NULL` |
|
|
423
|
+
|
|
424
|
+
Plusieurs opérateurs sur un même champ se combinent en **ET**.
|
|
425
|
+
|
|
426
|
+
> [!WARNING]
|
|
427
|
+
> **`$null` est celui qu'on oublie, et il coûte cher.** En SQL, `colonne = NULL` est **toujours
|
|
428
|
+
> faux** : un filtre « la colonne est vide » écrit naïvement ne remonte jamais rien — sans la
|
|
429
|
+
> moindre erreur. Deux formes équivalentes le résolvent : la valeur nue `{ publishedAt: null }`
|
|
430
|
+
> et l'opérateur `{ publishedAt: { $null: true } }` (`FieldOperators.$null`, `IRepository.ts:65`).
|
|
431
|
+
> La valeur nue n'est ouverte par le typage que si le champ est **nullable** (`FieldCriteria`,
|
|
432
|
+
> `IRepository.ts:128`) : chercher `IS NULL` sur une colonne non-nullable est une erreur de
|
|
433
|
+
> raisonnement, et le compilateur la refuse.
|
|
434
|
+
|
|
435
|
+
**Comment un objet est reconnu comme filtre plutôt que comme valeur** : `isFieldOperators()`
|
|
436
|
+
(`criteria.ts:42`) ne l'interprète que si **toutes** ses clés sont des opérateurs connus. Une
|
|
437
|
+
colonne JSON ou un sous-document (`{ meta: { auteur: "…" } }`) reste donc une égalité — c'est ce
|
|
438
|
+
qui évite qu'une donnée métier soit prise pour une requête.
|
|
439
|
+
|
|
440
|
+
### Les opérateurs d'écriture — `$max` et `$min`
|
|
441
|
+
|
|
442
|
+
Une seconde famille, distincte, s'applique **en écriture** dans un `upsert` :
|
|
443
|
+
`UpdateOperators` (`IRepository.ts:94`), dont les clés reconnues sont listées par
|
|
444
|
+
`UPDATE_OPERATOR_KEYS` (`criteria.ts:67`).
|
|
445
|
+
|
|
446
|
+
Ils existent parce qu'un upsert **ne peut pas porter de condition** : son `DO UPDATE` s'applique
|
|
447
|
+
dès qu'il y a conflit de clé (MySQL n'accepte pas de `WHERE` sur `ON DUPLICATE KEY UPDATE`). Pour
|
|
448
|
+
une valeur qui ne doit **jamais reculer**, la condition vit donc dans la valeur écrite.
|
|
449
|
+
|
|
450
|
+
```ts ignore
|
|
451
|
+
// Le seuil ne recule jamais, même sur deux appels simultanés — une seule instruction.
|
|
452
|
+
await quotas.upsert(
|
|
453
|
+
{ userId },
|
|
454
|
+
{ seuil: { $max: Date.now() } },
|
|
455
|
+
{ createdAt: Date.now() },
|
|
456
|
+
);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Le driver traduit en `MAX()` (sqlite), `GREATEST()` (postgres, mysql) ou `$max` natif (Mongo).
|
|
460
|
+
Sur une ligne dont on sait qu'elle **existe**, ils sont inutiles :
|
|
461
|
+
`updateMany({ id, seuil: { $lt: v } }, { seuil: v })` l'exprime déjà, tout aussi atomiquement.
|
|
462
|
+
|
|
463
|
+
### Ce que le critère ne couvre pas
|
|
464
|
+
|
|
465
|
+
Les `OR` logiques, les sous-requêtes, les agrégats et les jointures arbitraires n'en font pas
|
|
466
|
+
partie. Ce n'est pas un oubli : c'est la limite de ce qui se porte d'un moteur SQL à MongoDB. La
|
|
467
|
+
sortie est `IOrm.getNativeConnection()` (`IOrm.ts:51`), qui rend la connexion brute du driver.
|
|
468
|
+
|
|
469
|
+
Un champ absent de l'entité lève `UnknownCriteriaField` (`errors.ts:23`), et le message liste les
|
|
470
|
+
champs connus — le diagnostic d'une faute de frappe est immédiat.
|
|
471
|
+
|
|
472
|
+
## 📄 Pager les résultats
|
|
473
|
+
|
|
474
|
+
`find()` avec `limit`/`offset`/`order` (`RepositoryReadOptions`, `IRepository.ts:153`) suffit pour
|
|
475
|
+
une tranche. Pour une vraie page — celle qui sait s'il y a une suite — utilise `paginate()`
|
|
476
|
+
(`paginate.ts:47`) :
|
|
477
|
+
|
|
478
|
+
```ts ignore
|
|
479
|
+
const page = await paginate(posts, {
|
|
480
|
+
limit: 20,
|
|
481
|
+
offset: 0,
|
|
482
|
+
criteria: { publishedAt: { $null: false } },
|
|
483
|
+
order: [["publishedAt", "DESC"]],
|
|
484
|
+
withTotal: false, // le COUNT(*) est coûteux : on ne le paie que si on l'affiche
|
|
485
|
+
});
|
|
486
|
+
// page.items · page.hasNext · page.total (undefined si withTotal: false)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`hasNext` est obtenu **sans `COUNT`** : la fonction demande `limit + 1` lignes et retire la ligne
|
|
490
|
+
excédentaire. C'est la distinction « Page » (avec total) / « Slice » (sans), et elle change tout
|
|
491
|
+
sur une grosse table.
|
|
492
|
+
|
|
493
|
+
## ⚙️ Ranger le métier dans un service
|
|
494
|
+
|
|
495
|
+
Dès que la logique dépasse l'appel direct, elle quitte le controller. `AbstractCrudService`
|
|
496
|
+
(`AbstractCrudService.ts:37`) donne le socle : les lectures sont une **délégation pure** (aucun
|
|
497
|
+
surcoût sur le chemin chaud), les mutations sont encadrées par des points d'extension puis un
|
|
498
|
+
événement de cycle de vie.
|
|
499
|
+
|
|
500
|
+
```ts ignore
|
|
501
|
+
export class PostService extends AbstractCrudService<PostRow> {
|
|
502
|
+
constructor(repository: IRepository<PostRow>) {
|
|
503
|
+
super("postService", repository);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Valide avant insertion : un rejet devient un 422, quel que soit le transport. */
|
|
507
|
+
protected override beforeCreate(data: Partial<PostRow>): Partial<PostRow> {
|
|
508
|
+
return createPostSchema.parse(data) as Partial<PostRow>;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
L'intérêt est de n'avoir **qu'un seul endroit** à modifier quand la règle change : la même méthode
|
|
514
|
+
sert la route REST, l'appel WebSocket, un résolveur GraphQL et une commande CLI. Pour toute liste
|
|
515
|
+
d'administration, la primitive est `AbstractCrudService.findPage()` (`AbstractCrudService.ts:110`)
|
|
516
|
+
— elle ne charge qu'une page, quelle que soit la taille de la table.
|
|
517
|
+
|
|
518
|
+
> [!WARNING]
|
|
519
|
+
> Ce service est un **singleton** partagé. C'est légitime **parce qu'il est sans état** : ne jamais
|
|
520
|
+
> écrire `this.utilisateurCourant = …` pendant une requête. L'utilisateur, le tenant et la
|
|
521
|
+
> transaction voyagent dans le contexte, jamais sur l'instance.
|
|
522
|
+
|
|
523
|
+
## ⚠️ Pièges
|
|
524
|
+
|
|
525
|
+
| Symptôme | Cause | Correction |
|
|
526
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
527
|
+
| `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:274`) |
|
|
528
|
+
| « 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`) |
|
|
529
|
+
| 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:66`) |
|
|
530
|
+
| Une colonne ajoutée au schéma reste absente de la table | le DDL du boot est un `CREATE TABLE IF NOT EXISTS` : aucun `ALTER` n'est émis | supprimer la base de développement et redémarrer, ou passer par une migration |
|
|
531
|
+
| Un `DEFAULT` SQL ou un index déclaré n'apparaît pas | le DDL dérivé ne les émet pas | poser le défaut côté JavaScript (`$defaultFn`) ; créer l'index par migration |
|
|
532
|
+
| 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`) |
|
|
533
|
+
| `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()` |
|
|
534
|
+
| `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 |
|
|
535
|
+
| « 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`) |
|
|
536
|
+
| « 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`) |
|
|
537
|
+
| 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 |
|
|
538
|
+
| 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 |
|
|
539
|
+
|
|
540
|
+
## 🧪 Tests & couverture
|
|
541
|
+
|
|
542
|
+
Les compteurs de cette page sont recomptés à chaque génération — jamais figés dans le texte. Ils
|
|
543
|
+
portent sur les tests **unitaires** de `@nodefony/orm-core` : les deux registres, le décorateur
|
|
544
|
+
`entities`, les critères, la pagination, le service CRUD, les moniteurs et le data plane.
|
|
545
|
+
|
|
546
|
+
Les mécanismes enseignés ici sont couverts directement :
|
|
547
|
+
`tests/unit/entitiesDecorator.test.ts` (inscription, idempotence, phase, connecteur commun),
|
|
548
|
+
`tests/unit/EntityRegistry.test.ts` (résolution, ambiguïté), `tests/unit/criteria.test.ts`
|
|
549
|
+
(reconnaissance des opérateurs) et `tests/unit/paginate.test.ts` (bornes, `hasNext`, total).
|
|
550
|
+
|
|
551
|
+
**Ce que ces tests ne prouvent pas.** Le contrat `IRepository` n'a de sens qu'exécuté sur une
|
|
552
|
+
vraie base : cette preuve vit chez les **drivers**, sous forme de bancs de contrat rejoués par
|
|
553
|
+
dialecte — `@nodefony/drizzle` (sqlite en mémoire, PostgreSQL et MySQL en end-to-end) et
|
|
554
|
+
`@nodefony/mongoose` pour le documentaire.
|
|
555
|
+
|
|
556
|
+
> [!WARNING]
|
|
557
|
+
> Un compteur vert ne prouve pas qu'une base a été touchée : les bancs sur serveur réel se
|
|
558
|
+
> **skippent** faute de leur variable d'infra, et un test skippé compte comme vert. La source
|
|
559
|
+
> unique de ces variables et des commandes Docker correspondantes est `vitest.gates.ts` à la
|
|
560
|
+
> racine du dépôt ; les suites concernées affichent leur récapitulatif en fin d'exécution.
|
|
561
|
+
|
|
562
|
+
## 🔗 Pour aller plus loin
|
|
563
|
+
|
|
564
|
+
- ⬆️ **Retour au hub** : [ORM — le contrat de persistance](index.md) ·
|
|
565
|
+
[Toute la documentation](../../../../../docs/index.md)
|
|
566
|
+
- 🧰 **La suite logique** : la section [Les contrats](index.md#-les-contrats--la-surface-publique)
|
|
567
|
+
du hub (les quinze verbes en détail, transactions, eager-load) et
|
|
568
|
+
[Les critères de recherche](index.md#-les-critères-de-recherche)
|
|
569
|
+
- 🗄️ **Les drivers** : [`@nodefony/drizzle`](../../drizzle/docs/index.md) (dialectes, création des
|
|
570
|
+
tables, ERD) · [`@nodefony/mongoose`](../../mongoose/docs/index.md) et sa
|
|
571
|
+
[configuration](../../mongoose/docs/configuration.md)
|
|
572
|
+
- 🏛️ **Passer en production** : [guide persistance](../../../../../docs/guides/persistence.md) ·
|
|
573
|
+
[configuration d'une application](../../../../../docs/guides/configuration.md) ·
|
|
574
|
+
[stockage de session](../../../../../docs/guides/session-storage.md)
|
|
575
|
+
- 📐 **Pourquoi cette architecture** :
|
|
576
|
+
[ADR-0003 — abstraction Repository multi-ORM](../../../../../docs/adr/0003-orm-core-abstraction-repository-multi-orm.md)
|
|
577
|
+
- 📖 [Lexique général](../../../../../docs/lexique.md) du framework
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nodefony/orm-core",
|
|
3
|
+
"version": "10.0.0-alpha.1",
|
|
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
|
+
"contributors": [],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
|
|
19
|
+
"dev": "rolldown -c rolldown.config.ts --watch",
|
|
20
|
+
"clean": "rimraf dist",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"coverage": "vitest run --coverage",
|
|
23
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
|
|
24
|
+
},
|
|
25
|
+
"private": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=24.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"nodefony",
|
|
31
|
+
"orm",
|
|
32
|
+
"repository",
|
|
33
|
+
"database",
|
|
34
|
+
"abstraction",
|
|
35
|
+
"typescript",
|
|
36
|
+
"esm"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"nodefony": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "26.4.1",
|
|
43
|
+
"nodefony": "*",
|
|
44
|
+
"rimraf": "6.1.3",
|
|
45
|
+
"vitest": "5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/nodefony/nodefony-core.git",
|
|
50
|
+
"directory": "src/packages/@nodefony/orm-core"
|
|
51
|
+
},
|
|
52
|
+
"license": "CECILL-B",
|
|
53
|
+
"licenses": [
|
|
54
|
+
{
|
|
55
|
+
"type": "CECILL-B",
|
|
56
|
+
"url": "http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
60
|
+
"readmeFilename": "README.md",
|
|
61
|
+
"files": [
|
|
62
|
+
"dist",
|
|
63
|
+
"docs"
|
|
64
|
+
],
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {},
|
|
69
|
+
"homepage": "https://nodefony.github.io/nodefony-core/",
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/nodefony/nodefony-core/issues"
|
|
72
|
+
}
|
|
73
|
+
}
|