@mostajs/orm 1.0.0

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +548 -0
  3. package/dist/core/base-repository.d.ts +26 -0
  4. package/dist/core/base-repository.js +82 -0
  5. package/dist/core/config.d.ts +62 -0
  6. package/dist/core/config.js +116 -0
  7. package/dist/core/errors.d.ts +30 -0
  8. package/dist/core/errors.js +49 -0
  9. package/dist/core/factory.d.ts +41 -0
  10. package/dist/core/factory.js +142 -0
  11. package/dist/core/normalizer.d.ts +9 -0
  12. package/dist/core/normalizer.js +19 -0
  13. package/dist/core/registry.d.ts +43 -0
  14. package/dist/core/registry.js +78 -0
  15. package/dist/core/types.d.ts +228 -0
  16. package/dist/core/types.js +5 -0
  17. package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
  18. package/dist/dialects/abstract-sql.dialect.js +1071 -0
  19. package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
  20. package/dist/dialects/cockroachdb.dialect.js +23 -0
  21. package/dist/dialects/db2.dialect.d.ts +2 -0
  22. package/dist/dialects/db2.dialect.js +190 -0
  23. package/dist/dialects/hana.dialect.d.ts +2 -0
  24. package/dist/dialects/hana.dialect.js +199 -0
  25. package/dist/dialects/hsqldb.dialect.d.ts +2 -0
  26. package/dist/dialects/hsqldb.dialect.js +114 -0
  27. package/dist/dialects/mariadb.dialect.d.ts +2 -0
  28. package/dist/dialects/mariadb.dialect.js +87 -0
  29. package/dist/dialects/mongo.dialect.d.ts +2 -0
  30. package/dist/dialects/mongo.dialect.js +480 -0
  31. package/dist/dialects/mssql.dialect.d.ts +27 -0
  32. package/dist/dialects/mssql.dialect.js +127 -0
  33. package/dist/dialects/mysql.dialect.d.ts +24 -0
  34. package/dist/dialects/mysql.dialect.js +101 -0
  35. package/dist/dialects/oracle.dialect.d.ts +2 -0
  36. package/dist/dialects/oracle.dialect.js +206 -0
  37. package/dist/dialects/postgres.dialect.d.ts +26 -0
  38. package/dist/dialects/postgres.dialect.js +105 -0
  39. package/dist/dialects/spanner.dialect.d.ts +2 -0
  40. package/dist/dialects/spanner.dialect.js +259 -0
  41. package/dist/dialects/sqlite.dialect.d.ts +2 -0
  42. package/dist/dialects/sqlite.dialect.js +1027 -0
  43. package/dist/dialects/sybase.dialect.d.ts +2 -0
  44. package/dist/dialects/sybase.dialect.js +119 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +26 -0
  47. package/docs/api-reference.md +1009 -0
  48. package/docs/dialects.md +673 -0
  49. package/docs/tutorial.md +846 -0
  50. package/package.json +91 -0
