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