@lpdjs/firestore-repo-service 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 LPDJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,592 @@
1
+ # 🔥 Firestore Repository Service
2
+
3
+ [![Documentation](https://img.shields.io/badge/Documentation-Online-blue?style=for-the-badge&logo=read-the-docs)](https://frs.lpdjs.fr)
4
+ [![npm version](https://img.shields.io/npm/v/@lpdjs/firestore-repo-service?style=for-the-badge)](https://www.npmjs.com/package/@lpdjs/firestore-repo-service)
5
+ [![License](https://img.shields.io/npm/l/@lpdjs/firestore-repo-service?style=for-the-badge)](https://github.com/solarpush/firestore-repo-service/blob/master/LICENSE)
6
+
7
+ Un service de repository type-safe pour Firestore avec génération automatique des méthodes de requête et CRUD.
8
+
9
+ 📚 **Documentation complète disponible sur [frs.lpdjs.fr](https://frs.lpdjs.fr)**
10
+
11
+ ## ✨ Fonctionnalités
12
+
13
+ - 🎯 **Type-safe** : TypeScript avec inférence complète des types
14
+ - 🚀 **Auto-génération** : Méthodes `get.by*` et `query.by*` générées automatiquement
15
+ - 🔍 **Requêtes avancées** : Support des conditions OR, tri, pagination avec curseurs
16
+ - 📦 **Opérations en masse** : Batch et bulk operations
17
+ - 🏗️ **Collections et sous-collections** : Support complet
18
+ - 💡 **API intuitive** : Accesseurs directs via getters
19
+ - 📡 **Real-time** : Listeners `onSnapshot` pour les mises à jour en temps réel
20
+ - 🔢 **Agrégations** : Count, sum, average avec support serveur
21
+ - ✏️ **CRUD complet** : Create, set, update, delete avec types préservés
22
+ - 🔄 **Transactions** : Opérations transactionnelles type-safe
23
+
24
+ ## 📦 Installation
25
+
26
+ ```bash
27
+ npm install @lpdjs/firestore-repo-service firebase-admin
28
+ # ou
29
+ yarn add @lpdjs/firestore-repo-service firebase-admin
30
+ # ou
31
+ bun add @lpdjs/firestore-repo-service firebase-admin
32
+ ```
33
+
34
+ ## 🚀 Démarrage rapide
35
+
36
+ ### 1. Définir vos modèles
37
+
38
+ ```typescript
39
+ interface UserModel {
40
+ docId: string;
41
+ email: string;
42
+ name: string;
43
+ age: number;
44
+ isActive: boolean;
45
+ }
46
+
47
+ interface PostModel {
48
+ docId: string;
49
+ userId: string;
50
+ title: string;
51
+ status: "draft" | "published";
52
+ }
53
+ ```
54
+
55
+ ### 2. Créer votre mapping
56
+
57
+ ```typescript
58
+ import {
59
+ createRepositoryConfig,
60
+ buildRepositoryRelations,
61
+ createRepositoryMapping,
62
+ } from "@lpdjs/firestore-repo-service";
63
+ import { doc } from "firebase/firestore";
64
+ import type { Firestore } from "firebase/firestore";
65
+
66
+ // 1. Définir la configuration de base
67
+ const repositoryMapping = {
68
+ users: createRepositoryConfig<UserModel>()({
69
+ path: "users",
70
+ isGroup: false,
71
+ foreignKeys: ["docId", "email"] as const,
72
+ queryKeys: ["name", "isActive"] as const,
73
+ refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
74
+ }),
75
+
76
+ posts: createRepositoryConfig<PostModel>()({
77
+ path: "posts",
78
+ isGroup: false,
79
+ foreignKeys: ["docId", "userId"] as const,
80
+ queryKeys: ["status"] as const,
81
+ refCb: (db: Firestore, docId: string) => doc(db, "posts", docId),
82
+ }),
83
+ };
84
+
85
+ // 2. Ajouter les relations (Optionnel)
86
+ const mappingWithRelations = buildRepositoryRelations(repositoryMapping, {
87
+ posts: {
88
+ userId: { repo: "users", key: "docId", type: "one" as const },
89
+ },
90
+ });
91
+
92
+ // 3. Créer le service
93
+ export const repos = createRepositoryMapping(db, mappingWithRelations);
94
+ ```
95
+
96
+ ### 3. Utiliser les repositories
97
+
98
+ ```typescript
99
+ // Récupérer un document unique
100
+ const user = await repos.users.get.byDocId("user123");
101
+ const userByEmail = await repos.users.get.byEmail("john@example.com");
102
+
103
+ // Rechercher des documents
104
+ const activeUsers = await repos.users.query.byIsActive(true);
105
+
106
+ // Relations (Populate)
107
+ const post = await repos.posts.get.byDocId("post123");
108
+ if (post) {
109
+ const postWithUser = await repos.posts.populate(post, "userId");
110
+ console.log(postWithUser.populated.users?.name); // Type-safe!
111
+ }
112
+
113
+ // Recherche avec options
114
+ const filteredUsers = await repos.users.query.byName("John", {
115
+ where: [{ field: "age", operator: ">=", value: 18 }],
116
+ orderBy: [{ field: "createdAt", direction: "desc" }],
117
+ limit: 10,
118
+ });
119
+
120
+ // Mettre à jour un document
121
+ const updated = await repos.users.update("user123", {
122
+ name: "John Updated",
123
+ age: 31,
124
+ });
125
+ ```
126
+
127
+ ## 📚 Guide complet
128
+
129
+ ### Configuration
130
+
131
+ #### `createRepositoryConfig()`
132
+
133
+ Configure un repository avec ses clés et méthodes.
134
+
135
+ **Paramètres :**
136
+
137
+ - `path` : Chemin de la collection dans Firestore
138
+ - `isGroup` : `true` pour une collection group, `false` pour une collection simple
139
+ - `foreignKeys` : Clés pour les méthodes `get.by*` (recherche unique)
140
+ - `queryKeys` : Clés pour les méthodes `query.by*` (recherche multiple)
141
+ - `type` : Type TypeScript du modèle
142
+ - `refCb` : Fonction pour créer la référence du document
143
+
144
+ **Exemple collection simple :**
145
+
146
+ ```typescript
147
+ users: createRepositoryConfig<UserModel>()({
148
+ path: "users",
149
+ isGroup: false,
150
+ foreignKeys: ["docId", "email"] as const,
151
+ queryKeys: ["isActive", "role"] as const,
152
+ refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
153
+ });
154
+ ```
155
+
156
+ **Exemple sous-collection :**
157
+
158
+ ```typescript
159
+ comments: createRepositoryConfig<CommentModel>()({
160
+ path: "comments",
161
+ isGroup: true,
162
+ foreignKeys: ["docId"] as const,
163
+ queryKeys: ["postId", "userId"] as const,
164
+ refCb: (db: Firestore, postId: string, commentId: string) =>
165
+ doc(db, "posts", postId, "comments", commentId),
166
+ });
167
+ ```
168
+
169
+ ### Méthodes GET
170
+
171
+ Récupère un **document unique** par une clé étrangère.
172
+
173
+ ```typescript
174
+ // Méthodes générées automatiquement depuis foreignKeys
175
+ const user = await repos.users.get.byDocId("user123");
176
+ const userByEmail = await repos.users.get.byEmail("john@example.com");
177
+
178
+ // Avec le DocumentSnapshot
179
+ const result = await repos.users.get.byDocId("user123", true);
180
+ if (result) {
181
+ console.log(result.data); // UserModel
182
+ console.log(result.doc); // DocumentSnapshot
183
+ }
184
+
185
+ // Récupérer par liste de valeurs
186
+ const users = await repos.users.get.byList("docId", [
187
+ "user1",
188
+ "user2",
189
+ "user3",
190
+ ]);
191
+ ```
192
+
193
+ ### Méthodes QUERY
194
+
195
+ Recherche **plusieurs documents** par une clé de requête.
196
+
197
+ ```typescript
198
+ // Méthodes générées automatiquement depuis queryKeys
199
+ const activeUsers = await repos.users.query.byIsActive(true);
200
+ const usersByName = await repos.users.query.byName("John");
201
+
202
+ // Avec options
203
+ const results = await repos.users.query.byIsActive(true, {
204
+ where: [{ field: "age", operator: ">=", value: 18 }],
205
+ orderBy: [{ field: "name", direction: "asc" }],
206
+ limit: 50,
207
+ });
208
+
209
+ // Requête générique
210
+ const users = await repos.users.query.by({
211
+ where: [
212
+ { field: "isActive", operator: "==", value: true },
213
+ { field: "age", operator: ">=", value: 18 },
214
+ ],
215
+ orderBy: [{ field: "createdAt", direction: "desc" }],
216
+ limit: 10,
217
+ });
218
+
219
+ // Conditions OR
220
+ const posts = await repos.posts.query.by({
221
+ orWhere: [
222
+ [{ field: "status", operator: "==", value: "published" }],
223
+ [{ field: "status", operator: "==", value: "draft" }],
224
+ ],
225
+ });
226
+ ```
227
+
228
+ ### Options de requête
229
+
230
+ ```typescript
231
+ interface QueryOptions<T> {
232
+ where?: WhereClause<T>[]; // Conditions AND
233
+ orWhere?: WhereClause<T>[][]; // Conditions OR
234
+ orderBy?: {
235
+ field: keyof T;
236
+ direction?: "asc" | "desc";
237
+ }[];
238
+ limit?: number; // Nombre max de résultats
239
+ offset?: number; // Pagination (skip)
240
+ startAt?: DocumentSnapshot | any[]; // Cursor pagination - start at
241
+ startAfter?: DocumentSnapshot | any[]; // Cursor pagination - start after
242
+ endAt?: DocumentSnapshot | any[]; // Cursor pagination - end at
243
+ endBefore?: DocumentSnapshot | any[]; // Cursor pagination - end before
244
+ }
245
+ ```
246
+
247
+ ### Mise à jour
248
+
249
+ ```typescript
250
+ // Met à jour et retourne le document mis à jour
251
+ const updated = await repos.users.update("user123", {
252
+ name: "New Name",
253
+ age: 30,
254
+ });
255
+
256
+ // Pour sous-collections
257
+ const updatedComment = await repos.comments.update(
258
+ "post123", // postId
259
+ "comment456", // commentId
260
+ { text: "Updated text" }
261
+ );
262
+ ```
263
+
264
+ ### Références de documents
265
+
266
+ ```typescript
267
+ const userRef = repos.users.documentRef("user123");
268
+ const commentRef = repos.comments.documentRef("post123", "comment456");
269
+ ```
270
+
271
+ ### Opérations Batch
272
+
273
+ Pour des opérations atomiques (max 500 opérations).
274
+
275
+ ```typescript
276
+ const batch = repos.users.batch.create();
277
+
278
+ batch.set(repos.users.documentRef("user1"), {
279
+ name: "User One",
280
+ email: "user1@example.com",
281
+ });
282
+
283
+ batch.update(repos.users.documentRef("user2"), {
284
+ age: 25,
285
+ });
286
+
287
+ batch.delete(repos.users.documentRef("user3"));
288
+
289
+ await batch.commit();
290
+ ```
291
+
292
+ ### Opérations Bulk
293
+
294
+ Pour traiter de grandes quantités (automatiquement divisées en batches de 500).
295
+
296
+ ```typescript
297
+ // Set multiple
298
+ await repos.users.bulk.set([
299
+ {
300
+ docRef: repos.users.documentRef("user1"),
301
+ data: { name: "User 1", email: "user1@example.com" },
302
+ merge: true,
303
+ },
304
+ {
305
+ docRef: repos.users.documentRef("user2"),
306
+ data: { name: "User 2", email: "user2@example.com" },
307
+ },
308
+ // ... jusqu'à des milliers de documents
309
+ ]);
310
+
311
+ // Update multiple
312
+ await repos.users.bulk.update([
313
+ { docRef: repos.users.documentRef("user1"), data: { age: 30 } },
314
+ { docRef: repos.users.documentRef("user2"), data: { age: 25 } },
315
+ ]);
316
+
317
+ // Delete multiple
318
+ await repos.users.bulk.delete([
319
+ repos.users.documentRef("user1"),
320
+ repos.users.documentRef("user2"),
321
+ ]);
322
+ ```
323
+
324
+ ### Récupérer tous les documents
325
+
326
+ ```typescript
327
+ // Récupère tous les documents de la collection
328
+ const allUsers = await repos.users.query.getAll();
329
+
330
+ // Avec des options de filtrage et tri
331
+ const filteredUsers = await repos.users.query.getAll({
332
+ where: [{ field: "isActive", operator: "==", value: true }],
333
+ orderBy: [{ field: "createdAt", direction: "desc" }],
334
+ limit: 100,
335
+ });
336
+ ```
337
+
338
+ ### Real-time listeners (onSnapshot)
339
+
340
+ ```typescript
341
+ // Écouter les changements en temps réel
342
+ const unsubscribe = repos.users.query.onSnapshot(
343
+ {
344
+ where: [{ field: "isActive", operator: "==", value: true }],
345
+ orderBy: [{ field: "name", direction: "asc" }],
346
+ },
347
+ (users) => {
348
+ console.log("Données mises à jour:", users);
349
+ },
350
+ (error) => {
351
+ console.error("Erreur:", error);
352
+ }
353
+ );
354
+
355
+ // Arrêter l'écoute
356
+ unsubscribe();
357
+ ```
358
+
359
+ ### Pagination avec curseurs
360
+
361
+ La pagination basée sur les curseurs est plus efficace que `offset` pour de grandes collections.
362
+
363
+ ```typescript
364
+ // Première page
365
+ const firstPage = await repos.users.query.by({
366
+ orderBy: [{ field: "createdAt", direction: "desc" }],
367
+ limit: 10,
368
+ });
369
+
370
+ // Page suivante en utilisant le dernier document
371
+ const lastDoc = firstPage[firstPage.length - 1];
372
+ const nextPage = await repos.users.query.by({
373
+ orderBy: [{ field: "createdAt", direction: "desc" }],
374
+ startAfter: lastDoc, // ou utiliser un tableau de valeurs
375
+ limit: 10,
376
+ });
377
+
378
+ // Exemple avec des valeurs
379
+ const page = await repos.users.query.by({
380
+ orderBy: [{ field: "createdAt", direction: "desc" }],
381
+ startAfter: [new Date("2024-01-01")],
382
+ limit: 10,
383
+ });
384
+ ```
385
+
386
+ ### CRUD complet
387
+
388
+ ```typescript
389
+ // Create - Créer avec ID auto-généré
390
+ const newUser = await repos.users.create({
391
+ email: "new@example.com",
392
+ name: "New User",
393
+ age: 25,
394
+ isActive: true,
395
+ });
396
+ console.log(newUser.docId); // ID auto-généré
397
+
398
+ // Set - Créer ou remplacer complètement
399
+ await repos.users.set("user123", {
400
+ email: "user@example.com",
401
+ name: "User",
402
+ age: 30,
403
+ isActive: true,
404
+ });
405
+
406
+ // Set avec merge - Fusion partielle
407
+ await repos.users.set(
408
+ "user123",
409
+ { age: 31 }, // Seul 'age' sera modifié
410
+ { merge: true }
411
+ );
412
+
413
+ // Update - Mise à jour partielle
414
+ const updated = await repos.users.update("user123", {
415
+ age: 32,
416
+ name: "Updated Name",
417
+ });
418
+
419
+ // Delete - Supprimer un document
420
+ await repos.users.delete("user123");
421
+ ```
422
+
423
+ ### Transactions
424
+
425
+ ```typescript
426
+ // Transaction avec méthodes type-safe
427
+ const result = await repos.users.transaction.run(async (txn) => {
428
+ // Get document dans la transaction
429
+ const user = await txn.get(repos.users.documentRef("user123"));
430
+
431
+ if (user.exists()) {
432
+ const userData = user.data();
433
+
434
+ // Update dans la transaction
435
+ txn.update(repos.users.documentRef("user123"), {
436
+ age: userData.age + 1,
437
+ });
438
+
439
+ // Set dans la transaction
440
+ txn.set(repos.users.documentRef("user124"), {
441
+ email: "new@example.com",
442
+ name: "New User",
443
+ });
444
+
445
+ // Delete dans la transaction
446
+ txn.delete(repos.users.documentRef("user125"));
447
+ }
448
+
449
+ return { success: true };
450
+ });
451
+
452
+ // Accès à la transaction Firestore brute si nécessaire
453
+ await repos.users.transaction.run(async (txn) => {
454
+ const rawTransaction = txn.raw;
455
+ // Utiliser rawTransaction avec l'API Firestore native
456
+ });
457
+ ```
458
+
459
+ ### Agrégations
460
+
461
+ ```typescript
462
+ import { count, sum, average } from "@lpdjs/firestore-repo-service";
463
+
464
+ // Compter les documents
465
+ const activeCount = await repos.users.aggregate.count({
466
+ where: [{ field: "isActive", operator: "==", value: true }],
467
+ });
468
+
469
+ // Agrégations personnalisées (count, sum, average)
470
+ const stats = await repos.users.aggregate.query(
471
+ {
472
+ totalUsers: count(),
473
+ totalAge: sum("age"),
474
+ averageAge: average("age"),
475
+ },
476
+ {
477
+ where: [{ field: "isActive", operator: "==", value: true }],
478
+ }
479
+ );
480
+
481
+ console.log(stats.totalUsers); // nombre total
482
+ console.log(stats.totalAge); // somme des âges
483
+ console.log(stats.averageAge); // moyenne des âges
484
+ ```
485
+
486
+ ### Accès à la collection Firestore
487
+
488
+ ```typescript
489
+ // Référence brute si besoin
490
+ const collectionRef = repos.users.ref;
491
+ ```
492
+
493
+ ## 🎯 Exemples avancés
494
+
495
+ ### Collection imbriquée complexe
496
+
497
+ ```typescript
498
+ const repositoryMapping = {
499
+ eventRatings: createRepositoryConfig<RatingModel>()({
500
+ path: "ratings",
501
+ isGroup: true,
502
+ foreignKeys: ["docId"] as const,
503
+ queryKeys: ["eventId", "rating"] as const,
504
+ refCb: (
505
+ db: Firestore,
506
+ residenceId: string,
507
+ eventId: string,
508
+ ratingId: string
509
+ ) =>
510
+ doc(
511
+ db,
512
+ "residences",
513
+ residenceId,
514
+ "events",
515
+ eventId,
516
+ "ratings",
517
+ ratingId
518
+ ),
519
+ }),
520
+ };
521
+
522
+ // Utilisation
523
+ const rating = await repos.eventRatings.update(
524
+ "residence123",
525
+ "event456",
526
+ "rating789",
527
+ { score: 5 }
528
+ );
529
+ ```
530
+
531
+ ### Recherche complexe avec OR
532
+
533
+ ```typescript
534
+ // (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
535
+ const users = await repos.users.query.by({
536
+ orWhere: [
537
+ [
538
+ { field: "status", operator: "==", value: "active" },
539
+ { field: "age", operator: ">=", value: 18 },
540
+ ],
541
+ [
542
+ { field: "status", operator: "==", value: "pending" },
543
+ { field: "verified", operator: "==", value: true },
544
+ ],
545
+ ],
546
+ orderBy: [{ field: "createdAt", direction: "desc" }],
547
+ limit: 100,
548
+ });
549
+ ```
550
+
551
+ ## 🔧 Types exportés
552
+
553
+ ```typescript
554
+ // Types utiles
555
+ import type {
556
+ WhereClause,
557
+ QueryOptions,
558
+ RepositoryKey,
559
+ RepositoryModelType,
560
+ } from "@lpdjs/firestore-repo-service";
561
+ ```
562
+
563
+ ## 🧪 Tests avec l'émulateur
564
+
565
+ Pour tester rapidement sans projet Firebase :
566
+
567
+ ```bash
568
+ # 1. Installer Firebase CLI (si nécessaire)
569
+ npm install -g firebase-tools
570
+
571
+ # 2. Démarrer l'émulateur (terminal 1)
572
+ bun run emulator
573
+
574
+ # 3. Lancer les tests (terminal 2)
575
+ bun run test:emulator
576
+ ```
577
+
578
+ L'émulateur Firestore démarre sur `localhost:8080` avec une UI sur `http://localhost:4000`.
579
+
580
+ Voir `test/README.md` pour plus de détails.
581
+
582
+ ## 📝 Licence
583
+
584
+ MIT
585
+
586
+ ## 🤝 Contribution
587
+
588
+ Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
589
+
590
+ ## 📬 Support
591
+
592
+ Pour toute question ou problème, ouvrez une issue sur GitHub.