@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 +21 -0
- package/README.md +592 -0
- package/dist/index.cjs +755 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +746 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
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
|
+
[](https://frs.lpdjs.fr)
|
|
4
|
+
[](https://www.npmjs.com/package/@lpdjs/firestore-repo-service)
|
|
5
|
+
[](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.
|