@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,846 @@
1
+ # MostaORM — Tutorial complet
2
+
3
+ > Ce tutoriel vous guide de zéro à une application complète utilisant MostaORM avec SQLite, puis montre comment basculer vers PostgreSQL sans changer une seule ligne de code métier.
4
+
5
+ ---
6
+
7
+ ## Table des matières
8
+
9
+ 1. [Installation](#1-installation)
10
+ 2. [Configuration](#2-configuration)
11
+ 3. [Définir une entité (EntitySchema)](#3-définir-une-entité-entityschema)
12
+ 4. [Créer un Repository](#4-créer-un-repository)
13
+ 5. [Opérations CRUD](#5-opérations-crud)
14
+ 6. [Filtres avancés](#6-filtres-avancés)
15
+ 7. [Relations entre entités](#7-relations-entre-entités)
16
+ 8. [Agrégations](#8-agrégations)
17
+ 9. [Recherche plein texte](#9-recherche-plein-texte)
18
+ 10. [Changer de dialecte](#10-changer-de-dialecte)
19
+ 11. [Application Express complète](#11-application-express-complète)
20
+
21
+ ---
22
+
23
+ ## 1. Installation
24
+
25
+ ```bash
26
+ npm install @mosta/orm
27
+ ```
28
+
29
+ Installez uniquement le driver de votre base de données :
30
+
31
+ ```bash
32
+ # SQLite (léger, parfait pour commencer)
33
+ npm install better-sqlite3
34
+
35
+ # PostgreSQL
36
+ npm install pg
37
+
38
+ # MongoDB
39
+ npm install mongoose
40
+
41
+ # MySQL / MariaDB
42
+ npm install mysql2
43
+ npm install mariadb
44
+
45
+ # Autres — voir docs/dialects.md
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 2. Configuration
51
+
52
+ MostaORM se configure via **variables d'environnement** ou directement dans le code.
53
+
54
+ ### Via variables d'environnement
55
+
56
+ Créez un fichier `.env` :
57
+
58
+ ```env
59
+ DB_DIALECT=sqlite
60
+ SGBD_URI=./data/myapp.db
61
+ DB_SCHEMA_STRATEGY=update
62
+ DB_SHOW_SQL=true
63
+ ```
64
+
65
+ Le code se réduit à :
66
+
67
+ ```typescript
68
+ import { getDialect } from '@mosta/orm'
69
+
70
+ const dialect = await getDialect() // lit DB_DIALECT + SGBD_URI automatiquement
71
+ ```
72
+
73
+ ### Via code
74
+
75
+ ```typescript
76
+ import { createConnection } from '@mosta/orm'
77
+
78
+ const dialect = await createConnection({
79
+ dialect: 'sqlite',
80
+ uri: './data/myapp.db',
81
+ schemaStrategy: 'update', // crée/met à jour les tables automatiquement
82
+ showSql: true, // affiche les requêtes générées
83
+ })
84
+ ```
85
+
86
+ ### Valeurs de `schemaStrategy`
87
+
88
+ | Valeur | Comportement | Usage |
89
+ |--------|-------------|-------|
90
+ | `update` | Crée les tables manquantes, ajoute les colonnes | Développement |
91
+ | `create` | Supprime et recrée tout au démarrage | Tests |
92
+ | `create-drop` | Supprime au démarrage ET à l'arrêt | Tests unitaires |
93
+ | `validate` | Vérifie le schéma, refuse de démarrer si incohérent | Production |
94
+ | `none` | Ne touche rien | Production (migrations manuelles) |
95
+
96
+ ---
97
+
98
+ ## 3. Définir une entité (EntitySchema)
99
+
100
+ Un `EntitySchema` est l'équivalent d'une `@Entity` JPA ou d'un modèle Mongoose — il décrit la structure d'une table/collection.
101
+
102
+ ### Exemple : entité `Product`
103
+
104
+ ```typescript
105
+ // schemas/product.schema.ts
106
+ import type { EntitySchema } from '@mosta/orm'
107
+
108
+ export const ProductSchema: EntitySchema = {
109
+ name: 'Product',
110
+ collection: 'products', // nom de la table SQL ou collection MongoDB
111
+ timestamps: true, // ajoute createdAt + updatedAt automatiquement
112
+
113
+ fields: {
114
+ name: {
115
+ type: 'string',
116
+ required: true,
117
+ trim: true,
118
+ },
119
+ description: {
120
+ type: 'string',
121
+ },
122
+ price: {
123
+ type: 'number',
124
+ required: true,
125
+ },
126
+ stock: {
127
+ type: 'number',
128
+ default: 0,
129
+ },
130
+ category: {
131
+ type: 'string',
132
+ enum: ['electronics', 'clothing', 'food', 'books'],
133
+ required: true,
134
+ },
135
+ active: {
136
+ type: 'boolean',
137
+ default: true,
138
+ },
139
+ tags: {
140
+ type: 'array',
141
+ arrayOf: 'string',
142
+ },
143
+ metadata: {
144
+ type: 'json', // stocké comme JSON / JSONB selon le dialecte
145
+ },
146
+ },
147
+
148
+ relations: {
149
+ category: {
150
+ type: 'many-to-one',
151
+ target: 'Category',
152
+ required: false,
153
+ },
154
+ reviews: {
155
+ type: 'one-to-many',
156
+ target: 'Review',
157
+ },
158
+ },
159
+
160
+ indexes: [
161
+ { fields: { name: 'asc' }, unique: true },
162
+ { fields: { category: 'asc', price: 'asc' } },
163
+ { fields: { name: 'text' } }, // index full-text
164
+ ],
165
+ }
166
+ ```
167
+
168
+ ### Types de champs disponibles
169
+
170
+ | Type | SQL | MongoDB | Description |
171
+ |------|-----|---------|-------------|
172
+ | `string` | `VARCHAR(255)` / `TEXT` | `String` | Texte |
173
+ | `number` | `REAL` / `NUMERIC` | `Number` | Entier ou décimal |
174
+ | `boolean` | `INTEGER (0/1)` | `Boolean` | Vrai/faux |
175
+ | `date` | `DATETIME` / `TIMESTAMP` | `Date` | Date et heure |
176
+ | `json` | `TEXT` / `JSONB` | `Mixed` | Objet structuré |
177
+ | `array` | `TEXT (JSON sérialisé)` | `Array` | Liste de valeurs |
178
+
179
+ ### Champ embarqué (sous-document)
180
+
181
+ ```typescript
182
+ fields: {
183
+ address: {
184
+ type: 'json', // ou utiliser arrayOf pour des tableaux de sous-docs
185
+ // Pour un tableau de sous-documents structurés :
186
+ },
187
+ schedules: {
188
+ type: 'array',
189
+ arrayOf: {
190
+ kind: 'embedded',
191
+ fields: {
192
+ day: { type: 'string', required: true },
193
+ start: { type: 'string', required: true },
194
+ end: { type: 'string', required: true },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 4. Créer un Repository
204
+
205
+ Un repository encapsule toutes les opérations sur une entité. Étendez `BaseRepository` pour ajouter des méthodes métier.
206
+
207
+ ```typescript
208
+ // repositories/product.repository.ts
209
+ import { BaseRepository, type IDialect } from '@mosta/orm'
210
+ import { ProductSchema } from '../schemas/product.schema.js'
211
+
212
+ export interface Product {
213
+ id: string
214
+ name: string
215
+ description?: string
216
+ price: number
217
+ stock: number
218
+ category: string
219
+ active: boolean
220
+ tags?: string[]
221
+ createdAt?: Date
222
+ updatedAt?: Date
223
+ }
224
+
225
+ export class ProductRepository extends BaseRepository<Product> {
226
+ constructor(dialect: IDialect) {
227
+ super(ProductSchema, dialect)
228
+ }
229
+
230
+ // Méthodes métier personnalisées
231
+ async findActive(): Promise<Product[]> {
232
+ return this.findAll({ active: true }, { sort: { name: 1 } })
233
+ }
234
+
235
+ async findByCategory(category: string): Promise<Product[]> {
236
+ return this.findAll({ category, active: true })
237
+ }
238
+
239
+ async findInPriceRange(min: number, max: number): Promise<Product[]> {
240
+ return this.findAll({
241
+ price: { $gte: min, $lte: max },
242
+ active: true,
243
+ })
244
+ }
245
+
246
+ async deactivate(id: string): Promise<Product | null> {
247
+ return this.update(id, { active: false })
248
+ }
249
+
250
+ async adjustStock(id: string, delta: number): Promise<Product | null> {
251
+ return this.increment(id, 'stock', delta)
252
+ }
253
+ }
254
+ ```
255
+
256
+ ### Enregistrer les schémas et créer le repository
257
+
258
+ ```typescript
259
+ // db.ts
260
+ import { createConnection, registerSchemas } from '@mosta/orm'
261
+ import { ProductSchema } from './schemas/product.schema.js'
262
+ import { ProductRepository } from './repositories/product.repository.js'
263
+
264
+ let productRepo: ProductRepository
265
+
266
+ export async function initDB() {
267
+ // Enregistrer tous les schémas
268
+ registerSchemas([ProductSchema /*, CategorySchema, ReviewSchema */])
269
+
270
+ // Connecter (singleton — sûr d'appeler plusieurs fois)
271
+ const dialect = await createConnection({
272
+ dialect: 'sqlite',
273
+ uri: './data/shop.db',
274
+ schemaStrategy: 'update',
275
+ })
276
+
277
+ productRepo = new ProductRepository(dialect)
278
+ }
279
+
280
+ export function getProductRepo(): ProductRepository {
281
+ if (!productRepo) throw new Error('DB not initialized — call initDB() first')
282
+ return productRepo
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 5. Opérations CRUD
289
+
290
+ ### Créer
291
+
292
+ ```typescript
293
+ const product = await productRepo.create({
294
+ name: 'Laptop Pro 15"',
295
+ price: 1299.99,
296
+ stock: 50,
297
+ category: 'electronics',
298
+ tags: ['laptop', 'portable', 'pro'],
299
+ })
300
+
301
+ console.log(product.id) // UUID généré automatiquement
302
+ console.log(product.createdAt) // Date de création (si timestamps: true)
303
+ ```
304
+
305
+ ### Lire
306
+
307
+ ```typescript
308
+ // Par ID
309
+ const product = await productRepo.findById('abc123')
310
+
311
+ // Avec filtre
312
+ const products = await productRepo.findAll({ category: 'electronics' })
313
+
314
+ // Premier résultat
315
+ const cheapest = await productRepo.findOne(
316
+ { category: 'electronics' },
317
+ { sort: { price: 1 } }
318
+ )
319
+
320
+ // Avec pagination
321
+ const page1 = await productRepo.findAll(
322
+ { active: true },
323
+ { sort: { createdAt: -1 }, skip: 0, limit: 20 }
324
+ )
325
+ ```
326
+
327
+ ### Mettre à jour
328
+
329
+ ```typescript
330
+ // Mise à jour partielle — seuls les champs fournis sont modifiés
331
+ const updated = await productRepo.update('abc123', {
332
+ price: 999.99,
333
+ stock: 45,
334
+ })
335
+
336
+ // Mise à jour de masse
337
+ const count = await productRepo.updateMany(
338
+ { category: 'electronics', stock: { $lt: 5 } },
339
+ { active: false }
340
+ )
341
+ console.log(`${count} produits désactivés`)
342
+ ```
343
+
344
+ ### Supprimer
345
+
346
+ ```typescript
347
+ // Par ID
348
+ const deleted = await productRepo.delete('abc123') // true | false
349
+
350
+ // Suppression de masse
351
+ const count = await productRepo.deleteMany({ active: false })
352
+ ```
353
+
354
+ ### Compter
355
+
356
+ ```typescript
357
+ const total = await productRepo.count()
358
+ const active = await productRepo.count({ active: true })
359
+ const expensive = await productRepo.count({ price: { $gt: 1000 } })
360
+ ```
361
+
362
+ ### Upsert (créer ou mettre à jour)
363
+
364
+ ```typescript
365
+ // Équivalent Hibernate saveOrUpdate()
366
+ const product = await productRepo.upsert(
367
+ { name: 'Laptop Pro 15"' }, // critère de recherche
368
+ { name: 'Laptop Pro 15"', price: 1299.99, stock: 50, category: 'electronics' }
369
+ )
370
+ ```
371
+
372
+ ---
373
+
374
+ ## 6. Filtres avancés
375
+
376
+ MostaORM offre une API de filtrage inspirée de MongoDB, traduite automatiquement en SQL.
377
+
378
+ ```typescript
379
+ // Égalité (implicite)
380
+ await productRepo.findAll({ category: 'electronics' })
381
+
382
+ // Comparaisons
383
+ await productRepo.findAll({ price: { $gt: 100, $lte: 500 } })
384
+ await productRepo.findAll({ stock: { $gte: 1 } })
385
+
386
+ // Inclusion / exclusion
387
+ await productRepo.findAll({ category: { $in: ['electronics', 'books'] } })
388
+ await productRepo.findAll({ category: { $nin: ['food'] } })
389
+
390
+ // Inégalité
391
+ await productRepo.findAll({ category: { $ne: 'food' } })
392
+
393
+ // Existence d'un champ
394
+ await productRepo.findAll({ description: { $exists: true } })
395
+
396
+ // Regex (recherche partielle)
397
+ await productRepo.findAll({ name: { $regex: 'laptop', $regexFlags: 'i' } })
398
+
399
+ // Opérateurs logiques
400
+ await productRepo.findAll({
401
+ $or: [
402
+ { price: { $lt: 50 } },
403
+ { category: 'food' },
404
+ ],
405
+ })
406
+
407
+ await productRepo.findAll({
408
+ $and: [
409
+ { active: true },
410
+ { stock: { $gt: 0 } },
411
+ { price: { $lte: 100 } },
412
+ ],
413
+ })
414
+ ```
415
+
416
+ ### Options de requête (QueryOptions)
417
+
418
+ ```typescript
419
+ await productRepo.findAll(
420
+ { active: true },
421
+ {
422
+ sort: { price: 1, name: 1 }, // 1 = ASC, -1 = DESC
423
+ skip: 40, // décalage (pagination)
424
+ limit: 20, // maximum de résultats
425
+ select: ['id', 'name', 'price'], // projection — champs à inclure
426
+ exclude: ['metadata'], // champs à exclure
427
+ }
428
+ )
429
+ ```
430
+
431
+ ---
432
+
433
+ ## 7. Relations entre entités
434
+
435
+ ### Définir les schémas liés
436
+
437
+ ```typescript
438
+ // schemas/category.schema.ts
439
+ export const CategorySchema: EntitySchema = {
440
+ name: 'Category',
441
+ collection: 'categories',
442
+ timestamps: false,
443
+ fields: {
444
+ name: { type: 'string', required: true, unique: true },
445
+ slug: { type: 'string', required: true, unique: true },
446
+ },
447
+ relations: {},
448
+ indexes: [],
449
+ }
450
+
451
+ // Dans ProductSchema
452
+ relations: {
453
+ category: {
454
+ type: 'many-to-one',
455
+ target: 'Category',
456
+ select: ['id', 'name', 'slug'], // champs à inclure lors du JOIN/populate
457
+ },
458
+ }
459
+ ```
460
+
461
+ ### Charger avec les relations
462
+
463
+ ```typescript
464
+ // Un produit avec sa catégorie chargée
465
+ const product = await productRepo.findByIdWithRelations(
466
+ 'abc123',
467
+ ['category']
468
+ )
469
+ // product.category = { id: '...', name: 'Electronics', slug: 'electronics' }
470
+
471
+ // Plusieurs produits avec relations
472
+ const products = await productRepo.findWithRelations(
473
+ { active: true },
474
+ ['category', 'reviews'],
475
+ { sort: { name: 1 } }
476
+ )
477
+ ```
478
+
479
+ ### Types de relations
480
+
481
+ ```typescript
482
+ relations: {
483
+ // Un produit appartient à une catégorie
484
+ category: {
485
+ type: 'many-to-one',
486
+ target: 'Category',
487
+ required: true,
488
+ },
489
+
490
+ // Un produit a plusieurs avis
491
+ reviews: {
492
+ type: 'one-to-many',
493
+ target: 'Review',
494
+ },
495
+
496
+ // Relation many-to-many via table de jonction
497
+ tags: {
498
+ type: 'many-to-many',
499
+ target: 'Tag',
500
+ through: 'product_tags', // nom de la table de jonction (SQL)
501
+ },
502
+
503
+ // Un produit a un seul profil technique
504
+ specs: {
505
+ type: 'one-to-one',
506
+ target: 'ProductSpec',
507
+ nullable: true,
508
+ },
509
+ }
510
+ ```
511
+
512
+ ---
513
+
514
+ ## 8. Agrégations
515
+
516
+ L'API d'agrégation est inspirée du pipeline MongoDB — traduit en SQL GROUP BY / HAVING.
517
+
518
+ ### Compter par catégorie
519
+
520
+ ```typescript
521
+ const stats = await productRepo.aggregate([
522
+ { $match: { active: true } },
523
+ {
524
+ $group: {
525
+ _by: 'category',
526
+ count: { $count: true },
527
+ avgPrice: { $avg: 'price' },
528
+ minPrice: { $min: 'price' },
529
+ maxPrice: { $max: 'price' },
530
+ totalStock: { $sum: 'stock' },
531
+ },
532
+ },
533
+ { $sort: { count: -1 } },
534
+ ])
535
+
536
+ // Résultat :
537
+ // [
538
+ // { _by: 'electronics', count: 42, avgPrice: 450, minPrice: 9.99, maxPrice: 2999, totalStock: 380 },
539
+ // { _by: 'books', count: 120, avgPrice: 22, minPrice: 2.99, maxPrice: 89, totalStock: 1500 },
540
+ // ]
541
+ ```
542
+
543
+ ### Valeurs distinctes
544
+
545
+ ```typescript
546
+ const categories = await productRepo.distinct('category')
547
+ // ['electronics', 'clothing', 'food', 'books']
548
+
549
+ const brands = await productRepo.distinct('brand', { active: true })
550
+ ```
551
+
552
+ ### Accumulateurs disponibles
553
+
554
+ | Accumulateur | SQL | Description |
555
+ |-------------|-----|-------------|
556
+ | `$count: true` | `COUNT(*)` | Nombre de documents |
557
+ | `$sum: 'field'` | `SUM(field)` | Somme d'un champ numérique |
558
+ | `$sum: 1` | `COUNT(*)` | Comptage (alias) |
559
+ | `$avg: 'field'` | `AVG(field)` | Moyenne |
560
+ | `$min: 'field'` | `MIN(field)` | Valeur minimale |
561
+ | `$max: 'field'` | `MAX(field)` | Valeur maximale |
562
+
563
+ ---
564
+
565
+ ## 9. Recherche plein texte
566
+
567
+ ```typescript
568
+ // Recherche dans tous les champs string du schéma
569
+ const results = await productRepo.search('laptop pro')
570
+
571
+ // Avec options
572
+ const results = await productRepo.search(
573
+ 'laptop',
574
+ { sort: { price: 1 }, limit: 10 }
575
+ )
576
+ ```
577
+
578
+ > **Note** : La recherche utilise des index `text` sur MongoDB, et `LIKE %query%` sur les dialectes SQL. Pour des recherches avancées, utilisez l'opérateur `$regex` dans les filtres.
579
+
580
+ ### Opération sur les tableaux
581
+
582
+ ```typescript
583
+ // Ajouter un tag (sans doublon)
584
+ await productRepo.addToSet('abc123', 'tags', 'bestseller')
585
+
586
+ // Retirer un tag
587
+ await productRepo.pull('abc123', 'tags', 'discontinued')
588
+
589
+ // Incrémenter le stock
590
+ await productRepo.increment('abc123', 'stock', -3) // -3 = décrémenter
591
+ ```
592
+
593
+ ---
594
+
595
+ ## 10. Changer de dialecte
596
+
597
+ C'est la force principale de MostaORM : **zéro changement de code métier** pour changer de base de données.
598
+
599
+ ### Avant (SQLite local)
600
+
601
+ ```env
602
+ DB_DIALECT=sqlite
603
+ SGBD_URI=./data/shop.db
604
+ DB_SCHEMA_STRATEGY=update
605
+ ```
606
+
607
+ ### Après (PostgreSQL en production)
608
+
609
+ ```env
610
+ DB_DIALECT=postgres
611
+ SGBD_URI=postgresql://user:pass@db.example.com:5432/shopdb
612
+ DB_SCHEMA_STRATEGY=validate
613
+ DB_POOL_SIZE=10
614
+ ```
615
+
616
+ Le repository `ProductRepository` et tout le code métier restent **identiques**.
617
+
618
+ ### Migration SQLite → MongoDB
619
+
620
+ ```env
621
+ DB_DIALECT=mongodb
622
+ SGBD_URI=mongodb+srv://user:pass@cluster.mongodb.net/shopdb
623
+ DB_SCHEMA_STRATEGY=update
624
+ ```
625
+
626
+ Installez le driver : `npm install mongoose`
627
+
628
+ ---
629
+
630
+ ## 11. Application Express complète
631
+
632
+ Voici une API REST complète avec MostaORM.
633
+
634
+ ### Structure du projet
635
+
636
+ ```
637
+ my-shop-api/
638
+ ├── src/
639
+ │ ├── schemas/
640
+ │ │ └── product.schema.ts
641
+ │ ├── repositories/
642
+ │ │ └── product.repository.ts
643
+ │ ├── routes/
644
+ │ │ └── products.ts
645
+ │ ├── db.ts
646
+ │ └── index.ts
647
+ ├── .env
648
+ └── package.json
649
+ ```
650
+
651
+ ### `src/db.ts`
652
+
653
+ ```typescript
654
+ import { createConnection, registerSchemas } from '@mosta/orm'
655
+ import { ProductSchema } from './schemas/product.schema.js'
656
+ import { ProductRepository } from './repositories/product.repository.js'
657
+
658
+ let productRepository: ProductRepository
659
+
660
+ export async function initDB(): Promise<void> {
661
+ registerSchemas([ProductSchema])
662
+
663
+ const dialect = await createConnection({
664
+ dialect: (process.env.DB_DIALECT as any) || 'sqlite',
665
+ uri: process.env.SGBD_URI || './data/shop.db',
666
+ schemaStrategy: 'update',
667
+ showSql: process.env.NODE_ENV !== 'production',
668
+ })
669
+
670
+ productRepository = new ProductRepository(dialect)
671
+ console.log('✅ Database connected')
672
+ }
673
+
674
+ export const repos = {
675
+ get products() { return productRepository },
676
+ }
677
+ ```
678
+
679
+ ### `src/routes/products.ts`
680
+
681
+ ```typescript
682
+ import { Router } from 'express'
683
+ import { repos } from '../db.js'
684
+
685
+ const router = Router()
686
+
687
+ // GET /products?category=electronics&page=1&limit=20
688
+ router.get('/', async (req, res) => {
689
+ try {
690
+ const { category, search, page = '1', limit = '20', minPrice, maxPrice } = req.query
691
+
692
+ const filter: any = { active: true }
693
+ if (category) filter.category = category
694
+ if (minPrice || maxPrice) {
695
+ filter.price = {}
696
+ if (minPrice) filter.price.$gte = Number(minPrice)
697
+ if (maxPrice) filter.price.$lte = Number(maxPrice)
698
+ }
699
+
700
+ const pageNum = parseInt(page as string)
701
+ const limitNum = parseInt(limit as string)
702
+
703
+ let products
704
+ if (search) {
705
+ products = await repos.products.search(search as string, { limit: limitNum })
706
+ } else {
707
+ products = await repos.products.findAll(filter, {
708
+ sort: { name: 1 },
709
+ skip: (pageNum - 1) * limitNum,
710
+ limit: limitNum,
711
+ })
712
+ }
713
+
714
+ const total = await repos.products.count(filter)
715
+
716
+ res.json({
717
+ data: products,
718
+ meta: {
719
+ total,
720
+ page: pageNum,
721
+ limit: limitNum,
722
+ totalPages: Math.ceil(total / limitNum),
723
+ },
724
+ })
725
+ } catch (err) {
726
+ res.status(500).json({ error: 'Internal server error' })
727
+ }
728
+ })
729
+
730
+ // GET /products/:id
731
+ router.get('/:id', async (req, res) => {
732
+ const product = await repos.products.findById(req.params.id)
733
+ if (!product) return res.status(404).json({ error: 'Product not found' })
734
+ res.json({ data: product })
735
+ })
736
+
737
+ // POST /products
738
+ router.post('/', async (req, res) => {
739
+ try {
740
+ const product = await repos.products.create(req.body)
741
+ res.status(201).json({ data: product })
742
+ } catch (err: any) {
743
+ res.status(400).json({ error: err.message })
744
+ }
745
+ })
746
+
747
+ // PUT /products/:id
748
+ router.put('/:id', async (req, res) => {
749
+ const product = await repos.products.update(req.params.id, req.body)
750
+ if (!product) return res.status(404).json({ error: 'Product not found' })
751
+ res.json({ data: product })
752
+ })
753
+
754
+ // DELETE /products/:id
755
+ router.delete('/:id', async (req, res) => {
756
+ const deleted = await repos.products.delete(req.params.id)
757
+ if (!deleted) return res.status(404).json({ error: 'Product not found' })
758
+ res.status(204).send()
759
+ })
760
+
761
+ // GET /products/stats/by-category
762
+ router.get('/stats/by-category', async (req, res) => {
763
+ const stats = await repos.products.aggregate([
764
+ { $match: { active: true } },
765
+ {
766
+ $group: {
767
+ _by: 'category',
768
+ count: { $count: true },
769
+ avgPrice: { $avg: 'price' },
770
+ totalStock: { $sum: 'stock' },
771
+ },
772
+ },
773
+ { $sort: { count: -1 } },
774
+ ])
775
+ res.json({ data: stats })
776
+ })
777
+
778
+ export default router
779
+ ```
780
+
781
+ ### `src/index.ts`
782
+
783
+ ```typescript
784
+ import 'dotenv/config'
785
+ import express from 'express'
786
+ import { initDB } from './db.js'
787
+ import productsRouter from './routes/products.js'
788
+
789
+ const app = express()
790
+ app.use(express.json())
791
+
792
+ app.use('/api/products', productsRouter)
793
+
794
+ app.get('/health', (_, res) => res.json({ status: 'ok' }))
795
+
796
+ async function start() {
797
+ await initDB()
798
+ const PORT = process.env.PORT || 3000
799
+ app.listen(PORT, () => {
800
+ console.log(`🚀 Server running on http://localhost:${PORT}`)
801
+ })
802
+ }
803
+
804
+ start().catch(console.error)
805
+ ```
806
+
807
+ ### `.env`
808
+
809
+ ```env
810
+ DB_DIALECT=sqlite
811
+ SGBD_URI=./data/shop.db
812
+ DB_SCHEMA_STRATEGY=update
813
+ DB_SHOW_SQL=true
814
+ PORT=3000
815
+ ```
816
+
817
+ ### Lancer l'application
818
+
819
+ ```bash
820
+ npm install better-sqlite3 express dotenv
821
+ npx tsc
822
+ node dist/index.js
823
+ ```
824
+
825
+ Test rapide :
826
+
827
+ ```bash
828
+ # Créer un produit
829
+ curl -X POST http://localhost:3000/api/products \
830
+ -H 'Content-Type: application/json' \
831
+ -d '{"name":"Laptop Pro","price":999,"stock":10,"category":"electronics"}'
832
+
833
+ # Lister les produits
834
+ curl http://localhost:3000/api/products?category=electronics
835
+
836
+ # Statistiques par catégorie
837
+ curl http://localhost:3000/api/products/stats/by-category
838
+ ```
839
+
840
+ ---
841
+
842
+ ## Étapes suivantes
843
+
844
+ - [docs/dialects.md](./dialects.md) — Configuration détaillée de chaque dialecte
845
+ - [docs/api-reference.md](./api-reference.md) — Référence complète de l'API
846
+ - [examples/](../examples/) — Exemples complets (contact-manager, balloon-booking)