@zuzjs/flare-admin 0.1.5 → 0.1.6
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 +268 -304
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +99 -51
- package/dist/index.d.ts +38 -10
- package/dist/index.js +3 -3
- package/dist/lib/grpc.d.ts +1 -1
- package/dist/lib/http.d.ts +3 -0
- package/dist/lib/notifications.d.ts +1 -1
- package/dist/realtime/Connection.d.ts +14 -0
- package/dist/realtime/LiveCollection.d.ts +3 -1
- package/dist/realtime/WsConnection.d.ts +11 -1
- package/dist/types/index.d.ts +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ admin.auth()
|
|
|
22
22
|
socket elevated ✓
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
**No MongoDB URI is ever shared.** The SDK talks to FlareServer's `/admin/token` REST endpoint using only the `adminKey`.
|
|
25
|
+
**No MongoDB URI is ever shared.** The SDK talks to FlareServer's `/admin/token` REST endpoint using only the `adminKey`.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -44,8 +44,6 @@ pnpm add @zuzjs/flare-admin
|
|
|
44
44
|
flare app create my-app
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
This prints two configs:
|
|
48
|
-
|
|
49
47
|
| Config | Safe for... |
|
|
50
48
|
|---|---|
|
|
51
49
|
| `apiKey` | Browser / client-side |
|
|
@@ -55,7 +53,7 @@ This prints two configs:
|
|
|
55
53
|
|
|
56
54
|
```bash
|
|
57
55
|
# .env (server-side only)
|
|
58
|
-
FLARE_URL=
|
|
56
|
+
FLARE_URL=https://flare.zuzcdn.net
|
|
59
57
|
FLARE_APP_ID=my-app
|
|
60
58
|
FLARE_ADMIN_KEY=FA_ADMIN_xxxxxxxxxxxx
|
|
61
59
|
```
|
|
@@ -78,14 +76,9 @@ const admin = connectApp({
|
|
|
78
76
|
import { getApp } from "@zuzjs/flare-admin";
|
|
79
77
|
|
|
80
78
|
app.post("/login", async (req, res) => {
|
|
81
|
-
// --- your existing auth logic ---
|
|
82
79
|
const user = await myAuthLogic(req.body);
|
|
83
80
|
if (!user) return res.status(401).json({ error: "invalid credentials" });
|
|
84
81
|
|
|
85
|
-
// Set your own session / cookie as normal
|
|
86
|
-
req.session.userId = user.id;
|
|
87
|
-
|
|
88
|
-
// --- the bridge ---
|
|
89
82
|
const flareToken = await getApp().auth().createCustomToken(user.id, {
|
|
90
83
|
role: user.isAdmin ? "admin" : "user",
|
|
91
84
|
claims: { email: user.email, plan: user.plan },
|
|
@@ -104,12 +97,11 @@ import FlareClient from "@zuzjs/flare";
|
|
|
104
97
|
const flare = new FlareClient({
|
|
105
98
|
endpoint: "https://flare.zuzcdn.net",
|
|
106
99
|
appId: "my-app",
|
|
107
|
-
apiKey: "FA_xxxxxxxx",
|
|
100
|
+
apiKey: "FA_xxxxxxxx",
|
|
108
101
|
});
|
|
109
102
|
|
|
110
103
|
flare.connect();
|
|
111
104
|
|
|
112
|
-
// After login:
|
|
113
105
|
const { flareToken } = await fetch("/login", { method: "POST", body: ... }).then(r => r.json());
|
|
114
106
|
await flare.auth(flareToken);
|
|
115
107
|
// ✅ Socket is now elevated — auth.uid and auth.role are set
|
|
@@ -125,51 +117,80 @@ Initialize a FlareAdmin app. Idempotent — safe to call at module scope.
|
|
|
125
117
|
|
|
126
118
|
```typescript
|
|
127
119
|
const admin = connectApp({
|
|
128
|
-
serverUrl:
|
|
129
|
-
appId:
|
|
130
|
-
adminKey:
|
|
131
|
-
|
|
120
|
+
serverUrl: string; // FlareServer base URL
|
|
121
|
+
appId: string; // App ID from `flare app create`
|
|
122
|
+
adminKey: string; // Admin key — server-side only
|
|
123
|
+
httpBase?: string; // Override base URL for all admin HTTP APIs (e.g. proxy)
|
|
124
|
+
defaultTtl?: string; // Default token TTL, e.g. "24h" (default)
|
|
125
|
+
dataMapper?: Record<string, (row: any) => any>; // Per-collection response mappers
|
|
132
126
|
});
|
|
133
127
|
```
|
|
134
128
|
|
|
129
|
+
`httpBase` routes all admin HTTP traffic through a different base URL (useful for proxies or gateways). When set, it replaces `serverUrl` as the base for every `/admin/*` HTTP request. Trailing slashes are normalized automatically.
|
|
130
|
+
|
|
135
131
|
### `getApp(name?)`
|
|
136
132
|
|
|
137
|
-
Retrieve an already-initialized app instance.
|
|
133
|
+
Retrieve an already-initialized app instance. Throws if not initialized.
|
|
134
|
+
|
|
135
|
+
### `disconnectApp(name?)`
|
|
136
|
+
|
|
137
|
+
Disconnect and remove an app from the registry. Returns `true` if the app existed.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
disconnectApp(); // default app
|
|
141
|
+
disconnectApp("app-a"); // named app
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `disconnectAllApps()`
|
|
145
|
+
|
|
146
|
+
Disconnect and clear every initialized app.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
disconnectAllApps();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `app.disconnect()`
|
|
153
|
+
|
|
154
|
+
Close the WebSocket connection on a specific app instance. Safe to call multiple times.
|
|
138
155
|
|
|
139
|
-
|
|
156
|
+
```typescript
|
|
157
|
+
admin.disconnect();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
140
161
|
|
|
141
|
-
|
|
162
|
+
## Auth
|
|
142
163
|
|
|
143
164
|
### `admin.auth().createCustomToken(uid, opts?)`
|
|
144
165
|
|
|
145
|
-
Mint a custom auth token.
|
|
166
|
+
Mint a custom auth token for use by the browser client.
|
|
146
167
|
|
|
147
168
|
```typescript
|
|
148
169
|
const token = await admin.auth().createCustomToken(uid, {
|
|
149
170
|
role?: "user" | "admin" | "anon", // default: "user"
|
|
150
|
-
claims?: Record<string, unknown>,
|
|
171
|
+
claims?: Record<string, unknown>,
|
|
151
172
|
ttl?: string, // e.g. "1h", "7d"
|
|
152
173
|
});
|
|
153
174
|
```
|
|
154
175
|
|
|
155
176
|
### `admin.auth().getTicket(uid, opts?)`
|
|
156
177
|
|
|
157
|
-
Mint a one-time ticket
|
|
178
|
+
Mint a one-time ticket for WebSocket auth flows.
|
|
158
179
|
|
|
159
180
|
```typescript
|
|
160
181
|
const ticket = await admin.auth().getTicket(uid, {
|
|
161
|
-
role?:
|
|
162
|
-
email?:
|
|
163
|
-
sid?:
|
|
164
|
-
ttlSeconds?: number,
|
|
165
|
-
ip?:
|
|
182
|
+
role?: "user" | "admin" | "anon",
|
|
183
|
+
email?: string,
|
|
184
|
+
sid?: string,
|
|
185
|
+
ttlSeconds?: number,
|
|
186
|
+
ip?: string,
|
|
166
187
|
});
|
|
167
188
|
|
|
168
|
-
// ticket shape
|
|
189
|
+
// ticket shape:
|
|
169
190
|
// {
|
|
170
|
-
// ticket: "websocket:550e8400
|
|
191
|
+
// ticket: "websocket:550e8400-...",
|
|
171
192
|
// tag: "websocket",
|
|
172
|
-
// uuid: "550e8400
|
|
193
|
+
// uuid: "550e8400-...",
|
|
173
194
|
// expires_at: "2026-04-15T12:34:56Z",
|
|
174
195
|
// one_time: true,
|
|
175
196
|
// uid: "user_123",
|
|
@@ -178,39 +199,157 @@ const ticket = await admin.auth().getTicket(uid, {
|
|
|
178
199
|
// }
|
|
179
200
|
```
|
|
180
201
|
|
|
181
|
-
|
|
202
|
+
---
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
POST /admin/ticket
|
|
185
|
-
Authorization: Bearer FA_ADMIN_xxxxxxxxxxxx
|
|
186
|
-
Content-Type: application/json
|
|
204
|
+
## Database
|
|
187
205
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
206
|
+
```typescript
|
|
207
|
+
// One-shot queries (bypasses security rules)
|
|
208
|
+
const users = await admin.db().collection("users").get();
|
|
209
|
+
|
|
210
|
+
await admin.db().collection("users").doc("alice").set({ name: "Alice" });
|
|
211
|
+
await admin.db().collection("users").doc("alice").update({ plan: "pro" });
|
|
212
|
+
await admin.db().collection("users").doc("alice").delete();
|
|
213
|
+
|
|
214
|
+
// Rich queries
|
|
215
|
+
const seniors = await admin.db()
|
|
216
|
+
.collection("users")
|
|
217
|
+
.where({ age: ">= 60" })
|
|
218
|
+
.orderBy("name")
|
|
219
|
+
.limit(10)
|
|
220
|
+
.get();
|
|
194
221
|
```
|
|
195
222
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
### Query Builder
|
|
224
|
+
|
|
225
|
+
`admin.db().collection(...)` supports the full structured query API:
|
|
226
|
+
|
|
227
|
+
**Filters:** `where`, `and`, `or`, `in`, `andIn`, `orIn`, `notIn`, `andNotIn`, `orNotIn`, `arrayContains`, `andArrayContains`, `orArrayContains`, `arrayContainsAny`, `andArrayContainsAny`, `orArrayContainsAny`, `some`, `andSome`, `orSome`, `like`, `andLike`, `orLike`, `notLike`, `andNotLike`, `orNotLike`, `exists`, `andExists`, `orExists`, `notExists`, `andNotExists`, `orNotExists`
|
|
228
|
+
|
|
229
|
+
**Sort / cursor / aggregate:** `latest`, `newest`, `oldest`, `orderBy`, `limit`, `offset`, `startAt`, `startAfter`, `endAt`, `endBefore`, `count`, `sum`, `avg`, `min`, `max`, `distinct`, `groupBy`, `having`, `select`, `distinctField`, `vectorSearch`
|
|
230
|
+
|
|
231
|
+
**Joins:** `join`, `Join`, `joinNested`, `JoinNested`, `withRelation`
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const rows = await admin.db()
|
|
235
|
+
.collection("boards")
|
|
236
|
+
.where({ uid: userId })
|
|
237
|
+
.orSome("team", { uid: userId })
|
|
238
|
+
.join("lists", { source: "id", target: "boardId", as: "lists" })
|
|
239
|
+
.join("users", { source: "team.uid", target: "id", as: "teamMembers" })
|
|
240
|
+
.joinNested("lists", "cards", { source: "id", target: "listId", as: "cards" })
|
|
241
|
+
.withRelation("team.uid->users.id as collaborators")
|
|
242
|
+
.orderBy("updatedAt", "desc")
|
|
243
|
+
.limit(20)
|
|
244
|
+
.get();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `allowSensitiveAuthUserFields(false)`
|
|
248
|
+
|
|
249
|
+
Auth-user joins default to full fields. Pass `false` to restrict to public-profile-only output:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const safeBoards = await admin.db()
|
|
253
|
+
.collection("boards")
|
|
254
|
+
.join("users", { source: "team.uid", target: "id", as: "team" })
|
|
255
|
+
.allowSensitiveAuthUserFields(false)
|
|
256
|
+
.get();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### `getRawQuery()`
|
|
260
|
+
|
|
261
|
+
Inspect the structured query that will be sent:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const raw = admin.db()
|
|
265
|
+
.collection("boards")
|
|
266
|
+
.where({ uid: userId })
|
|
267
|
+
.getRawQuery();
|
|
268
|
+
|
|
269
|
+
console.log(raw.collection, raw.query);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Realtime
|
|
275
|
+
|
|
276
|
+
### `admin.live().collection(...).onSnapshot(cb)`
|
|
277
|
+
|
|
278
|
+
Single-snapshot subscription — fires once on the initial data load, then auto-unsubscribes:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const unsub = admin.live()
|
|
282
|
+
.collection("orders")
|
|
283
|
+
.where({ status: "pending" })
|
|
284
|
+
.orderBy("createdAt", "desc")
|
|
285
|
+
.onSnapshot((snap) => {
|
|
286
|
+
console.log(snap.type, snap.data);
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `.stream(options?)` — live batched stream
|
|
291
|
+
|
|
292
|
+
Returns a long-lived `AdminCollectionStream<T>` that maintains a local snapshot and fans out batched change events to listeners.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
const stream = admin.live()
|
|
296
|
+
.collection("orders")
|
|
297
|
+
.where({ status: "pending" })
|
|
298
|
+
.stream({
|
|
299
|
+
flushMs?: number, // batch flush delay in ms (default: 24)
|
|
300
|
+
maxBatchSize?: number, // max changes per flush (default: 200)
|
|
301
|
+
insertAt?: "start" | "end", // where new docs land (default: "end")
|
|
302
|
+
maxDocs?: number, // cap the local snapshot size
|
|
303
|
+
sort?: (a: T, b: T) => number, // comparator applied after each flush
|
|
304
|
+
idField?: string, // identity field name (default: "id")
|
|
305
|
+
getId?: (doc: T) => string, // custom id extractor
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Subscribe to changes
|
|
309
|
+
const off = stream.listen((docs, meta) => {
|
|
310
|
+
console.log(meta.reason, meta.version, docs.length);
|
|
311
|
+
// meta.reason: "snapshot" | "change-batch"
|
|
312
|
+
// meta.ready: true after the first snapshot arrives
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Read the current snapshot without subscribing
|
|
316
|
+
const current = stream.getSnapshot();
|
|
317
|
+
|
|
318
|
+
// Remove a specific listener (does not close the stream)
|
|
319
|
+
off();
|
|
320
|
+
|
|
321
|
+
// Close the stream and release the WebSocket subscription
|
|
322
|
+
stream.close();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `.asStore(options?)` — framework-agnostic external store
|
|
326
|
+
|
|
327
|
+
Returns an `AdminCollectionExternalStore<T>` compatible with React `useSyncExternalStore` or any subscribe/getSnapshot pattern:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
const store = admin.live()
|
|
331
|
+
.collection("orders")
|
|
332
|
+
.asStore({ flushMs: 50 });
|
|
333
|
+
|
|
334
|
+
// React
|
|
335
|
+
import { useSyncExternalStore } from "react";
|
|
336
|
+
const orders = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
337
|
+
|
|
338
|
+
// Or directly
|
|
339
|
+
const unsub = store.subscribe(() => {
|
|
340
|
+
console.log(store.getSnapshot());
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
store.close(); // release when done
|
|
209
344
|
```
|
|
210
345
|
|
|
346
|
+
### Realtime query builder parity
|
|
347
|
+
|
|
348
|
+
The same query builder surface (`where`, `orderBy`, `join`, `limit`, etc.) is available on `admin.live().collection(...)` just like on `admin.db().collection(...)`.
|
|
349
|
+
|
|
211
350
|
---
|
|
212
351
|
|
|
213
|
-
## Security rules
|
|
352
|
+
## Security rules
|
|
214
353
|
|
|
215
354
|
Once a client calls `flare.auth(token)`, the FlareServer socket has:
|
|
216
355
|
|
|
@@ -219,8 +358,6 @@ auth.uid = the uid you passed to createCustomToken()
|
|
|
219
358
|
auth.role = "user" | "admin" | "anon"
|
|
220
359
|
```
|
|
221
360
|
|
|
222
|
-
Use these in your security rules:
|
|
223
|
-
|
|
224
361
|
```json
|
|
225
362
|
{
|
|
226
363
|
"users": { ".read": "auth != null", ".write": "auth.uid == $docId" },
|
|
@@ -240,211 +377,115 @@ const adminB = connectApp({ serverUrl, appId: "app-b", adminKey: "..." }, "b");
|
|
|
240
377
|
|
|
241
378
|
const tokenA = await getApp("a").auth().createCustomToken(userId);
|
|
242
379
|
const tokenB = await getApp("b").auth().createCustomToken(userId);
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
## Admin-Only Auth Join Field Control
|
|
246
380
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Use `allowSensitiveAuthUserFields(false)` per query when you want public-profile-only output:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
const safeBoards = await admin.db()
|
|
253
|
-
.collection("boards")
|
|
254
|
-
.join("users", { source: "team.uid", target: "id", as: "team" })
|
|
255
|
-
.allowSensitiveAuthUserFields(false)
|
|
256
|
-
.get();
|
|
381
|
+
disconnectAllApps();
|
|
257
382
|
```
|
|
258
383
|
|
|
259
|
-
|
|
260
|
-
- This option is admin-only.
|
|
261
|
-
- Normal client/system query paths cannot enable sensitive auth-user fields.
|
|
262
|
-
|
|
263
|
-
## Data Mapper (Admin)
|
|
384
|
+
---
|
|
264
385
|
|
|
265
|
-
|
|
386
|
+
## Data Mapper
|
|
266
387
|
|
|
267
|
-
|
|
268
|
-
- Base collection rows use the mapper key matching the collection name.
|
|
269
|
-
- Join payload rows use the mapper key matching join `as`.
|
|
388
|
+
Pass `dataMapper` in `connectApp(...)` to shape inbound data. Keys match collection names or join aliases (`as`).
|
|
270
389
|
|
|
271
390
|
```typescript
|
|
272
391
|
const admin = connectApp({
|
|
273
392
|
serverUrl: process.env.FLARE_URL!,
|
|
274
|
-
appId:
|
|
275
|
-
adminKey:
|
|
393
|
+
appId: process.env.FLARE_APP_ID!,
|
|
394
|
+
adminKey: process.env.FLARE_ADMIN_KEY!,
|
|
276
395
|
dataMapper: {
|
|
277
396
|
boards: (row) => ({
|
|
278
|
-
id:
|
|
279
|
-
name:
|
|
280
|
-
description: row.description,
|
|
397
|
+
id: row.id,
|
|
398
|
+
name: row.name,
|
|
281
399
|
createdAt: new Date(row.createdAt ?? row.created_at),
|
|
282
400
|
}),
|
|
283
401
|
team: (row) => ({
|
|
284
|
-
id:
|
|
285
|
-
name:
|
|
402
|
+
id: row.id,
|
|
403
|
+
name: row.authMeta?.additionalParams?.name || "Unknown",
|
|
286
404
|
email: row.email,
|
|
287
|
-
createdAt: new Date(row.createdAt ?? row.created_at),
|
|
288
405
|
}),
|
|
289
406
|
},
|
|
290
407
|
});
|
|
291
|
-
|
|
292
|
-
const rows = await admin.db()
|
|
293
|
-
.collection("boards")
|
|
294
|
-
.where({ boardId: "123" })
|
|
295
|
-
.join("users", {
|
|
296
|
-
source: "team.uid",
|
|
297
|
-
target: "uid",
|
|
298
|
-
as: "team",
|
|
299
|
-
})
|
|
300
|
-
.get();
|
|
301
|
-
|
|
302
|
-
// rows[*] is mapped by dataMapper.boards
|
|
303
|
-
// rows[*].team[*] is mapped by dataMapper.team (join alias)
|
|
304
408
|
```
|
|
305
409
|
|
|
306
|
-
|
|
410
|
+
For `join(..., { as: "team" })`, define `dataMapper.team`.
|
|
307
411
|
|
|
308
412
|
---
|
|
309
413
|
|
|
310
|
-
## Query Builder Parity (Admin)
|
|
311
|
-
|
|
312
|
-
`admin.db().collection(...)` and `admin.connection().collection(...)` are aligned with the client-style query builder API.
|
|
313
|
-
|
|
314
|
-
### Supported filter helpers
|
|
315
|
-
|
|
316
|
-
`where`, `and`, `or`, `in`, `andIn`, `orIn`, `notIn`, `andNotIn`, `orNotIn`, `arrayContains`, `andArrayContains`, `orArrayContains`, `arrayContainsAny`, `andArrayContainsAny`, `orArrayContainsAny`, `some`, `andSome`, `orSome`, `like`, `andLike`, `orLike`, `notLike`, `andNotLike`, `orNotLike`, `exists`, `andExists`, `orExists`, `notExists`, `andNotExists`, `orNotExists`
|
|
317
|
-
|
|
318
|
-
### Supported sort / cursor / aggregate helpers
|
|
319
|
-
|
|
320
|
-
`latest`, `newest`, `oldest`, `orderBy`, `limit`, `offset`, `startAt`, `startAfter`, `endAt`, `endBefore`, `count`, `sum`, `avg`, `min`, `max`, `distinct`, `groupBy`, `having`, `select`, `distinctField`, `vectorSearch`
|
|
321
|
-
|
|
322
|
-
### Supported join helpers
|
|
323
|
-
|
|
324
|
-
`join`, `Join`, `joinNested`, `JoinNested`, `withRelation`
|
|
325
|
-
|
|
326
|
-
Example:
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
const rows = await admin.db()
|
|
330
|
-
.collection("boards")
|
|
331
|
-
.where({ uid: userId })
|
|
332
|
-
.orSome("team", { uid: userId })
|
|
333
|
-
.join("lists", { source: "id", target: "boardId", as: "lists" })
|
|
334
|
-
.join("users", { source: "team.uid", target: "id", as: "teamMembers" })
|
|
335
|
-
.joinNested("lists", "cards", { source: "id", target: "listId", as: "cards" })
|
|
336
|
-
.withRelation("team.uid->users.id as collaborators")
|
|
337
|
-
.orderBy("updatedAt", "desc")
|
|
338
|
-
.limit(20)
|
|
339
|
-
.get();
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### Realtime parity
|
|
343
|
-
|
|
344
|
-
The same query builder surface is available on socket subscriptions:
|
|
345
|
-
|
|
346
|
-
```typescript
|
|
347
|
-
const stop = admin.connection()
|
|
348
|
-
.collection("boards")
|
|
349
|
-
.where({ uid: userId })
|
|
350
|
-
.join("lists", { source: "id", target: "boardId", as: "lists" })
|
|
351
|
-
.onSnapshot((event) => {
|
|
352
|
-
console.log(event.type, event.data);
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Debugging structured query output
|
|
357
|
-
|
|
358
|
-
Both DB and realtime collection references provide `getRawQuery()`:
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
const raw = admin.db()
|
|
362
|
-
.collection("boards")
|
|
363
|
-
.where({ uid: userId })
|
|
364
|
-
.withRelation("team.uid->users.id as collaborators")
|
|
365
|
-
.getRawQuery();
|
|
366
|
-
|
|
367
|
-
console.log(raw.collection, raw.query);
|
|
368
|
-
```
|
|
369
|
-
|
|
370
414
|
## Storage API (S3-like)
|
|
371
415
|
|
|
372
416
|
```typescript
|
|
373
417
|
import { connectApp, AdminStorageSignedAction } from "@zuzjs/flare-admin";
|
|
374
418
|
|
|
375
|
-
const admin = connectApp({
|
|
376
|
-
serverUrl: process.env.FLARE_URL!,
|
|
377
|
-
appId: process.env.FLARE_APP_ID!,
|
|
378
|
-
adminKey: process.env.FLARE_ADMIN_KEY!,
|
|
379
|
-
});
|
|
380
|
-
|
|
381
419
|
const storage = admin.storage();
|
|
382
420
|
|
|
383
421
|
await storage.createBucket("reports");
|
|
384
422
|
|
|
385
423
|
await storage.putObject({
|
|
386
|
-
bucket:
|
|
387
|
-
key:
|
|
388
|
-
body:
|
|
424
|
+
bucket: "reports",
|
|
425
|
+
key: "weekly/summary.json",
|
|
426
|
+
body: Buffer.from(JSON.stringify({ ok: true })),
|
|
389
427
|
contentType: "application/json",
|
|
390
|
-
encrypt:
|
|
428
|
+
encrypt: true,
|
|
391
429
|
});
|
|
392
430
|
|
|
393
|
-
const meta
|
|
394
|
-
|
|
395
|
-
key: "weekly/summary.json",
|
|
396
|
-
});
|
|
431
|
+
const meta = await storage.headObject({ bucket: "reports", key: "weekly/summary.json" });
|
|
432
|
+
const downloaded = await storage.getObject({ bucket: "reports", key: "weekly/summary.json", decrypt: true });
|
|
397
433
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
key: "weekly/summary.json",
|
|
401
|
-
decrypt: true,
|
|
402
|
-
});
|
|
434
|
+
await storage.deleteObjects({ bucket: "reports", keys: ["weekly/summary.json"] });
|
|
435
|
+
```
|
|
403
436
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
437
|
+
### Signed URLs
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
const signedUpload = await admin.createSignedUrl({
|
|
441
|
+
bucket: "reports",
|
|
442
|
+
key: "uploads/big-video.mp4",
|
|
443
|
+
action: AdminStorageSignedAction.Upload,
|
|
444
|
+
expiresInSeconds: 300,
|
|
445
|
+
contentType: "video/mp4",
|
|
446
|
+
encrypt: true,
|
|
412
447
|
});
|
|
413
448
|
|
|
414
|
-
await
|
|
415
|
-
|
|
416
|
-
|
|
449
|
+
await fetch(signedUpload.url, {
|
|
450
|
+
method: signedUpload.method,
|
|
451
|
+
headers: { "Content-Type": "video/mp4" },
|
|
452
|
+
body: videoBuffer,
|
|
417
453
|
});
|
|
418
454
|
|
|
419
|
-
|
|
455
|
+
const signedDownload = await admin.createSignedUrl({
|
|
456
|
+
bucket: "reports",
|
|
457
|
+
key: "uploads/big-video.mp4",
|
|
458
|
+
action: AdminStorageSignedAction.Download,
|
|
459
|
+
expiresInSeconds: 300,
|
|
460
|
+
decrypt: true,
|
|
461
|
+
allowedOrigins: ["https://app.example.com"],
|
|
462
|
+
});
|
|
420
463
|
```
|
|
421
464
|
|
|
422
|
-
|
|
465
|
+
Notes:
|
|
466
|
+
- `forceDownload` and `embedOnly` are mutually exclusive.
|
|
467
|
+
- `allowedOrigins` defaults to `['*']` if omitted.
|
|
468
|
+
|
|
469
|
+
### Direct download helpers
|
|
423
470
|
|
|
424
471
|
```typescript
|
|
425
472
|
const directUrl = await admin.getObjectUrl({
|
|
426
|
-
bucket:
|
|
427
|
-
key:
|
|
473
|
+
bucket: "reports",
|
|
474
|
+
key: "weekly/summary.json",
|
|
428
475
|
expiresInSeconds: 120,
|
|
429
|
-
allowedOrigins:
|
|
476
|
+
allowedOrigins: ["*"],
|
|
430
477
|
});
|
|
431
478
|
|
|
432
479
|
const triggered = await admin.downloadObject({
|
|
433
|
-
bucket:
|
|
434
|
-
key:
|
|
435
|
-
filename:
|
|
436
|
-
forceDownload:
|
|
480
|
+
bucket: "reports",
|
|
481
|
+
key: "weekly/summary.json",
|
|
482
|
+
filename: "summary.json",
|
|
483
|
+
forceDownload: true,
|
|
437
484
|
allowedOrigins: ["https://app.example.com"],
|
|
438
485
|
});
|
|
439
|
-
|
|
440
|
-
console.log(directUrl, triggered.triggered);
|
|
441
486
|
```
|
|
442
487
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
Use `service zuz.storage` rules to enforce per-app policy on top of platform limits.
|
|
446
|
-
|
|
447
|
-
Example 1: Authenticated read, quota-aware write
|
|
488
|
+
### Storage rules
|
|
448
489
|
|
|
449
490
|
```txt
|
|
450
491
|
service zuz.storage {
|
|
@@ -454,9 +495,7 @@ service zuz.storage {
|
|
|
454
495
|
|
|
455
496
|
allow write: if auth != null
|
|
456
497
|
&& requestData.storage.usage.storageBytes + requestData.size
|
|
457
|
-
<= requestData.storage.plan.storageBytes
|
|
458
|
-
&& requestData.storage.usage.bandwidthBytes + requestData.size
|
|
459
|
-
<= requestData.storage.plan.bandwidthBytes;
|
|
498
|
+
<= requestData.storage.plan.storageBytes;
|
|
460
499
|
|
|
461
500
|
allow delete: if auth != null;
|
|
462
501
|
}
|
|
@@ -464,115 +503,40 @@ service zuz.storage {
|
|
|
464
503
|
}
|
|
465
504
|
```
|
|
466
505
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
```txt
|
|
470
|
-
service zuz.storage {
|
|
471
|
-
match /storage/{bucket}/objects {
|
|
472
|
-
match /{document=**} {
|
|
473
|
-
allow read: if auth != null;
|
|
474
|
-
|
|
475
|
-
allow write: if auth != null
|
|
476
|
-
&& (
|
|
477
|
-
auth.role == 'admin'
|
|
478
|
-
|| auth.claims.plan == 'pro'
|
|
479
|
-
)
|
|
480
|
-
&& requestData.storage.usage.storageBytes + requestData.size
|
|
481
|
-
<= requestData.storage.plan.storageBytes
|
|
482
|
-
&& requestData.storage.usage.bandwidthBytes + requestData.size
|
|
483
|
-
<= requestData.storage.plan.bandwidthBytes;
|
|
484
|
-
|
|
485
|
-
allow delete: if auth != null
|
|
486
|
-
&& (auth.role == 'admin' || auth.claims.plan == 'pro');
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Apply rules with admin API:
|
|
493
|
-
|
|
494
|
-
```http
|
|
495
|
-
POST /v1/apps/{appId}/admin/storage/rules
|
|
496
|
-
Authorization: Bearer FA_ADMIN_xxxxxxxxxxxx
|
|
497
|
-
Content-Type: application/json
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
"rulesDsl": "service zuz.storage { match /storage/{bucket}/objects { match /{document=**} { allow read: if auth != null; } } }"
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
Tip: map users to app-level plans through app settings (`storagePlans`, `storageUserPlans`, `storageDefaultPlanId`) so `requestData.storage.plan.*` resolves correctly at runtime.
|
|
505
|
-
|
|
506
|
-
## Storage Signed URLs
|
|
507
|
-
|
|
508
|
-
Create signed URLs for first-party upload/download/delete/edit without exposing admin key to file-transfer clients.
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
const signedUpload = await admin.createSignedUrl({
|
|
512
|
-
bucket: "reports",
|
|
513
|
-
key: "uploads/big-video.mp4",
|
|
514
|
-
action: AdminStorageSignedAction.Upload,
|
|
515
|
-
expiresInSeconds: 300,
|
|
516
|
-
contentType: "video/mp4",
|
|
517
|
-
encrypt: true,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
await fetch(signedUpload.url, {
|
|
521
|
-
method: signedUpload.method,
|
|
522
|
-
headers: { "Content-Type": "video/mp4" },
|
|
523
|
-
body: videoBuffer,
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
const signedDownload = await admin.createSignedUrl({
|
|
527
|
-
bucket: "reports",
|
|
528
|
-
key: "uploads/big-video.mp4",
|
|
529
|
-
action: AdminStorageSignedAction.Download,
|
|
530
|
-
expiresInSeconds: 300,
|
|
531
|
-
decrypt: true,
|
|
532
|
-
forceDownload: false,
|
|
533
|
-
allowedOrigins: ["https://app.example.com"],
|
|
534
|
-
embedOnly: true,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
const bytes = await fetch(signedDownload.url, { method: signedDownload.method }).then((r) => r.arrayBuffer());
|
|
538
|
-
|
|
539
|
-
console.log("expiresAt", signedDownload.expiresAt, "ttl", signedDownload.expiresInSeconds);
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
Notes:
|
|
543
|
-
- `token` (for example `st_...`) is a signed ticket identifier; expiry is returned as `expiresAt`/`expiresInSeconds`.
|
|
544
|
-
- `forceDownload` and `embedOnly` are mutually exclusive.
|
|
545
|
-
- `allowedOrigins` defaults to `['*']` if omitted.
|
|
546
|
-
- `downloadObject(...)` triggers a browser click only when `document` is available. In server runtimes, it returns `{ ok, url, filename, triggered: false }` so you can redirect, proxy, or return the signed URL.
|
|
547
|
-
|
|
548
|
-
## External AWS SDK Init From Flare `/aws` Config
|
|
506
|
+
### External AWS SDK from Flare `/aws` config
|
|
549
507
|
|
|
550
508
|
```typescript
|
|
551
509
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
552
|
-
import { connectApp } from "@zuzjs/flare-admin";
|
|
553
|
-
|
|
554
|
-
const admin = connectApp({
|
|
555
|
-
serverUrl: process.env.FLARE_URL!,
|
|
556
|
-
appId: process.env.FLARE_APP_ID!,
|
|
557
|
-
adminKey: process.env.FLARE_ADMIN_KEY!,
|
|
558
|
-
});
|
|
559
510
|
|
|
560
511
|
const aws = await admin.storage().awsConfig("storage-server-id");
|
|
561
512
|
|
|
562
513
|
const s3 = new S3Client({
|
|
563
|
-
endpoint:
|
|
564
|
-
region:
|
|
514
|
+
endpoint: aws.endpoint,
|
|
515
|
+
region: aws.region,
|
|
565
516
|
forcePathStyle: Boolean(aws.forcePathStyle),
|
|
566
517
|
credentials: {
|
|
567
|
-
accessKeyId:
|
|
518
|
+
accessKeyId: aws.accessKeyId,
|
|
568
519
|
secretAccessKey: aws.secretAccessKey,
|
|
569
520
|
},
|
|
570
521
|
});
|
|
571
522
|
|
|
572
523
|
await s3.send(new PutObjectCommand({
|
|
573
|
-
Bucket:
|
|
574
|
-
Key:
|
|
575
|
-
Body:
|
|
524
|
+
Bucket: aws.bucket,
|
|
525
|
+
Key: `${aws.prefix ?? ""}manual/test.txt`,
|
|
526
|
+
Body: "hello",
|
|
576
527
|
ContentType: "text/plain",
|
|
577
528
|
}));
|
|
578
529
|
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Push Notifications
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
await admin.notifications().send({
|
|
537
|
+
uid: "user_123",
|
|
538
|
+
title: "Hello",
|
|
539
|
+
body: "Your order shipped!",
|
|
540
|
+
data: { orderId: "abc" },
|
|
541
|
+
});
|
|
542
|
+
```
|