@lpdjs/firestore-repo-service 2.2.1 → 2.2.3

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/README.md CHANGED
@@ -1,43 +1,22 @@
1
- # 🔥 Firestore Repository Service
1
+ # firestore-repo-service
2
2
 
3
3
  [![Documentation](https://img.shields.io/badge/Documentation-Online-blue?style=for-the-badge&logo=read-the-docs)](https://frs.lpdjs.fr)
4
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
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
6
 
7
- Un service de repository type-safe pour Firestore avec génération automatique des méthodes de requête et CRUD.
7
+ Type-safe Firestore repository layer with auto-generated query methods, CRUD, and a Firestore→SQL sync pipeline via Pub/Sub + BigQuery.
8
8
 
9
- 📚 **Documentation complète disponible sur [frs.lpdjs.fr](https://frs.lpdjs.fr)**
9
+ **Full documentation at [frs.lpdjs.fr](https://frs.lpdjs.fr)**
10
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
- - 🔗 **Relations** : Populate avec select typé (champs projetés)
24
- - 📄 **Pagination** : Curseurs, include avec relations, select
25
- - 🔄 **Firestore → SQL Sync** : Réplication vers BigQuery via Pub/Sub avec admin UI
26
- - 🖥️ **Serveur Admin** : UI admin auto-générée avec formulaires Zod, filtrage, navigation de relations
27
-
28
- ## 📦 Installation
11
+ ## Installation
29
12
 
30
13
  ```bash
31
14
  npm install @lpdjs/firestore-repo-service firebase-admin
32
- # ou
33
- yarn add @lpdjs/firestore-repo-service firebase-admin
34
- # ou
35
- bun add @lpdjs/firestore-repo-service firebase-admin
36
15
  ```
37
16
 
38
- ## 🚀 Démarrage rapide
17
+ ## Quick start
39
18
 
40
- ### 1. Définir vos modèles
19
+ ### Define your models
41
20
 
42
21
  ```typescript
43
22
  interface UserModel {
@@ -56,7 +35,7 @@ interface PostModel {
56
35
  }
57
36
  ```
58
37
 
59
- ### 2. Créer votre mapping
38
+ ### Create the repository mapping
60
39
 
61
40
  ```typescript
62
41
  import {
@@ -67,7 +46,6 @@ import {
67
46
  import { doc } from "firebase/firestore";
68
47
  import type { Firestore } from "firebase/firestore";
69
48
 
70
- // 1. Définir la configuration de base
71
49
  const repositoryMapping = {
72
50
  users: createRepositoryConfig<UserModel>()({
73
51
  path: "users",
@@ -76,7 +54,6 @@ const repositoryMapping = {
76
54
  queryKeys: ["name", "isActive"] as const,
77
55
  refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
78
56
  }),
79
-
80
57
  posts: createRepositoryConfig<PostModel>()({
81
58
  path: "posts",
82
59
  isGroup: false,
@@ -86,78 +63,57 @@ const repositoryMapping = {
86
63
  }),
87
64
  };
88
65
 
89
- // 2. Ajouter les relations (Optionnel)
66
+ // Optional: add relations
90
67
  const mappingWithRelations = buildRepositoryRelations(repositoryMapping, {
91
68
  posts: {
92
69
  userId: { repo: "users", key: "docId", type: "one" as const },
93
70
  },
94
71
  });
95
72
 
96
- // 3. Créer le service
97
73
  export const repos = createRepositoryMapping(db, mappingWithRelations);
98
74
  ```
99
75
 
100
- ### 3. Utiliser les repositories
76
+ ### Use the repositories
101
77
 
102
78
  ```typescript
103
- // Récupérer un document unique
79
+ // Fetch a single document
104
80
  const user = await repos.users.get.byDocId("user123");
105
81
  const userByEmail = await repos.users.get.byEmail("john@example.com");
106
82
 
107
- // Rechercher des documents
83
+ // Query multiple documents
108
84
  const activeUsers = await repos.users.query.byIsActive(true);
109
85
 
110
- // Relations (Populate)
86
+ // With query options
87
+ const results = await repos.users.query.byIsActive(true, {
88
+ where: [["age", ">=", 18]],
89
+ orderBy: [{ field: "name", direction: "asc" }],
90
+ limit: 50,
91
+ });
92
+
93
+ // Populate a relation
111
94
  const post = await repos.posts.get.byDocId("post123");
112
95
  if (post) {
113
96
  const postWithUser = await repos.posts.populate(post, "userId");
114
- console.log(postWithUser.populated.users?.name); // Type-safe!
97
+ console.log(postWithUser.populated.users?.name); // type-safe
115
98
  }
116
99
 
117
- // Recherche avec options (tuples where)
118
- const filteredUsers = await repos.users.query.byName("John", {
119
- where: [["age", ">=", 18]],
120
- orderBy: [{ field: "createdAt", direction: "desc" }],
121
- limit: 10,
122
- });
123
-
124
- // Mettre à jour un document
125
- const updated = await repos.users.update("user123", {
126
- name: "John Updated",
127
- age: 31,
128
- });
100
+ // Update
101
+ const updated = await repos.users.update("user123", { name: "New name", age: 31 });
129
102
  ```
130
103
 
131
- ## 📚 Guide complet
132
-
133
- ### Configuration
134
-
135
- #### `createRepositoryConfig()`
136
-
137
- Configure un repository avec ses clés et méthodes.
104
+ ## API reference
138
105
 
139
- **Paramètres :**
106
+ ### `createRepositoryConfig()`
140
107
 
141
- - `path` : Chemin de la collection dans Firestore
142
- - `isGroup` : `true` pour une collection group, `false` pour une collection simple
143
- - `foreignKeys` : Clés pour les méthodes `get.by*` (recherche unique)
144
- - `queryKeys` : Clés pour les méthodes `query.by*` (recherche multiple)
145
- - `type` : Type TypeScript du modèle
146
- - `refCb` : Fonction pour créer la référence du document
147
-
148
- **Exemple collection simple :**
149
-
150
- ```typescript
151
- users: createRepositoryConfig<UserModel>()({
152
- path: "users",
153
- isGroup: false,
154
- foreignKeys: ["docId", "email"] as const,
155
- queryKeys: ["isActive", "role"] as const,
156
- refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
157
- });
158
- ```
108
+ | Option | Description |
109
+ | ------------- | ------------------------------------------------------- |
110
+ | `path` | Collection path in Firestore |
111
+ | `isGroup` | `true` for collection group, `false` for simple |
112
+ | `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
113
+ | `queryKeys` | Keys for `query.by*` methods (multi-document query) |
114
+ | `refCb` | Function that returns the document reference |
159
115
 
160
- **Exemple sous-collection :**
116
+ **Sub-collection example:**
161
117
 
162
118
  ```typescript
163
119
  comments: createRepositoryConfig<CommentModel>()({
@@ -165,449 +121,144 @@ comments: createRepositoryConfig<CommentModel>()({
165
121
  isGroup: true,
166
122
  foreignKeys: ["docId"] as const,
167
123
  queryKeys: ["postId", "userId"] as const,
168
- refCb: (db: Firestore, postId: string, commentId: string) =>
124
+ refCb: (db, postId, commentId) =>
169
125
  doc(db, "posts", postId, "comments", commentId),
170
126
  });
171
127
  ```
172
128
 
173
- ### Méthodes GET
174
-
175
- Récupère un **document unique** par une clé étrangère.
129
+ ### Query options
176
130
 
177
131
  ```typescript
178
- // Méthodes générées automatiquement depuis foreignKeys
179
- const user = await repos.users.get.byDocId("user123");
180
- const userByEmail = await repos.users.get.byEmail("john@example.com");
181
-
182
- // Avec le DocumentSnapshot
183
- const result = await repos.users.get.byDocId("user123", true);
184
- if (result) {
185
- console.log(result.data); // UserModel
186
- console.log(result.doc); // DocumentSnapshot
132
+ interface QueryOptions<T> {
133
+ where?: [keyof T, WhereFilterOp, any][]; // AND conditions
134
+ orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
135
+ orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
136
+ limit?: number;
137
+ offset?: number;
138
+ select?: (keyof T)[];
139
+ startAt?: DocumentSnapshot | any[];
140
+ startAfter?: DocumentSnapshot | any[];
141
+ endAt?: DocumentSnapshot | any[];
142
+ endBefore?: DocumentSnapshot | any[];
187
143
  }
188
-
189
- // Récupérer par liste de valeurs
190
- const users = await repos.users.get.byList("docId", [
191
- "user1",
192
- "user2",
193
- "user3",
194
- ]);
195
- ```
196
-
197
- ### Méthodes QUERY
198
-
199
- Recherche **plusieurs documents** par une clé de requête.
200
-
201
- ```typescript
202
- // Méthodes générées automatiquement depuis queryKeys
203
- const activeUsers = await repos.users.query.byIsActive(true);
204
- const usersByName = await repos.users.query.byName("John");
205
-
206
- // Avec options (syntaxe tuple pour where)
207
- const results = await repos.users.query.byIsActive(true, {
208
- where: [["age", ">=", 18]],
209
- orderBy: [{ field: "name", direction: "asc" }],
210
- limit: 50,
211
- });
212
-
213
- // Requête générique
214
- const users = await repos.users.query.by({
215
- where: [
216
- ["isActive", "==", true],
217
- ["age", ">=", 18],
218
- ],
219
- orderBy: [{ field: "createdAt", direction: "desc" }],
220
- limit: 10,
221
- });
222
-
223
- // Conditions OR
224
- const posts = await repos.posts.query.by({
225
- orWhere: [[["status", "==", "published"]], [["status", "==", "draft"]]],
226
- });
227
144
  ```
228
145
 
229
- ### Options de requête
146
+ ### CRUD
230
147
 
231
148
  ```typescript
232
- // WhereClause en tuple : [field, operator, value]
233
- type WhereClause<T> = [keyof T, WhereFilterOp, any];
149
+ // Create (auto-generated ID)
150
+ const newUser = await repos.users.create({ email: "...", name: "...", age: 25, isActive: true });
234
151
 
235
- interface QueryOptions<T> {
236
- where?: WhereClause<T>[]; // Conditions AND - tuples [field, op, value]
237
- orWhere?: WhereClause<T>[][]; // Conditions OR
238
- orderBy?: {
239
- field: keyof T;
240
- direction?: "asc" | "desc";
241
- }[];
242
- limit?: number; // Nombre max de résultats
243
- offset?: number; // Pagination (skip)
244
- select?: (keyof T)[]; // Champs à récupérer (projection)
245
- startAt?: DocumentSnapshot | any[]; // Cursor pagination - start at
246
- startAfter?: DocumentSnapshot | any[]; // Cursor pagination - start after
247
- endAt?: DocumentSnapshot | any[]; // Cursor pagination - end at
248
- endBefore?: DocumentSnapshot | any[]; // Cursor pagination - end before
249
- }
250
- ```
152
+ // Set (create or replace)
153
+ await repos.users.set("user123", { ... });
251
154
 
252
- ### Mise à jour
155
+ // Set with merge
156
+ await repos.users.set("user123", { age: 31 }, { merge: true });
253
157
 
254
- ```typescript
255
- // Met à jour et retourne le document mis à jour
256
- const updated = await repos.users.update("user123", {
257
- name: "New Name",
258
- age: 30,
259
- });
158
+ // Update (partial)
159
+ await repos.users.update("user123", { age: 32 });
260
160
 
261
- // Pour sous-collections
262
- const updatedComment = await repos.comments.update(
263
- "post123", // postId
264
- "comment456", // commentId
265
- { text: "Updated text" },
266
- );
267
- ```
161
+ // Delete
162
+ await repos.users.delete("user123");
268
163
 
269
- ### Références de documents
164
+ // Document ref
165
+ const ref = repos.users.documentRef("user123");
270
166
 
271
- ```typescript
272
- const userRef = repos.users.documentRef("user123");
273
- const commentRef = repos.comments.documentRef("post123", "comment456");
167
+ // Raw collection ref
168
+ const colRef = repos.users.ref;
274
169
  ```
275
170
 
276
- ### Opérations Batch
277
-
278
- Pour des opérations atomiques (max 500 opérations).
171
+ ### Batch & Bulk
279
172
 
280
173
  ```typescript
174
+ // Atomic batch (max 500 operations)
281
175
  const batch = repos.users.batch.create();
282
-
283
- batch.set(repos.users.documentRef("user1"), {
284
- name: "User One",
285
- email: "user1@example.com",
286
- });
287
-
288
- batch.update(repos.users.documentRef("user2"), {
289
- age: 25,
290
- });
291
-
292
- batch.delete(repos.users.documentRef("user3"));
293
-
176
+ batch.set(repos.users.documentRef("u1"), { ... });
177
+ batch.update(repos.users.documentRef("u2"), { age: 25 });
178
+ batch.delete(repos.users.documentRef("u3"));
294
179
  await batch.commit();
295
- ```
296
-
297
- ### Opérations Bulk
298
180
 
299
- Pour traiter de grandes quantités (automatiquement divisées en batches de 500).
300
-
301
- ```typescript
302
- // Set multiple
181
+ // Bulk (auto-split into batches of 500)
303
182
  await repos.users.bulk.set([
304
- {
305
- docRef: repos.users.documentRef("user1"),
306
- data: { name: "User 1", email: "user1@example.com" },
307
- merge: true,
308
- },
309
- {
310
- docRef: repos.users.documentRef("user2"),
311
- data: { name: "User 2", email: "user2@example.com" },
312
- },
313
- // ... jusqu'à des milliers de documents
314
- ]);
315
-
316
- // Update multiple
317
- await repos.users.bulk.update([
318
- { docRef: repos.users.documentRef("user1"), data: { age: 30 } },
319
- { docRef: repos.users.documentRef("user2"), data: { age: 25 } },
183
+ { docRef: repos.users.documentRef("u1"), data: { ... }, merge: true },
320
184
  ]);
321
-
322
- // Delete multiple
323
- await repos.users.bulk.delete([
324
- repos.users.documentRef("user1"),
325
- repos.users.documentRef("user2"),
326
- ]);
327
- ```
328
-
329
- ### Récupérer tous les documents
330
-
331
- ```typescript
332
- // Récupère tous les documents de la collection
333
- const allUsers = await repos.users.query.getAll();
334
-
335
- // Avec des options de filtrage et tri
336
- const filteredUsers = await repos.users.query.getAll({
337
- where: [["isActive", "==", true]],
338
- orderBy: [{ field: "createdAt", direction: "desc" }],
339
- limit: 100,
340
- });
341
-
342
- // Avec projection (select)
343
- const userNames = await repos.users.query.getAll({
344
- select: ["docId", "name", "email"],
345
- });
185
+ await repos.users.bulk.update([...]);
186
+ await repos.users.bulk.delete([...]);
346
187
  ```
347
188
 
348
- ### Real-time listeners (onSnapshot)
189
+ ### Real-time listeners
349
190
 
350
191
  ```typescript
351
- // Écouter les changements en temps réel
352
192
  const unsubscribe = repos.users.query.onSnapshot(
353
- {
354
- where: [["isActive", "==", true]],
355
- orderBy: [{ field: "name", direction: "asc" }],
356
- },
357
- (users) => {
358
- console.log("Données mises à jour:", users);
359
- },
360
- (error) => {
361
- console.error("Erreur:", error);
362
- },
193
+ { where: [["isActive", "==", true]] },
194
+ (users) => console.log(users),
363
195
  );
364
-
365
- // Arrêter l'écoute
366
196
  unsubscribe();
367
197
  ```
368
198
 
369
- ### Pagination avec curseurs
370
-
371
- La pagination basée sur les curseurs est plus efficace que `offset` pour de grandes collections.
199
+ ### Pagination
372
200
 
373
201
  ```typescript
374
- // Première page
375
202
  const firstPage = await repos.users.query.by({
376
203
  orderBy: [{ field: "createdAt", direction: "desc" }],
377
204
  limit: 10,
378
205
  });
379
206
 
380
- // Page suivante en utilisant le dernier document
381
- const lastDoc = firstPage[firstPage.length - 1];
382
207
  const nextPage = await repos.users.query.by({
383
208
  orderBy: [{ field: "createdAt", direction: "desc" }],
384
- startAfter: lastDoc, // ou utiliser un tableau de valeurs
209
+ startAfter: firstPage[firstPage.length - 1],
385
210
  limit: 10,
386
211
  });
387
212
 
388
- // Exemple avec des valeurs
389
- const page = await repos.users.query.by({
390
- orderBy: [{ field: "createdAt", direction: "desc" }],
391
- startAfter: [new Date("2024-01-01")],
392
- limit: 10,
213
+ // Paginate with relations
214
+ const page = await repos.posts.query.paginate({
215
+ pageSize: 10,
216
+ include: [{ relation: "userId", select: ["docId", "name", "email"] }],
393
217
  });
394
218
  ```
395
219
 
396
- ### CRUD complet
220
+ ### Aggregations
397
221
 
398
222
  ```typescript
399
- // Create - Créer avec ID auto-généré
400
- const newUser = await repos.users.create({
401
- email: "new@example.com",
402
- name: "New User",
403
- age: 25,
404
- isActive: true,
405
- });
406
- console.log(newUser.docId); // ID auto-généré
407
-
408
- // Set - Créer ou remplacer complètement
409
- await repos.users.set("user123", {
410
- email: "user@example.com",
411
- name: "User",
412
- age: 30,
413
- isActive: true,
414
- });
415
-
416
- // Set avec merge - Fusion partielle
417
- await repos.users.set(
418
- "user123",
419
- { age: 31 }, // Seul 'age' sera modifié
420
- { merge: true },
421
- );
422
-
423
- // Update - Mise à jour partielle
424
- const updated = await repos.users.update("user123", {
425
- age: 32,
426
- name: "Updated Name",
427
- });
223
+ import { count, sum, average } from "@lpdjs/firestore-repo-service";
428
224
 
429
- // Delete - Supprimer un document
430
- await repos.users.delete("user123");
225
+ const activeCount = await repos.users.aggregate.count({ where: [["isActive", "==", true]] });
226
+ const totalViews = await repos.posts.aggregate.sum("views");
227
+ const avgAge = await repos.users.aggregate.average("age");
431
228
  ```
432
229
 
433
230
  ### Transactions
434
231
 
435
232
  ```typescript
436
- // Transaction avec méthodes type-safe
437
233
  const result = await repos.users.transaction.run(async (txn) => {
438
- // Get document dans la transaction
439
234
  const user = await txn.get(repos.users.documentRef("user123"));
440
-
441
235
  if (user.exists()) {
442
- const userData = user.data();
443
-
444
- // Update dans la transaction
445
- txn.update(repos.users.documentRef("user123"), {
446
- age: userData.age + 1,
447
- });
448
-
449
- // Set dans la transaction
450
- txn.set(repos.users.documentRef("user124"), {
451
- email: "new@example.com",
452
- name: "New User",
453
- });
454
-
455
- // Delete dans la transaction
456
- txn.delete(repos.users.documentRef("user125"));
236
+ txn.update(repos.users.documentRef("user123"), { age: user.data().age + 1 });
457
237
  }
458
-
459
238
  return { success: true };
460
239
  });
461
-
462
- // Accès à la transaction Firestore brute si nécessaire
463
- await repos.users.transaction.run(async (txn) => {
464
- const rawTransaction = txn.raw;
465
- // Utiliser rawTransaction avec l'API Firestore native
466
- });
467
240
  ```
468
241
 
469
- ### Agrégations
470
-
471
- ```typescript
472
- import { count, sum, average } from "@lpdjs/firestore-repo-service";
473
-
474
- // Compter les documents
475
- const activeCount = await repos.users.aggregate.count({
476
- where: [["isActive", "==", true]],
477
- });
478
-
479
- // Somme d'un champ
480
- const totalViews = await repos.posts.aggregate.sum("views");
481
-
482
- // Moyenne d'un champ
483
- const avgAge = await repos.users.aggregate.average("age");
484
-
485
- // Avec filtres
486
- const publishedViews = await repos.posts.aggregate.sum("views", {
487
- where: [["status", "==", "published"]],
488
- });
489
- ```
490
-
491
- ### Accès à la collection Firestore
492
-
493
- ```typescript
494
- // Référence brute si besoin
495
- const collectionRef = repos.users.ref;
496
- ```
497
-
498
- ## 🎯 Exemples avancés
499
-
500
- ### Collection imbriquée complexe
501
-
502
- ```typescript
503
- const repositoryMapping = {
504
- eventRatings: createRepositoryConfig<RatingModel>()({
505
- path: "ratings",
506
- isGroup: true,
507
- foreignKeys: ["docId"] as const,
508
- queryKeys: ["eventId", "rating"] as const,
509
- refCb: (
510
- db: Firestore,
511
- residenceId: string,
512
- eventId: string,
513
- ratingId: string,
514
- ) =>
515
- doc(
516
- db,
517
- "residences",
518
- residenceId,
519
- "events",
520
- eventId,
521
- "ratings",
522
- ratingId,
523
- ),
524
- }),
525
- };
526
-
527
- // Utilisation
528
- const rating = await repos.eventRatings.update(
529
- "residence123",
530
- "event456",
531
- "rating789",
532
- { score: 5 },
533
- );
534
- ```
535
-
536
- ### Recherche complexe avec OR
242
+ ### OR queries
537
243
 
538
244
  ```typescript
539
245
  // (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
540
246
  const users = await repos.users.query.by({
541
247
  orWhere: [
542
- [
543
- ["status", "==", "active"],
544
- ["age", ">=", 18],
545
- ],
546
- [
547
- ["status", "==", "pending"],
548
- ["verified", "==", true],
549
- ],
248
+ [["status", "==", "active"], ["age", ">=", 18]],
249
+ [["status", "==", "pending"], ["verified", "==", true]],
550
250
  ],
551
- orderBy: [{ field: "createdAt", direction: "desc" }],
552
- limit: 100,
553
- });
554
- ```
555
-
556
- ### Populate avec select (projection)
557
-
558
- ```typescript
559
- // Populate avec tous les champs
560
- const postWithUser = await repos.posts.populate(post, "userId");
561
- console.log(postWithUser.populated.users?.name);
562
-
563
- // Populate avec select (seulement certains champs)
564
- const userWithPosts = await repos.users.populate(
565
- { docId: user.docId },
566
- {
567
- relation: "docId",
568
- select: ["docId", "title", "status"], // Type-safe: keyof PostModel
569
- },
570
- );
571
-
572
- // Populate plusieurs relations
573
- const postWithAll = await repos.posts.populate(post, ["userId", "categoryId"]);
574
- ```
575
-
576
- ### Pagination avec include
577
-
578
- ```typescript
579
- // Paginer avec relations incluses
580
- const page = await repos.posts.query.paginate({
581
- pageSize: 10,
582
- orderBy: [{ field: "createdAt", direction: "desc" }],
583
- include: ["userId"], // Inclut l'auteur automatiquement
584
- });
585
-
586
- // Avec select sur les relations
587
- const pageWithSelect = await repos.posts.query.paginate({
588
- pageSize: 10,
589
- include: [{ relation: "userId", select: ["docId", "name", "email"] }],
590
251
  });
591
-
592
- // Accès aux données peuplées
593
- for (const post of page.data) {
594
- console.log(post.title);
595
- console.log(post.populated.users?.name); // Type-safe!
596
- }
597
252
  ```
598
253
 
599
- ## 🔄 Firestore → SQL Sync
600
-
601
- Répliquez automatiquement vos collections Firestore vers BigQuery (ou toute base SQL) via Cloud Pub/Sub.
254
+ ## Firestore → SQL Sync
602
255
 
603
- ### Architecture
256
+ Replicate Firestore collections to BigQuery (or any SQL database) via Cloud Pub/Sub and Cloud Functions v2.
604
257
 
605
258
  ```
606
- Firestore TriggersCloud Pub/Sub → Worker → BigQuery
259
+ Firestore triggers → Pub/Sub → CF v2 worker → BigQuery MERGE
607
260
  ```
608
261
 
609
- ### Démarrage rapide
610
-
611
262
  ```typescript
612
263
  import { createFirestoreSync } from "@lpdjs/firestore-repo-service/sync";
613
264
  import { BigQueryAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery";
@@ -625,130 +276,61 @@ const sync = createFirestoreSync(repos, {
625
276
  }),
626
277
  topicPrefix: "firestore-sync",
627
278
  autoMigrate: true,
279
+ batchSize: 500,
280
+ flushIntervalMs: 10_000,
281
+ workerOptions: {
282
+ concurrency: 5,
283
+ maxInstances: 10,
284
+ },
628
285
  admin: {
629
286
  onRequest,
630
287
  httpsOptions: { invoker: "public" },
631
288
  auth: { type: "basic", username: "admin", password: "secret" },
632
- featuresFlag: {
633
- healthCheck: true,
634
- manualSync: true,
635
- viewQueue: true,
636
- configCheck: true,
637
- },
289
+ featuresFlag: { healthCheck: true, manualSync: true, viewQueue: true, configCheck: true },
638
290
  },
639
291
  repos: {
640
292
  users: { tableName: "users", columnMap: { docId: "user_id" } },
641
293
  posts: { columnMap: { docId: "post_id" } },
642
- // Collection groups nécessitent triggerPath
643
294
  comments: { triggerPath: "posts/{postId}/comments/{docId}" },
644
295
  },
645
296
  });
646
297
 
647
- // Export des Cloud Functions (adminsync auto-wrappé via onRequest + httpsOptions)
648
298
  export const {
649
- users_onCreate,
650
- users_onUpdate,
651
- users_onDelete,
652
- sync_users,
653
- posts_onCreate,
654
- posts_onUpdate,
655
- posts_onDelete,
656
- sync_posts,
657
- comments_onCreate,
658
- comments_onUpdate,
659
- comments_onDelete,
660
- sync_comments,
299
+ users_onCreate, users_onUpdate, users_onDelete, sync_users,
300
+ posts_onCreate, posts_onUpdate, posts_onDelete, sync_posts,
301
+ comments_onCreate, comments_onUpdate, comments_onDelete, sync_comments,
661
302
  adminsync,
662
303
  } = sync.functions;
663
304
  ```
664
305
 
665
- ### Sync Admin
666
-
667
- L'endpoint admin fournit :
668
-
669
- - **Health Check** : Compare le schéma attendu (Zod) vs les colonnes BigQuery réelles
670
- - **Force Sync** : Re-synchronise tous les documents d'une collection
671
- - **View Queues** : Inspecte les éléments en attente
672
- - **Config Check** : Vérifie APIs GCP, topics, tables — avec commandes `gcloud` pour corriger
306
+ The admin endpoint (`/`) exposes a UI for health checks, force-sync, queue inspection, and GCP config verification.
673
307
 
674
- ### Adaptateur personnalisé
675
-
676
- Implémentez l'interface `SqlAdapter` pour d'autres bases de données :
308
+ For a custom SQL backend, implement the `SqlAdapter` interface:
677
309
 
678
310
  ```typescript
679
311
  import type { SqlAdapter } from "@lpdjs/firestore-repo-service/sync";
680
312
 
681
313
  class MyAdapter implements SqlAdapter {
682
- // tableExists, getTableColumns, createTable, insertRows,
683
- // upsertRows, deleteRows, executeRaw
314
+ // tableExists, getTableColumns, createTable, upsertRows, deleteRows, executeRaw
684
315
  }
685
316
  ```
686
317
 
687
- 📚 **Documentation complète** : [frs.lpdjs.fr/guide/sync](https://frs.lpdjs.fr/guide/sync)
688
-
689
- ## 🔧 Types exportés
690
-
691
- ```typescript
692
- import type {
693
- // Core types
694
- WhereClause,
695
- QueryOptions,
696
- RepositoryConfig,
697
- RelationConfig,
698
-
699
- // Populate/Include types (avec typage keyof)
700
- PopulateOptions,
701
- PopulateOptionsTyped,
702
- IncludeConfig,
703
- IncludeConfigTyped,
704
-
705
- // Pagination
706
- PaginationOptions,
707
- PaginationResult,
708
- PaginationWithIncludeOptions,
709
- PaginationWithIncludeOptionsTyped,
710
- } from "@lpdjs/firestore-repo-service";
711
-
712
- // Sync types
713
- import type {
714
- FirestoreSyncConfig,
715
- SqlAdapter,
716
- SqlColumn,
717
- SqlTableDef,
718
- RepoSyncConfig,
719
- adminsyncConfig,
720
- } from "@lpdjs/firestore-repo-service/sync";
721
- ```
722
-
723
- ## 🧪 Tests avec l'émulateur
318
+ Full sync documentation: [frs.lpdjs.fr/guide/sync](https://frs.lpdjs.fr/guide/sync)
724
319
 
725
- Pour tester rapidement sans projet Firebase :
320
+ ## Testing
726
321
 
727
322
  ```bash
728
- # Installer Firebase CLI (si nécessaire)
729
- npm install -g firebase-tools
730
-
731
- # Option 1 : Mode automatique (recommandé)
323
+ # Run emulator + tests (watch mode)
732
324
  bun run test:watch
733
- # → Lance l'émulateur + tests en mode watch automatiquement
734
325
 
735
- # Option 2 : Mode manuel (deux terminaux)
736
- bun run emulator # Terminal 1
737
- bun run test # Terminal 2
326
+ # Two-terminal alternative
327
+ bun run emulator # terminal 1
328
+ bun run test # terminal 2
738
329
  ```
739
330
 
740
- L'émulateur Firestore démarre sur `localhost:8080` avec une UI sur `http://localhost:4000`.
741
-
742
- Voir `test/README.md` pour plus de détails.
331
+ Firestore emulator runs on `localhost:8080`, UI on `http://localhost:4000`.
743
332
 
744
- ## 📝 Licence
333
+ ## License
745
334
 
746
335
  MIT
747
336
 
748
- ## 🤝 Contribution
749
-
750
- Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
751
-
752
- ## 📬 Support
753
-
754
- Pour toute question ou problème, ouvrez une issue sur GitHub.