@@ -0,0 +1,1009 @@
1
+ # MostaORM — Référence de l'API
2
+
3
+ > Référence complète de toutes les fonctions, classes, types et interfaces exportés par `@mosta/orm`.
4
+
5
+ ---
6
+
7
+ ## Table des matières
8
+
9
+ - [createConnection()](#createconnection)
10
+ - [getDialect()](#getdialect)
11
+ - [getConfigFromEnv()](#getconfigfromenv)
12
+ - [disconnectDialect()](#disconnectdialect)
13
+ - [testConnection()](#testconnection)
14
+ - [registerSchema()](#registerschema--registerschemas)
15
+ - [registerSchemas()](#registerschema--registerschemas)
16
+ - [getSchema()](#getschema)
17
+ - [getAllSchemas()](#getallschemas)
18
+ - [BaseRepository\<T\>](#baserepositoryt)
19
+ - [findAll()](#findall)
20
+ - [findOne()](#findone)
21
+ - [findById()](#findbyid)
22
+ - [findByIdWithRelations()](#findbyidwithrelations)
23
+ - [findWithRelations()](#findwithrelations)
24
+ - [create()](#create)
25
+ - [update()](#update)
26
+ - [updateMany()](#updatemany)
27
+ - [delete()](#delete)
28
+ - [deleteMany()](#deletemany)
29
+ - [count()](#count)
30
+ - [search()](#search)
31
+ - [distinct()](#distinct)
32
+ - [aggregate()](#aggregate)
33
+ - [upsert()](#upsert)
34
+ - [increment()](#increment)
35
+ - [addToSet()](#addtoset)
36
+ - [pull()](#pull)
37
+ - [Types](#types)
38
+ - [EntitySchema](#entityschema)
39
+ - [FieldDef](#fielddef)
40
+ - [RelationDef](#relationdef)
41
+ - [IndexDef](#indexdef)
42
+ - [ConnectionConfig](#connectionconfig)
43
+ - [FilterQuery](#filterquery)
44
+ - [QueryOptions](#queryoptions)
45
+ - [AggregateStage](#aggregatestage)
46
+ - [PaginatedResult](#paginatedresult)
47
+ - [Erreurs](#erreurs)
48
+
49
+ ---
50
+
51
+ ## createConnection()
52
+
53
+ Initialise et connecte le dialecte. Enregistre optionnellement des schémas.
54
+ Retourne un **singleton** — les appels suivants retournent la même instance.
55
+
56
+ ```typescript
57
+ function createConnection(
58
+ config: ConnectionConfig,
59
+ schemas?: EntitySchema[]
60
+ ): Promise<IDialect>
61
+ ```
62
+
63
+ **Paramètres**
64
+
65
+ | Paramètre | Type | Requis | Description |
66
+ |-----------|------|--------|-------------|
67
+ | `config` | `ConnectionConfig` | ✅ | Configuration de la connexion |
68
+ | `schemas` | `EntitySchema[]` | ❌ | Schémas à enregistrer avant connexion |
69
+
70
+ **Exemple**
71
+
72
+ ```typescript
73
+ import { createConnection } from '@mosta/orm'
74
+ import { UserSchema, PostSchema } from './schemas/index.js'
75
+
76
+ const dialect = await createConnection(
77
+ {
78
+ dialect: 'postgres',
79
+ uri: process.env.DATABASE_URL!,
80
+ schemaStrategy: 'update',
81
+ showSql: true,
82
+ poolSize: 10,
83
+ cacheEnabled: true,
84
+ cacheTtlSeconds: 60,
85
+ },
86
+ [UserSchema, PostSchema]
87
+ )
88
+ ```
89
+
90
+ ---
91
+
92
+ ## getDialect()
93
+
94
+ Retourne le dialect courant (singleton). Si aucune connexion n'existe, en crée une à partir de la configuration fournie ou des variables d'environnement.
95
+
96
+ ```typescript
97
+ function getDialect(config?: ConnectionConfig): Promise<IDialect>
98
+ ```
99
+
100
+ **Exemple**
101
+
102
+ ```typescript
103
+ // Depuis les variables d'environnement (DB_DIALECT + SGBD_URI)
104
+ const dialect = await getDialect()
105
+
106
+ // Ou avec une config explicite
107
+ const dialect = await getDialect({ dialect: 'sqlite', uri: ':memory:' })
108
+ ```
109
+
110
+ ---
111
+
112
+ ## getConfigFromEnv()
113
+
114
+ Lit la configuration depuis les variables d'environnement. Lance une erreur si `DB_DIALECT` ou `SGBD_URI` sont absentes.
115
+
116
+ ```typescript
117
+ function getConfigFromEnv(): ConnectionConfig
118
+ ```
119
+
120
+ **Variables lues**
121
+
122
+ | Variable | Requis | Description |
123
+ |----------|--------|-------------|
124
+ | `DB_DIALECT` | ✅ | Identifiant du dialecte |
125
+ | `SGBD_URI` | ✅ | URI de connexion |
126
+ | `DB_SCHEMA_STRATEGY` | ❌ | Stratégie de schéma (défaut: `'none'`) |
127
+ | `DB_SHOW_SQL` | ❌ | Afficher les requêtes (`'true'/'false'`) |
128
+ | `DB_FORMAT_SQL` | ❌ | Indenter les requêtes affichées |
129
+ | `DB_POOL_SIZE` | ❌ | Taille du pool de connexions |
130
+ | `DB_CACHE_ENABLED` | ❌ | Activer le cache |
131
+ | `DB_CACHE_TTL` | ❌ | Durée de vie du cache (secondes) |
132
+ | `DB_BATCH_SIZE` | ❌ | Taille des lots pour les opérations bulk |
133
+
134
+ ---
135
+
136
+ ## disconnectDialect()
137
+
138
+ Ferme la connexion et réinitialise le singleton.
139
+
140
+ ```typescript
141
+ function disconnectDialect(): Promise<void>
142
+ ```
143
+
144
+ **Exemple**
145
+
146
+ ```typescript
147
+ // Nettoyage en fin d'application
148
+ process.on('SIGTERM', async () => {
149
+ await disconnectDialect()
150
+ process.exit(0)
151
+ })
152
+
153
+ // Indispensable entre les tests
154
+ afterAll(async () => {
155
+ await disconnectDialect()
156
+ })
157
+ ```
158
+
159
+ ---
160
+
161
+ ## testConnection()
162
+
163
+ Teste une connexion sans modifier le dialecte actif. Utile dans les assistants de configuration.
164
+
165
+ ```typescript
166
+ function testConnection(config: ConnectionConfig): Promise<boolean>
167
+ ```
168
+
169
+ **Exemple**
170
+
171
+ ```typescript
172
+ const ok = await testConnection({
173
+ dialect: 'postgres',
174
+ uri: 'postgresql://user:pass@localhost:5432/mydb',
175
+ })
176
+ console.log(ok ? '✅ Connexion réussie' : '❌ Connexion échouée')
177
+ ```
178
+
179
+ ---
180
+
181
+ ## registerSchema() / registerSchemas()
182
+
183
+ Enregistre un ou plusieurs schémas dans le registre global.
184
+
185
+ ```typescript
186
+ function registerSchema(schema: EntitySchema): void
187
+ function registerSchemas(schemas: EntitySchema[]): void
188
+ ```
189
+
190
+ > Doit être appelé **avant** `getDialect()` ou `createConnection()` si vous n'utilisez pas le paramètre `schemas` de `createConnection()`.
191
+
192
+ ---
193
+
194
+ ## getSchema()
195
+
196
+ Récupère un schéma enregistré par son nom d'entité.
197
+
198
+ ```typescript
199
+ function getSchema(name: string): EntitySchema
200
+ ```
201
+
202
+ Lance `DialectNotFoundError` si le schéma n'est pas enregistré.
203
+
204
+ ---
205
+
206
+ ## getAllSchemas()
207
+
208
+ Retourne tous les schémas enregistrés.
209
+
210
+ ```typescript
211
+ function getAllSchemas(): EntitySchema[]
212
+ ```
213
+
214
+ ---
215
+
216
+ ## BaseRepository\<T\>
217
+
218
+ Classe générique à étendre pour créer vos repositories.
219
+
220
+ ```typescript
221
+ class BaseRepository<T extends { id: string }> implements IRepository<T> {
222
+ constructor(schema: EntitySchema, dialect: IDialect)
223
+ }
224
+ ```
225
+
226
+ **Paramètres du constructeur**
227
+
228
+ | Paramètre | Type | Description |
229
+ |-----------|------|-------------|
230
+ | `schema` | `EntitySchema` | Schéma de l'entité |
231
+ | `dialect` | `IDialect` | Instance du dialecte (depuis `createConnection`) |
232
+
233
+ **Pattern recommandé**
234
+
235
+ ```typescript
236
+ class UserRepository extends BaseRepository<User> {
237
+ constructor(dialect: IDialect) {
238
+ super(UserSchema, dialect)
239
+ }
240
+
241
+ async findByEmail(email: string): Promise<User | null> {
242
+ return this.findOne({ email })
243
+ }
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ### findAll()
250
+
251
+ ```typescript
252
+ findAll(filter?: FilterQuery, options?: QueryOptions): Promise<T[]>
253
+ ```
254
+
255
+ Retourne tous les documents correspondant au filtre.
256
+
257
+ ```typescript
258
+ // Tous les utilisateurs actifs, triés par nom
259
+ const users = await userRepo.findAll(
260
+ { active: true },
261
+ { sort: { name: 1 }, limit: 50 }
262
+ )
263
+ ```
264
+
265
+ ---
266
+
267
+ ### findOne()
268
+
269
+ ```typescript
270
+ findOne(filter: FilterQuery, options?: QueryOptions): Promise<T | null>
271
+ ```
272
+
273
+ Retourne le premier document correspondant, ou `null`.
274
+
275
+ ```typescript
276
+ const user = await userRepo.findOne({ email: 'alice@example.com' })
277
+ ```
278
+
279
+ ---
280
+
281
+ ### findById()
282
+
283
+ ```typescript
284
+ findById(id: string, options?: QueryOptions): Promise<T | null>
285
+ ```
286
+
287
+ Recherche par identifiant unique (UUID ou ObjectId MongoDB).
288
+
289
+ ```typescript
290
+ const user = await userRepo.findById('550e8400-e29b-41d4-a716-446655440000')
291
+ if (!user) throw new Error('User not found')
292
+ ```
293
+
294
+ ---
295
+
296
+ ### findByIdWithRelations()
297
+
298
+ ```typescript
299
+ findByIdWithRelations(
300
+ id: string,
301
+ relations?: string[],
302
+ options?: QueryOptions
303
+ ): Promise<T | null>
304
+ ```
305
+
306
+ Charge un document avec ses relations (JOIN SQL / populate MongoDB).
307
+
308
+ ```typescript
309
+ const post = await postRepo.findByIdWithRelations('abc123', ['author', 'tags'])
310
+ // post.author = { id: '...', name: 'Alice', email: '...' }
311
+ // post.tags = [{ id: '...', name: 'typescript' }, ...]
312
+ ```
313
+
314
+ ---
315
+
316
+ ### findWithRelations()
317
+
318
+ ```typescript
319
+ findWithRelations(
320
+ filter: FilterQuery,
321
+ relations: string[],
322
+ options?: QueryOptions
323
+ ): Promise<T[]>
324
+ ```
325
+
326
+ Charge plusieurs documents avec leurs relations.
327
+
328
+ ```typescript
329
+ const posts = await postRepo.findWithRelations(
330
+ { published: true },
331
+ ['author', 'category'],
332
+ { sort: { createdAt: -1 }, limit: 10 }
333
+ )
334
+ ```
335
+
336
+ ---
337
+
338
+ ### create()
339
+
340
+ ```typescript
341
+ create(data: Partial<T>): Promise<T>
342
+ ```
343
+
344
+ Crée un nouveau document. Un `id` (UUID) est généré automatiquement si non fourni.
345
+
346
+ ```typescript
347
+ const user = await userRepo.create({
348
+ name: 'Alice Dupont',
349
+ email: 'alice@example.com',
350
+ role: 'user',
351
+ })
352
+ console.log(user.id) // UUID généré
353
+ console.log(user.createdAt) // Date courante (si timestamps: true)
354
+ ```
355
+
356
+ ---
357
+
358
+ ### update()
359
+
360
+ ```typescript
361
+ update(id: string, data: Partial<T>): Promise<T | null>
362
+ ```
363
+
364
+ Mise à jour partielle — seuls les champs fournis sont modifiés.
365
+ Retourne le document mis à jour, ou `null` si introuvable.
366
+
367
+ ```typescript
368
+ const updated = await userRepo.update('abc123', {
369
+ name: 'Alice Martin',
370
+ updatedAt: new Date(), // mis à jour automatiquement si timestamps: true
371
+ })
372
+ ```
373
+
374
+ ---
375
+
376
+ ### updateMany()
377
+
378
+ ```typescript
379
+ updateMany(filter: FilterQuery, data: Partial<T>): Promise<number>
380
+ ```
381
+
382
+ Met à jour tous les documents correspondant au filtre.
383
+ Retourne le nombre de documents modifiés.
384
+
385
+ ```typescript
386
+ // Désactiver tous les comptes expirés
387
+ const count = await userRepo.updateMany(
388
+ { expiresAt: { $lt: new Date() } },
389
+ { active: false }
390
+ )
391
+ console.log(`${count} comptes désactivés`)
392
+ ```
393
+
394
+ ---
395
+
396
+ ### delete()
397
+
398
+ ```typescript
399
+ delete(id: string): Promise<boolean>
400
+ ```
401
+
402
+ Supprime un document par son ID.
403
+ Retourne `true` si supprimé, `false` si introuvable.
404
+
405
+ ```typescript
406
+ const ok = await userRepo.delete('abc123')
407
+ ```
408
+
409
+ ---
410
+
411
+ ### deleteMany()
412
+
413
+ ```typescript
414
+ deleteMany(filter: FilterQuery): Promise<number>
415
+ ```
416
+
417
+ Supprime tous les documents correspondant au filtre.
418
+ Retourne le nombre de documents supprimés.
419
+
420
+ ```typescript
421
+ const count = await userRepo.deleteMany({ active: false, createdAt: { $lt: cutoffDate } })
422
+ ```
423
+
424
+ ---
425
+
426
+ ### count()
427
+
428
+ ```typescript
429
+ count(filter?: FilterQuery): Promise<number>
430
+ ```
431
+
432
+ Compte les documents correspondant au filtre (tous si omis).
433
+
434
+ ```typescript
435
+ const total = await userRepo.count()
436
+ const admins = await userRepo.count({ role: 'admin' })
437
+ ```
438
+
439
+ ---
440
+
441
+ ### search()
442
+
443
+ ```typescript
444
+ search(query: string, options?: QueryOptions): Promise<T[]>
445
+ ```
446
+
447
+ Recherche textuelle dans tous les champs de type `string` du schéma.
448
+
449
+ - MongoDB : utilise les index `text`
450
+ - SQL : génère `WHERE field1 LIKE '%query%' OR field2 LIKE '%query%' ...`
451
+
452
+ ```typescript
453
+ const results = await productRepo.search('laptop pro', { limit: 10 })
454
+ ```
455
+
456
+ ---
457
+
458
+ ### distinct()
459
+
460
+ ```typescript
461
+ distinct(field: string, filter?: FilterQuery): Promise<unknown[]>
462
+ ```
463
+
464
+ Retourne les valeurs uniques d'un champ.
465
+
466
+ ```typescript
467
+ const categories = await productRepo.distinct('category')
468
+ // ['electronics', 'clothing', 'food']
469
+
470
+ const activeRoles = await userRepo.distinct('role', { active: true })
471
+ ```
472
+
473
+ ---
474
+
475
+ ### aggregate()
476
+
477
+ ```typescript
478
+ aggregate<R = Record<string, unknown>>(stages: AggregateStage[]): Promise<R[]>
479
+ ```
480
+
481
+ Exécute un pipeline d'agrégation.
482
+
483
+ ```typescript
484
+ interface CategoryStats {
485
+ _by: string
486
+ count: number
487
+ avgPrice: number
488
+ }
489
+
490
+ const stats = await productRepo.aggregate<CategoryStats>([
491
+ { $match: { active: true } },
492
+ {
493
+ $group: {
494
+ _by: 'category',
495
+ count: { $count: true },
496
+ avgPrice: { $avg: 'price' },
497
+ },
498
+ },
499
+ { $sort: { count: -1 } },
500
+ { $limit: 10 },
501
+ ])
502
+ ```
503
+
504
+ ---
505
+
506
+ ### upsert()
507
+
508
+ ```typescript
509
+ upsert(filter: FilterQuery, data: Partial<T>): Promise<T>
510
+ ```
511
+
512
+ Équivalent de `INSERT OR UPDATE` — crée si inexistant, met à jour sinon.
513
+ Correspond à `saveOrUpdate()` en Hibernate.
514
+
515
+ ```typescript
516
+ const config = await configRepo.upsert(
517
+ { key: 'theme' },
518
+ { key: 'theme', value: 'dark', updatedAt: new Date() }
519
+ )
520
+ ```
521
+
522
+ ---
523
+
524
+ ### increment()
525
+
526
+ ```typescript
527
+ increment(id: string, field: string, amount: number): Promise<T | null>
528
+ ```
529
+
530
+ Incrémente (ou décrémente si négatif) un champ numérique de façon atomique.
531
+
532
+ ```typescript
533
+ // Incrémenter le compteur de vues
534
+ await articleRepo.increment('abc123', 'views', 1)
535
+
536
+ // Décrémenter le stock
537
+ await productRepo.increment('xyz789', 'stock', -3)
538
+ ```
539
+
540
+ ---
541
+
542
+ ### addToSet()
543
+
544
+ ```typescript
545
+ addToSet(id: string, field: string, value: unknown): Promise<T | null>
546
+ ```
547
+
548
+ Ajoute une valeur dans un champ tableau, **sans créer de doublon**.
549
+ Équivalent de `$addToSet` MongoDB, ou d'une vérification `CONTAINS` en SQL.
550
+
551
+ ```typescript
552
+ // Ajouter un tag (sans doublon)
553
+ await productRepo.addToSet('abc123', 'tags', 'bestseller')
554
+
555
+ // Ajouter un rôle à un utilisateur
556
+ await userRepo.addToSet('user123', 'roles', 'editor')
557
+ ```
558
+
559
+ ---
560
+
561
+ ### pull()
562
+
563
+ ```typescript
564
+ pull(id: string, field: string, value: unknown): Promise<T | null>
565
+ ```
566
+
567
+ Retire une valeur d'un champ tableau.
568
+
569
+ ```typescript
570
+ // Retirer un tag
571
+ await productRepo.pull('abc123', 'tags', 'discontinued')
572
+
573
+ // Révoquer un rôle
574
+ await userRepo.pull('user123', 'roles', 'editor')
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Types
580
+
581
+ ### EntitySchema
582
+
583
+ ```typescript
584
+ interface EntitySchema {
585
+ /** Nom PascalCase de l'entité (ex: 'User', 'Product') */
586
+ name: string
587
+
588
+ /** Nom de la table SQL ou collection MongoDB (ex: 'users', 'products') */
589
+ collection: string
590
+
591
+ /** Définition des champs */
592
+ fields: Record<string, FieldDef>
593
+
594
+ /** Relations vers d'autres entités */
595
+ relations: Record<string, RelationDef>
596
+
597
+ /** Index de la table/collection */
598
+ indexes: IndexDef[]
599
+
600
+ /** Ajoute automatiquement createdAt et updatedAt */
601
+ timestamps: boolean
602
+ }
603
+ ```
604
+
605
+ ---
606
+
607
+ ### FieldDef
608
+
609
+ ```typescript
610
+ interface FieldDef {
611
+ /** Type du champ */
612
+ type: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array'
613
+
614
+ /** Le champ est obligatoire (NOT NULL en SQL) */
615
+ required?: boolean
616
+
617
+ /** Le champ doit être unique (UNIQUE en SQL, unique index en MongoDB) */
618
+ unique?: boolean
619
+
620
+ /** L'index unique accepte les valeurs NULL multiples (MongoDB sparse) */
621
+ sparse?: boolean
622
+
623
+ /** Valeur par défaut */
624
+ default?: unknown
625
+
626
+ /** Valeurs acceptées (validation) */
627
+ enum?: string[]
628
+
629
+ /** Convertir en minuscules avant stockage */
630
+ lowercase?: boolean
631
+
632
+ /** Supprimer les espaces en début/fin avant stockage */
633
+ trim?: boolean
634
+
635
+ /** Type des éléments (pour type: 'array') */
636
+ arrayOf?: FieldType | EmbeddedSchemaDef
637
+ }
638
+ ```
639
+
640
+ ---
641
+
642
+ ### RelationDef
643
+
644
+ ```typescript
645
+ interface RelationDef {
646
+ /** Nom de l'entité cible (doit être enregistrée dans le registry) */
647
+ target: string
648
+
649
+ /** Type de relation */
650
+ type: 'one-to-one' | 'many-to-one' | 'one-to-many' | 'many-to-many'
651
+
652
+ /** Cette relation est obligatoire */
653
+ required?: boolean
654
+
655
+ /** Champs à sélectionner lors du chargement (projection) */
656
+ select?: string[]
657
+
658
+ /** La relation peut être null */
659
+ nullable?: boolean
660
+
661
+ /** Nom de la table de jonction (many-to-many SQL uniquement) */
662
+ through?: string
663
+ }
664
+ ```
665
+
666
+ ---
667
+
668
+ ### IndexDef
669
+
670
+ ```typescript
671
+ interface IndexDef {
672
+ /** Champs et direction de tri */
673
+ fields: Record<string, 'asc' | 'desc' | 'text'>
674
+
675
+ /** Index unique */
676
+ unique?: boolean
677
+
678
+ /** Exclure les valeurs NULL de l'index */
679
+ sparse?: boolean
680
+ }
681
+ ```
682
+
683
+ ---
684
+
685
+ ### ConnectionConfig
686
+
687
+ ```typescript
688
+ interface ConnectionConfig {
689
+ /** Identifiant du dialecte */
690
+ dialect: DialectType
691
+
692
+ /** URI de connexion */
693
+ uri: string
694
+
695
+ /** Afficher les requêtes générées (défaut: false) */
696
+ showSql?: boolean
697
+
698
+ /** Indenter les requêtes affichées (défaut: false) */
699
+ formatSql?: boolean
700
+
701
+ /**
702
+ * Stratégie de gestion du schéma (défaut: 'none')
703
+ * - 'none' : ne rien faire
704
+ * - 'validate' : vérifier, refuser si incohérent
705
+ * - 'update' : créer/modifier les tables/collections
706
+ * - 'create' : supprimer et recréer au démarrage
707
+ * - 'create-drop' : supprimer au démarrage ET à l'arrêt
708
+ */
709
+ schemaStrategy?: 'none' | 'validate' | 'update' | 'create' | 'create-drop'
710
+
711
+ /** Taille max du pool de connexions */
712
+ poolSize?: number
713
+
714
+ /** Activer le cache des résultats de requête */
715
+ cacheEnabled?: boolean
716
+
717
+ /** Durée de vie du cache en secondes (défaut: 60) */
718
+ cacheTtlSeconds?: number
719
+
720
+ /** Taille des lots pour les opérations bulk (défaut: 25) */
721
+ batchSize?: number
722
+
723
+ /** Options supplémentaires spécifiques au dialecte */
724
+ options?: Record<string, unknown>
725
+ }
726
+ ```
727
+
728
+ **Dialectes disponibles** : `'mongodb' | 'sqlite' | 'postgres' | 'mysql' | 'mariadb' | 'oracle' | 'mssql' | 'cockroachdb' | 'db2' | 'hana' | 'hsqldb' | 'spanner' | 'sybase'`
729
+
730
+ ---
731
+
732
+ ### FilterQuery
733
+
734
+ ```typescript
735
+ interface FilterQuery {
736
+ [field: string]: FilterValue // égalité implicite ou opérateur
737
+
738
+ /** OU logique */
739
+ $or?: FilterQuery[]
740
+
741
+ /** ET logique */
742
+ $and?: FilterQuery[]
743
+ }
744
+
745
+ interface FilterOperator {
746
+ $eq?: unknown // égal à
747
+ $ne?: unknown // différent de
748
+ $gt?: unknown // supérieur à (strictement)
749
+ $gte?: unknown // supérieur ou égal
750
+ $lt?: unknown // inférieur à (strictement)
751
+ $lte?: unknown // inférieur ou égal
752
+ $in?: unknown[] // dans la liste
753
+ $nin?: unknown[] // hors de la liste
754
+ $regex?: string // expression régulière
755
+ $regexFlags?: string // flags regex (ex: 'i' = insensible à la casse)
756
+ $exists?: boolean // le champ existe
757
+ }
758
+ ```
759
+
760
+ **Exemples**
761
+
762
+ ```typescript
763
+ // Égalité implicite
764
+ { status: 'active' }
765
+
766
+ // Opérateurs de comparaison
767
+ { age: { $gte: 18, $lt: 65 } }
768
+
769
+ // Liste
770
+ { role: { $in: ['admin', 'moderator'] } }
771
+
772
+ // Regex
773
+ { name: { $regex: '^alice', $regexFlags: 'i' } }
774
+
775
+ // OU
776
+ { $or: [{ role: 'admin' }, { permissions: { $in: ['manage_users'] } }] }
777
+
778
+ // ET
779
+ { $and: [{ active: true }, { verified: true }] }
780
+
781
+ // Existence
782
+ { deletedAt: { $exists: false } }
783
+ ```
784
+
785
+ ---
786
+
787
+ ### QueryOptions
788
+
789
+ ```typescript
790
+ interface QueryOptions {
791
+ /** Tri des résultats : 1 = ASC, -1 = DESC */
792
+ sort?: Record<string, 1 | -1>
793
+
794
+ /** Nombre de documents à sauter (pagination) */
795
+ skip?: number
796
+
797
+ /** Nombre maximum de documents à retourner */
798
+ limit?: number
799
+
800
+ /** Champs à inclure dans les résultats (projection inclusive) */
801
+ select?: string[]
802
+
803
+ /** Champs à exclure des résultats (projection exclusive) */
804
+ exclude?: string[]
805
+ }
806
+ ```
807
+
808
+ **Exemple — pagination**
809
+
810
+ ```typescript
811
+ const PAGE = 2
812
+ const LIMIT = 20
813
+
814
+ const users = await userRepo.findAll(
815
+ { active: true },
816
+ {
817
+ sort: { createdAt: -1 },
818
+ skip: (PAGE - 1) * LIMIT,
819
+ limit: LIMIT,
820
+ select: ['id', 'name', 'email', 'role'],
821
+ }
822
+ )
823
+ ```
824
+
825
+ ---
826
+
827
+ ### AggregateStage
828
+
829
+ ```typescript
830
+ // Filtrage ($match → WHERE / $match)
831
+ type AggregateMatchStage = { $match: FilterQuery }
832
+
833
+ // Groupement ($group → GROUP BY)
834
+ type AggregateGroupStage = {
835
+ $group: {
836
+ _by: string | null // null = grouper tous les documents ensemble
837
+ [field: string]: AggregateAccumulator | string | null
838
+ }
839
+ }
840
+
841
+ // Accumulateurs disponibles dans $group
842
+ interface AggregateAccumulator {
843
+ $count?: true // COUNT(*)
844
+ $sum?: number | string // SUM(field) ou COUNT(*) si 1
845
+ $avg?: string // AVG(field)
846
+ $min?: string // MIN(field)
847
+ $max?: string // MAX(field)
848
+ }
849
+
850
+ // Tri ($sort → ORDER BY)
851
+ type AggregateSortStage = { $sort: Record<string, 1 | -1> }
852
+
853
+ // Limitation ($limit → LIMIT)
854
+ type AggregateLimitStage = { $limit: number }
855
+
856
+ // Union
857
+ type AggregateStage =
858
+ | AggregateMatchStage
859
+ | AggregateGroupStage
860
+ | AggregateSortStage
861
+ | AggregateLimitStage
862
+ ```
863
+
864
+ **Exemple complet**
865
+
866
+ ```typescript
867
+ // Chiffre d'affaires par mois, pour les 6 derniers mois
868
+ const revenue = await orderRepo.aggregate([
869
+ {
870
+ $match: {
871
+ createdAt: { $gte: sixMonthsAgo },
872
+ status: 'completed',
873
+ },
874
+ },
875
+ {
876
+ $group: {
877
+ _by: 'month',
878
+ totalRevenue: { $sum: 'total' },
879
+ orderCount: { $count: true },
880
+ avgOrderValue: { $avg: 'total' },
881
+ },
882
+ },
883
+ { $sort: { _by: 1 } },
884
+ ])
885
+ ```
886
+
887
+ ---
888
+
889
+ ### PaginatedResult
890
+
891
+ Type utilitaire pour les réponses paginées. Non retourné directement par `findAll` — à construire dans vos repositories.
892
+
893
+ ```typescript
894
+ interface PaginatedResult<T> {
895
+ data: T[]
896
+ total: number
897
+ page: number
898
+ limit: number
899
+ totalPages: number
900
+ }
901
+ ```
902
+
903
+ **Exemple d'implémentation dans un repository**
904
+
905
+ ```typescript
906
+ async findPaginated(
907
+ filter: FilterQuery = {},
908
+ page: number = 1,
909
+ limit: number = 20,
910
+ options?: Omit<QueryOptions, 'skip' | 'limit'>
911
+ ): Promise<PaginatedResult<T>> {
912
+ const [data, total] = await Promise.all([
913
+ this.findAll(filter, { ...options, skip: (page - 1) * limit, limit }),
914
+ this.count(filter),
915
+ ])
916
+ return {
917
+ data,
918
+ total,
919
+ page,
920
+ limit,
921
+ totalPages: Math.ceil(total / limit),
922
+ }
923
+ }
924
+ ```
925
+
926
+ ---
927
+
928
+ ## Erreurs
929
+
930
+ ```typescript
931
+ import {
932
+ MostaORMError,
933
+ EntityNotFoundError,
934
+ ConnectionError,
935
+ ValidationError,
936
+ DialectNotFoundError,
937
+ } from '@mosta/orm'
938
+ ```
939
+
940
+ | Classe | Hérite de | Quand |
941
+ |--------|-----------|-------|
942
+ | `MostaORMError` | `Error` | Erreur générique MostaORM |
943
+ | `EntityNotFoundError` | `MostaORMError` | Entité introuvable par ID |
944
+ | `ConnectionError` | `MostaORMError` | Échec de connexion au serveur DB |
945
+ | `ValidationError` | `MostaORMError` | Violation de contrainte de schéma |
946
+ | `DialectNotFoundError` | `MostaORMError` | Dialecte inconnu ou driver manquant |
947
+
948
+ **Gestion des erreurs**
949
+
950
+ ```typescript
951
+ import { EntityNotFoundError, ConnectionError } from '@mosta/orm'
952
+
953
+ try {
954
+ const user = await userRepo.findById(id)
955
+ if (!user) throw new EntityNotFoundError('User', id)
956
+ return user
957
+ } catch (err) {
958
+ if (err instanceof EntityNotFoundError) {
959
+ return res.status(404).json({ error: err.message })
960
+ }
961
+ if (err instanceof ConnectionError) {
962
+ return res.status(503).json({ error: 'Database unavailable' })
963
+ }
964
+ throw err
965
+ }
966
+ ```
967
+
968
+ ---
969
+
970
+ ## Interface IPlugin
971
+
972
+ Permet d'étendre le comportement du repository via des hooks.
973
+
974
+ ```typescript
975
+ interface IPlugin {
976
+ name: string
977
+
978
+ /** Modifier le schéma au démarrage (ajout de champs, index...) */
979
+ onSchemaInit?(schema: EntitySchema): EntitySchema
980
+
981
+ /** Avant insertion */
982
+ preSave?(doc: Record<string, unknown>, ctx: HookContext): Promise<Record<string, unknown>> | Record<string, unknown>
983
+
984
+ /** Après insertion */
985
+ postSave?(doc: Record<string, unknown>, ctx: HookContext): Promise<void> | void
986
+
987
+ /** Avant mise à jour */
988
+ preUpdate?(id: string, data: Record<string, unknown>, ctx: HookContext): Promise<Record<string, unknown>> | Record<string, unknown>
989
+
990
+ /** Après mise à jour */
991
+ postUpdate?(doc: Record<string, unknown>, ctx: HookContext): Promise<void> | void
992
+
993
+ /** Avant suppression */
994
+ preDelete?(id: string, ctx: HookContext): Promise<void> | void
995
+
996
+ /** Modifier le filtre des requêtes (ex: soft-delete) */
997
+ onQuery?(filter: FilterQuery, ctx: HookContext): FilterQuery
998
+
999
+ /** Transformer les résultats (ex: normalisation) */
1000
+ onResult?(doc: Record<string, unknown>, ctx: HookContext): Record<string, unknown>
1001
+ }
1002
+
1003
+ interface HookContext {
1004
+ entity: EntitySchema
1005
+ dialect: DialectType
1006
+ operation: 'create' | 'update' | 'delete' | 'find'
1007
+ userId?: string
1008
+ }
1009
+ ```