hydrousdb 3.5.0 → 3.5.1
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 +904 -193
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,17 +10,73 @@ npm install hydrousdb
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Setup](#setup)
|
|
16
|
+
- [Records](#records)
|
|
17
|
+
- [Create, Read, Update, Delete](#create-read-update-delete)
|
|
18
|
+
- [Querying & Filtering](#querying--filtering)
|
|
19
|
+
- [Time Scope on Queries](#time-scope-on-queries)
|
|
20
|
+
- [Pagination](#pagination)
|
|
21
|
+
- [Atomic Field Updates](#atomic-field-updates)
|
|
22
|
+
- [Batch Operations](#batch-operations)
|
|
23
|
+
- [Version History](#version-history)
|
|
24
|
+
- [Custom Record IDs](#custom-record-ids)
|
|
25
|
+
- [Existence Check](#existence-check)
|
|
26
|
+
- [Get All Records](#get-all-records)
|
|
27
|
+
- [Auth](#auth)
|
|
28
|
+
- [Sign Up & Log In](#sign-up--log-in)
|
|
29
|
+
- [Session Management](#session-management)
|
|
30
|
+
- [User Profile](#user-profile)
|
|
31
|
+
- [Password & Email](#password--email)
|
|
32
|
+
- [Admin Controls](#admin-controls)
|
|
33
|
+
- [File Storage](#file-storage)
|
|
34
|
+
- [Upload](#upload)
|
|
35
|
+
- [Large Files with Progress](#large-files-with-progress)
|
|
36
|
+
- [Batch Uploads](#batch-uploads)
|
|
37
|
+
- [Download](#download)
|
|
38
|
+
- [Batch Download](#batch-download)
|
|
39
|
+
- [List, Metadata & Signed URLs](#list-metadata--signed-urls)
|
|
40
|
+
- [Move, Copy & Delete](#move-copy--delete)
|
|
41
|
+
- [Visibility](#visibility)
|
|
42
|
+
- [Folders](#folders)
|
|
43
|
+
- [Scoped Storage](#scoped-storage)
|
|
44
|
+
- [Storage Stats](#storage-stats)
|
|
45
|
+
- [Analytics](#analytics)
|
|
46
|
+
- [Date Range (Time Scope)](#date-range-time-scope)
|
|
47
|
+
- [Count](#count)
|
|
48
|
+
- [Distribution](#distribution)
|
|
49
|
+
- [Sum](#sum)
|
|
50
|
+
- [Time Series](#time-series)
|
|
51
|
+
- [Field Time Series](#field-time-series)
|
|
52
|
+
- [Top N](#top-n)
|
|
53
|
+
- [Stats](#stats)
|
|
54
|
+
- [Records via BigQuery](#records-via-bigquery)
|
|
55
|
+
- [Multi-Metric](#multi-metric)
|
|
56
|
+
- [Storage Stats](#storage-stats-1)
|
|
57
|
+
- [Cross-Bucket](#cross-bucket)
|
|
58
|
+
- [Raw Query](#raw-query)
|
|
59
|
+
- [Error Handling](#error-handling)
|
|
60
|
+
- [TypeScript](#typescript)
|
|
61
|
+
- [Security](#security)
|
|
62
|
+
- [Framework Examples](#framework-examples)
|
|
63
|
+
- [API Reference](#api-reference)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
13
67
|
## Setup
|
|
14
68
|
|
|
15
69
|
```ts
|
|
16
70
|
import { createClient } from 'hydrousdb';
|
|
17
71
|
|
|
18
72
|
const db = createClient({
|
|
19
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
20
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
73
|
+
authKey: process.env.HYDROUS_AUTH_KEY!, // hk_auth_…
|
|
74
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!, // hk_bucket_…
|
|
21
75
|
storageKeys: {
|
|
22
|
-
main: process.env.HYDROUS_STORAGE_KEY!,
|
|
76
|
+
main: process.env.HYDROUS_STORAGE_KEY!, // ssk_…
|
|
77
|
+
// add more named storage keys as needed
|
|
23
78
|
},
|
|
79
|
+
// baseUrl: 'https://custom-endpoint.example.com', // optional override
|
|
24
80
|
});
|
|
25
81
|
```
|
|
26
82
|
|
|
@@ -32,104 +88,225 @@ Works in **React, Next.js, Vue, React Native, Node.js** — anywhere that runs m
|
|
|
32
88
|
|
|
33
89
|
## Records
|
|
34
90
|
|
|
35
|
-
JSON objects stored in named buckets. Every record gets `id`, `createdAt`, and `updatedAt
|
|
91
|
+
JSON objects stored in named buckets. Every record automatically gets `id`, `createdAt`, and `updatedAt`.
|
|
36
92
|
|
|
37
93
|
```ts
|
|
38
94
|
const posts = db.records('blog-posts');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Create, Read, Update, Delete
|
|
39
98
|
|
|
99
|
+
```ts
|
|
40
100
|
// Create
|
|
41
101
|
const post = await posts.create(
|
|
42
102
|
{ title: 'Hello', status: 'draft', views: 0 },
|
|
43
|
-
{
|
|
103
|
+
{
|
|
104
|
+
queryableFields: ['status', 'views'], // declare fields you want to filter on
|
|
105
|
+
userEmail: 'alice@example.com', // optional — for audit trails
|
|
106
|
+
},
|
|
44
107
|
);
|
|
45
|
-
console.log(post.id);
|
|
108
|
+
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
109
|
+
console.log(post.createdAt); // Unix ms timestamp
|
|
46
110
|
|
|
47
|
-
// Read
|
|
111
|
+
// Read by ID
|
|
48
112
|
const found = await posts.get(post.id);
|
|
49
113
|
|
|
50
|
-
//
|
|
114
|
+
// Partial update — only the fields you pass are changed
|
|
51
115
|
await posts.patch(post.id, { status: 'published' });
|
|
52
116
|
|
|
53
|
-
//
|
|
117
|
+
// Merge mode — deeply merge nested objects instead of replacing them
|
|
118
|
+
await posts.patch(post.id, { meta: { seo: true } }, { merge: true });
|
|
119
|
+
|
|
120
|
+
// Delete permanently
|
|
54
121
|
await posts.delete(post.id);
|
|
55
122
|
```
|
|
56
123
|
|
|
57
|
-
> **Why `queryableFields`?** Records are stored as compressed blobs. Fields you want to filter or sort by must be
|
|
124
|
+
> **Why `queryableFields`?** Records are stored as compressed blobs. Fields you want to filter or sort by must be declared at write time. You only pay the indexing overhead for what you actually query.
|
|
58
125
|
|
|
59
|
-
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Querying & Filtering
|
|
60
129
|
|
|
61
130
|
```ts
|
|
62
131
|
const { records, hasMore, nextCursor } = await posts.query({
|
|
63
132
|
filters: [
|
|
64
|
-
{ field: 'status', op: '==',
|
|
65
|
-
{ field: 'views', op: '>',
|
|
133
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
134
|
+
{ field: 'views', op: '>', value: 100 },
|
|
135
|
+
{ field: 'title', op: 'contains', value: 'hello' },
|
|
66
136
|
],
|
|
67
137
|
orderBy: 'createdAt',
|
|
68
138
|
order: 'desc',
|
|
69
139
|
limit: 20,
|
|
140
|
+
fields: 'id,title,status', // optional — return only these fields
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Supported filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Time Scope on Queries
|
|
149
|
+
|
|
150
|
+
Pass `timeScope` to restrict records to a specific **day, month, or year** using the record ID prefix convention. This is the fastest way to scope a query by time — no timestamp arithmetic needed.
|
|
151
|
+
|
|
152
|
+
| Scope | Format | Example | Matches |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| Day | `_day_YYMMDD` | `_day_260305` | March 5, 2026 |
|
|
155
|
+
| Month | `_month_YYMM` | `_month_2603` | March 2026 |
|
|
156
|
+
| Year | `_year_YY` | `_year_26` | All of 2026 |
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// All records from March 2026
|
|
160
|
+
const { records, hasMore, nextCursor } = await posts.query({
|
|
161
|
+
timeScope: '_month_2603',
|
|
162
|
+
order: 'desc',
|
|
163
|
+
limit: 50,
|
|
70
164
|
});
|
|
71
165
|
|
|
72
|
-
//
|
|
166
|
+
// Paginate through a time-scoped result set
|
|
73
167
|
if (hasMore) {
|
|
74
|
-
const page2 = await posts.query({
|
|
168
|
+
const page2 = await posts.query({
|
|
169
|
+
timeScope: '_month_2603',
|
|
170
|
+
startAfter: nextCursor,
|
|
171
|
+
});
|
|
75
172
|
}
|
|
173
|
+
|
|
174
|
+
// A specific day
|
|
175
|
+
const { records: dayRecords } = await posts.query({
|
|
176
|
+
timeScope: '_day_260305', // March 5, 2026
|
|
177
|
+
order: 'asc',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// An entire year
|
|
181
|
+
const { records: yearRecords } = await posts.query({
|
|
182
|
+
timeScope: '_year_26',
|
|
183
|
+
orderBy: 'createdAt',
|
|
184
|
+
order: 'asc',
|
|
185
|
+
limit: 100,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Fully composable with filters
|
|
189
|
+
const { records: published } = await posts.query({
|
|
190
|
+
timeScope: '_month_2603',
|
|
191
|
+
filters: [{ field: 'status', op: '==', value: 'published' }],
|
|
192
|
+
orderBy: 'createdAt',
|
|
193
|
+
order: 'desc',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// getAll() also respects timeScope
|
|
197
|
+
const all = await posts.getAll({ timeScope: '_year_26' });
|
|
76
198
|
```
|
|
77
199
|
|
|
78
|
-
|
|
200
|
+
---
|
|
79
201
|
|
|
80
|
-
###
|
|
202
|
+
### Pagination
|
|
81
203
|
|
|
82
|
-
|
|
204
|
+
`query()` returns a cursor you can pass straight into the next call.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Page 1
|
|
208
|
+
const page1 = await posts.query({ limit: 20, orderBy: 'createdAt', order: 'desc' });
|
|
209
|
+
|
|
210
|
+
// Page 2
|
|
211
|
+
if (page1.hasMore) {
|
|
212
|
+
const page2 = await posts.query({
|
|
213
|
+
limit: 20,
|
|
214
|
+
orderBy: 'createdAt',
|
|
215
|
+
order: 'desc',
|
|
216
|
+
startAfter: page1.nextCursor, // cursor-based — no offset drift
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// You can also use startAt / endAt for range-based cursor control
|
|
221
|
+
const window = await posts.query({ startAt: cursorA, endAt: cursorB });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Atomic Field Updates
|
|
227
|
+
|
|
228
|
+
Avoid race conditions with server-side sentinels inside `patch()`:
|
|
83
229
|
|
|
84
230
|
```ts
|
|
85
231
|
await posts.patch(post.id, {
|
|
86
|
-
views: { __op: 'increment',
|
|
87
|
-
credits: { __op: 'decrement',
|
|
88
|
-
slug: { __op: 'setOnce',
|
|
89
|
-
tags: { __op: 'appendUnique',
|
|
90
|
-
tags: { __op: 'removeFromArray', item: 'draft' },
|
|
91
|
-
rating: { __op: 'clamp',
|
|
92
|
-
price: { __op: 'multiplyBy',
|
|
93
|
-
active: { __op: 'toggleBool' },
|
|
94
|
-
syncedAt: { __op: 'serverTimestamp' },
|
|
232
|
+
views: { __op: 'increment', delta: 1 }, // add N
|
|
233
|
+
credits: { __op: 'decrement', delta: 5 }, // subtract N
|
|
234
|
+
slug: { __op: 'setOnce', value: 'my-post' }, // set only if currently empty
|
|
235
|
+
tags: { __op: 'appendUnique', item: 'featured' }, // add to array, no duplicates
|
|
236
|
+
tags: { __op: 'removeFromArray', item: 'draft' }, // remove from array
|
|
237
|
+
rating: { __op: 'clamp', value: 6, min: 0, max: 5 }, // clamp to range
|
|
238
|
+
price: { __op: 'multiplyBy', factor: 1.1 }, // multiply
|
|
239
|
+
active: { __op: 'toggleBool' }, // flip boolean
|
|
240
|
+
syncedAt: { __op: 'serverTimestamp' }, // set to server time
|
|
95
241
|
} as any);
|
|
96
242
|
```
|
|
97
243
|
|
|
98
|
-
|
|
244
|
+
Enable audit trails and history with extra `patch` options:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
await posts.patch(
|
|
248
|
+
post.id,
|
|
249
|
+
{ status: 'published' },
|
|
250
|
+
{ userEmail: 'alice@example.com', trackHistory: true },
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### Batch Operations
|
|
99
257
|
|
|
100
258
|
```ts
|
|
101
259
|
// Create up to 500 records at once
|
|
102
|
-
const { results, errors } = await posts.batchCreate(
|
|
103
|
-
[{ title: 'A' }, { title: 'B' }],
|
|
104
|
-
{ queryableFields: ['title'] },
|
|
260
|
+
const { results, errors, successful, failed } = await posts.batchCreate(
|
|
261
|
+
[{ title: 'A', status: 'draft' }, { title: 'B', status: 'draft' }],
|
|
262
|
+
{ queryableFields: ['title', 'status'], userEmail: 'alice@example.com' },
|
|
105
263
|
);
|
|
106
264
|
|
|
107
265
|
// Update up to 500 records at once
|
|
108
|
-
await posts.batchUpdate(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
266
|
+
const { successful, failed } = await posts.batchUpdate(
|
|
267
|
+
[
|
|
268
|
+
{ recordId: 'id1', values: { status: 'archived' } },
|
|
269
|
+
{ recordId: 'id2', values: { status: 'archived' } },
|
|
270
|
+
],
|
|
271
|
+
'alice@example.com', // optional userEmail
|
|
272
|
+
);
|
|
112
273
|
|
|
113
274
|
// Delete up to 500 records at once
|
|
114
275
|
const { successful, failed } = await posts.batchDelete(['id1', 'id2']);
|
|
115
276
|
```
|
|
116
277
|
|
|
117
|
-
|
|
278
|
+
Batch upsert using custom IDs — include `_customRecordId` on each item:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
await posts.batchCreate([
|
|
282
|
+
{ _customRecordId: '260601-post_hello', title: 'Hello' },
|
|
283
|
+
{ _customRecordId: '260601-post_world', title: 'World' },
|
|
284
|
+
] as any);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### Version History
|
|
118
290
|
|
|
119
|
-
Every write is automatically versioned.
|
|
291
|
+
Every write is automatically versioned when `trackHistory` is enabled.
|
|
120
292
|
|
|
121
293
|
```ts
|
|
294
|
+
// List all saved versions
|
|
122
295
|
const history = await posts.getHistory(post.id);
|
|
123
|
-
// [{ generation, savedAt, savedBy, sizeBytes }, …]
|
|
296
|
+
// → [{ generation, savedAt, savedBy, sizeBytes }, …]
|
|
124
297
|
|
|
125
|
-
//
|
|
298
|
+
// Retrieve a specific past version
|
|
126
299
|
const v1 = await posts.getVersion(post.id, history[0].generation!);
|
|
127
300
|
```
|
|
128
301
|
|
|
129
|
-
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Custom Record IDs
|
|
305
|
+
|
|
306
|
+
Supply your own ID at creation time — if it already exists the record is upserted in-place.
|
|
130
307
|
|
|
131
308
|
```ts
|
|
132
|
-
//
|
|
309
|
+
// Format: YYMMDD-segment1__segment2
|
|
133
310
|
const post = await posts.create(
|
|
134
311
|
{ title: 'Welcome' },
|
|
135
312
|
{ customRecordId: '260601-post_welcome' },
|
|
@@ -138,6 +315,30 @@ const post = await posts.create(
|
|
|
138
315
|
|
|
139
316
|
---
|
|
140
317
|
|
|
318
|
+
### Existence Check
|
|
319
|
+
|
|
320
|
+
A lightweight HEAD request — much cheaper than fetching the full record:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const exists = await posts.exists(post.id); // true | false
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### Get All Records
|
|
329
|
+
|
|
330
|
+
Fetches every record matching the options without filter support. Use `query()` when you need filters.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const all = await posts.getAll({
|
|
334
|
+
orderBy: 'createdAt',
|
|
335
|
+
order: 'desc',
|
|
336
|
+
limit: 500,
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
141
342
|
## Auth
|
|
142
343
|
|
|
143
344
|
A complete user system — signup, login, sessions, password reset, email verification, and admin controls.
|
|
@@ -146,7 +347,7 @@ A complete user system — signup, login, sessions, password reset, email verifi
|
|
|
146
347
|
const auth = db.auth();
|
|
147
348
|
```
|
|
148
349
|
|
|
149
|
-
### Sign
|
|
350
|
+
### Sign Up & Log In
|
|
150
351
|
|
|
151
352
|
```ts
|
|
152
353
|
// Sign up — extra fields beyond email/password are stored on the user
|
|
@@ -163,40 +364,75 @@ const { user, session } = await auth.login({
|
|
|
163
364
|
password: 'hunter2',
|
|
164
365
|
});
|
|
165
366
|
|
|
166
|
-
// Log out
|
|
367
|
+
// Log out this device
|
|
167
368
|
await auth.logout({ sessionId: session.sessionId });
|
|
369
|
+
|
|
370
|
+
// Log out everywhere
|
|
371
|
+
await auth.logout({ sessionId: session.sessionId, allDevices: true });
|
|
168
372
|
```
|
|
169
373
|
|
|
170
|
-
Store `session.sessionId` and `session.refreshToken` in your app.
|
|
374
|
+
Store `session.sessionId` and `session.refreshToken` in your app.
|
|
375
|
+
|
|
376
|
+
| Token | Lifetime |
|
|
377
|
+
|---|---|
|
|
378
|
+
| `sessionId` | 24 hours |
|
|
379
|
+
| `refreshToken` | 30 days |
|
|
380
|
+
|
|
381
|
+
---
|
|
171
382
|
|
|
172
|
-
### Session
|
|
383
|
+
### Session Management
|
|
173
384
|
|
|
174
385
|
```ts
|
|
175
|
-
//
|
|
176
|
-
const { user } = await auth.validateSession(session.sessionId);
|
|
386
|
+
// Validate a session and get the current user (use on your backend)
|
|
387
|
+
const { user, session } = await auth.validateSession(session.sessionId);
|
|
388
|
+
// session → { sessionId, expiresAt }
|
|
177
389
|
|
|
178
|
-
// Get a new session
|
|
390
|
+
// Get a brand-new session from a refresh token (before the old one expires)
|
|
179
391
|
const newSession = await auth.refreshSession(session.refreshToken);
|
|
180
392
|
```
|
|
181
393
|
|
|
182
|
-
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
### User Profile
|
|
183
397
|
|
|
184
398
|
```ts
|
|
399
|
+
// Fetch a user by ID
|
|
185
400
|
const user = await auth.getUser(session.userId);
|
|
186
401
|
|
|
402
|
+
// Update profile fields (users can update themselves; admins can update anyone)
|
|
187
403
|
await auth.updateUser({
|
|
188
404
|
sessionId: session.sessionId,
|
|
189
405
|
userId: user.id,
|
|
190
406
|
updates: { fullName: 'Alice Johnson', plan: 'enterprise' },
|
|
191
407
|
});
|
|
192
408
|
|
|
409
|
+
// Soft-delete (users can delete themselves; admins can delete anyone)
|
|
193
410
|
await auth.deleteUser(session.sessionId, user.id);
|
|
194
411
|
```
|
|
195
412
|
|
|
196
|
-
|
|
413
|
+
The `UserRecord` shape:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
interface UserRecord {
|
|
417
|
+
id: string;
|
|
418
|
+
email: string;
|
|
419
|
+
fullName?: string | null;
|
|
420
|
+
emailVerified: boolean;
|
|
421
|
+
accountStatus: 'active' | 'locked' | 'suspended';
|
|
422
|
+
role: 'user' | 'admin';
|
|
423
|
+
createdAt: number; // Unix ms
|
|
424
|
+
updatedAt: number; // Unix ms
|
|
425
|
+
metadata?: Record<string, unknown>;
|
|
426
|
+
[key: string]: unknown; // custom fields from signup
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
### Password & Email
|
|
197
433
|
|
|
198
434
|
```ts
|
|
199
|
-
// Change password
|
|
435
|
+
// Change password — requires an active session AND the current password
|
|
200
436
|
await auth.changePassword({
|
|
201
437
|
sessionId: session.sessionId,
|
|
202
438
|
userId: user.id,
|
|
@@ -204,8 +440,8 @@ await auth.changePassword({
|
|
|
204
440
|
newPassword: 'correcthorsebatterystaple',
|
|
205
441
|
});
|
|
206
442
|
|
|
207
|
-
// Forgot
|
|
208
|
-
await auth.requestPasswordReset('alice@example.com');
|
|
443
|
+
// Forgot-password flow
|
|
444
|
+
await auth.requestPasswordReset('alice@example.com'); // always succeeds (prevents enumeration)
|
|
209
445
|
await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');
|
|
210
446
|
|
|
211
447
|
// Email verification
|
|
@@ -213,25 +449,38 @@ await auth.requestEmailVerification(user.id);
|
|
|
213
449
|
await auth.confirmEmailVerification(tokenFromEmail);
|
|
214
450
|
```
|
|
215
451
|
|
|
216
|
-
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### Admin Controls
|
|
455
|
+
|
|
456
|
+
All admin methods require an active admin session.
|
|
217
457
|
|
|
218
458
|
```ts
|
|
219
|
-
//
|
|
459
|
+
// Paginated user list
|
|
220
460
|
const { users, hasMore, nextCursor } = await auth.listUsers({
|
|
221
461
|
sessionId: adminSession.sessionId,
|
|
222
462
|
limit: 50,
|
|
463
|
+
cursor: previousNextCursor, // optional — for subsequent pages
|
|
223
464
|
});
|
|
224
465
|
|
|
225
|
-
// Lock
|
|
226
|
-
await auth.lockAccount({
|
|
466
|
+
// Lock an account (default: 15 minutes)
|
|
467
|
+
const { lockedUntil, unlockTime } = await auth.lockAccount({
|
|
468
|
+
sessionId: adminSession.sessionId,
|
|
469
|
+
userId: user.id,
|
|
470
|
+
duration: 60 * 60 * 1000, // optional ms — lock for 1 hour
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Unlock an account
|
|
227
474
|
await auth.unlockAccount(adminSession.sessionId, user.id);
|
|
228
475
|
|
|
229
|
-
//
|
|
476
|
+
// Permanent (hard) delete — cannot be undone
|
|
230
477
|
await auth.hardDeleteUser(adminSession.sessionId, user.id);
|
|
231
|
-
|
|
478
|
+
|
|
479
|
+
// Bulk delete — soft or hard
|
|
480
|
+
const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
232
481
|
sessionId: adminSession.sessionId,
|
|
233
|
-
userIds: ['id1', 'id2'],
|
|
234
|
-
hard: true,
|
|
482
|
+
userIds: ['id1', 'id2', 'id3'],
|
|
483
|
+
hard: true, // optional — defaults to soft delete
|
|
235
484
|
});
|
|
236
485
|
```
|
|
237
486
|
|
|
@@ -239,79 +488,227 @@ await auth.bulkDeleteUsers({
|
|
|
239
488
|
|
|
240
489
|
## File Storage
|
|
241
490
|
|
|
491
|
+
Files are **private by default** and scoped to your storage key server-side.
|
|
492
|
+
|
|
242
493
|
```ts
|
|
243
|
-
const storage = db.storage('main');
|
|
494
|
+
const storage = db.storage('main'); // 'main' matches a key in storageKeys config
|
|
244
495
|
```
|
|
245
496
|
|
|
246
497
|
### Upload
|
|
247
498
|
|
|
248
499
|
```ts
|
|
249
|
-
// Simple upload
|
|
250
|
-
const result = await storage.upload(file, 'avatars/alice.jpg', {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
|
|
500
|
+
// Simple upload — anything up to 500 MB
|
|
501
|
+
const result = await storage.upload(file, 'avatars/alice.jpg', {
|
|
502
|
+
isPublic: true, // public CDN URL (default: false)
|
|
503
|
+
overwrite: true, // replace if the path exists (default: false)
|
|
504
|
+
mimeType: 'image/jpeg', // optional — auto-detected from content if omitted
|
|
505
|
+
expiresInSeconds: 3600, // optional — auto-delete after N seconds
|
|
506
|
+
});
|
|
507
|
+
console.log(result.publicUrl); // permanent CDN URL (if isPublic: true)
|
|
508
|
+
console.log(result.downloadUrl); // authenticated URL (if private)
|
|
509
|
+
console.log(result.path);
|
|
510
|
+
console.log(result.size);
|
|
511
|
+
console.log(result.mimeType);
|
|
512
|
+
|
|
513
|
+
// Upload a JS object or string directly as a file
|
|
514
|
+
await storage.uploadRaw({ theme: 'dark', lang: 'en' }, 'settings/config.json');
|
|
515
|
+
await storage.uploadRaw('<html>…</html>', 'pages/home.html', { mimeType: 'text/html' });
|
|
256
516
|
```
|
|
257
517
|
|
|
258
|
-
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### Large Files with Progress
|
|
521
|
+
|
|
522
|
+
Recommended for files > 10 MB or when you need a progress indicator (browsers only).
|
|
259
523
|
|
|
260
524
|
```ts
|
|
261
|
-
// Step 1 — get a signed
|
|
262
|
-
const { uploadUrl, path } = await storage.getUploadUrl({
|
|
263
|
-
path:
|
|
525
|
+
// Step 1 — get a signed GCS PUT URL
|
|
526
|
+
const { uploadUrl, path, expiresAt, expiresIn } = await storage.getUploadUrl({
|
|
527
|
+
path: 'videos/intro.mp4',
|
|
528
|
+
mimeType: 'video/mp4',
|
|
529
|
+
size: file.size,
|
|
530
|
+
isPublic: true,
|
|
531
|
+
overwrite: false,
|
|
532
|
+
expiresInSeconds: 3600, // URL lifetime
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Step 2 — upload directly to GCS with progress callback (browser XHR)
|
|
536
|
+
await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (percent) => {
|
|
537
|
+
console.log(`${percent}% uploaded`);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Step 3 — confirm and register metadata server-side
|
|
541
|
+
const result = await storage.confirmUpload({
|
|
542
|
+
path,
|
|
264
543
|
mimeType: 'video/mp4',
|
|
265
|
-
size: file.size,
|
|
266
544
|
isPublic: true,
|
|
267
545
|
});
|
|
546
|
+
```
|
|
268
547
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
### Batch Uploads
|
|
551
|
+
|
|
552
|
+
Get signed URLs for up to 50 files at once, upload them in parallel, then confirm in one call.
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
// Step 1 — get signed URLs for multiple files
|
|
556
|
+
const { files: urls } = await storage.getBatchUploadUrls([
|
|
557
|
+
{ path: 'docs/a.pdf', mimeType: 'application/pdf', size: fileA.size, isPublic: false },
|
|
558
|
+
{ path: 'docs/b.pdf', mimeType: 'application/pdf', size: fileB.size, isPublic: false },
|
|
559
|
+
]);
|
|
560
|
+
|
|
561
|
+
// Step 2 — upload each file (run in parallel)
|
|
562
|
+
await Promise.all(
|
|
563
|
+
urls.map(({ uploadUrl, path }, i) =>
|
|
564
|
+
storage.uploadToSignedUrl(uploadUrl, files[i], 'application/pdf'),
|
|
565
|
+
),
|
|
566
|
+
);
|
|
273
567
|
|
|
274
|
-
// Step 3 — confirm
|
|
275
|
-
const
|
|
568
|
+
// Step 3 — confirm all at once
|
|
569
|
+
const { succeeded, failed } = await storage.batchConfirmUploads([
|
|
570
|
+
{ path: 'docs/a.pdf', mimeType: 'application/pdf' },
|
|
571
|
+
{ path: 'docs/b.pdf', mimeType: 'application/pdf' },
|
|
572
|
+
]);
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
### Download
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
// Download a private file as ArrayBuffer
|
|
581
|
+
const buffer = await storage.download('private/report.pdf');
|
|
582
|
+
|
|
583
|
+
// Convert to a Blob for use in the browser
|
|
584
|
+
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
585
|
+
const url = URL.createObjectURL(blob);
|
|
276
586
|
```
|
|
277
587
|
|
|
278
|
-
|
|
588
|
+
For **public** files, just use the `publicUrl` directly — no SDK or authentication needed.
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
### Batch Download
|
|
593
|
+
|
|
594
|
+
Downloads up to 20 files at once. Content is returned as base64-encoded strings.
|
|
279
595
|
|
|
280
596
|
```ts
|
|
281
|
-
|
|
282
|
-
|
|
597
|
+
const { succeeded, failed } = await storage.batchDownload([
|
|
598
|
+
'docs/report.pdf',
|
|
599
|
+
'images/chart.png',
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
for (const file of succeeded) {
|
|
603
|
+
console.log(file.path, file.mimeType, file.size);
|
|
604
|
+
// file.content is base64 — decode with atob() or Buffer.from(content, 'base64')
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
for (const err of failed) {
|
|
608
|
+
console.error(err.path, err.error, err.code);
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
283
613
|
|
|
284
|
-
|
|
285
|
-
|
|
614
|
+
### List, Metadata & Signed URLs
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
// List files and folders (paginated)
|
|
618
|
+
const { files, folders, hasMore, nextCursor } = await storage.list({
|
|
619
|
+
prefix: 'avatars/', // optional path prefix to list under
|
|
620
|
+
limit: 50,
|
|
621
|
+
cursor: previousNextCursor,
|
|
622
|
+
recursive: true, // include files in sub-folders (default: false)
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Page 2
|
|
626
|
+
if (hasMore) {
|
|
627
|
+
const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
|
|
628
|
+
}
|
|
286
629
|
|
|
287
630
|
// Get file metadata
|
|
288
631
|
const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
632
|
+
// → { path, size, mimeType, isPublic, publicUrl, downloadUrl, createdAt, updatedAt }
|
|
289
633
|
|
|
290
|
-
//
|
|
291
|
-
|
|
634
|
+
// Generate a time-limited link — anyone with the URL can download (no key needed)
|
|
635
|
+
// Note: downloads via signed URL bypass the server, so download stats are NOT tracked
|
|
636
|
+
const { signedUrl, expiresAt, expiresIn } = await storage.getSignedUrl(
|
|
637
|
+
'private/report.pdf',
|
|
638
|
+
3600, // lifetime in seconds (default: 3600)
|
|
639
|
+
);
|
|
640
|
+
```
|
|
292
641
|
|
|
293
|
-
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
### Move, Copy & Delete
|
|
645
|
+
|
|
646
|
+
```ts
|
|
294
647
|
await storage.move('old/path.jpg', 'new/path.jpg');
|
|
295
|
-
await storage.copy('
|
|
648
|
+
await storage.copy('templates/base.html', 'pages/home.html');
|
|
296
649
|
await storage.deleteFile('avatars/old.jpg');
|
|
297
|
-
await storage.deleteFolder('temp/');
|
|
650
|
+
await storage.deleteFolder('temp/'); // recursively deletes all contents
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
### Visibility
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
// Make a private file public
|
|
659
|
+
const result = await storage.setVisibility('reports/q1.pdf', true);
|
|
660
|
+
console.log(result.publicUrl);
|
|
298
661
|
|
|
299
|
-
//
|
|
300
|
-
await storage.setVisibility('
|
|
662
|
+
// Make a public file private
|
|
663
|
+
await storage.setVisibility('reports/q1.pdf', false);
|
|
301
664
|
```
|
|
302
665
|
|
|
303
|
-
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### Folders
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
// Create an explicit folder marker (usually not needed — folders are implicit)
|
|
672
|
+
await storage.createFolder('projects/2025/');
|
|
673
|
+
```
|
|
304
674
|
|
|
305
|
-
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
### Scoped Storage
|
|
678
|
+
|
|
679
|
+
Automatically prefix every path — ideal for per-user file isolation.
|
|
306
680
|
|
|
307
681
|
```ts
|
|
308
682
|
const userFiles = db.storage('main').scope(`users/${userId}/`);
|
|
309
683
|
|
|
310
684
|
await userFiles.upload(pdf, 'contract.pdf'); // → users/{userId}/contract.pdf
|
|
311
|
-
|
|
685
|
+
await userFiles.uploadRaw({ key: 'val' }, 'prefs.json');
|
|
686
|
+
const { files } = await userFiles.list(); // → only lists users/{userId}/
|
|
312
687
|
|
|
313
688
|
// Nest deeper
|
|
314
|
-
const reports = userFiles.scope('reports/');
|
|
689
|
+
const reports = userFiles.scope('reports/'); // → users/{userId}/reports/
|
|
690
|
+
await reports.upload(file, 'q1.pdf'); // → users/{userId}/reports/q1.pdf
|
|
691
|
+
|
|
692
|
+
// All StorageManager methods are available on ScopedStorage
|
|
693
|
+
const meta = await userFiles.getMetadata('contract.pdf');
|
|
694
|
+
const { signedUrl } = await userFiles.getSignedUrl('contract.pdf', 900);
|
|
695
|
+
await userFiles.move('old.pdf', 'new.pdf');
|
|
696
|
+
await userFiles.deleteFile('contract.pdf');
|
|
697
|
+
await userFiles.deleteFolder('reports/');
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### Storage Stats
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
// Stats for this storage key
|
|
706
|
+
const stats = await storage.getStats();
|
|
707
|
+
// → { totalFiles, totalBytes, uploadCount, downloadCount, deleteCount }
|
|
708
|
+
|
|
709
|
+
// Server info (no auth required)
|
|
710
|
+
const info = await storage.info();
|
|
711
|
+
// → { ok: true, storageRoot: '…' }
|
|
315
712
|
```
|
|
316
713
|
|
|
317
714
|
---
|
|
@@ -322,48 +719,267 @@ BigQuery-powered aggregations. No ETL, no pipelines — just query.
|
|
|
322
719
|
|
|
323
720
|
```ts
|
|
324
721
|
const analytics = db.analytics('orders');
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### Date Range (Time Scope)
|
|
725
|
+
|
|
726
|
+
Almost every analytics method accepts an optional `dateRange` to restrict results to a time window. Both `start` and `end` are Unix timestamps **in milliseconds** — the server converts them to ISO strings internally. Both fields are optional; omit either for an open-ended range.
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
interface DateRange {
|
|
730
|
+
start?: number; // Unix ms — inclusive lower bound
|
|
731
|
+
end?: number; // Unix ms — inclusive upper bound
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
**Granularity options** (for time series methods):
|
|
736
|
+
|
|
737
|
+
| Value | Buckets results by |
|
|
738
|
+
|---|---|
|
|
739
|
+
| `'hour'` | Each hour |
|
|
740
|
+
| `'day'` | Each calendar day |
|
|
741
|
+
| `'week'` | Each week |
|
|
742
|
+
| `'month'` | Each month |
|
|
743
|
+
| `'year'` | Each year |
|
|
744
|
+
|
|
745
|
+
**Aggregation options** (for numeric field methods):
|
|
746
|
+
|
|
747
|
+
| Value | Meaning |
|
|
748
|
+
|---|---|
|
|
749
|
+
| `'sum'` | Total |
|
|
750
|
+
| `'avg'` | Average |
|
|
751
|
+
| `'min'` | Minimum |
|
|
752
|
+
| `'max'` | Maximum |
|
|
753
|
+
| `'count'` | Record count |
|
|
754
|
+
|
|
755
|
+
---
|
|
325
756
|
|
|
326
|
-
|
|
757
|
+
### Count
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
// All-time total
|
|
327
761
|
const { count } = await analytics.count();
|
|
328
762
|
|
|
329
|
-
//
|
|
330
|
-
const
|
|
763
|
+
// Within a time window
|
|
764
|
+
const { count: lastWeek } = await analytics.count({
|
|
765
|
+
dateRange: {
|
|
766
|
+
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
767
|
+
end: Date.now(),
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
```
|
|
331
771
|
|
|
332
|
-
|
|
333
|
-
const sums = await analytics.sum({ field: 'amount', groupBy: 'region' });
|
|
772
|
+
---
|
|
334
773
|
|
|
335
|
-
|
|
336
|
-
const daily = await analytics.timeSeries({ granularity: 'day' });
|
|
774
|
+
### Distribution
|
|
337
775
|
|
|
338
|
-
|
|
339
|
-
const trend = await analytics.fieldTimeSeries({ field: 'revenue', aggregation: 'sum' });
|
|
776
|
+
How many records have each value of a field.
|
|
340
777
|
|
|
341
|
-
|
|
342
|
-
const
|
|
778
|
+
```ts
|
|
779
|
+
const dist = await analytics.distribution({
|
|
780
|
+
field: 'status',
|
|
781
|
+
limit: 10,
|
|
782
|
+
order: 'desc', // 'asc' | 'desc'
|
|
783
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
784
|
+
});
|
|
785
|
+
// → [{ value: 'published', count: 320 }, { value: 'draft', count: 80 }, …]
|
|
786
|
+
```
|
|
343
787
|
|
|
344
|
-
|
|
345
|
-
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
### Sum
|
|
791
|
+
|
|
792
|
+
Sum a numeric field, optionally grouped by another field.
|
|
793
|
+
|
|
794
|
+
```ts
|
|
795
|
+
// Total revenue
|
|
796
|
+
const [{ sum: totalRevenue }] = await analytics.sum({ field: 'amount' });
|
|
797
|
+
|
|
798
|
+
// Revenue by region, last quarter
|
|
799
|
+
const sums = await analytics.sum({
|
|
800
|
+
field: 'amount',
|
|
801
|
+
groupBy: 'region',
|
|
802
|
+
limit: 20,
|
|
803
|
+
dateRange: {
|
|
804
|
+
start: new Date('2025-01-01').getTime(),
|
|
805
|
+
end: new Date('2025-03-31').getTime(),
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
// → [{ group: 'Europe', sum: 18200 }, { group: 'Americas', sum: 29400 }, …]
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
### Time Series
|
|
814
|
+
|
|
815
|
+
Count of records over time — useful for activity graphs.
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
// Daily record counts, last 30 days
|
|
819
|
+
const daily = await analytics.timeSeries({
|
|
820
|
+
granularity: 'day',
|
|
821
|
+
dateRange: {
|
|
822
|
+
start: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
823
|
+
end: Date.now(),
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
// → [{ date: '2025-03-01', count: 42 }, { date: '2025-03-02', count: 55 }, …]
|
|
827
|
+
|
|
828
|
+
// Monthly, all time
|
|
829
|
+
const monthly = await analytics.timeSeries({ granularity: 'month' });
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
### Field Time Series
|
|
835
|
+
|
|
836
|
+
Aggregate a numeric field over time — useful for revenue, score, or usage trends.
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
// Daily sum of revenue, last 90 days
|
|
840
|
+
const revTrend = await analytics.fieldTimeSeries({
|
|
841
|
+
field: 'amount',
|
|
842
|
+
aggregation: 'sum',
|
|
843
|
+
granularity: 'day',
|
|
844
|
+
dateRange: {
|
|
845
|
+
start: Date.now() - 90 * 24 * 60 * 60 * 1000,
|
|
846
|
+
end: Date.now(),
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
// → [{ date: '2025-01-01', value: 4820.5 }, …]
|
|
850
|
+
|
|
851
|
+
// Monthly average order value
|
|
852
|
+
const avgTrend = await analytics.fieldTimeSeries({
|
|
853
|
+
field: 'amount',
|
|
854
|
+
aggregation: 'avg',
|
|
855
|
+
granularity: 'month',
|
|
856
|
+
});
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
### Top N
|
|
862
|
+
|
|
863
|
+
Most frequent values for a field by record count.
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
// Top 10 countries
|
|
867
|
+
const top10 = await analytics.topN({
|
|
868
|
+
field: 'countryCode',
|
|
869
|
+
n: 10,
|
|
870
|
+
labelField: 'countryName', // optional — include a human-readable label alongside the value
|
|
871
|
+
order: 'desc',
|
|
872
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
873
|
+
});
|
|
874
|
+
// → [{ value: 'US', label: 'United States', count: 420 }, …]
|
|
875
|
+
```
|
|
346
876
|
|
|
347
|
-
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
### Stats
|
|
880
|
+
|
|
881
|
+
Statistical summary for a numeric field.
|
|
882
|
+
|
|
883
|
+
```ts
|
|
884
|
+
const priceStats = await analytics.stats({
|
|
885
|
+
field: 'price',
|
|
886
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
887
|
+
});
|
|
888
|
+
// → { min, max, avg, sum, count, stddev }
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
### Records via BigQuery
|
|
894
|
+
|
|
895
|
+
Fetch filtered records through the BigQuery engine instead of Firestore. Useful for large result sets or complex server-side filtering.
|
|
896
|
+
|
|
897
|
+
```ts
|
|
898
|
+
const records = await analytics.records<Order>({
|
|
899
|
+
filters: [
|
|
900
|
+
{ field: 'status', op: '==', value: 'paid' },
|
|
901
|
+
{ field: 'amount', op: '>=', value: 100 },
|
|
902
|
+
{ field: 'country', op: 'CONTAINS', value: 'US' },
|
|
903
|
+
],
|
|
904
|
+
selectFields: ['id', 'amount', 'country', 'createdAt'], // optional projection
|
|
905
|
+
orderBy: 'createdAt',
|
|
906
|
+
order: 'desc',
|
|
907
|
+
limit: 1000,
|
|
908
|
+
offset: 0,
|
|
909
|
+
dateRange: {
|
|
910
|
+
start: new Date('2025-01-01').getTime(),
|
|
911
|
+
end: new Date('2025-03-31').getTime(),
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Analytics filter operators: `==` `!=` `>` `<` `>=` `<=` `CONTAINS`
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
### Multi-Metric
|
|
921
|
+
|
|
922
|
+
Compute multiple aggregations in a single round-trip — perfect for dashboards.
|
|
923
|
+
|
|
924
|
+
```ts
|
|
348
925
|
const dashboard = await analytics.multiMetric({
|
|
349
926
|
metrics: [
|
|
350
927
|
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
351
928
|
{ field: 'amount', name: 'avgOrder', aggregation: 'avg' },
|
|
929
|
+
{ field: 'amount', name: 'maxOrder', aggregation: 'max' },
|
|
352
930
|
{ field: 'recordId', name: 'orderCount', aggregation: 'count' },
|
|
353
931
|
],
|
|
932
|
+
dateRange: { start: Date.now() - 30 * 24 * 60 * 60 * 1000 },
|
|
354
933
|
});
|
|
355
|
-
// → { totalRevenue: 48200, avgOrder: 96.4, orderCount: 500 }
|
|
934
|
+
// → { totalRevenue: 48200, avgOrder: 96.4, maxOrder: 999, orderCount: 500 }
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
### Storage Stats (Analytics)
|
|
940
|
+
|
|
941
|
+
Record count and byte statistics for the bucket.
|
|
356
942
|
|
|
357
|
-
|
|
943
|
+
```ts
|
|
944
|
+
const storageInfo = await analytics.storageStats({
|
|
945
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
946
|
+
});
|
|
947
|
+
// → { totalRecords, totalBytes, avgBytes, minBytes, maxBytes }
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
### Cross-Bucket
|
|
953
|
+
|
|
954
|
+
Compare the same metric across multiple buckets in a single query. Your key must have read access to all listed buckets. System buckets are blocked.
|
|
955
|
+
|
|
956
|
+
```ts
|
|
358
957
|
const compare = await analytics.crossBucket({
|
|
359
958
|
bucketKeys: ['orders-2024', 'orders-2025'],
|
|
360
959
|
field: 'amount',
|
|
361
960
|
aggregation: 'sum',
|
|
961
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
362
962
|
});
|
|
963
|
+
// → [{ bucket: 'orders-2024', value: 38200 }, { bucket: 'orders-2025', value: 52100 }]
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
### Raw Query
|
|
969
|
+
|
|
970
|
+
Escape hatch for query types not covered by the typed helpers.
|
|
363
971
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
972
|
+
```ts
|
|
973
|
+
import type { AnalyticsQuery, AnalyticsResult } from 'hydrousdb';
|
|
974
|
+
|
|
975
|
+
const result = await analytics.query<MyResultType>({
|
|
976
|
+
queryType: 'distribution',
|
|
977
|
+
field: 'category',
|
|
978
|
+
granularity: 'month',
|
|
979
|
+
filters: [{ field: 'active', op: '==', value: true }],
|
|
980
|
+
dateRange: { start: Date.now() - 30 * 24 * 60 * 60 * 1000 },
|
|
981
|
+
limit: 50,
|
|
982
|
+
order: 'desc',
|
|
367
983
|
});
|
|
368
984
|
```
|
|
369
985
|
|
|
@@ -372,37 +988,52 @@ const { count } = await analytics.count({
|
|
|
372
988
|
## Error Handling
|
|
373
989
|
|
|
374
990
|
```ts
|
|
375
|
-
import {
|
|
991
|
+
import {
|
|
992
|
+
HydrousError,
|
|
993
|
+
AuthError,
|
|
994
|
+
RecordError,
|
|
995
|
+
StorageError,
|
|
996
|
+
AnalyticsError,
|
|
997
|
+
ValidationError,
|
|
998
|
+
NetworkError,
|
|
999
|
+
} from 'hydrousdb';
|
|
376
1000
|
|
|
377
1001
|
try {
|
|
378
1002
|
await db.auth().login({ email: 'x@x.com', password: 'wrong' });
|
|
379
1003
|
} catch (err) {
|
|
380
|
-
if (err instanceof
|
|
381
|
-
console.log(err.code);
|
|
382
|
-
console.log(err.status);
|
|
383
|
-
console.log(err.message);
|
|
1004
|
+
if (err instanceof AuthError) {
|
|
1005
|
+
console.log(err.code); // 'INVALID_CREDENTIALS'
|
|
1006
|
+
console.log(err.status); // 401
|
|
1007
|
+
console.log(err.message); // human-readable
|
|
1008
|
+
console.log(err.requestId); // for support
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (err instanceof ValidationError) {
|
|
1012
|
+
console.log(err.details); // string[] of specific validation failures
|
|
384
1013
|
}
|
|
1014
|
+
|
|
385
1015
|
if (err instanceof NetworkError) {
|
|
386
1016
|
console.log('No internet or server unreachable');
|
|
1017
|
+
console.log(err.cause); // original error
|
|
387
1018
|
}
|
|
388
1019
|
}
|
|
389
1020
|
```
|
|
390
1021
|
|
|
391
1022
|
| Error class | When it's thrown |
|
|
392
1023
|
|---|---|
|
|
393
|
-
| `HydrousError` | Base class — all SDK errors extend this |
|
|
394
|
-
| `AuthError` | Login failures, invalid sessions, permission denied |
|
|
395
|
-
| `RecordError` | Record not found, validation failures |
|
|
1024
|
+
| `HydrousError` | Base class — all SDK errors extend this. Has `code`, `status`, `requestId`, `details`. |
|
|
1025
|
+
| `AuthError` | Login failures, invalid/expired sessions, permission denied |
|
|
1026
|
+
| `RecordError` | Record not found, write validation failures |
|
|
396
1027
|
| `StorageError` | Upload/download failures, file not found |
|
|
397
1028
|
| `AnalyticsError` | Invalid query, bucket not found |
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
1029
|
+
| `ValidationError` | Bad input caught client-side before the request is sent. Has `details: string[]`. |
|
|
1030
|
+
| `NetworkError` | No network, server unreachable, request timed out. Has `cause`. |
|
|
400
1031
|
|
|
401
1032
|
---
|
|
402
1033
|
|
|
403
1034
|
## TypeScript
|
|
404
1035
|
|
|
405
|
-
The SDK is written in TypeScript. Type your records for full autocomplete:
|
|
1036
|
+
The SDK is written in TypeScript. Type your records for full autocomplete and safety:
|
|
406
1037
|
|
|
407
1038
|
```ts
|
|
408
1039
|
interface Order {
|
|
@@ -421,6 +1052,27 @@ const order = await orders.create({
|
|
|
421
1052
|
items: [{ sku: 'SHOE-42', qty: 1 }],
|
|
422
1053
|
});
|
|
423
1054
|
// order.status → 'pending' | 'paid' | 'refunded' ✓
|
|
1055
|
+
// order.id, order.createdAt, order.updatedAt are automatically added ✓
|
|
1056
|
+
|
|
1057
|
+
const result = await orders.query({
|
|
1058
|
+
filters: [{ field: 'status', op: '==', value: 'paid' }],
|
|
1059
|
+
});
|
|
1060
|
+
// result.records → (Order & RecordResult)[] ✓
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
Key exported types:
|
|
1064
|
+
|
|
1065
|
+
```ts
|
|
1066
|
+
import type {
|
|
1067
|
+
HydrousConfig,
|
|
1068
|
+
RecordData, RecordResult, QueryOptions, QueryFilter, QueryResult,
|
|
1069
|
+
DateRange, Granularity, Aggregation, SortOrder,
|
|
1070
|
+
AnalyticsQuery, AnalyticsResult, AnalyticsFilter,
|
|
1071
|
+
UserRecord, AuthResult, Session,
|
|
1072
|
+
UploadOptions, UploadResult,
|
|
1073
|
+
ListOptions, ListResult, FileMetadata, SignedUrlResult,
|
|
1074
|
+
StorageStats,
|
|
1075
|
+
} from 'hydrousdb';
|
|
424
1076
|
```
|
|
425
1077
|
|
|
426
1078
|
---
|
|
@@ -428,25 +1080,34 @@ const order = await orders.create({
|
|
|
428
1080
|
## Security
|
|
429
1081
|
|
|
430
1082
|
- **Never commit API keys.** Use environment variables (`process.env.…`).
|
|
431
|
-
- **Never expose keys in the browser.** For client-side apps, proxy
|
|
432
|
-
- **Keys travel in headers only** — the SDK enforces this. They never appear in URLs, access logs, or browser history.
|
|
1083
|
+
- **Never expose keys in the browser.** For client-side apps, proxy all SDK calls through your own backend.
|
|
1084
|
+
- **Keys travel in headers only** — the SDK enforces this. They never appear in URLs, query strings, access logs, or browser history.
|
|
433
1085
|
- **Files are private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for temporary external sharing.
|
|
434
|
-
- **Use scoped storage** (`storage.scope('prefix/')`) to isolate files per user.
|
|
1086
|
+
- **Use scoped storage** (`storage.scope('prefix/')`) to isolate files per user and prevent path-traversal bugs.
|
|
435
1087
|
|
|
436
1088
|
---
|
|
437
1089
|
|
|
438
|
-
## Framework
|
|
1090
|
+
## Framework Examples
|
|
439
1091
|
|
|
440
1092
|
**Next.js (App Router)**
|
|
441
1093
|
```ts
|
|
442
1094
|
// lib/db.ts
|
|
443
1095
|
import { createClient } from 'hydrousdb';
|
|
444
|
-
export const db = createClient({
|
|
1096
|
+
export const db = createClient({
|
|
1097
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1098
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1099
|
+
storageKeys: { main: process.env.HYDROUS_STORAGE_KEY! },
|
|
1100
|
+
});
|
|
445
1101
|
|
|
446
1102
|
// app/api/posts/route.ts
|
|
447
1103
|
import { db } from '@/lib/db';
|
|
448
1104
|
export async function GET() {
|
|
449
|
-
const { records } = await db.records('posts').query({
|
|
1105
|
+
const { records } = await db.records('posts').query({
|
|
1106
|
+
filters: [{ field: 'status', op: '==', value: 'published' }],
|
|
1107
|
+
orderBy: 'createdAt',
|
|
1108
|
+
order: 'desc',
|
|
1109
|
+
limit: 10,
|
|
1110
|
+
});
|
|
450
1111
|
return Response.json(records);
|
|
451
1112
|
}
|
|
452
1113
|
```
|
|
@@ -462,14 +1123,22 @@ const data = await res.json();
|
|
|
462
1123
|
```ts
|
|
463
1124
|
// composables/useDb.ts
|
|
464
1125
|
import { createClient } from 'hydrousdb';
|
|
465
|
-
export const db = createClient({
|
|
1126
|
+
export const db = createClient({
|
|
1127
|
+
authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
|
|
1128
|
+
bucketSecurityKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
|
|
1129
|
+
storageKeys: { main: import.meta.env.VITE_HYDROUS_STORAGE_KEY },
|
|
1130
|
+
});
|
|
466
1131
|
```
|
|
467
1132
|
|
|
468
1133
|
**React Native**
|
|
469
1134
|
```ts
|
|
470
1135
|
import { createClient } from 'hydrousdb';
|
|
471
1136
|
// Works out of the box — uses the global fetch available in React Native
|
|
472
|
-
const db = createClient({
|
|
1137
|
+
const db = createClient({
|
|
1138
|
+
authKey: HYDROUS_AUTH_KEY,
|
|
1139
|
+
bucketSecurityKey: HYDROUS_BUCKET_KEY,
|
|
1140
|
+
storageKeys: { main: HYDROUS_STORAGE_KEY },
|
|
1141
|
+
});
|
|
473
1142
|
```
|
|
474
1143
|
|
|
475
1144
|
---
|
|
@@ -478,79 +1147,121 @@ const db = createClient({ … });
|
|
|
478
1147
|
|
|
479
1148
|
### `createClient(config)`
|
|
480
1149
|
|
|
1150
|
+
| Field | Type | Required | Description |
|
|
1151
|
+
|---|---|---|---|
|
|
1152
|
+
| `authKey` | `string` | ✓ | `hk_auth_…` — for auth routes |
|
|
1153
|
+
| `bucketSecurityKey` | `string` | ✓ | `hk_bucket_…` — for records & analytics |
|
|
1154
|
+
| `storageKeys` | `{ [name]: string }` | ✓ | One or more `ssk_…` keys for file storage |
|
|
1155
|
+
| `baseUrl` | `string` | — | Override the API endpoint (no trailing slash) |
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
### `db.records<T>(bucket)` — all methods
|
|
1160
|
+
|
|
1161
|
+
| Method | Returns | Description |
|
|
1162
|
+
|---|---|---|
|
|
1163
|
+
| `create(data, opts?)` | `T & RecordResult` | Create a record. `opts.queryableFields` enables filtering. `opts.customRecordId` enables upsert. |
|
|
1164
|
+
| `get(id)` | `T & RecordResult` | Fetch a record by ID. |
|
|
1165
|
+
| `patch(id, data, opts?)` | `{ id, updatedAt? }` | Partial update. `opts.merge` for deep merge. `opts.trackHistory` to save a version. |
|
|
1166
|
+
| `delete(id)` | `void` | Permanently delete a record. |
|
|
1167
|
+
| `exists(id)` | `boolean` | Lightweight existence check (HEAD request). |
|
|
1168
|
+
| `query(opts?)` | `QueryResult<T>` | Filter, sort, paginate. Supports `dateRange`. |
|
|
1169
|
+
| `getAll(opts?)` | `(T & RecordResult)[]` | Fetch all records (no filter support — use `query` for filters). |
|
|
1170
|
+
| `batchCreate(items, opts?)` | `{ results, errors, successful, failed }` | Up to 500 records at once. |
|
|
1171
|
+
| `batchUpdate(updates, userEmail?)` | `{ successful, failed }` | Up to 500 records at once. |
|
|
1172
|
+
| `batchDelete(ids, userEmail?)` | `{ successful, failed }` | Up to 500 records at once. |
|
|
1173
|
+
| `getHistory(id)` | `RecordHistoryEntry[]` | List all saved versions. |
|
|
1174
|
+
| `getVersion(id, generation)` | `T & RecordResult` | Fetch a specific past version. |
|
|
1175
|
+
|
|
1176
|
+
**`QueryOptions` fields:**
|
|
1177
|
+
|
|
481
1178
|
| Field | Type | Description |
|
|
482
1179
|
|---|---|---|
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
1180
|
+
| `filters` | `QueryFilter[]` | Array of `{ field, op, value }` |
|
|
1181
|
+
| `fields` | `string` | Comma-separated list of fields to return |
|
|
1182
|
+
| `orderBy` | `string` | Field to sort by |
|
|
1183
|
+
| `order` | `'asc' \| 'desc'` | Sort direction |
|
|
1184
|
+
| `limit` | `number` | Max records to return |
|
|
1185
|
+
| `offset` | `number` | Skip N records |
|
|
1186
|
+
| `startAfter` | `string` | Cursor from `nextCursor` for next-page pagination |
|
|
1187
|
+
| `startAt` | `string` | Cursor — include the record at this cursor |
|
|
1188
|
+
| `endAt` | `string` | Cursor — stop at this cursor |
|
|
1189
|
+
| `dateRange` | `DateRange` | `{ start?, end? }` in Unix ms |
|
|
1190
|
+
| `timeScope` | `string` | Prefix-based time filter: `_day_YYMMDD`, `_month_YYMM`, or `_year_YY` |
|
|
487
1191
|
|
|
488
|
-
|
|
1192
|
+
---
|
|
489
1193
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
|
493
|
-
| `get(id)` | Fetch by ID. |
|
|
494
|
-
| `patch(id, data)` | Partial update — only the fields you pass are changed. |
|
|
495
|
-
| `delete(id)` | Permanent delete. |
|
|
496
|
-
| `query(opts?)` | Filter, sort, paginate. |
|
|
497
|
-
| `batchCreate(items, opts?)` | Up to 500 at once. |
|
|
498
|
-
| `batchUpdate(updates)` | Up to 500 at once. |
|
|
499
|
-
| `batchDelete(ids)` | Up to 500 at once. |
|
|
500
|
-
| `getHistory(id)` | Full version list. |
|
|
501
|
-
| `getVersion(id, generation)` | Read a past version. |
|
|
502
|
-
| `exists(id)` | Lightweight existence check (HEAD request). |
|
|
503
|
-
|
|
504
|
-
### `db.auth()` — key methods
|
|
505
|
-
|
|
506
|
-
| Method | What it does |
|
|
1194
|
+
### `db.auth()` — all methods
|
|
1195
|
+
|
|
1196
|
+
| Method | Description |
|
|
507
1197
|
|---|---|
|
|
508
|
-
| `signup(opts)` | Register + create session. |
|
|
1198
|
+
| `signup(opts)` | Register + create session. Extra fields on `opts` are stored on the user. |
|
|
509
1199
|
| `login(opts)` | Authenticate + create session. |
|
|
510
|
-
| `logout({ sessionId })` | Revoke session
|
|
511
|
-
| `validateSession(sessionId)` | Check if a session is active. |
|
|
1200
|
+
| `logout({ sessionId, allDevices? })` | Revoke one session or all sessions. |
|
|
1201
|
+
| `validateSession(sessionId)` | Check if a session is active; returns current user. |
|
|
512
1202
|
| `refreshSession(refreshToken)` | Get a new session from a refresh token. |
|
|
513
1203
|
| `getUser(userId)` | Fetch a user by ID. |
|
|
514
1204
|
| `updateUser(opts)` | Update profile fields. |
|
|
515
|
-
| `changePassword(opts)` | Authenticated password change. |
|
|
516
|
-
| `requestPasswordReset(email)` |
|
|
517
|
-
| `confirmPasswordReset(token, newPw)` | Apply new password. |
|
|
518
|
-
| `
|
|
519
|
-
| `
|
|
520
|
-
| `
|
|
1205
|
+
| `changePassword(opts)` | Authenticated password change (requires current password). |
|
|
1206
|
+
| `requestPasswordReset(email)` | Send reset email (always succeeds to prevent enumeration). |
|
|
1207
|
+
| `confirmPasswordReset(token, newPw)` | Apply new password from reset token. |
|
|
1208
|
+
| `requestEmailVerification(userId)` | Send verification email. |
|
|
1209
|
+
| `confirmEmailVerification(token)` | Mark email verified from token. |
|
|
1210
|
+
| `getUser(userId)` | Fetch a user by ID. |
|
|
1211
|
+
| `listUsers(opts)` | Paginated user list. Admin only. |
|
|
1212
|
+
| `lockAccount(opts)` | Lock a user account. Admin only. |
|
|
1213
|
+
| `unlockAccount(sessionId, userId)` | Unlock a user account. Admin only. |
|
|
1214
|
+
| `hardDeleteUser(sessionId, userId)` | Permanent delete. Admin only. |
|
|
1215
|
+
| `bulkDeleteUsers(opts)` | Delete many users at once (soft or hard). Admin only. |
|
|
1216
|
+
|
|
1217
|
+
---
|
|
521
1218
|
|
|
522
|
-
### `db.storage(keyName)` —
|
|
1219
|
+
### `db.storage(keyName)` — all methods
|
|
523
1220
|
|
|
524
|
-
| Method |
|
|
1221
|
+
| Method | Description |
|
|
525
1222
|
|---|---|
|
|
526
1223
|
| `upload(data, path, opts?)` | Server-buffered upload (up to 500 MB). |
|
|
527
1224
|
| `uploadRaw(data, path, opts?)` | Upload a JS object or string as a file. |
|
|
528
|
-
| `getUploadUrl(opts)` | Step 1 of
|
|
529
|
-
| `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2 — upload directly to GCS. |
|
|
530
|
-
| `confirmUpload(opts)` | Step 3 — register metadata. |
|
|
531
|
-
| `
|
|
532
|
-
| `
|
|
533
|
-
| `
|
|
534
|
-
| `
|
|
535
|
-
| `
|
|
536
|
-
| `
|
|
537
|
-
| `
|
|
538
|
-
| `
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
|
543
|
-
|
|
544
|
-
| `
|
|
545
|
-
| `
|
|
546
|
-
| `
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1225
|
+
| `getUploadUrl(opts)` | Step 1 of signed-URL upload — get a GCS PUT URL. |
|
|
1226
|
+
| `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2 — upload directly to GCS (supports progress in browsers). |
|
|
1227
|
+
| `confirmUpload(opts)` | Step 3 — register metadata server-side. |
|
|
1228
|
+
| `getBatchUploadUrls(files)` | Get signed URLs for up to 50 files at once. |
|
|
1229
|
+
| `batchConfirmUploads(items)` | Confirm multiple direct uploads at once. |
|
|
1230
|
+
| `download(path)` | Download a private file as `ArrayBuffer`. |
|
|
1231
|
+
| `batchDownload(paths, concurrency?)` | Download up to 20 files at once (base64 content). |
|
|
1232
|
+
| `list(opts?)` | List files and folders. Supports `prefix`, `limit`, `cursor`, `recursive`. |
|
|
1233
|
+
| `getMetadata(path)` | File size, MIME type, visibility, URLs. |
|
|
1234
|
+
| `getSignedUrl(path, expiresIn?)` | Time-limited share link (default 3600 s). |
|
|
1235
|
+
| `setVisibility(path, isPublic)` | Toggle a file between public and private. |
|
|
1236
|
+
| `createFolder(path)` | Create an explicit folder marker. |
|
|
1237
|
+
| `move(from, to)` | Move (rename) a file. |
|
|
1238
|
+
| `copy(from, to)` | Copy a file to a new path. |
|
|
1239
|
+
| `deleteFile(path)` | Permanently delete a file. |
|
|
1240
|
+
| `deleteFolder(path)` | Recursively delete a folder and all its contents. |
|
|
1241
|
+
| `getStats()` | Upload/download/delete counts and total bytes. |
|
|
1242
|
+
| `info()` | Server info — no auth required. |
|
|
1243
|
+
| `scope(prefix)` | Get a `ScopedStorage` that auto-prefixes all paths. |
|
|
1244
|
+
|
|
1245
|
+
`ScopedStorage` exposes every method above (except `info`) plus `scope(subPrefix)` for nesting deeper.
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
### `db.analytics(bucket)` — all methods
|
|
1250
|
+
|
|
1251
|
+
| Method | Returns | Description |
|
|
1252
|
+
|---|---|---|
|
|
1253
|
+
| `count(opts?)` | `{ count }` | Total record count, optionally within a `dateRange`. |
|
|
1254
|
+
| `distribution(opts)` | `DistributionRow[]` | Per-value counts for a field. |
|
|
1255
|
+
| `sum(opts)` | `SumRow[]` | Sum a numeric field, optional `groupBy`. |
|
|
1256
|
+
| `timeSeries(opts?)` | `TimeSeriesRow[]` | Record counts bucketed by `granularity`. |
|
|
1257
|
+
| `fieldTimeSeries(opts)` | `FieldTimeSeriesRow[]` | Numeric field aggregated over time. |
|
|
1258
|
+
| `topN(opts)` | `TopNRow[]` | Most frequent field values. |
|
|
1259
|
+
| `stats(opts)` | `FieldStats` | min / max / avg / sum / count / stddev. |
|
|
1260
|
+
| `records(opts?)` | `(T & RecordResult)[]` | Filtered records via BigQuery (good for large sets). |
|
|
1261
|
+
| `multiMetric(opts)` | `MultiMetricResult` | Multiple aggregations in one request. |
|
|
1262
|
+
| `storageStats(opts?)` | `StorageStatsResult` | Record count and byte stats for the bucket. |
|
|
1263
|
+
| `crossBucket(opts)` | `CrossBucketRow[]` | Compare a metric across multiple buckets. |
|
|
1264
|
+
| `query(query)` | `AnalyticsResult<T>` | Raw escape hatch for any `AnalyticsQuery`. |
|
|
554
1265
|
|
|
555
1266
|
---
|
|
556
1267
|
|