@lpdjs/firestore-repo-service 2.2.9-beta.9 → 2.3.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/README.md +480 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/servers/admin/index.cjs.map +1 -1
- package/dist/servers/admin/index.js.map +1 -1
- package/dist/servers/auth/index.cjs +2 -2
- package/dist/servers/auth/index.cjs.map +1 -1
- package/dist/servers/auth/index.js +2 -2
- package/dist/servers/auth/index.js.map +1 -1
- package/dist/servers/crud/index.cjs.map +1 -1
- package/dist/servers/crud/index.js.map +1 -1
- package/dist/servers/hono/cli.cjs +92 -34
- package/dist/servers/hono/cli.cjs.map +1 -1
- package/dist/servers/hono/cli.js +92 -34
- package/dist/servers/hono/cli.js.map +1 -1
- package/dist/servers/hono/index.cjs +5 -5
- package/dist/servers/hono/index.cjs.map +1 -1
- package/dist/servers/hono/index.d.cts +90 -51
- package/dist/servers/hono/index.d.ts +90 -51
- package/dist/servers/hono/index.js +5 -5
- package/dist/servers/hono/index.js.map +1 -1
- package/dist/servers/index.cjs.map +1 -1
- package/dist/servers/index.js.map +1 -1
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,10 +4,489 @@
|
|
|
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
|
-
Type-safe Firestore repository layer with auto-generated query methods, CRUD,
|
|
7
|
+
Type-safe Firestore repository layer with auto-generated query methods, CRUD,
|
|
8
|
+
a Firestore→SQL sync pipeline via Pub/Sub + BigQuery, change-history tracking,
|
|
9
|
+
and a file-based Hono HTTP server for Firebase Cloud Functions v2.
|
|
8
10
|
|
|
9
11
|
**Full documentation at [frs.lpdjs.fr](https://frs.lpdjs.fr)**
|
|
10
12
|
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @lpdjs/firestore-repo-service firebase-admin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Define your models
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
interface UserModel {
|
|
27
|
+
docId: string;
|
|
28
|
+
email: string;
|
|
29
|
+
name: string;
|
|
30
|
+
age: number;
|
|
31
|
+
isActive: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PostModel {
|
|
35
|
+
docId: string;
|
|
36
|
+
userId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
status: "draft" | "published";
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Create the repository mapping
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {
|
|
46
|
+
createRepositoryConfig,
|
|
47
|
+
buildRepositoryRelations,
|
|
48
|
+
createRepositoryMapping,
|
|
49
|
+
} from "@lpdjs/firestore-repo-service";
|
|
50
|
+
import { doc } from "firebase/firestore";
|
|
51
|
+
import type { Firestore } from "firebase/firestore";
|
|
52
|
+
|
|
53
|
+
const repositoryMapping = {
|
|
54
|
+
users: createRepositoryConfig<UserModel>()({
|
|
55
|
+
path: "users",
|
|
56
|
+
isGroup: false,
|
|
57
|
+
foreignKeys: ["docId", "email"] as const,
|
|
58
|
+
queryKeys: ["name", "isActive"] as const,
|
|
59
|
+
refCb: (db: Firestore, docId: string) => doc(db, "users", docId),
|
|
60
|
+
}),
|
|
61
|
+
posts: createRepositoryConfig<PostModel>()({
|
|
62
|
+
path: "posts",
|
|
63
|
+
isGroup: false,
|
|
64
|
+
foreignKeys: ["docId", "userId"] as const,
|
|
65
|
+
queryKeys: ["status"] as const,
|
|
66
|
+
refCb: (db: Firestore, docId: string) => doc(db, "posts", docId),
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Optional: add relations
|
|
71
|
+
const mappingWithRelations = buildRepositoryRelations(repositoryMapping, {
|
|
72
|
+
posts: {
|
|
73
|
+
userId: { repo: "users", key: "docId", type: "one" as const },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const repos = createRepositoryMapping(db, mappingWithRelations);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Use the repositories
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Fetch a single document
|
|
84
|
+
const user = await repos.users.get.byDocId("user123");
|
|
85
|
+
const userByEmail = await repos.users.get.byEmail("john@example.com");
|
|
86
|
+
|
|
87
|
+
// Query multiple documents
|
|
88
|
+
const activeUsers = await repos.users.query.byIsActive(true);
|
|
89
|
+
|
|
90
|
+
// With query options
|
|
91
|
+
const results = await repos.users.query.byIsActive(true, {
|
|
92
|
+
where: [["age", ">=", 18]],
|
|
93
|
+
orderBy: [{ field: "name", direction: "asc" }],
|
|
94
|
+
limit: 50,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Populate a relation
|
|
98
|
+
const post = await repos.posts.get.byDocId("post123");
|
|
99
|
+
if (post) {
|
|
100
|
+
const postWithUser = await repos.posts.populate(post, "userId");
|
|
101
|
+
console.log(postWithUser.populated.users?.name); // type-safe
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update
|
|
105
|
+
const updated = await repos.users.update("user123", { name: "New name", age: 31 });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API reference
|
|
109
|
+
|
|
110
|
+
### `createRepositoryConfig()`
|
|
111
|
+
|
|
112
|
+
| Option | Description |
|
|
113
|
+
| ------------- | ------------------------------------------------------- |
|
|
114
|
+
| `path` | Collection path in Firestore |
|
|
115
|
+
| `isGroup` | `true` for collection group, `false` for simple |
|
|
116
|
+
| `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
|
|
117
|
+
| `queryKeys` | Keys for `query.by*` methods (multi-document query) |
|
|
118
|
+
| `refCb` | Function that returns the document reference |
|
|
119
|
+
|
|
120
|
+
**Sub-collection example:**
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
comments: createRepositoryConfig<CommentModel>()({
|
|
124
|
+
path: "comments",
|
|
125
|
+
isGroup: true,
|
|
126
|
+
foreignKeys: ["docId"] as const,
|
|
127
|
+
queryKeys: ["postId", "userId"] as const,
|
|
128
|
+
refCb: (db, postId, commentId) =>
|
|
129
|
+
doc(db, "posts", postId, "comments", commentId),
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Query options
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
interface QueryOptions<T> {
|
|
137
|
+
where?: [keyof T, WhereFilterOp, any][]; // AND conditions
|
|
138
|
+
orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
|
|
139
|
+
orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
|
|
140
|
+
limit?: number;
|
|
141
|
+
offset?: number;
|
|
142
|
+
select?: (keyof T)[];
|
|
143
|
+
startAt?: DocumentSnapshot | any[];
|
|
144
|
+
startAfter?: DocumentSnapshot | any[];
|
|
145
|
+
endAt?: DocumentSnapshot | any[];
|
|
146
|
+
endBefore?: DocumentSnapshot | any[];
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### CRUD
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Create (auto-generated ID)
|
|
154
|
+
const newUser = await repos.users.create({ email: "...", name: "...", age: 25, isActive: true });
|
|
155
|
+
|
|
156
|
+
// Set (create or replace)
|
|
157
|
+
await repos.users.set("user123", { ... });
|
|
158
|
+
|
|
159
|
+
// Set with merge
|
|
160
|
+
await repos.users.set("user123", { age: 31 }, { merge: true });
|
|
161
|
+
|
|
162
|
+
// Update (partial)
|
|
163
|
+
await repos.users.update("user123", { age: 32 });
|
|
164
|
+
|
|
165
|
+
// Delete
|
|
166
|
+
await repos.users.delete("user123");
|
|
167
|
+
|
|
168
|
+
// Document ref
|
|
169
|
+
const ref = repos.users.documentRef("user123");
|
|
170
|
+
|
|
171
|
+
// Raw collection ref
|
|
172
|
+
const colRef = repos.users.ref;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Batch & Bulk
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Atomic batch (max 500 operations)
|
|
179
|
+
const batch = repos.users.batch.create();
|
|
180
|
+
batch.set(repos.users.documentRef("u1"), { ... });
|
|
181
|
+
batch.update(repos.users.documentRef("u2"), { age: 25 });
|
|
182
|
+
batch.delete(repos.users.documentRef("u3"));
|
|
183
|
+
await batch.commit();
|
|
184
|
+
|
|
185
|
+
// Bulk (auto-split into batches of 500)
|
|
186
|
+
await repos.users.bulk.set([
|
|
187
|
+
{ docRef: repos.users.documentRef("u1"), data: { ... }, merge: true },
|
|
188
|
+
]);
|
|
189
|
+
await repos.users.bulk.update([...]);
|
|
190
|
+
await repos.users.bulk.delete([...]);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Real-time listeners
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const unsubscribe = repos.users.query.onSnapshot(
|
|
197
|
+
{ where: [["isActive", "==", true]] },
|
|
198
|
+
(users) => console.log(users),
|
|
199
|
+
);
|
|
200
|
+
unsubscribe();
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pagination
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const firstPage = await repos.users.query.by({
|
|
207
|
+
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
208
|
+
limit: 10,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const nextPage = await repos.users.query.by({
|
|
212
|
+
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
213
|
+
startAfter: firstPage[firstPage.length - 1],
|
|
214
|
+
limit: 10,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Paginate with relations
|
|
218
|
+
const page = await repos.posts.query.paginate({
|
|
219
|
+
pageSize: 10,
|
|
220
|
+
include: [{ relation: "userId", select: ["docId", "name", "email"] }],
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Aggregations
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { count, sum, average } from "@lpdjs/firestore-repo-service";
|
|
228
|
+
|
|
229
|
+
const activeCount = await repos.users.aggregate.count({ where: [["isActive", "==", true]] });
|
|
230
|
+
const totalViews = await repos.posts.aggregate.sum("views");
|
|
231
|
+
const avgAge = await repos.users.aggregate.average("age");
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Transactions
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const result = await repos.users.transaction.run(async (txn) => {
|
|
238
|
+
const user = await txn.get(repos.users.documentRef("user123"));
|
|
239
|
+
if (user.exists()) {
|
|
240
|
+
txn.update(repos.users.documentRef("user123"), { age: user.data().age + 1 });
|
|
241
|
+
}
|
|
242
|
+
return { success: true };
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### OR queries
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
|
|
250
|
+
const users = await repos.users.query.by({
|
|
251
|
+
orWhere: [
|
|
252
|
+
[["status", "==", "active"], ["age", ">=", 18]],
|
|
253
|
+
[["status", "==", "pending"], ["verified", "==", true]],
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Change History
|
|
261
|
+
|
|
262
|
+
Track every write (create / update / delete) to any Firestore document with
|
|
263
|
+
zero-boilerplate Firestore triggers. Enabled per-repository via the
|
|
264
|
+
`createServers` config.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const servers = createServers(repos, { onRequest, firestoreTriggers });
|
|
268
|
+
|
|
269
|
+
export const { historyTriggers } = servers.history({
|
|
270
|
+
enabled: true,
|
|
271
|
+
repos: { posts: true, users: true },
|
|
272
|
+
// Optional: retention and relational tracking
|
|
273
|
+
relational: true,
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Each change is stored as an immutable snapshot in a `__history` sub-collection:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Read history entries for a document
|
|
281
|
+
const entries = await repos.posts.history.byDocId("post123");
|
|
282
|
+
|
|
283
|
+
// Field-level lookup
|
|
284
|
+
const titleHistory = await repos.posts.history.byField("post123", "title");
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Full documentation: [frs.lpdjs.fr/guide/history](https://frs.lpdjs.fr/guide/history)
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Hono File-Based API Server
|
|
292
|
+
|
|
293
|
+
A typed, file-based HTTP server built on [Hono](https://hono.dev/), designed
|
|
294
|
+
to ship one Firebase Cloud Function v2 per logical API — with auto-generated
|
|
295
|
+
OpenAPI 3.1, Zod validation, and a CLI scaffolder.
|
|
296
|
+
|
|
297
|
+
### Install extras
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm i hono @hono/node-server
|
|
301
|
+
npm i -D @asteasolutions/zod-to-openapi
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Bootstrap
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npx frs-hono init # interactive — creates apis.ts + manifest stub
|
|
308
|
+
npx frs-hono new createPost --domain posts --method post --api v1
|
|
309
|
+
npx frs-hono gen --root src/domains # refresh manifest (run before each build)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Configure your APIs (`apis.ts`)
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
316
|
+
|
|
317
|
+
export const apis = createApiRegistry({
|
|
318
|
+
v1: {
|
|
319
|
+
basePath: "/v1",
|
|
320
|
+
openapi: { info: { title: "Public API", version: "1.0.0" } },
|
|
321
|
+
interceptor: async ({ next, c }) => {
|
|
322
|
+
const data = await next();
|
|
323
|
+
return c.json({ success: true, data, error: null });
|
|
324
|
+
},
|
|
325
|
+
verbose: process.env["NODE_ENV"] !== "production",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
export const defineRoute = apis.defineRoute;
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Write a route
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// src/domains/posts/useCases/createPost/routes.ts
|
|
336
|
+
import { z } from "zod";
|
|
337
|
+
import { defineRoute } from "../../../../apis.js";
|
|
338
|
+
|
|
339
|
+
export default defineRoute({
|
|
340
|
+
api: "v1", // typed: only registered tags accepted
|
|
341
|
+
method: "post",
|
|
342
|
+
input: z.object({ title: z.string() }),
|
|
343
|
+
output: z.object({ id: z.string() }),
|
|
344
|
+
summary: "Create a post",
|
|
345
|
+
tags: ["posts"],
|
|
346
|
+
handler: async ({ input }) => ({ id: input.title }),
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Export an **array** of `defineRoute(...)` to expose the same logic under
|
|
351
|
+
multiple APIs with different schemas — each call infers `input` independently.
|
|
352
|
+
|
|
353
|
+
### Wire Cloud Functions
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// src/index.ts
|
|
357
|
+
import { onRequest } from "firebase-functions/v2/https";
|
|
358
|
+
import { apis } from "./apis.js";
|
|
359
|
+
import { routes } from "./domains/__generated__/routes.js";
|
|
360
|
+
|
|
361
|
+
export const { v1 } = apis.toFunctions(routes, onRequest, {
|
|
362
|
+
defaults: { region: "us-central1", invoker: "public" },
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Key features
|
|
367
|
+
|
|
368
|
+
| Feature | Details |
|
|
369
|
+
| --- | --- |
|
|
370
|
+
| **File-based routing** | `routes.ts` next to each useCase, scanned at build time |
|
|
371
|
+
| **Multi-API registry** | One Cloud Function per tag, typed `api` field |
|
|
372
|
+
| **Zod validation** | Body / query / path params + optional response validation |
|
|
373
|
+
| **OpenAPI 3.1** | Auto-generated from Zod schemas; Scalar UI at `/docs` |
|
|
374
|
+
| **Interceptor** | Around-style hook for envelopes, error mapping, tracing |
|
|
375
|
+
| **Middlewares** | Per-API and per-route Hono middlewares |
|
|
376
|
+
| **Typed context** | Augment `ContextVariableMap` once, `c.get("user")` typed everywhere |
|
|
377
|
+
| **CLI** | `init` / `new` (interactive) / `gen` |
|
|
378
|
+
|
|
379
|
+
Full documentation: [frs.lpdjs.fr/guide/hono](https://frs.lpdjs.fr/guide/hono)
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Servers (admin UI · CRUD REST · Firestore → SQL sync)
|
|
384
|
+
|
|
385
|
+
A single unified factory binds all servers to your repository registry. Per-repo `repo: …` is no longer needed — the registry key drives both the runtime binding and the inferred model type for `fieldsConfig` autocomplete.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { createServers } from "@lpdjs/firestore-repo-service";
|
|
389
|
+
import { onRequest } from "firebase-functions/v2/https";
|
|
390
|
+
import { BigQueryAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery";
|
|
391
|
+
import { BigQuery } from "@google-cloud/bigquery";
|
|
392
|
+
import { PubSub } from "@google-cloud/pubsub";
|
|
393
|
+
import * as firestoreTriggers from "firebase-functions/v2/firestore";
|
|
394
|
+
import * as pubsubHandler from "firebase-functions/v2/pubsub";
|
|
395
|
+
|
|
396
|
+
const servers = createServers(repos, {
|
|
397
|
+
onRequest,
|
|
398
|
+
httpsOptions: { invoker: "public" },
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Admin UI — repo auto-injected from the key, fieldsConfig typed against the model
|
|
402
|
+
export const admin = servers.admin({
|
|
403
|
+
basePath: "/admin",
|
|
404
|
+
auth: { type: "basic", username: "admin", password: "secret" },
|
|
405
|
+
repos: {
|
|
406
|
+
posts: {
|
|
407
|
+
path: "posts",
|
|
408
|
+
fieldsConfig: { title: ["create", "mutable"], status: ["filterable"] },
|
|
409
|
+
allowDelete: true,
|
|
410
|
+
},
|
|
411
|
+
users: { path: "users" },
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// CRUD REST API
|
|
416
|
+
export const api = servers.crud({
|
|
417
|
+
basePath: "/api",
|
|
418
|
+
repos: {
|
|
419
|
+
posts: { path: "posts", allowDelete: true },
|
|
420
|
+
users: { path: "users" },
|
|
421
|
+
},
|
|
422
|
+
openapi: { title: "My API", version: "1.0.0" },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Firestore → BigQuery sync (triggers + worker + admin Cloud Functions)
|
|
426
|
+
export const { functions } = servers.sync({
|
|
427
|
+
deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
|
|
428
|
+
adapter: new BigQueryAdapter({
|
|
429
|
+
bigquery: new BigQuery({ projectId: "my-project" }),
|
|
430
|
+
datasetId: "firestore_sync",
|
|
431
|
+
}),
|
|
432
|
+
topicPrefix: "firestore-sync",
|
|
433
|
+
autoMigrate: true,
|
|
434
|
+
admin: {
|
|
435
|
+
auth: { type: "basic", username: "admin", password: "secret" },
|
|
436
|
+
featuresFlag: { healthCheck: true, manualSync: true, viewQueue: true, configCheck: true },
|
|
437
|
+
},
|
|
438
|
+
repos: {
|
|
439
|
+
users: { tableName: "users", columnMap: { docId: "user_id" } },
|
|
440
|
+
posts: { columnMap: { docId: "post_id" } },
|
|
441
|
+
comments: { triggerPath: "posts/{postId}/comments/{docId}" },
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Spread Cloud Functions into your exports
|
|
446
|
+
export const {
|
|
447
|
+
users_onCreate, users_onUpdate, users_onDelete, sync_users,
|
|
448
|
+
posts_onCreate, posts_onUpdate, posts_onDelete, sync_posts,
|
|
449
|
+
comments_onCreate, comments_onUpdate, comments_onDelete, sync_comments,
|
|
450
|
+
adminsync,
|
|
451
|
+
} = functions;
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
When `onRequest` is passed to `createServers`, `servers.admin()` and `servers.crud()` return ready-to-export Cloud Functions. Without it, they return raw HTTP handlers you can wrap yourself.
|
|
455
|
+
|
|
456
|
+
The sync admin endpoint (`/`) exposes a UI for health checks, force-sync, queue inspection, and GCP config verification.
|
|
457
|
+
|
|
458
|
+
For a custom SQL backend, implement the `SqlAdapter` interface:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import type { SqlAdapter } from "@lpdjs/firestore-repo-service/sync";
|
|
462
|
+
|
|
463
|
+
class MyAdapter implements SqlAdapter {
|
|
464
|
+
// tableExists, getTableColumns, createTable, upsertRows, deleteRows, executeRaw
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Full sync documentation: [frs.lpdjs.fr/guide/sync](https://frs.lpdjs.fr/guide/sync)
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Testing
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
# Run emulator + tests (watch mode)
|
|
476
|
+
bun run test:watch
|
|
477
|
+
|
|
478
|
+
# Two-terminal alternative
|
|
479
|
+
bun run emulator # terminal 1
|
|
480
|
+
bun run test # terminal 2
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Firestore emulator runs on `localhost:8080`, UI on `http://localhost:4000`.
|
|
484
|
+
|
|
485
|
+
## License
|
|
486
|
+
|
|
487
|
+
MIT
|
|
488
|
+
|
|
489
|
+
|
|
11
490
|
## Installation
|
|
12
491
|
|
|
13
492
|
```bash
|