@lpdjs/firestore-repo-service 2.0.1 → 2.1.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 +93 -0
- package/dist/index-28xuzvxq.d.cts +530 -0
- package/dist/index-nquQrxW6.d.ts +530 -0
- package/dist/index.cjs +527 -886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -395
- package/dist/index.d.ts +53 -395
- package/dist/index.js +528 -878
- package/dist/index.js.map +1 -1
- package/dist/servers/admin/index.cjs +557 -0
- package/dist/servers/admin/index.cjs.map +1 -0
- package/dist/servers/admin/index.d.cts +4 -0
- package/dist/servers/admin/index.d.ts +4 -0
- package/dist/servers/admin/index.js +557 -0
- package/dist/servers/admin/index.js.map +1 -0
- package/dist/servers/crud/index.cjs +13 -0
- package/dist/servers/crud/index.cjs.map +1 -0
- package/dist/servers/crud/index.d.cts +180 -0
- package/dist/servers/crud/index.d.ts +180 -0
- package/dist/servers/crud/index.js +13 -0
- package/dist/servers/crud/index.js.map +1 -0
- package/dist/servers/index.cjs +567 -0
- package/dist/servers/index.cjs.map +1 -0
- package/dist/servers/index.d.cts +159 -0
- package/dist/servers/index.d.ts +159 -0
- package/dist/servers/index.js +567 -0
- package/dist/servers/index.js.map +1 -0
- package/dist/sync/bigquery.cjs +10 -0
- package/dist/sync/bigquery.cjs.map +1 -0
- package/dist/sync/bigquery.d.cts +58 -0
- package/dist/sync/bigquery.d.ts +58 -0
- package/dist/sync/bigquery.js +10 -0
- package/dist/sync/bigquery.js.map +1 -0
- package/dist/sync/index.cjs +92 -0
- package/dist/sync/index.cjs.map +1 -0
- package/dist/sync/index.d.cts +305 -0
- package/dist/sync/index.d.ts +305 -0
- package/dist/sync/index.js +92 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/types-C0YpUzog.d.cts +902 -0
- package/dist/types-C0YpUzog.d.ts +902 -0
- package/dist/types-CwjqHFNC.d.cts +268 -0
- package/dist/types-CwjqHFNC.d.ts +268 -0
- package/package.json +72 -9
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Un service de repository type-safe pour Firestore avec génération automatique
|
|
|
22
22
|
- 🔄 **Transactions** : Opérations transactionnelles type-safe
|
|
23
23
|
- 🔗 **Relations** : Populate avec select typé (champs projetés)
|
|
24
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
|
|
25
27
|
|
|
26
28
|
## 📦 Installation
|
|
27
29
|
|
|
@@ -594,6 +596,87 @@ for (const post of page.data) {
|
|
|
594
596
|
}
|
|
595
597
|
```
|
|
596
598
|
|
|
599
|
+
## 🔄 Firestore → SQL Sync
|
|
600
|
+
|
|
601
|
+
Répliquez automatiquement vos collections Firestore vers BigQuery (ou toute base SQL) via Cloud Pub/Sub.
|
|
602
|
+
|
|
603
|
+
### Architecture
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
Firestore Triggers → Cloud Pub/Sub → Worker → BigQuery
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Démarrage rapide
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
import { createFirestoreSync } from "@lpdjs/firestore-repo-service/sync";
|
|
613
|
+
import { BigQueryAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery";
|
|
614
|
+
import { BigQuery } from "@google-cloud/bigquery";
|
|
615
|
+
import { PubSub } from "@google-cloud/pubsub";
|
|
616
|
+
import * as firestoreTriggers from "firebase-functions/v2/firestore";
|
|
617
|
+
import * as pubsubHandler from "firebase-functions/v2/pubsub";
|
|
618
|
+
import { onRequest } from "firebase-functions/v2/https";
|
|
619
|
+
|
|
620
|
+
const sync = createFirestoreSync(repos, {
|
|
621
|
+
deps: { firestoreTriggers, pubsubHandler, pubsub: new PubSub() },
|
|
622
|
+
adapter: new BigQueryAdapter({
|
|
623
|
+
bigquery: new BigQuery({ projectId: "my-project" }),
|
|
624
|
+
datasetId: "firestore_sync",
|
|
625
|
+
}),
|
|
626
|
+
topicPrefix: "firestore-sync",
|
|
627
|
+
autoMigrate: true,
|
|
628
|
+
admin: {
|
|
629
|
+
auth: { type: "basic", username: "admin", password: "secret" },
|
|
630
|
+
featuresFlag: {
|
|
631
|
+
healthCheck: true,
|
|
632
|
+
manualSync: true,
|
|
633
|
+
viewQueue: true,
|
|
634
|
+
configCheck: true,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
repos: {
|
|
638
|
+
users: { tableName: "users", columnMap: { docId: "user_id" } },
|
|
639
|
+
posts: { columnMap: { docId: "post_id" } },
|
|
640
|
+
// Collection groups nécessitent triggerPath
|
|
641
|
+
comments: { triggerPath: "posts/{postId}/comments/{docId}" },
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Export des Cloud Functions
|
|
646
|
+
export const {
|
|
647
|
+
users_onCreate, users_onUpdate, users_onDelete, sync_users,
|
|
648
|
+
posts_onCreate, posts_onUpdate, posts_onDelete, sync_posts,
|
|
649
|
+
comments_onCreate, comments_onUpdate, comments_onDelete, sync_comments,
|
|
650
|
+
} = sync.functions;
|
|
651
|
+
|
|
652
|
+
// Admin (optionnel)
|
|
653
|
+
export const syncAdmin = onRequest(sync.adminHandler!);
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Sync Admin
|
|
657
|
+
|
|
658
|
+
L'endpoint admin fournit :
|
|
659
|
+
|
|
660
|
+
- **Health Check** : Compare le schéma attendu (Zod) vs les colonnes BigQuery réelles
|
|
661
|
+
- **Force Sync** : Re-synchronise tous les documents d'une collection
|
|
662
|
+
- **View Queues** : Inspecte les éléments en attente
|
|
663
|
+
- **Config Check** : Vérifie APIs GCP, topics, tables — avec commandes `gcloud` pour corriger
|
|
664
|
+
|
|
665
|
+
### Adaptateur personnalisé
|
|
666
|
+
|
|
667
|
+
Implémentez l'interface `SqlAdapter` pour d'autres bases de données :
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
import type { SqlAdapter } from "@lpdjs/firestore-repo-service/sync";
|
|
671
|
+
|
|
672
|
+
class MyAdapter implements SqlAdapter {
|
|
673
|
+
// tableExists, getTableColumns, createTable, insertRows,
|
|
674
|
+
// upsertRows, deleteRows, executeRaw
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
📚 **Documentation complète** : [frs.lpdjs.fr/guide/sync](https://frs.lpdjs.fr/guide/sync)
|
|
679
|
+
|
|
597
680
|
## 🔧 Types exportés
|
|
598
681
|
|
|
599
682
|
```typescript
|
|
@@ -616,6 +699,16 @@ import type {
|
|
|
616
699
|
PaginationWithIncludeOptions,
|
|
617
700
|
PaginationWithIncludeOptionsTyped,
|
|
618
701
|
} from "@lpdjs/firestore-repo-service";
|
|
702
|
+
|
|
703
|
+
// Sync types
|
|
704
|
+
import type {
|
|
705
|
+
FirestoreSyncConfig,
|
|
706
|
+
SqlAdapter,
|
|
707
|
+
SqlColumn,
|
|
708
|
+
SqlTableDef,
|
|
709
|
+
RepoSyncConfig,
|
|
710
|
+
SyncAdminConfig,
|
|
711
|
+
} from "@lpdjs/firestore-repo-service/sync";
|
|
619
712
|
```
|
|
620
713
|
|
|
621
714
|
## 🧪 Tests avec l'émulateur
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { C as ConfiguredRepository, R as RepositoryConfig, F as FieldPath, n as FieldRole } from './types-C0YpUzog.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal zero-dependency HTTP router for Firebase Functions.
|
|
6
|
+
* Compatible with any Express-like (req, res) handler.
|
|
7
|
+
*
|
|
8
|
+
* Supports:
|
|
9
|
+
* - Named path parameters (e.g. "/repos/:name/:id")
|
|
10
|
+
* - GET, POST, DELETE methods
|
|
11
|
+
* - Global middleware (before each route)
|
|
12
|
+
* - 404 / error fallbacks
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { MiniRouter } from "@lpdjs/firestore-repo-service/servers/admin";
|
|
17
|
+
*
|
|
18
|
+
* // Create router
|
|
19
|
+
* const router = new MiniRouter();
|
|
20
|
+
*
|
|
21
|
+
* // Add global middleware (executed before every route)
|
|
22
|
+
* router.use(async (req, res, next) => {
|
|
23
|
+
* console.log(`${req.method} ${req.url}`);
|
|
24
|
+
* await next();
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Auth middleware
|
|
28
|
+
* router.use((req, res, next) => {
|
|
29
|
+
* if (!req.headers?.authorization) {
|
|
30
|
+
* res.status(401).send("Unauthorized");
|
|
31
|
+
* return;
|
|
32
|
+
* }
|
|
33
|
+
* next();
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Define routes with path parameters
|
|
37
|
+
* router.get("/users", async (req, res) => {
|
|
38
|
+
* res.json({ users: await getAllUsers() });
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* router.get("/users/:id", async (req, res) => {
|
|
42
|
+
* const user = await getUser(req.params.id); // Access path params
|
|
43
|
+
* if (!user) {
|
|
44
|
+
* res.status(404).send("User not found");
|
|
45
|
+
* return;
|
|
46
|
+
* }
|
|
47
|
+
* res.json(user);
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* router.post("/users", async (req, res) => {
|
|
51
|
+
* const user = await createUser(req.body);
|
|
52
|
+
* res.status(201).json(user);
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* router.delete("/users/:id", async (req, res) => {
|
|
56
|
+
* await deleteUser(req.params.id);
|
|
57
|
+
* res.status(204).end();
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // Custom 404 handler
|
|
61
|
+
* router.onNotFound((req, res) => {
|
|
62
|
+
* res.status(404).json({ error: "Route not found", path: req.url });
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Custom error handler
|
|
66
|
+
* router.onError((err, req, res) => {
|
|
67
|
+
* console.error("Error:", err);
|
|
68
|
+
* res.status(500).json({ error: "Internal server error" });
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Use with Firebase Functions
|
|
72
|
+
* export const api = onRequest(async (req, res) => {
|
|
73
|
+
* await router.handle(req, res);
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
type AnyReq = {
|
|
78
|
+
method?: string;
|
|
79
|
+
url?: string;
|
|
80
|
+
/** Express originalUrl — preserved before any router stripping, contains the full path including the Firebase Functions prefix */
|
|
81
|
+
originalUrl?: string;
|
|
82
|
+
path?: string;
|
|
83
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
84
|
+
body?: unknown;
|
|
85
|
+
query?: Record<string, string | string[] | undefined>;
|
|
86
|
+
};
|
|
87
|
+
type AnyRes = {
|
|
88
|
+
status: (code: number) => AnyRes;
|
|
89
|
+
set: (key: string, value: string) => AnyRes;
|
|
90
|
+
send: (body: string) => void;
|
|
91
|
+
json: (body: unknown) => void;
|
|
92
|
+
end: () => void;
|
|
93
|
+
};
|
|
94
|
+
type RouteParams = Record<string, string>;
|
|
95
|
+
type RouteHandler = (req: AnyReq & {
|
|
96
|
+
params: RouteParams;
|
|
97
|
+
}, res: AnyRes) => void | Promise<void>;
|
|
98
|
+
type Middleware = (req: AnyReq & {
|
|
99
|
+
params: RouteParams;
|
|
100
|
+
}, res: AnyRes, next: () => void | Promise<void>) => void | Promise<void>;
|
|
101
|
+
declare class MiniRouter {
|
|
102
|
+
private routes;
|
|
103
|
+
private middlewares;
|
|
104
|
+
private notFoundHandler;
|
|
105
|
+
private errorHandler;
|
|
106
|
+
use(middleware: Middleware): this;
|
|
107
|
+
get(path: string, handler: RouteHandler): this;
|
|
108
|
+
post(path: string, handler: RouteHandler): this;
|
|
109
|
+
put(path: string, handler: RouteHandler): this;
|
|
110
|
+
patch(path: string, handler: RouteHandler): this;
|
|
111
|
+
delete(path: string, handler: RouteHandler): this;
|
|
112
|
+
onNotFound(handler: RouteHandler): this;
|
|
113
|
+
onError(handler: (err: unknown, req: AnyReq, res: AnyRes) => void): this;
|
|
114
|
+
private addRoute;
|
|
115
|
+
handle(req: AnyReq, res: AnyRes): Promise<void>;
|
|
116
|
+
private runMiddlewareChain;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface PageOptions {
|
|
120
|
+
title: string;
|
|
121
|
+
breadcrumb?: {
|
|
122
|
+
label: string;
|
|
123
|
+
href?: string;
|
|
124
|
+
}[];
|
|
125
|
+
flash?: {
|
|
126
|
+
type: "success" | "error";
|
|
127
|
+
message: string;
|
|
128
|
+
};
|
|
129
|
+
basePath?: string;
|
|
130
|
+
}
|
|
131
|
+
/** Firestore WHERE operator */
|
|
132
|
+
type WhereOp = "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "array-contains-any";
|
|
133
|
+
/** One active filter — serialized in URL as fv_{field} + fo_{field} */
|
|
134
|
+
interface FilterState {
|
|
135
|
+
field: string;
|
|
136
|
+
op: WhereOp;
|
|
137
|
+
value: string;
|
|
138
|
+
}
|
|
139
|
+
/** Per-column metadata used to render appropriate filter inputs/operators */
|
|
140
|
+
interface ColumnMeta {
|
|
141
|
+
name: string;
|
|
142
|
+
/** Innermost Zod type name, e.g. "ZodString", "ZodNumber" */
|
|
143
|
+
zodType: string;
|
|
144
|
+
}
|
|
145
|
+
/** Active sort state for the list view */
|
|
146
|
+
interface SortState {
|
|
147
|
+
field: string;
|
|
148
|
+
dir: "asc" | "desc";
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Metadata for one relational action column appended to the list table.
|
|
152
|
+
* Each entry produces a dedicated button column (not a cell replacement).
|
|
153
|
+
*/
|
|
154
|
+
interface RelationalFieldMeta {
|
|
155
|
+
/** Field in this document whose value is used to build the link */
|
|
156
|
+
key: string;
|
|
157
|
+
/** Column header label, e.g. "Posts" or "Author" */
|
|
158
|
+
column: string;
|
|
159
|
+
/** Name of the target repository in the admin registry */
|
|
160
|
+
targetRepo: string;
|
|
161
|
+
/** Field name on the target repo used for the lookup */
|
|
162
|
+
targetKey: string;
|
|
163
|
+
/**
|
|
164
|
+
* - "one" → doc[key] = docId on the target → link to edit page
|
|
165
|
+
* - "many" → doc[key] = filter value on the target → link to filtered list
|
|
166
|
+
*/
|
|
167
|
+
type: "one" | "many";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* HTTP route handlers for the admin ORM server.
|
|
172
|
+
* Each handler corresponds to a URL pattern registered in the router.
|
|
173
|
+
*
|
|
174
|
+
* Routes:
|
|
175
|
+
* GET / → dashboard GET /:repoName → document list (paginated)
|
|
176
|
+
* GET /:repoName/create → create form
|
|
177
|
+
* POST /:repoName/create → submit create
|
|
178
|
+
* GET /:repoName/:id/edit → edit form (pre-filled)
|
|
179
|
+
* POST /:repoName/:id/edit → submit update
|
|
180
|
+
* POST /:repoName/:id/delete → delete document
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
interface AdminRepoEntry {
|
|
184
|
+
name: string;
|
|
185
|
+
path: string;
|
|
186
|
+
repo: ConfiguredRepository<RepositoryConfig<any, any, any, any, any, any, any, any, any, any>>;
|
|
187
|
+
schema: z.ZodObject<any>;
|
|
188
|
+
/** document key field name (default: "docId") */
|
|
189
|
+
documentKey?: string;
|
|
190
|
+
/** Field name that stores the full Firestore document path (e.g. "documentPath") */
|
|
191
|
+
pathKey?: string;
|
|
192
|
+
/** Whether this repo is a collection group (subcollection) */
|
|
193
|
+
isGroup?: boolean;
|
|
194
|
+
/** Parent key field names needed to build a subcollection document ref (auto-detected from refCb) */
|
|
195
|
+
parentKeys?: string[];
|
|
196
|
+
/** Field name for the creation timestamp (auto-set on create) */
|
|
197
|
+
createdKey?: string;
|
|
198
|
+
/** List of columns to display in the table (defaults to schema keys) */
|
|
199
|
+
listColumns?: string[];
|
|
200
|
+
/** Page size for list view (default: 25) */
|
|
201
|
+
pageSize?: number;
|
|
202
|
+
/** Fields exposed in the filter bar (defaults to all schema keys) */
|
|
203
|
+
filterableFields?: string[];
|
|
204
|
+
/** Fields shown in the edit form (defaults to all schema fields if unset) */
|
|
205
|
+
mutableFields?: string[];
|
|
206
|
+
/** Fields shown in the create form (defaults to all schema fields if unset) */
|
|
207
|
+
createFields?: string[];
|
|
208
|
+
/** Whether delete is allowed (default: false) */
|
|
209
|
+
allowDelete?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Fields that link to another repository.
|
|
212
|
+
* Populated automatically from the repo's relationalKeys.
|
|
213
|
+
*/
|
|
214
|
+
relationalMeta?: RelationalFieldMeta[];
|
|
215
|
+
}
|
|
216
|
+
type RepoRegistry = Record<string, AdminRepoEntry>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @module servers/admin
|
|
220
|
+
*
|
|
221
|
+
* Creates a static ORM admin interface served as a Firebase HTTPS function.
|
|
222
|
+
*
|
|
223
|
+
* Features:
|
|
224
|
+
* - Dashboard listing all registered repositories
|
|
225
|
+
* - Document list with cursor-based pagination
|
|
226
|
+
* - Create / Edit / Delete forms generated from Zod schemas
|
|
227
|
+
* - Forms map **exactly** to the repository model type
|
|
228
|
+
* - Zero JavaScript framework — plain HTML + inline CSS + vanilla JS
|
|
229
|
+
* - Body parsing for `application/x-www-form-urlencoded` (default HTML forms)
|
|
230
|
+
* and `application/json` (API clients)
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* import * as functions from "firebase-functions";
|
|
235
|
+
* import { z } from "zod";
|
|
236
|
+
* import { createAdminServer } from "@lpdjs/firestore-repo-service/servers/admin";
|
|
237
|
+
*
|
|
238
|
+
* const postSchema = z.object({
|
|
239
|
+
* title: z.string().min(1),
|
|
240
|
+
* content: z.string(),
|
|
241
|
+
* status: z.enum(["draft", "published"]),
|
|
242
|
+
* authorId: z.string(),
|
|
243
|
+
* });
|
|
244
|
+
*
|
|
245
|
+
* export const adminApp = functions.https.onRequest(
|
|
246
|
+
* createAdminServer({
|
|
247
|
+
* basePath: "/admin",
|
|
248
|
+
* repos: {
|
|
249
|
+
* posts: { repo: repos.posts, schema: postSchema, path: "posts" },
|
|
250
|
+
* },
|
|
251
|
+
* })
|
|
252
|
+
* );
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extracts the model type `T` from a `ConfiguredRepository`.
|
|
258
|
+
* @internal
|
|
259
|
+
*/
|
|
260
|
+
type RepoModelType<TRepo> = TRepo extends ConfiguredRepository<RepositoryConfig<infer T, any, any, any, any, any, any, any, any, any>> ? T : never;
|
|
261
|
+
/**
|
|
262
|
+
* Configuration for a single repository in the admin server.
|
|
263
|
+
*
|
|
264
|
+
* @template TRepo - The `ConfiguredRepository` type; used to derive typed field names.
|
|
265
|
+
*
|
|
266
|
+
* If the repository was created with `createRepositoryConfig(schema)(config)`,
|
|
267
|
+
* the `schema` field is optional — it is auto-detected from the repo.
|
|
268
|
+
* Otherwise, pass `schema` explicitly.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* posts: {
|
|
273
|
+
* repo: repos.posts,
|
|
274
|
+
* fieldsConfig: {
|
|
275
|
+
* title: ["create", "mutable", "filterable"],
|
|
276
|
+
* content: ["create", "mutable"],
|
|
277
|
+
* status: ["create", "filterable"],
|
|
278
|
+
* },
|
|
279
|
+
* allowDelete: true,
|
|
280
|
+
* }
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
interface AdminRepoConfig<TRepo extends ConfiguredRepository<any> = ConfiguredRepository<any>> {
|
|
284
|
+
/** The configured repository instance. Drives type inference for all other fields. */
|
|
285
|
+
repo: TRepo;
|
|
286
|
+
/**
|
|
287
|
+
* Zod schema — optional when the repo was created with `createRepositoryConfig(schema)`.
|
|
288
|
+
* Pass explicitly for repos created with the legacy `createRepositoryConfig<T>()` form.
|
|
289
|
+
*/
|
|
290
|
+
schema?: z.ZodObject<any>;
|
|
291
|
+
/** Firestore collection path (for display only) */
|
|
292
|
+
path: string;
|
|
293
|
+
/** Key used to identify documents (default: "docId") */
|
|
294
|
+
documentKey?: string;
|
|
295
|
+
/** Columns to display in the list view (default: all schema keys) */
|
|
296
|
+
listColumns?: string[];
|
|
297
|
+
/** Number of documents per page in the list view (default: 25) */
|
|
298
|
+
pageSize?: number;
|
|
299
|
+
/**
|
|
300
|
+
* Per-field role configuration.
|
|
301
|
+
* Each key is a model field (with autocomplete); the value is an array of roles.
|
|
302
|
+
*
|
|
303
|
+
* Roles:
|
|
304
|
+
* - `"create"` — field is shown in the create form
|
|
305
|
+
* - `"mutable"` — field is shown in the edit form
|
|
306
|
+
* - `"filterable"` — field is shown in the filter bar
|
|
307
|
+
*
|
|
308
|
+
* If omitted, all schema fields are shown in all contexts.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* fieldsConfig: {
|
|
313
|
+
* title: ["create", "mutable", "filterable"],
|
|
314
|
+
* content: ["create", "mutable"],
|
|
315
|
+
* status: ["create", "filterable"],
|
|
316
|
+
* }
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
fieldsConfig?: Partial<Record<FieldPath<RepoModelType<TRepo>>, readonly FieldRole[]>>;
|
|
320
|
+
/**
|
|
321
|
+
* Whether to show the delete button in the list view.
|
|
322
|
+
* Default: false — delete is disabled unless explicitly set to true.
|
|
323
|
+
*/
|
|
324
|
+
allowDelete?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Relational action columns appended to the list table.
|
|
327
|
+
* Each entry adds a dedicated button that navigates to the linked repository.
|
|
328
|
+
*
|
|
329
|
+
* - **type "one"** (e.g. `userId` on a post) → button links to the target
|
|
330
|
+
* document edit page: `/{targetRepo}/{value}/edit`
|
|
331
|
+
* - **type "many"** (e.g. `docId` on a user) → button links to the target
|
|
332
|
+
* repo list filtered by value: `/{targetRepo}?fv_{targetKey}={value}`
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* users: {
|
|
337
|
+
* repo: repos.users,
|
|
338
|
+
* relationalFields: [
|
|
339
|
+
* { key: "docId", column: "Posts" }, // many → list of posts by this user
|
|
340
|
+
* ]
|
|
341
|
+
* }
|
|
342
|
+
* posts: {
|
|
343
|
+
* repo: repos.posts,
|
|
344
|
+
* relationalFields: [
|
|
345
|
+
* { key: "userId", column: "Author" }, // one → edit page of the user
|
|
346
|
+
* ]
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
relationalFields?: {
|
|
351
|
+
key: keyof RepoModelType<TRepo> & string;
|
|
352
|
+
column: string;
|
|
353
|
+
}[];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* HTTP Basic Auth configuration.
|
|
357
|
+
* The browser will show a native login dialog.
|
|
358
|
+
*/
|
|
359
|
+
interface BasicAuthConfig {
|
|
360
|
+
type: "basic";
|
|
361
|
+
/** Realm displayed in the browser login dialog */
|
|
362
|
+
realm?: string;
|
|
363
|
+
username: string;
|
|
364
|
+
password: string;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Options for `createAdminServer`.
|
|
368
|
+
*
|
|
369
|
+
* Made generic so TypeScript can infer the model type of each repo entry
|
|
370
|
+
* and provide autocomplete + type-checking on `fieldsConfig`.
|
|
371
|
+
*
|
|
372
|
+
* @template TRepos - Shape of the repos map (inferred automatically at the call site)
|
|
373
|
+
*/
|
|
374
|
+
interface AdminServerOptions<TRepos extends Record<string, ConfiguredRepository<any>> = Record<string, ConfiguredRepository<any>>> {
|
|
375
|
+
/**
|
|
376
|
+
* Base URL path of the function (e.g. "/admin").
|
|
377
|
+
* Must match the path where the Firebase Function is mounted.
|
|
378
|
+
* Default: "/"
|
|
379
|
+
*/
|
|
380
|
+
basePath?: string;
|
|
381
|
+
/**
|
|
382
|
+
* Repository entries keyed by a display name.
|
|
383
|
+
* TypeScript infers the model type from each `repo` field,
|
|
384
|
+
* so `fieldsConfig` keys are typed to that model's field paths.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```ts
|
|
388
|
+
* repos: {
|
|
389
|
+
* posts: {
|
|
390
|
+
* repo: repos.posts,
|
|
391
|
+
* fieldsConfig: {
|
|
392
|
+
* title: ["create", "mutable", "filterable"],
|
|
393
|
+
* content: ["create", "mutable"],
|
|
394
|
+
* status: ["create", "filterable"],
|
|
395
|
+
* },
|
|
396
|
+
* },
|
|
397
|
+
* }
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
repos: {
|
|
401
|
+
[K in keyof TRepos]: AdminRepoConfig<TRepos[K]>;
|
|
402
|
+
};
|
|
403
|
+
/** Whether to parse URL-encoded bodies. Default: true. */
|
|
404
|
+
parseBody?: boolean;
|
|
405
|
+
/**
|
|
406
|
+
* Authentication guard executed before every request.
|
|
407
|
+
* - Pass a `BasicAuthConfig` to enable HTTP Basic Auth.
|
|
408
|
+
* - Pass a `Middleware` function for custom auth logic.
|
|
409
|
+
*/
|
|
410
|
+
auth?: BasicAuthConfig | Middleware;
|
|
411
|
+
/**
|
|
412
|
+
* Additional middleware functions executed after auth, before route handlers.
|
|
413
|
+
*/
|
|
414
|
+
middleware?: Middleware[];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Creates an Express-compatible request handler for the admin ORM UI.
|
|
418
|
+
* Generates a complete admin interface with dashboard, lists, and CRUD forms.
|
|
419
|
+
*
|
|
420
|
+
* @template TRepos - Shape of the repos map (inferred automatically)
|
|
421
|
+
* @param options - Admin server configuration
|
|
422
|
+
* @returns Express-compatible request handler for Firebase Functions
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```typescript
|
|
426
|
+
* // Basic admin server
|
|
427
|
+
* import { onRequest } from "firebase-functions/https";
|
|
428
|
+
* import { createAdminServer } from "@lpdjs/firestore-repo-service/servers/admin";
|
|
429
|
+
*
|
|
430
|
+
* export const admin = onRequest(
|
|
431
|
+
* createAdminServer({
|
|
432
|
+
* basePath: "/admin",
|
|
433
|
+
* repos: {
|
|
434
|
+
* users: {
|
|
435
|
+
* repo: repos.users,
|
|
436
|
+
* path: "users",
|
|
437
|
+
* },
|
|
438
|
+
* posts: {
|
|
439
|
+
* repo: repos.posts,
|
|
440
|
+
* path: "posts",
|
|
441
|
+
* },
|
|
442
|
+
* },
|
|
443
|
+
* })
|
|
444
|
+
* );
|
|
445
|
+
*
|
|
446
|
+
* // With HTTP Basic Auth
|
|
447
|
+
* export const admin = onRequest(
|
|
448
|
+
* createAdminServer({
|
|
449
|
+
* basePath: "/admin",
|
|
450
|
+
* auth: {
|
|
451
|
+
* type: "basic",
|
|
452
|
+
* realm: "Admin Area",
|
|
453
|
+
* username: "admin",
|
|
454
|
+
* password: process.env.ADMIN_PASSWORD!,
|
|
455
|
+
* },
|
|
456
|
+
* repos: { ... },
|
|
457
|
+
* })
|
|
458
|
+
* );
|
|
459
|
+
*
|
|
460
|
+
* // With custom auth middleware
|
|
461
|
+
* export const admin = onRequest(
|
|
462
|
+
* createAdminServer({
|
|
463
|
+
* auth: async (req, res, next) => {
|
|
464
|
+
* const token = req.headers?.authorization?.replace("Bearer ", "");
|
|
465
|
+
* if (!token || !(await verifyToken(token))) {
|
|
466
|
+
* res.status(401).send("Unauthorized");
|
|
467
|
+
* return;
|
|
468
|
+
* }
|
|
469
|
+
* next();
|
|
470
|
+
* },
|
|
471
|
+
* repos: { ... },
|
|
472
|
+
* })
|
|
473
|
+
* );
|
|
474
|
+
*
|
|
475
|
+
* // Full configuration with all options
|
|
476
|
+
* export const admin = onRequest(
|
|
477
|
+
* createAdminServer({
|
|
478
|
+
* basePath: "/admin",
|
|
479
|
+
* parseBody: true, // Parse URL-encoded bodies
|
|
480
|
+
* auth: { type: "basic", username: "admin", password: "secret" },
|
|
481
|
+
* middleware: [loggingMiddleware], // Additional middleware
|
|
482
|
+
* repos: {
|
|
483
|
+
* posts: {
|
|
484
|
+
* repo: repos.posts,
|
|
485
|
+
* path: "posts",
|
|
486
|
+
* documentKey: "docId", // Field used as document ID
|
|
487
|
+
* listColumns: ["title", "status", "createdAt"], // Columns in list
|
|
488
|
+
* pageSize: 25, // Items per page in list
|
|
489
|
+
* fieldsConfig: {
|
|
490
|
+
* title: ["create", "mutable", "filterable"],
|
|
491
|
+
* content: ["create", "mutable"],
|
|
492
|
+
* status: ["create", "mutable", "filterable"],
|
|
493
|
+
* userId: ["filterable"],
|
|
494
|
+
* },
|
|
495
|
+
* allowDelete: true, // Enable delete button
|
|
496
|
+
* relationalFields: [ // Relation navigation buttons
|
|
497
|
+
* { key: "userId", column: "Author" }, // Link to user
|
|
498
|
+
* { key: "docId", column: "Comments" }, // Link to comments list
|
|
499
|
+
* ],
|
|
500
|
+
* },
|
|
501
|
+
* users: {
|
|
502
|
+
* repo: repos.users,
|
|
503
|
+
* path: "users",
|
|
504
|
+
* fieldsConfig: {
|
|
505
|
+
* name: ["create", "mutable"],
|
|
506
|
+
* email: ["create", "mutable", "filterable"],
|
|
507
|
+
* isActive: ["mutable", "filterable"],
|
|
508
|
+
* },
|
|
509
|
+
* allowDelete: false,
|
|
510
|
+
* relationalFields: [
|
|
511
|
+
* { key: "docId", column: "Posts" },
|
|
512
|
+
* ],
|
|
513
|
+
* },
|
|
514
|
+
* },
|
|
515
|
+
* })
|
|
516
|
+
* );
|
|
517
|
+
*
|
|
518
|
+
* // Routes generated automatically:
|
|
519
|
+
* // GET /admin/ → Dashboard (list of repos)
|
|
520
|
+
* // GET /admin/:repo → Document list with pagination
|
|
521
|
+
* // GET /admin/:repo/create → Create form
|
|
522
|
+
* // POST /admin/:repo/create → Submit create
|
|
523
|
+
* // GET /admin/:repo/:id/edit → Edit form
|
|
524
|
+
* // POST /admin/:repo/:id/edit → Submit edit
|
|
525
|
+
* // POST /admin/:repo/:id/delete → Delete document
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
declare function createAdminServer<TRepos extends Record<string, ConfiguredRepository<any>>>(options: AdminServerOptions<TRepos>): (req: any, res: any) => Promise<void>;
|
|
529
|
+
|
|
530
|
+
export { type AdminRepoConfig as A, type BasicAuthConfig as B, type ColumnMeta as C, type FilterState as F, MiniRouter as M, type PageOptions as P, type RelationalFieldMeta as R, type SortState as S, type AdminRepoEntry as a, type AdminServerOptions as b, createAdminServer as c, type Middleware as d, type RepoRegistry as e, type RouteHandler as f };
|