@lpdjs/firestore-repo-service 2.2.1 → 2.2.2
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 +111 -529
- package/dist/{index-BJQXYHTC.d.ts → index-BnXFmcVp.d.ts} +4 -3
- package/dist/{index-BjH87AI4.d.cts → index-KbSSRIWJ.d.cts} +4 -3
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/servers/admin/index.cjs +2 -2
- package/dist/servers/admin/index.cjs.map +1 -1
- package/dist/servers/admin/index.d.cts +3 -2
- package/dist/servers/admin/index.d.ts +3 -2
- package/dist/servers/admin/index.js +2 -2
- package/dist/servers/admin/index.js.map +1 -1
- package/dist/servers/crud/index.cjs +2 -2
- package/dist/servers/crud/index.cjs.map +1 -1
- package/dist/servers/crud/index.d.cts +4 -3
- package/dist/servers/crud/index.d.ts +4 -3
- package/dist/servers/crud/index.js +2 -2
- package/dist/servers/crud/index.js.map +1 -1
- package/dist/servers/index.cjs +6 -6
- package/dist/servers/index.cjs.map +1 -1
- package/dist/servers/index.d.cts +4 -3
- package/dist/servers/index.d.ts +4 -3
- package/dist/servers/index.js +6 -6
- package/dist/servers/index.js.map +1 -1
- package/dist/{types-CYVwoOQx.d.cts → types-B5NdBY1Z.d.cts} +2 -1
- package/dist/{types-CYVwoOQx.d.ts → types-B5NdBY1Z.d.ts} +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# firestore-repo-service
|
|
2
2
|
|
|
3
3
|
[](https://frs.lpdjs.fr)
|
|
4
4
|
[](https://www.npmjs.com/package/@lpdjs/firestore-repo-service)
|
|
5
5
|
[](https://github.com/solarpush/firestore-repo-service/blob/master/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
**Full documentation at [frs.lpdjs.fr](https://frs.lpdjs.fr)**
|
|
10
10
|
|
|
11
|
-
##
|
|
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
|
-
##
|
|
17
|
+
## Quick start
|
|
39
18
|
|
|
40
|
-
###
|
|
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
|
-
###
|
|
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
|
-
//
|
|
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
|
-
###
|
|
76
|
+
### Use the repositories
|
|
101
77
|
|
|
102
78
|
```typescript
|
|
103
|
-
//
|
|
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
|
-
//
|
|
83
|
+
// Query multiple documents
|
|
108
84
|
const activeUsers = await repos.users.query.byIsActive(true);
|
|
109
85
|
|
|
110
|
-
//
|
|
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); //
|
|
97
|
+
console.log(postWithUser.populated.users?.name); // type-safe
|
|
115
98
|
}
|
|
116
99
|
|
|
117
|
-
//
|
|
118
|
-
const
|
|
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
|
-
##
|
|
132
|
-
|
|
133
|
-
### Configuration
|
|
134
|
-
|
|
135
|
-
#### `createRepositoryConfig()`
|
|
136
|
-
|
|
137
|
-
Configure un repository avec ses clés et méthodes.
|
|
104
|
+
## API reference
|
|
138
105
|
|
|
139
|
-
|
|
106
|
+
### `createRepositoryConfig()`
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
**
|
|
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
|
|
124
|
+
refCb: (db, postId, commentId) =>
|
|
169
125
|
doc(db, "posts", postId, "comments", commentId),
|
|
170
126
|
});
|
|
171
127
|
```
|
|
172
128
|
|
|
173
|
-
###
|
|
174
|
-
|
|
175
|
-
Récupère un **document unique** par une clé étrangère.
|
|
129
|
+
### Query options
|
|
176
130
|
|
|
177
131
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
###
|
|
146
|
+
### CRUD
|
|
230
147
|
|
|
231
148
|
```typescript
|
|
232
|
-
//
|
|
233
|
-
|
|
149
|
+
// Create (auto-generated ID)
|
|
150
|
+
const newUser = await repos.users.create({ email: "...", name: "...", age: 25, isActive: true });
|
|
234
151
|
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
155
|
+
// Set with merge
|
|
156
|
+
await repos.users.set("user123", { age: 31 }, { merge: true });
|
|
253
157
|
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
//
|
|
262
|
-
|
|
263
|
-
"post123", // postId
|
|
264
|
-
"comment456", // commentId
|
|
265
|
-
{ text: "Updated text" },
|
|
266
|
-
);
|
|
267
|
-
```
|
|
161
|
+
// Delete
|
|
162
|
+
await repos.users.delete("user123");
|
|
268
163
|
|
|
269
|
-
|
|
164
|
+
// Document ref
|
|
165
|
+
const ref = repos.users.documentRef("user123");
|
|
270
166
|
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const commentRef = repos.comments.documentRef("post123", "comment456");
|
|
167
|
+
// Raw collection ref
|
|
168
|
+
const colRef = repos.users.ref;
|
|
274
169
|
```
|
|
275
170
|
|
|
276
|
-
###
|
|
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.
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
209
|
+
startAfter: firstPage[firstPage.length - 1],
|
|
385
210
|
limit: 10,
|
|
386
211
|
});
|
|
387
212
|
|
|
388
|
-
//
|
|
389
|
-
const page = await repos.
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
###
|
|
220
|
+
### Aggregations
|
|
397
221
|
|
|
398
222
|
```typescript
|
|
399
|
-
|
|
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
|
-
|
|
430
|
-
await repos.
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
256
|
+
Replicate Firestore collections to BigQuery (or any SQL database) via Cloud Pub/Sub and Cloud Functions v2.
|
|
604
257
|
|
|
605
258
|
```
|
|
606
|
-
Firestore
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
683
|
-
// upsertRows, deleteRows, executeRaw
|
|
314
|
+
// tableExists, getTableColumns, createTable, upsertRows, deleteRows, executeRaw
|
|
684
315
|
}
|
|
685
316
|
```
|
|
686
317
|
|
|
687
|
-
|
|
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
|
-
|
|
320
|
+
## Testing
|
|
726
321
|
|
|
727
322
|
```bash
|
|
728
|
-
#
|
|
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
|
-
#
|
|
736
|
-
bun run emulator
|
|
737
|
-
bun run test
|
|
326
|
+
# Two-terminal alternative
|
|
327
|
+
bun run emulator # terminal 1
|
|
328
|
+
bun run test # terminal 2
|
|
738
329
|
```
|
|
739
330
|
|
|
740
|
-
|
|
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
|
-
##
|
|
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.
|