hydrousdb 3.2.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 +832 -1124
- package/dist/index.cjs +618 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +519 -222
- package/dist/index.d.ts +519 -222
- package/dist/index.mjs +613 -322
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,723 +1,486 @@
|
|
|
1
|
-
# HydrousDB
|
|
1
|
+
# HydrousDB JS/TS SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<strong>Store, retrieve, and query massive JSON records in milliseconds — with auth, file storage, and analytics built in.</strong><br><br>
|
|
5
|
-
<a href="https://hydrousdb.com/dashboard"><strong>→ Create a free account and run your first query in 5 minutes</strong></a>
|
|
6
|
-
</p>
|
|
3
|
+
**A database that doesn't choke on big JSON.** Store, query, and analyse massive records — with auth and file storage built in.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</p>
|
|
5
|
+
```bash
|
|
6
|
+
npm install hydrousdb
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
→ [Get your free API keys at hydrousdb.com](https://hydrousdb.com/dashboard)
|
|
14
10
|
|
|
15
11
|
---
|
|
16
12
|
|
|
17
13
|
## Table of Contents
|
|
18
14
|
|
|
19
|
-
- [
|
|
20
|
-
- [How It Works](#how-it-works)
|
|
21
|
-
- [Quick Start](#quick-start)
|
|
22
|
-
- [Installation](#installation)
|
|
23
|
-
- [Module Formats — ESM & CommonJS](#module-formats--esm--commonjs)
|
|
15
|
+
- [Setup](#setup)
|
|
24
16
|
- [Records](#records)
|
|
25
|
-
- [Create](#create-
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [Batch Create](#batch-create)
|
|
32
|
-
- [Batch Delete](#batch-delete)
|
|
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)
|
|
33
23
|
- [Version History](#version-history)
|
|
34
|
-
- [Write-Filter Sentinels](#write-filter-sentinels)
|
|
35
24
|
- [Custom Record IDs](#custom-record-ids)
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
|
|
25
|
+
- [Existence Check](#existence-check)
|
|
26
|
+
- [Get All Records](#get-all-records)
|
|
27
|
+
- [Auth](#auth)
|
|
28
|
+
- [Sign Up & Log In](#sign-up--log-in)
|
|
39
29
|
- [Session Management](#session-management)
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [Password Reset Flow](#password-reset-flow)
|
|
44
|
-
- [Email Verification](#email-verification)
|
|
45
|
-
- [Admin — List Users](#admin--list-users)
|
|
46
|
-
- [Admin — Lock / Unlock](#admin--lock--unlock)
|
|
47
|
-
- [Admin — Delete Users](#admin--delete-users)
|
|
30
|
+
- [User Profile](#user-profile)
|
|
31
|
+
- [Password & Email](#password--email)
|
|
32
|
+
- [Admin Controls](#admin-controls)
|
|
48
33
|
- [File Storage](#file-storage)
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
- [Batch Upload](#batch-upload)
|
|
34
|
+
- [Upload](#upload)
|
|
35
|
+
- [Large Files with Progress](#large-files-with-progress)
|
|
36
|
+
- [Batch Uploads](#batch-uploads)
|
|
53
37
|
- [Download](#download)
|
|
54
38
|
- [Batch Download](#batch-download)
|
|
55
|
-
- [List
|
|
56
|
-
- [
|
|
57
|
-
- [File Metadata](#file-metadata)
|
|
58
|
-
- [Signed Share URLs](#signed-share-urls)
|
|
39
|
+
- [List, Metadata & Signed URLs](#list-metadata--signed-urls)
|
|
40
|
+
- [Move, Copy & Delete](#move-copy--delete)
|
|
59
41
|
- [Visibility](#visibility)
|
|
60
|
-
- [
|
|
42
|
+
- [Folders](#folders)
|
|
43
|
+
- [Scoped Storage](#scoped-storage)
|
|
61
44
|
- [Storage Stats](#storage-stats)
|
|
62
45
|
- [Analytics](#analytics)
|
|
63
|
-
- [
|
|
46
|
+
- [Date Range (Time Scope)](#date-range-time-scope)
|
|
47
|
+
- [Count](#count)
|
|
64
48
|
- [Distribution](#distribution)
|
|
65
49
|
- [Sum](#sum)
|
|
66
50
|
- [Time Series](#time-series)
|
|
67
51
|
- [Field Time Series](#field-time-series)
|
|
68
52
|
- [Top N](#top-n)
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
72
|
-
- [Cross-Bucket Comparison](#cross-bucket-comparison)
|
|
53
|
+
- [Stats](#stats)
|
|
54
|
+
- [Records via BigQuery](#records-via-bigquery)
|
|
55
|
+
- [Multi-Metric](#multi-metric)
|
|
73
56
|
- [Storage Stats](#storage-stats-1)
|
|
57
|
+
- [Cross-Bucket](#cross-bucket)
|
|
74
58
|
- [Raw Query](#raw-query)
|
|
75
|
-
- [TypeScript](#typescript)
|
|
76
59
|
- [Error Handling](#error-handling)
|
|
77
|
-
- [
|
|
78
|
-
- [
|
|
79
|
-
- [
|
|
80
|
-
- [
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## What is HydrousDB?
|
|
85
|
-
|
|
86
|
-
Traditional databases start choking when your JSON records get large. Postgres hits row-size limits. Firestore charges per field read. MongoDB buckles under millions of 500 KB+ documents. They were built for structured rows and small payloads — not the deeply nested, real-world JSON that modern apps actually produce.
|
|
87
|
-
|
|
88
|
-
HydrousDB is built specifically for that problem. It stores every record as a compressed GCS blob, retrieves any record in a single network call (the storage path is computed directly from the record ID — no index lookups), and runs analytics at BigQuery scale without ETL. The bigger and messier your JSON, the more it outperforms traditional databases.
|
|
89
|
-
|
|
90
|
-
| Domain | Example records | Why traditional DBs struggle |
|
|
91
|
-
|---|---|---|
|
|
92
|
-
| 🏥 **Hospital / EMR** | Full patient charts — vitals, medications, notes, imaging | 850 KB+ per chart, millions of patients, strict audit trails |
|
|
93
|
-
| 🎓 **School management** | Student portfolios — grades, assessments, teacher notes | Deep nesting, bursty writes at term-end, long-term archival |
|
|
94
|
-
| 🏭 **IoT / Industrial** | Sensor telemetry — readings, device state, calibration | Billions of records, append-heavy, rarely updated |
|
|
95
|
-
| 🛒 **E-commerce** | Orders — line items, fulfilment events, return history | Variable shape, fast analytics across date ranges |
|
|
96
|
-
| ⚖️ **Legal / compliance** | Case files — filings, correspondence, version history | 1 MB+ records, immutable audit log, cross-case analytics |
|
|
97
|
-
| 🎮 **Gaming** | Player save states — inventory, quest progress, replays | Large payloads, millions of users, burst writes |
|
|
98
|
-
|
|
99
|
-
**What you get out of the box:**
|
|
100
|
-
|
|
101
|
-
| Feature | What it does |
|
|
102
|
-
|---|---|
|
|
103
|
-
| **Records** | Schemaless JSON store. Billion-scale, gzip-compressed, date-encoded IDs for zero-lookup retrieval. |
|
|
104
|
-
| **Auth** | Full user system — signup, login, sessions, password reset, email verification, admin controls. |
|
|
105
|
-
| **Storage** | File uploads to GCS. Direct uploads, public/private visibility, signed share URLs. |
|
|
106
|
-
| **Analytics** | BigQuery-powered — counts, distributions, time series, top-N, cross-bucket. Zero ETL. |
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## How It Works
|
|
111
|
-
|
|
112
|
-
Every record ID encodes its creation date as a prefix (e.g. `260203-rec_01JA2XYZ`). This means the full GCS storage path to any record can be computed in memory — no index lookup needed.
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
260203-rec_01JA2XYZ
|
|
116
|
-
↓ parse date prefix
|
|
117
|
-
YY=26 MM=02 DD=03
|
|
118
|
-
↓ compute path in memory
|
|
119
|
-
projects/pid/buckets/bk/records/26/02/03/rec_01JA.json.gz
|
|
120
|
-
↓ fetch from GCS directly
|
|
121
|
-
0 index reads ✓
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Records are gzip-compressed on write (60–80% size reduction). An 850 KB hospital chart becomes ~255 KB on disk, automatically, every time.
|
|
60
|
+
- [TypeScript](#typescript)
|
|
61
|
+
- [Security](#security)
|
|
62
|
+
- [Framework Examples](#framework-examples)
|
|
63
|
+
- [API Reference](#api-reference)
|
|
125
64
|
|
|
126
65
|
---
|
|
127
66
|
|
|
128
|
-
##
|
|
129
|
-
|
|
130
|
-
### 1. Create your account
|
|
131
|
-
|
|
132
|
-
Sign up at [https://hydrousdb.com](https://hydrousdb.com).
|
|
133
|
-
|
|
134
|
-
### 2. Get your API keys
|
|
135
|
-
|
|
136
|
-
From the dashboard → **Settings → API Keys**, create three key types:
|
|
137
|
-
|
|
138
|
-
| Key | Prefix | Used for |
|
|
139
|
-
|---|---|---|
|
|
140
|
-
| **Auth Key** | `hk_auth_…` | All auth routes — signup, login, sessions |
|
|
141
|
-
| **Bucket Security Key** | `hk_bucket_…` | Records and analytics |
|
|
142
|
-
| **Storage Key(s)** | `ssk_…` | File storage — one key per storage bucket |
|
|
143
|
-
|
|
144
|
-
> ⚠️ Never commit these to Git. Store them in environment variables.
|
|
145
|
-
|
|
146
|
-
### 3. Install
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npm install hydrousdb
|
|
150
|
-
# or: yarn add hydrousdb / pnpm add hydrousdb
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Requirements:** Node.js 18+ (uses the native `fetch` API).
|
|
154
|
-
|
|
155
|
-
### 4. Create the client and write your first record
|
|
67
|
+
## Setup
|
|
156
68
|
|
|
157
|
-
```
|
|
69
|
+
```ts
|
|
158
70
|
import { createClient } from 'hydrousdb';
|
|
159
71
|
|
|
160
|
-
// Create once — reuse everywhere in your app
|
|
161
72
|
const db = createClient({
|
|
162
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
163
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
73
|
+
authKey: process.env.HYDROUS_AUTH_KEY!, // hk_auth_…
|
|
74
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!, // hk_bucket_…
|
|
164
75
|
storageKeys: {
|
|
165
|
-
main: process.env.
|
|
76
|
+
main: process.env.HYDROUS_STORAGE_KEY!, // ssk_…
|
|
77
|
+
// add more named storage keys as needed
|
|
166
78
|
},
|
|
79
|
+
// baseUrl: 'https://custom-endpoint.example.com', // optional override
|
|
167
80
|
});
|
|
168
|
-
|
|
169
|
-
// Write
|
|
170
|
-
const post = await db.records('my-bucket').create({
|
|
171
|
-
title: 'Hello, HydrousDB!',
|
|
172
|
-
published: false,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
176
|
-
console.log(post.createdAt); // 1717200000000
|
|
177
|
-
|
|
178
|
-
// Read back — zero database reads, path computed from ID
|
|
179
|
-
const fetched = await db.records('my-bucket').get(post.id);
|
|
180
|
-
|
|
181
|
-
// Update
|
|
182
|
-
await db.records('my-bucket').patch(post.id, { published: true });
|
|
183
|
-
|
|
184
|
-
// Delete
|
|
185
|
-
await db.records('my-bucket').delete(post.id);
|
|
186
81
|
```
|
|
187
82
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
## Installation
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
npm install hydrousdb
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Module Formats — ESM & CommonJS
|
|
197
|
-
|
|
198
|
-
The package ships both ESM (`.mjs`) and CommonJS (`.cjs`) builds. Your toolchain picks the right one automatically based on your `import` or `require` call.
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
// ESM — Next.js, Vite, modern Node, TypeScript
|
|
202
|
-
import { createClient } from 'hydrousdb';
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
```javascript
|
|
206
|
-
// CommonJS — legacy Node, Jest without transform, older tooling
|
|
207
|
-
const { createClient } = require('hydrousdb');
|
|
208
|
-
```
|
|
83
|
+
You get three key types from the dashboard — one for auth, one for records/analytics, one (or more) for file storage. Keep them in environment variables, never in your code.
|
|
209
84
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
```json
|
|
213
|
-
{
|
|
214
|
-
"exports": {
|
|
215
|
-
".": {
|
|
216
|
-
"import": "./dist/index.mjs",
|
|
217
|
-
"require": "./dist/index.cjs",
|
|
218
|
-
"types": "./dist/index.d.ts"
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Using with Next.js?** If Next.js resolves the CJS build instead of ESM (common with the Pages Router or older Next configs), add this to `next.config.js`:
|
|
225
|
-
|
|
226
|
-
```javascript
|
|
227
|
-
const nextConfig = {
|
|
228
|
-
webpack(config) {
|
|
229
|
-
config.resolve.conditionNames = ['import', 'module', 'require', 'default'];
|
|
230
|
-
return config;
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
```
|
|
85
|
+
Works in **React, Next.js, Vue, React Native, Node.js** — anywhere that runs modern JavaScript.
|
|
234
86
|
|
|
235
87
|
---
|
|
236
88
|
|
|
237
89
|
## Records
|
|
238
90
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- `id` — date-prefixed unique identifier (`"260601-rec_01JA2XYZ"`) — encodes the GCS path
|
|
242
|
-
- `createdAt` — Unix timestamp in milliseconds
|
|
243
|
-
- `updatedAt` — Unix timestamp in milliseconds
|
|
91
|
+
JSON objects stored in named buckets. Every record automatically gets `id`, `createdAt`, and `updatedAt`.
|
|
244
92
|
|
|
245
|
-
```
|
|
93
|
+
```ts
|
|
246
94
|
const posts = db.records('blog-posts');
|
|
247
|
-
// or typed:
|
|
248
|
-
const orders = db.records<Order>('orders');
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Create a Record
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
const post = await posts.create({
|
|
255
|
-
title: 'My First Post',
|
|
256
|
-
body: 'Hello world.',
|
|
257
|
-
status: 'draft',
|
|
258
|
-
views: 0,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// post.id, post.createdAt, post.updatedAt are added automatically
|
|
262
|
-
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
263
95
|
```
|
|
264
96
|
|
|
265
|
-
|
|
97
|
+
### Create, Read, Update, Delete
|
|
266
98
|
|
|
267
|
-
```
|
|
99
|
+
```ts
|
|
100
|
+
// Create
|
|
268
101
|
const post = await posts.create(
|
|
102
|
+
{ title: 'Hello', status: 'draft', views: 0 },
|
|
269
103
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
authorId: 'usr_abc',
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
queryableFields: ['status', 'authorId'], // index these for filtering
|
|
276
|
-
userEmail: 'alice@example.com', // optional audit trail
|
|
104
|
+
queryableFields: ['status', 'views'], // declare fields you want to filter on
|
|
105
|
+
userEmail: 'alice@example.com', // optional — for audit trails
|
|
277
106
|
},
|
|
278
107
|
);
|
|
279
|
-
|
|
108
|
+
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
109
|
+
console.log(post.createdAt); // Unix ms timestamp
|
|
280
110
|
|
|
281
|
-
|
|
111
|
+
// Read by ID
|
|
112
|
+
const found = await posts.get(post.id);
|
|
282
113
|
|
|
283
|
-
|
|
114
|
+
// Partial update — only the fields you pass are changed
|
|
115
|
+
await posts.patch(post.id, { status: 'published' });
|
|
284
116
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const post = await posts.get('260601-rec_01JA2XYZ');
|
|
117
|
+
// Merge mode — deeply merge nested objects instead of replacing them
|
|
118
|
+
await posts.patch(post.id, { meta: { seo: true } }, { merge: true });
|
|
288
119
|
|
|
289
|
-
//
|
|
120
|
+
// Delete permanently
|
|
121
|
+
await posts.delete(post.id);
|
|
290
122
|
```
|
|
291
123
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
**`patch(id, data)` — merge update.** Only the fields you provide are changed. All other fields on the record are left untouched.
|
|
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.
|
|
295
125
|
|
|
296
|
-
|
|
297
|
-
const updated = await posts.patch('260601-rec_01JA2XYZ', {
|
|
298
|
-
status: 'published',
|
|
299
|
-
views: 1,
|
|
300
|
-
});
|
|
301
|
-
```
|
|
126
|
+
---
|
|
302
127
|
|
|
303
|
-
|
|
128
|
+
### Querying & Filtering
|
|
304
129
|
|
|
305
|
-
```
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
130
|
+
```ts
|
|
131
|
+
const { records, hasMore, nextCursor } = await posts.query({
|
|
132
|
+
filters: [
|
|
133
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
134
|
+
{ field: 'views', op: '>', value: 100 },
|
|
135
|
+
{ field: 'title', op: 'contains', value: 'hello' },
|
|
136
|
+
],
|
|
137
|
+
orderBy: 'createdAt',
|
|
138
|
+
order: 'desc',
|
|
139
|
+
limit: 20,
|
|
140
|
+
fields: 'id,title,status', // optional — return only these fields
|
|
311
141
|
});
|
|
312
142
|
```
|
|
313
143
|
|
|
314
|
-
**
|
|
144
|
+
**Supported filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
|
|
315
145
|
|
|
316
|
-
|
|
317
|
-
// merge: false means fields not in `data` are removed
|
|
318
|
-
await posts.patch('260601-rec_01JA2XYZ', { status: 'archived' }, { merge: false });
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Delete a Record
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
await posts.delete('260601-rec_01JA2XYZ');
|
|
325
|
-
```
|
|
146
|
+
---
|
|
326
147
|
|
|
327
|
-
###
|
|
148
|
+
### Time Scope on Queries
|
|
328
149
|
|
|
329
|
-
|
|
330
|
-
// All records (up to 100 by default)
|
|
331
|
-
const { records } = await posts.query();
|
|
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.
|
|
332
151
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
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 |
|
|
339
157
|
|
|
340
|
-
|
|
158
|
+
```ts
|
|
159
|
+
// All records from March 2026
|
|
341
160
|
const { records, hasMore, nextCursor } = await posts.query({
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
],
|
|
346
|
-
orderBy: 'createdAt',
|
|
347
|
-
order: 'desc',
|
|
348
|
-
limit: 20,
|
|
161
|
+
timeScope: '_month_2603',
|
|
162
|
+
order: 'desc',
|
|
163
|
+
limit: 50,
|
|
349
164
|
});
|
|
350
165
|
|
|
351
|
-
//
|
|
166
|
+
// Paginate through a time-scoped result set
|
|
352
167
|
if (hasMore) {
|
|
353
168
|
const page2 = await posts.query({
|
|
354
|
-
|
|
355
|
-
order: 'desc',
|
|
356
|
-
limit: 20,
|
|
169
|
+
timeScope: '_month_2603',
|
|
357
170
|
startAfter: nextCursor,
|
|
358
171
|
});
|
|
359
172
|
}
|
|
360
173
|
|
|
361
|
-
//
|
|
362
|
-
const { records:
|
|
363
|
-
|
|
174
|
+
// A specific day
|
|
175
|
+
const { records: dayRecords } = await posts.query({
|
|
176
|
+
timeScope: '_day_260305', // March 5, 2026
|
|
177
|
+
order: 'asc',
|
|
364
178
|
});
|
|
365
179
|
|
|
366
|
-
//
|
|
367
|
-
const { records:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
180
|
+
// An entire year
|
|
181
|
+
const { records: yearRecords } = await posts.query({
|
|
182
|
+
timeScope: '_year_26',
|
|
183
|
+
orderBy: 'createdAt',
|
|
184
|
+
order: 'asc',
|
|
185
|
+
limit: 100,
|
|
372
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' });
|
|
373
198
|
```
|
|
374
199
|
|
|
375
|
-
|
|
200
|
+
---
|
|
376
201
|
|
|
377
|
-
|
|
378
|
-
|---|---|
|
|
379
|
-
| `==` | Equal |
|
|
380
|
-
| `!=` | Not equal |
|
|
381
|
-
| `>` | Greater than |
|
|
382
|
-
| `<` | Less than |
|
|
383
|
-
| `>=` | Greater than or equal |
|
|
384
|
-
| `<=` | Less than or equal |
|
|
385
|
-
| `CONTAINS` | String contains |
|
|
202
|
+
### Pagination
|
|
386
203
|
|
|
387
|
-
|
|
204
|
+
`query()` returns a cursor you can pass straight into the next call.
|
|
388
205
|
|
|
389
|
-
|
|
206
|
+
```ts
|
|
207
|
+
// Page 1
|
|
208
|
+
const page1 = await posts.query({ limit: 20, orderBy: 'createdAt', order: 'desc' });
|
|
390
209
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
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
|
+
}
|
|
394
219
|
|
|
395
|
-
//
|
|
396
|
-
const
|
|
397
|
-
{ field: 'status', op: '==', value: 'published' },
|
|
398
|
-
]);
|
|
220
|
+
// You can also use startAt / endAt for range-based cursor control
|
|
221
|
+
const window = await posts.query({ startAt: cursorA, endAt: cursorB });
|
|
399
222
|
```
|
|
400
223
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
Up to 500 records per call.
|
|
224
|
+
---
|
|
404
225
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
|
|
226
|
+
### Atomic Field Updates
|
|
227
|
+
|
|
228
|
+
Avoid race conditions with server-side sentinels inside `patch()`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
await posts.patch(post.id, {
|
|
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
|
|
241
|
+
} as any);
|
|
418
242
|
```
|
|
419
243
|
|
|
420
|
-
|
|
244
|
+
Enable audit trails and history with extra `patch` options:
|
|
421
245
|
|
|
422
|
-
```
|
|
423
|
-
await posts.
|
|
424
|
-
|
|
425
|
-
{
|
|
426
|
-
|
|
246
|
+
```ts
|
|
247
|
+
await posts.patch(
|
|
248
|
+
post.id,
|
|
249
|
+
{ status: 'published' },
|
|
250
|
+
{ userEmail: 'alice@example.com', trackHistory: true },
|
|
251
|
+
);
|
|
427
252
|
```
|
|
428
253
|
|
|
429
|
-
|
|
254
|
+
---
|
|
430
255
|
|
|
431
|
-
|
|
256
|
+
### Batch Operations
|
|
432
257
|
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
'
|
|
437
|
-
'
|
|
438
|
-
|
|
258
|
+
```ts
|
|
259
|
+
// Create up to 500 records at once
|
|
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' },
|
|
263
|
+
);
|
|
439
264
|
|
|
440
|
-
|
|
265
|
+
// Update up to 500 records at once
|
|
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
|
+
);
|
|
273
|
+
|
|
274
|
+
// Delete up to 500 records at once
|
|
275
|
+
const { successful, failed } = await posts.batchDelete(['id1', 'id2']);
|
|
441
276
|
```
|
|
442
277
|
|
|
443
|
-
|
|
278
|
+
Batch upsert using custom IDs — include `_customRecordId` on each item:
|
|
444
279
|
|
|
445
|
-
|
|
280
|
+
```ts
|
|
281
|
+
await posts.batchCreate([
|
|
282
|
+
{ _customRecordId: '260601-post_hello', title: 'Hello' },
|
|
283
|
+
{ _customRecordId: '260601-post_world', title: 'World' },
|
|
284
|
+
] as any);
|
|
285
|
+
```
|
|
446
286
|
|
|
447
|
-
|
|
448
|
-
// Get the full history list (most recent first)
|
|
449
|
-
const history = await posts.getHistory('260601-rec_01JA2XYZ');
|
|
450
|
-
// [{ id, version: 3, createdAt, data }, { version: 2, … }, { version: 1, … }]
|
|
287
|
+
---
|
|
451
288
|
|
|
452
|
-
|
|
453
|
-
const restored = await posts.restoreVersion('260601-rec_01JA2XYZ', history[2]!.version);
|
|
454
|
-
```
|
|
289
|
+
### Version History
|
|
455
290
|
|
|
456
|
-
|
|
291
|
+
Every write is automatically versioned when `trackHistory` is enabled.
|
|
457
292
|
|
|
458
|
-
|
|
293
|
+
```ts
|
|
294
|
+
// List all saved versions
|
|
295
|
+
const history = await posts.getHistory(post.id);
|
|
296
|
+
// → [{ generation, savedAt, savedBy, sizeBytes }, …]
|
|
459
297
|
|
|
460
|
-
|
|
461
|
-
await posts.
|
|
462
|
-
|
|
463
|
-
views: { __op: 'increment', delta: 1 },
|
|
464
|
-
credits: { __op: 'decrement', delta: 5 },
|
|
298
|
+
// Retrieve a specific past version
|
|
299
|
+
const v1 = await posts.getVersion(post.id, history[0].generation!);
|
|
300
|
+
```
|
|
465
301
|
|
|
466
|
-
|
|
467
|
-
slug: { __op: 'setOnce', value: 'my-first-post' },
|
|
302
|
+
---
|
|
468
303
|
|
|
469
|
-
|
|
470
|
-
discount: { __op: 'setIf', value: 10, cond: { op: '>=', value: 100 } },
|
|
304
|
+
### Custom Record IDs
|
|
471
305
|
|
|
472
|
-
|
|
473
|
-
tags: { __op: 'appendUnique', item: 'featured' },
|
|
306
|
+
Supply your own ID at creation time — if it already exists the record is upserted in-place.
|
|
474
307
|
|
|
475
|
-
|
|
476
|
-
|
|
308
|
+
```ts
|
|
309
|
+
// Format: YYMMDD-segment1__segment2
|
|
310
|
+
const post = await posts.create(
|
|
311
|
+
{ title: 'Welcome' },
|
|
312
|
+
{ customRecordId: '260601-post_welcome' },
|
|
313
|
+
);
|
|
314
|
+
```
|
|
477
315
|
|
|
478
|
-
|
|
479
|
-
rating: { __op: 'clamp', value: 6, min: 0, max: 5 },
|
|
316
|
+
---
|
|
480
317
|
|
|
481
|
-
|
|
482
|
-
price: { __op: 'multiplyBy', factor: 1.1 },
|
|
318
|
+
### Existence Check
|
|
483
319
|
|
|
484
|
-
|
|
485
|
-
active: { __op: 'toggleBool' },
|
|
320
|
+
A lightweight HEAD request — much cheaper than fetching the full record:
|
|
486
321
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
} as any);
|
|
322
|
+
```ts
|
|
323
|
+
const exists = await posts.exists(post.id); // true | false
|
|
490
324
|
```
|
|
491
325
|
|
|
492
|
-
|
|
326
|
+
---
|
|
493
327
|
|
|
494
|
-
|
|
328
|
+
### Get All Records
|
|
495
329
|
|
|
496
|
-
|
|
497
|
-
// Single record
|
|
498
|
-
const post = await posts.create(
|
|
499
|
-
{ title: 'Welcome', status: 'published' },
|
|
500
|
-
{ customRecordId: '260601-post_welcome' },
|
|
501
|
-
);
|
|
330
|
+
Fetches every record matching the options without filter support. Use `query()` when you need filters.
|
|
502
331
|
|
|
503
|
-
|
|
504
|
-
await posts.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
332
|
+
```ts
|
|
333
|
+
const all = await posts.getAll({
|
|
334
|
+
orderBy: 'createdAt',
|
|
335
|
+
order: 'desc',
|
|
336
|
+
limit: 500,
|
|
337
|
+
});
|
|
508
338
|
```
|
|
509
339
|
|
|
510
|
-
Custom IDs must match `^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$`.
|
|
511
|
-
|
|
512
340
|
---
|
|
513
341
|
|
|
514
|
-
##
|
|
342
|
+
## Auth
|
|
515
343
|
|
|
516
|
-
|
|
344
|
+
A complete user system — signup, login, sessions, password reset, email verification, and admin controls.
|
|
517
345
|
|
|
518
|
-
```
|
|
519
|
-
const auth = db.auth(
|
|
346
|
+
```ts
|
|
347
|
+
const auth = db.auth();
|
|
520
348
|
```
|
|
521
349
|
|
|
522
|
-
### Sign Up
|
|
350
|
+
### Sign Up & Log In
|
|
523
351
|
|
|
524
|
-
```
|
|
352
|
+
```ts
|
|
353
|
+
// Sign up — extra fields beyond email/password are stored on the user
|
|
525
354
|
const { user, session } = await auth.signup({
|
|
526
355
|
email: 'alice@example.com',
|
|
527
|
-
password: 'hunter2',
|
|
528
|
-
fullName: 'Alice
|
|
529
|
-
//
|
|
530
|
-
plan: 'pro',
|
|
531
|
-
referral: 'friend123',
|
|
356
|
+
password: 'hunter2',
|
|
357
|
+
fullName: 'Alice Smith',
|
|
358
|
+
plan: 'pro', // any custom fields you want
|
|
532
359
|
});
|
|
533
360
|
|
|
534
|
-
// Persist these in your app / session store:
|
|
535
|
-
// session.sessionId
|
|
536
|
-
// session.refreshToken
|
|
537
|
-
// session.expiresAt
|
|
538
|
-
|
|
539
|
-
console.log(user.id); // "usr_xxxxxxxxxxxx"
|
|
540
|
-
console.log(user.emailVerified); // false — send a verification email
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Log In / Log Out
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
361
|
// Log in
|
|
547
362
|
const { user, session } = await auth.login({
|
|
548
363
|
email: 'alice@example.com',
|
|
549
364
|
password: 'hunter2',
|
|
550
365
|
});
|
|
551
366
|
|
|
552
|
-
// Log out
|
|
367
|
+
// Log out this device
|
|
553
368
|
await auth.logout({ sessionId: session.sessionId });
|
|
554
369
|
|
|
555
|
-
// Log out
|
|
370
|
+
// Log out everywhere
|
|
556
371
|
await auth.logout({ sessionId: session.sessionId, allDevices: true });
|
|
557
372
|
```
|
|
558
373
|
|
|
559
|
-
|
|
374
|
+
Store `session.sessionId` and `session.refreshToken` in your app.
|
|
560
375
|
|
|
561
|
-
|
|
376
|
+
| Token | Lifetime |
|
|
377
|
+
|---|---|
|
|
378
|
+
| `sessionId` | 24 hours |
|
|
379
|
+
| `refreshToken` | 30 days |
|
|
562
380
|
|
|
563
|
-
|
|
564
|
-
// Refresh before expiry to get a new session
|
|
565
|
-
const newSession = await auth.refreshSession({
|
|
566
|
-
refreshToken: session.refreshToken,
|
|
567
|
-
});
|
|
568
|
-
// Store newSession.sessionId and newSession.refreshToken
|
|
381
|
+
---
|
|
569
382
|
|
|
570
|
-
|
|
571
|
-
const user = await auth.getUser({ userId: session.userId });
|
|
572
|
-
```
|
|
383
|
+
### Session Management
|
|
573
384
|
|
|
574
|
-
|
|
385
|
+
```ts
|
|
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 }
|
|
575
389
|
|
|
576
|
-
|
|
390
|
+
// Get a brand-new session from a refresh token (before the old one expires)
|
|
391
|
+
const newSession = await auth.refreshSession(session.refreshToken);
|
|
392
|
+
```
|
|
577
393
|
|
|
578
|
-
|
|
579
|
-
const { user, session: activeSession } = await auth.validateSession({
|
|
580
|
-
sessionId: session.sessionId,
|
|
581
|
-
});
|
|
394
|
+
---
|
|
582
395
|
|
|
583
|
-
|
|
584
|
-
console.log(activeSession.expiresAt); // timestamp
|
|
585
|
-
```
|
|
396
|
+
### User Profile
|
|
586
397
|
|
|
587
|
-
|
|
398
|
+
```ts
|
|
399
|
+
// Fetch a user by ID
|
|
400
|
+
const user = await auth.getUser(session.userId);
|
|
588
401
|
|
|
589
|
-
|
|
590
|
-
|
|
402
|
+
// Update profile fields (users can update themselves; admins can update anyone)
|
|
403
|
+
await auth.updateUser({
|
|
591
404
|
sessionId: session.sessionId,
|
|
592
405
|
userId: user.id,
|
|
593
|
-
updates:
|
|
594
|
-
fullName: 'Alice Smith',
|
|
595
|
-
plan: 'enterprise',
|
|
596
|
-
// Any field on the user record can be updated here
|
|
597
|
-
avatar: 'https://example.com/avatar.jpg',
|
|
598
|
-
},
|
|
406
|
+
updates: { fullName: 'Alice Johnson', plan: 'enterprise' },
|
|
599
407
|
});
|
|
408
|
+
|
|
409
|
+
// Soft-delete (users can delete themselves; admins can delete anyone)
|
|
410
|
+
await auth.deleteUser(session.sessionId, user.id);
|
|
600
411
|
```
|
|
601
412
|
|
|
602
|
-
|
|
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
|
+
```
|
|
603
429
|
|
|
604
|
-
|
|
430
|
+
---
|
|
605
431
|
|
|
606
|
-
|
|
432
|
+
### Password & Email
|
|
607
433
|
|
|
608
|
-
```
|
|
434
|
+
```ts
|
|
435
|
+
// Change password — requires an active session AND the current password
|
|
609
436
|
await auth.changePassword({
|
|
610
437
|
sessionId: session.sessionId,
|
|
611
438
|
userId: user.id,
|
|
612
439
|
currentPassword: 'hunter2',
|
|
613
440
|
newPassword: 'correcthorsebatterystaple',
|
|
614
441
|
});
|
|
615
|
-
// All existing sessions for this user are automatically revoked
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Password Reset Flow
|
|
619
442
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
await auth.
|
|
443
|
+
// Forgot-password flow
|
|
444
|
+
await auth.requestPasswordReset('alice@example.com'); // always succeeds (prevents enumeration)
|
|
445
|
+
await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');
|
|
623
446
|
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
await auth.confirmPasswordReset({
|
|
628
|
-
resetToken: 'tok_from_email',
|
|
629
|
-
newPassword: 'correcthorsebatterystaple',
|
|
630
|
-
});
|
|
631
|
-
// All existing sessions are automatically revoked
|
|
447
|
+
// Email verification
|
|
448
|
+
await auth.requestEmailVerification(user.id);
|
|
449
|
+
await auth.confirmEmailVerification(tokenFromEmail);
|
|
632
450
|
```
|
|
633
451
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
// 1. Send the verification email
|
|
638
|
-
await auth.requestEmailVerification({ userId: user.id });
|
|
639
|
-
|
|
640
|
-
// 2. User clicks link in their inbox — your app extracts the token from the URL
|
|
641
|
-
|
|
642
|
-
// 3. Confirm the token
|
|
643
|
-
await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
|
|
644
|
-
```
|
|
452
|
+
---
|
|
645
453
|
|
|
646
|
-
### Admin
|
|
454
|
+
### Admin Controls
|
|
647
455
|
|
|
648
|
-
|
|
456
|
+
All admin methods require an active admin session.
|
|
649
457
|
|
|
650
|
-
```
|
|
651
|
-
// Paginated list
|
|
458
|
+
```ts
|
|
459
|
+
// Paginated user list
|
|
652
460
|
const { users, hasMore, nextCursor } = await auth.listUsers({
|
|
653
461
|
sessionId: adminSession.sessionId,
|
|
654
462
|
limit: 50,
|
|
463
|
+
cursor: previousNextCursor, // optional — for subsequent pages
|
|
655
464
|
});
|
|
656
465
|
|
|
657
|
-
|
|
658
|
-
const page2 = await auth.listUsers({
|
|
659
|
-
sessionId: adminSession.sessionId,
|
|
660
|
-
limit: 50,
|
|
661
|
-
cursor: nextCursor!,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
Each user in the list includes:
|
|
667
|
-
|
|
668
|
-
```typescript
|
|
669
|
-
{
|
|
670
|
-
id: 'usr_xxxxxxxxxxxx',
|
|
671
|
-
email: 'alice@example.com',
|
|
672
|
-
fullName: 'Alice Wonderland',
|
|
673
|
-
emailVerified: true,
|
|
674
|
-
accountStatus: 'active', // 'active' | 'locked' | 'suspended'
|
|
675
|
-
role: 'user', // 'user' | 'admin'
|
|
676
|
-
createdAt: 1717200000000,
|
|
677
|
-
updatedAt: 1717200000000,
|
|
678
|
-
// ...any extra fields stored at signup
|
|
679
|
-
}
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### Admin — Lock / Unlock
|
|
683
|
-
|
|
684
|
-
```typescript
|
|
685
|
-
// Lock an account (prevents login)
|
|
466
|
+
// Lock an account (default: 15 minutes)
|
|
686
467
|
const { lockedUntil, unlockTime } = await auth.lockAccount({
|
|
687
468
|
sessionId: adminSession.sessionId,
|
|
688
|
-
userId:
|
|
689
|
-
duration: 60 * 60 * 1000, //
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
console.log(`Account locked until ${unlockTime}`);
|
|
693
|
-
|
|
694
|
-
// Unlock manually
|
|
695
|
-
await auth.unlockAccount({
|
|
696
|
-
sessionId: adminSession.sessionId,
|
|
697
|
-
userId: 'usr_abc123',
|
|
469
|
+
userId: user.id,
|
|
470
|
+
duration: 60 * 60 * 1000, // optional ms — lock for 1 hour
|
|
698
471
|
});
|
|
699
|
-
```
|
|
700
472
|
|
|
701
|
-
|
|
473
|
+
// Unlock an account
|
|
474
|
+
await auth.unlockAccount(adminSession.sessionId, user.id);
|
|
702
475
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
await auth.deleteUser({
|
|
706
|
-
sessionId: adminSession.sessionId,
|
|
707
|
-
userId: 'usr_abc123',
|
|
708
|
-
});
|
|
476
|
+
// Permanent (hard) delete — cannot be undone
|
|
477
|
+
await auth.hardDeleteUser(adminSession.sessionId, user.id);
|
|
709
478
|
|
|
710
|
-
//
|
|
711
|
-
await auth.hardDeleteUser({
|
|
712
|
-
sessionId: adminSession.sessionId,
|
|
713
|
-
userId: 'usr_abc123',
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Bulk delete — up to 500 users, soft or hard
|
|
479
|
+
// Bulk delete — soft or hard
|
|
717
480
|
const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
718
481
|
sessionId: adminSession.sessionId,
|
|
719
|
-
userIds: ['
|
|
720
|
-
hard:
|
|
482
|
+
userIds: ['id1', 'id2', 'id3'],
|
|
483
|
+
hard: true, // optional — defaults to soft delete
|
|
721
484
|
});
|
|
722
485
|
```
|
|
723
486
|
|
|
@@ -725,323 +488,279 @@ const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
|
725
488
|
|
|
726
489
|
## File Storage
|
|
727
490
|
|
|
728
|
-
|
|
491
|
+
Files are **private by default** and scoped to your storage key server-side.
|
|
729
492
|
|
|
730
|
-
```
|
|
731
|
-
const
|
|
732
|
-
const avatars = db.storage('avatars');
|
|
733
|
-
const documents = db.storage('documents');
|
|
493
|
+
```ts
|
|
494
|
+
const storage = db.storage('main'); // 'main' matches a key in storageKeys config
|
|
734
495
|
```
|
|
735
496
|
|
|
736
|
-
###
|
|
497
|
+
### Upload
|
|
737
498
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
overwrite: false, // throw if file already exists (default: false)
|
|
746
|
-
mimeType: 'image/jpeg', // optional — auto-detected from extension if omitted
|
|
499
|
+
```ts
|
|
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
|
|
747
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);
|
|
748
512
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
console.log(result.mimeType); // 'image/jpeg'
|
|
753
|
-
|
|
754
|
-
// Node.js — from a Buffer or file path
|
|
755
|
-
import { readFileSync } from 'fs';
|
|
756
|
-
const buf = readFileSync('./report.pdf');
|
|
757
|
-
const result = await db.storage('documents').upload(buf, 'reports/q3.pdf');
|
|
758
|
-
console.log(result.downloadUrl); // auth-required download URL
|
|
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' });
|
|
759
516
|
```
|
|
760
517
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
// Upload a JS object as JSON
|
|
765
|
-
const result = await db.storage('main').uploadRaw(
|
|
766
|
-
{ theme: 'dark', language: 'en', version: 3 },
|
|
767
|
-
'settings/alice.json',
|
|
768
|
-
{ isPublic: false },
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
// Upload a plain string
|
|
772
|
-
await db.storage('main').uploadRaw(
|
|
773
|
-
'<html><body>Hello</body></html>',
|
|
774
|
-
'exports/page.html',
|
|
775
|
-
{ mimeType: 'text/html', isPublic: true },
|
|
776
|
-
);
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
### Large File Upload with Progress
|
|
518
|
+
---
|
|
780
519
|
|
|
781
|
-
|
|
520
|
+
### Large Files with Progress
|
|
782
521
|
|
|
783
|
-
|
|
784
|
-
const storage = db.storage('main');
|
|
522
|
+
Recommended for files > 10 MB or when you need a progress indicator (browsers only).
|
|
785
523
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
524
|
+
```ts
|
|
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
|
|
793
533
|
});
|
|
794
534
|
|
|
795
|
-
// Step 2 —
|
|
796
|
-
await storage.uploadToSignedUrl(
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
'video/mp4',
|
|
800
|
-
(percent) => {
|
|
801
|
-
progressBar.style.width = `${percent}%`;
|
|
802
|
-
console.log(`${percent}% uploaded`);
|
|
803
|
-
},
|
|
804
|
-
);
|
|
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
|
+
});
|
|
805
539
|
|
|
806
|
-
// Step 3 —
|
|
540
|
+
// Step 3 — confirm and register metadata server-side
|
|
807
541
|
const result = await storage.confirmUpload({
|
|
808
|
-
path
|
|
542
|
+
path,
|
|
809
543
|
mimeType: 'video/mp4',
|
|
810
544
|
isPublic: true,
|
|
811
545
|
});
|
|
812
|
-
|
|
813
|
-
console.log(result.publicUrl); // live CDN URL
|
|
814
546
|
```
|
|
815
547
|
|
|
816
|
-
|
|
548
|
+
---
|
|
817
549
|
|
|
818
|
-
|
|
550
|
+
### Batch Uploads
|
|
819
551
|
|
|
820
|
-
|
|
821
|
-
const storage = db.storage('main');
|
|
552
|
+
Get signed URLs for up to 50 files at once, upload them in parallel, then confirm in one call.
|
|
822
553
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
{ path: '
|
|
827
|
-
{ path: '
|
|
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 },
|
|
828
559
|
]);
|
|
829
560
|
|
|
830
|
-
// Step 2 —
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
// Step 3 — Confirm all at once
|
|
836
|
-
const { succeeded, failed } = await storage.batchConfirmUploads(
|
|
837
|
-
files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true })),
|
|
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
|
+
),
|
|
838
566
|
);
|
|
839
567
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
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
|
+
]);
|
|
844
573
|
```
|
|
845
574
|
|
|
575
|
+
---
|
|
576
|
+
|
|
846
577
|
### Download
|
|
847
578
|
|
|
848
|
-
```
|
|
849
|
-
//
|
|
850
|
-
const buffer = await
|
|
851
|
-
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
579
|
+
```ts
|
|
580
|
+
// Download a private file as ArrayBuffer
|
|
581
|
+
const buffer = await storage.download('private/report.pdf');
|
|
852
582
|
|
|
853
|
-
//
|
|
583
|
+
// Convert to a Blob for use in the browser
|
|
584
|
+
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
854
585
|
const url = URL.createObjectURL(blob);
|
|
855
|
-
const a = document.createElement('a');
|
|
856
|
-
a.href = url;
|
|
857
|
-
a.download = 'q3.pdf';
|
|
858
|
-
a.click();
|
|
859
586
|
```
|
|
860
587
|
|
|
861
|
-
|
|
588
|
+
For **public** files, just use the `publicUrl` directly — no SDK or authentication needed.
|
|
589
|
+
|
|
590
|
+
---
|
|
862
591
|
|
|
863
592
|
### Batch Download
|
|
864
593
|
|
|
865
|
-
|
|
594
|
+
Downloads up to 20 files at once. Content is returned as base64-encoded strings.
|
|
866
595
|
|
|
867
|
-
```
|
|
868
|
-
const { succeeded, failed } = await
|
|
869
|
-
'
|
|
870
|
-
'
|
|
871
|
-
'reports/q3.pdf',
|
|
596
|
+
```ts
|
|
597
|
+
const { succeeded, failed } = await storage.batchDownload([
|
|
598
|
+
'docs/report.pdf',
|
|
599
|
+
'images/chart.png',
|
|
872
600
|
]);
|
|
873
601
|
|
|
874
|
-
for (const
|
|
875
|
-
console.log(
|
|
876
|
-
|
|
877
|
-
console.log(f.size); // bytes
|
|
878
|
-
const bytes = Buffer.from(f.content, 'base64');
|
|
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')
|
|
879
605
|
}
|
|
880
606
|
|
|
881
|
-
for (const
|
|
882
|
-
console.error(
|
|
607
|
+
for (const err of failed) {
|
|
608
|
+
console.error(err.path, err.error, err.code);
|
|
883
609
|
}
|
|
884
610
|
```
|
|
885
611
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
```typescript
|
|
889
|
-
const storage = db.storage('main');
|
|
612
|
+
---
|
|
890
613
|
|
|
891
|
-
|
|
892
|
-
const { files, folders, hasMore, nextCursor } = await storage.list();
|
|
614
|
+
### List, Metadata & Signed URLs
|
|
893
615
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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)
|
|
898
623
|
});
|
|
899
624
|
|
|
900
|
-
//
|
|
901
|
-
if (
|
|
902
|
-
const page2 = await storage.list({
|
|
903
|
-
prefix: 'gallery/',
|
|
904
|
-
cursor: result.nextCursor,
|
|
905
|
-
});
|
|
625
|
+
// Page 2
|
|
626
|
+
if (hasMore) {
|
|
627
|
+
const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
|
|
906
628
|
}
|
|
907
|
-
```
|
|
908
629
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
downloadUrl: null,
|
|
920
|
-
updatedAt: '2025-06-01T12:00:00.000Z',
|
|
921
|
-
}
|
|
630
|
+
// Get file metadata
|
|
631
|
+
const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
632
|
+
// → { path, size, mimeType, isPublic, publicUrl, downloadUrl, createdAt, updatedAt }
|
|
633
|
+
|
|
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
|
+
);
|
|
922
640
|
```
|
|
923
641
|
|
|
924
|
-
|
|
642
|
+
---
|
|
925
643
|
|
|
926
|
-
|
|
644
|
+
### Move, Copy & Delete
|
|
927
645
|
|
|
928
|
-
```
|
|
929
|
-
|
|
646
|
+
```ts
|
|
647
|
+
await storage.move('old/path.jpg', 'new/path.jpg');
|
|
648
|
+
await storage.copy('templates/base.html', 'pages/home.html');
|
|
649
|
+
await storage.deleteFile('avatars/old.jpg');
|
|
650
|
+
await storage.deleteFolder('temp/'); // recursively deletes all contents
|
|
651
|
+
```
|
|
930
652
|
|
|
931
|
-
|
|
932
|
-
await userDocs.upload(pdfBuffer, 'contract.pdf');
|
|
653
|
+
---
|
|
933
654
|
|
|
934
|
-
|
|
935
|
-
const { files } = await userDocs.list();
|
|
655
|
+
### Visibility
|
|
936
656
|
|
|
937
|
-
|
|
938
|
-
|
|
657
|
+
```ts
|
|
658
|
+
// Make a private file public
|
|
659
|
+
const result = await storage.setVisibility('reports/q1.pdf', true);
|
|
660
|
+
console.log(result.publicUrl);
|
|
939
661
|
|
|
940
|
-
//
|
|
941
|
-
|
|
942
|
-
// All ops under: users/{userId}/thumbnails/
|
|
662
|
+
// Make a public file private
|
|
663
|
+
await storage.setVisibility('reports/q1.pdf', false);
|
|
943
664
|
```
|
|
944
665
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
### File Metadata
|
|
666
|
+
---
|
|
948
667
|
|
|
949
|
-
|
|
950
|
-
const meta = await db.storage('documents').getMetadata('reports/q3.pdf');
|
|
668
|
+
### Folders
|
|
951
669
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
console.log(meta.downloadUrl); // auth-required URL
|
|
956
|
-
console.log(meta.createdAt); // ISO string
|
|
957
|
-
console.log(meta.updatedAt); // ISO string
|
|
670
|
+
```ts
|
|
671
|
+
// Create an explicit folder marker (usually not needed — folders are implicit)
|
|
672
|
+
await storage.createFolder('projects/2025/');
|
|
958
673
|
```
|
|
959
674
|
|
|
960
|
-
|
|
675
|
+
---
|
|
961
676
|
|
|
962
|
-
|
|
677
|
+
### Scoped Storage
|
|
963
678
|
|
|
964
|
-
|
|
965
|
-
const { signedUrl, expiresAt, expiresIn } = await db.storage('documents')
|
|
966
|
-
.getSignedUrl('reports/q3.pdf', 3600); // expires in 1 hour (default)
|
|
679
|
+
Automatically prefix every path — ideal for per-user file isolation.
|
|
967
680
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
```
|
|
681
|
+
```ts
|
|
682
|
+
const userFiles = db.storage('main').scope(`users/${userId}/`);
|
|
971
683
|
|
|
972
|
-
|
|
684
|
+
await userFiles.upload(pdf, 'contract.pdf'); // → users/{userId}/contract.pdf
|
|
685
|
+
await userFiles.uploadRaw({ key: 'val' }, 'prefs.json');
|
|
686
|
+
const { files } = await userFiles.list(); // → only lists users/{userId}/
|
|
973
687
|
|
|
974
|
-
|
|
688
|
+
// Nest deeper
|
|
689
|
+
const reports = userFiles.scope('reports/'); // → users/{userId}/reports/
|
|
690
|
+
await reports.upload(file, 'q1.pdf'); // → users/{userId}/reports/q1.pdf
|
|
975
691
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const result2 = await db.storage('main').setVisibility('docs/report.pdf', false);
|
|
983
|
-
console.log(result2.downloadUrl); // now requires auth
|
|
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/');
|
|
984
698
|
```
|
|
985
699
|
|
|
986
|
-
|
|
700
|
+
---
|
|
987
701
|
|
|
988
|
-
|
|
989
|
-
const storage = db.storage('main');
|
|
702
|
+
### Storage Stats
|
|
990
703
|
|
|
991
|
-
|
|
992
|
-
|
|
704
|
+
```ts
|
|
705
|
+
// Stats for this storage key
|
|
706
|
+
const stats = await storage.getStats();
|
|
707
|
+
// → { totalFiles, totalBytes, uploadCount, downloadCount, deleteCount }
|
|
993
708
|
|
|
994
|
-
//
|
|
995
|
-
await storage.
|
|
709
|
+
// Server info (no auth required)
|
|
710
|
+
const info = await storage.info();
|
|
711
|
+
// → { ok: true, storageRoot: '…' }
|
|
712
|
+
```
|
|
996
713
|
|
|
997
|
-
|
|
998
|
-
await storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
714
|
+
---
|
|
999
715
|
|
|
1000
|
-
|
|
1001
|
-
await storage.createFolder('archive/2025/');
|
|
716
|
+
## Analytics
|
|
1002
717
|
|
|
1003
|
-
|
|
1004
|
-
await storage.deleteFile('temp/scratch.txt');
|
|
718
|
+
BigQuery-powered aggregations. No ETL, no pipelines — just query.
|
|
1005
719
|
|
|
1006
|
-
|
|
1007
|
-
|
|
720
|
+
```ts
|
|
721
|
+
const analytics = db.analytics('orders');
|
|
1008
722
|
```
|
|
1009
723
|
|
|
1010
|
-
###
|
|
724
|
+
### Date Range (Time Scope)
|
|
1011
725
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
// deleteCount: 58,
|
|
1020
|
-
// }
|
|
1021
|
-
|
|
1022
|
-
// Ping — no auth required
|
|
1023
|
-
const { ok, storageRoot } = await db.storage('main').info();
|
|
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
|
+
}
|
|
1024
733
|
```
|
|
1025
734
|
|
|
1026
|
-
|
|
735
|
+
**Granularity options** (for time series methods):
|
|
1027
736
|
|
|
1028
|
-
|
|
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 |
|
|
1029
744
|
|
|
1030
|
-
|
|
745
|
+
**Aggregation options** (for numeric field methods):
|
|
1031
746
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
747
|
+
| Value | Meaning |
|
|
748
|
+
|---|---|
|
|
749
|
+
| `'sum'` | Total |
|
|
750
|
+
| `'avg'` | Average |
|
|
751
|
+
| `'min'` | Minimum |
|
|
752
|
+
| `'max'` | Maximum |
|
|
753
|
+
| `'count'` | Record count |
|
|
1035
754
|
|
|
1036
|
-
|
|
755
|
+
---
|
|
1037
756
|
|
|
1038
757
|
### Count
|
|
1039
758
|
|
|
1040
|
-
```
|
|
1041
|
-
//
|
|
759
|
+
```ts
|
|
760
|
+
// All-time total
|
|
1042
761
|
const { count } = await analytics.count();
|
|
1043
762
|
|
|
1044
|
-
//
|
|
763
|
+
// Within a time window
|
|
1045
764
|
const { count: lastWeek } = await analytics.count({
|
|
1046
765
|
dateRange: {
|
|
1047
766
|
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
@@ -1050,283 +769,225 @@ const { count: lastWeek } = await analytics.count({
|
|
|
1050
769
|
});
|
|
1051
770
|
```
|
|
1052
771
|
|
|
772
|
+
---
|
|
773
|
+
|
|
1053
774
|
### Distribution
|
|
1054
775
|
|
|
1055
|
-
How many records have each
|
|
776
|
+
How many records have each value of a field.
|
|
1056
777
|
|
|
1057
|
-
```
|
|
1058
|
-
const
|
|
1059
|
-
field:
|
|
1060
|
-
limit:
|
|
1061
|
-
order:
|
|
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() },
|
|
1062
784
|
});
|
|
1063
|
-
// [
|
|
1064
|
-
// { value: 'completed', count: 8234 },
|
|
1065
|
-
// { value: 'pending', count: 1203 },
|
|
1066
|
-
// { value: 'refunded', count: 412 },
|
|
1067
|
-
// ]
|
|
785
|
+
// → [{ value: 'published', count: 320 }, { value: 'draft', count: 80 }, …]
|
|
1068
786
|
```
|
|
1069
787
|
|
|
788
|
+
---
|
|
789
|
+
|
|
1070
790
|
### Sum
|
|
1071
791
|
|
|
1072
|
-
|
|
792
|
+
Sum a numeric field, optionally grouped by another field.
|
|
793
|
+
|
|
794
|
+
```ts
|
|
1073
795
|
// Total revenue
|
|
1074
|
-
const [{ sum:
|
|
796
|
+
const [{ sum: totalRevenue }] = await analytics.sum({ field: 'amount' });
|
|
1075
797
|
|
|
1076
|
-
// Revenue by
|
|
1077
|
-
const
|
|
1078
|
-
field:
|
|
1079
|
-
groupBy:
|
|
1080
|
-
limit:
|
|
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
|
+
},
|
|
1081
807
|
});
|
|
1082
|
-
// [{ group: '
|
|
808
|
+
// → [{ group: 'Europe', sum: 18200 }, { group: 'Americas', sum: 29400 }, …]
|
|
1083
809
|
```
|
|
1084
810
|
|
|
811
|
+
---
|
|
812
|
+
|
|
1085
813
|
### Time Series
|
|
1086
814
|
|
|
1087
|
-
|
|
815
|
+
Count of records over time — useful for activity graphs.
|
|
1088
816
|
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
|
|
817
|
+
```ts
|
|
818
|
+
// Daily record counts, last 30 days
|
|
819
|
+
const daily = await analytics.timeSeries({
|
|
820
|
+
granularity: 'day',
|
|
1092
821
|
dateRange: {
|
|
1093
|
-
start:
|
|
1094
|
-
end:
|
|
822
|
+
start: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
823
|
+
end: Date.now(),
|
|
1095
824
|
},
|
|
1096
825
|
});
|
|
1097
|
-
// [{ date: '2025-
|
|
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' });
|
|
1098
830
|
```
|
|
1099
831
|
|
|
832
|
+
---
|
|
833
|
+
|
|
1100
834
|
### Field Time Series
|
|
1101
835
|
|
|
1102
|
-
Aggregate a numeric field over time.
|
|
836
|
+
Aggregate a numeric field over time — useful for revenue, score, or usage trends.
|
|
1103
837
|
|
|
1104
|
-
```
|
|
1105
|
-
|
|
838
|
+
```ts
|
|
839
|
+
// Daily sum of revenue, last 90 days
|
|
840
|
+
const revTrend = await analytics.fieldTimeSeries({
|
|
1106
841
|
field: 'amount',
|
|
1107
|
-
aggregation: 'sum',
|
|
1108
|
-
granularity: '
|
|
842
|
+
aggregation: 'sum',
|
|
843
|
+
granularity: 'day',
|
|
1109
844
|
dateRange: {
|
|
1110
|
-
start:
|
|
845
|
+
start: Date.now() - 90 * 24 * 60 * 60 * 1000,
|
|
1111
846
|
end: Date.now(),
|
|
1112
847
|
},
|
|
1113
848
|
});
|
|
1114
|
-
// [{ date: '2025-
|
|
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
|
+
});
|
|
1115
857
|
```
|
|
1116
858
|
|
|
859
|
+
---
|
|
860
|
+
|
|
1117
861
|
### Top N
|
|
1118
862
|
|
|
1119
|
-
Most
|
|
863
|
+
Most frequent values for a field by record count.
|
|
1120
864
|
|
|
1121
|
-
```
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
n:
|
|
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
|
|
1126
871
|
order: 'desc',
|
|
872
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
1127
873
|
});
|
|
1128
|
-
// [
|
|
1129
|
-
// { value: 'prod_123', label: 'Widget Pro', count: 892 },
|
|
1130
|
-
// { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
|
|
1131
|
-
// ]
|
|
874
|
+
// → [{ value: 'US', label: 'United States', count: 420 }, …]
|
|
1132
875
|
```
|
|
1133
876
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
Statistical summary for any numeric field.
|
|
1137
|
-
|
|
1138
|
-
```typescript
|
|
1139
|
-
const stats = await analytics.stats({ field: 'orderValue' });
|
|
1140
|
-
// {
|
|
1141
|
-
// min: 4.99,
|
|
1142
|
-
// max: 9999.99,
|
|
1143
|
-
// avg: 87.23,
|
|
1144
|
-
// sum: 420948.27,
|
|
1145
|
-
// count: 4823,
|
|
1146
|
-
// stddev: 143.2,
|
|
1147
|
-
// }
|
|
1148
|
-
```
|
|
877
|
+
---
|
|
1149
878
|
|
|
1150
|
-
###
|
|
879
|
+
### Stats
|
|
1151
880
|
|
|
1152
|
-
|
|
881
|
+
Statistical summary for a numeric field.
|
|
1153
882
|
|
|
1154
|
-
```
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
{ field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
|
|
1159
|
-
{ field: 'amount', name: 'maxOrder', aggregation: 'max' },
|
|
1160
|
-
{ field: 'userId', name: 'uniqueOrders', aggregation: 'count' },
|
|
1161
|
-
],
|
|
1162
|
-
dateRange: {
|
|
1163
|
-
start: new Date('2025-01-01').getTime(),
|
|
1164
|
-
end: Date.now(),
|
|
1165
|
-
},
|
|
883
|
+
```ts
|
|
884
|
+
const priceStats = await analytics.stats({
|
|
885
|
+
field: 'price',
|
|
886
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
1166
887
|
});
|
|
1167
|
-
// {
|
|
1168
|
-
// totalRevenue: 198432.50,
|
|
1169
|
-
// avgOrderValue: 87.23,
|
|
1170
|
-
// maxOrder: 9999.99,
|
|
1171
|
-
// uniqueOrders: 2275,
|
|
1172
|
-
// }
|
|
888
|
+
// → { min, max, avg, sum, count, stddev }
|
|
1173
889
|
```
|
|
1174
890
|
|
|
1175
|
-
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
### Records via BigQuery
|
|
1176
894
|
|
|
1177
|
-
Fetch
|
|
895
|
+
Fetch filtered records through the BigQuery engine instead of Firestore. Useful for large result sets or complex server-side filtering.
|
|
1178
896
|
|
|
1179
|
-
```
|
|
1180
|
-
const records = await analytics.records({
|
|
897
|
+
```ts
|
|
898
|
+
const records = await analytics.records<Order>({
|
|
1181
899
|
filters: [
|
|
1182
|
-
{ field: 'status',
|
|
1183
|
-
{ field: 'amount',
|
|
900
|
+
{ field: 'status', op: '==', value: 'paid' },
|
|
901
|
+
{ field: 'amount', op: '>=', value: 100 },
|
|
902
|
+
{ field: 'country', op: 'CONTAINS', value: 'US' },
|
|
1184
903
|
],
|
|
1185
|
-
selectFields: ['
|
|
1186
|
-
orderBy: '
|
|
904
|
+
selectFields: ['id', 'amount', 'country', 'createdAt'], // optional projection
|
|
905
|
+
orderBy: 'createdAt',
|
|
1187
906
|
order: 'desc',
|
|
1188
|
-
limit:
|
|
907
|
+
limit: 1000,
|
|
1189
908
|
offset: 0,
|
|
1190
909
|
dateRange: {
|
|
1191
910
|
start: new Date('2025-01-01').getTime(),
|
|
1192
|
-
end: Date.
|
|
911
|
+
end: new Date('2025-03-31').getTime(),
|
|
1193
912
|
},
|
|
1194
913
|
});
|
|
1195
914
|
```
|
|
1196
915
|
|
|
1197
|
-
|
|
916
|
+
Analytics filter operators: `==` `!=` `>` `<` `>=` `<=` `CONTAINS`
|
|
1198
917
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
```typescript
|
|
1202
|
-
const comparison = await analytics.crossBucket({
|
|
1203
|
-
bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
|
|
1204
|
-
field: 'amount',
|
|
1205
|
-
aggregation: 'sum',
|
|
1206
|
-
});
|
|
1207
|
-
// [
|
|
1208
|
-
// { bucket: 'orders-us', value: 120000 },
|
|
1209
|
-
// { bucket: 'orders-eu', value: 45000 },
|
|
1210
|
-
// { bucket: 'orders-apac', value: 33000 },
|
|
1211
|
-
// ]
|
|
1212
|
-
```
|
|
918
|
+
---
|
|
1213
919
|
|
|
1214
|
-
|
|
920
|
+
### Multi-Metric
|
|
1215
921
|
|
|
1216
|
-
|
|
922
|
+
Compute multiple aggregations in a single round-trip — perfect for dashboards.
|
|
1217
923
|
|
|
1218
|
-
```
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
924
|
+
```ts
|
|
925
|
+
const dashboard = await analytics.multiMetric({
|
|
926
|
+
metrics: [
|
|
927
|
+
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
928
|
+
{ field: 'amount', name: 'avgOrder', aggregation: 'avg' },
|
|
929
|
+
{ field: 'amount', name: 'maxOrder', aggregation: 'max' },
|
|
930
|
+
{ field: 'recordId', name: 'orderCount', aggregation: 'count' },
|
|
931
|
+
],
|
|
932
|
+
dateRange: { start: Date.now() - 30 * 24 * 60 * 60 * 1000 },
|
|
933
|
+
});
|
|
934
|
+
// → { totalRevenue: 48200, avgOrder: 96.4, maxOrder: 999, orderCount: 500 }
|
|
1227
935
|
```
|
|
1228
936
|
|
|
1229
|
-
|
|
937
|
+
---
|
|
1230
938
|
|
|
1231
|
-
|
|
939
|
+
### Storage Stats (Analytics)
|
|
1232
940
|
|
|
1233
|
-
|
|
1234
|
-
import type { AnalyticsQuery } from 'hydrousdb';
|
|
941
|
+
Record count and byte statistics for the bucket.
|
|
1235
942
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
dateRange: { start: Date
|
|
943
|
+
```ts
|
|
944
|
+
const storageInfo = await analytics.storageStats({
|
|
945
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
1239
946
|
});
|
|
1240
|
-
|
|
1241
|
-
console.log(result.queryType); // 'count'
|
|
1242
|
-
console.log(result.data); // { count: 142 }
|
|
947
|
+
// → { totalRecords, totalBytes, avgBytes, minBytes, maxBytes }
|
|
1243
948
|
```
|
|
1244
949
|
|
|
1245
950
|
---
|
|
1246
951
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
The SDK is written entirely in TypeScript and ships full type definitions. Use generics for end-to-end type safety.
|
|
952
|
+
### Cross-Bucket
|
|
1250
953
|
|
|
1251
|
-
|
|
1252
|
-
import { createClient } from 'hydrousdb';
|
|
1253
|
-
import type {
|
|
1254
|
-
HydrousConfig,
|
|
1255
|
-
RecordResult,
|
|
1256
|
-
QueryFilter,
|
|
1257
|
-
QueryOptions,
|
|
1258
|
-
QueryResult,
|
|
1259
|
-
CreateRecordOptions,
|
|
1260
|
-
BatchCreateOptions,
|
|
1261
|
-
UploadResult,
|
|
1262
|
-
UploadUrlResult,
|
|
1263
|
-
ListResult,
|
|
1264
|
-
FileMetadata,
|
|
1265
|
-
SignedUrlResult,
|
|
1266
|
-
BatchDownloadResult,
|
|
1267
|
-
StorageStats,
|
|
1268
|
-
DateRange,
|
|
1269
|
-
AnalyticsQuery,
|
|
1270
|
-
AnalyticsResult,
|
|
1271
|
-
CountResult,
|
|
1272
|
-
DistributionRow,
|
|
1273
|
-
SumRow,
|
|
1274
|
-
TimeSeriesRow,
|
|
1275
|
-
FieldTimeSeriesRow,
|
|
1276
|
-
TopNRow,
|
|
1277
|
-
FieldStats,
|
|
1278
|
-
MultiMetricResult,
|
|
1279
|
-
StorageStatsResult,
|
|
1280
|
-
CrossBucketRow,
|
|
1281
|
-
UserRecord,
|
|
1282
|
-
Session,
|
|
1283
|
-
AuthResult,
|
|
1284
|
-
ListUsersResult,
|
|
1285
|
-
} from 'hydrousdb';
|
|
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.
|
|
1286
955
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
}
|
|
956
|
+
```ts
|
|
957
|
+
const compare = await analytics.crossBucket({
|
|
958
|
+
bucketKeys: ['orders-2024', 'orders-2025'],
|
|
959
|
+
field: 'amount',
|
|
960
|
+
aggregation: 'sum',
|
|
961
|
+
dateRange: { start: new Date('2025-01-01').getTime() },
|
|
962
|
+
});
|
|
963
|
+
// → [{ bucket: 'orders-2024', value: 38200 }, { bucket: 'orders-2025', value: 52100 }]
|
|
964
|
+
```
|
|
1295
965
|
|
|
1296
|
-
|
|
1297
|
-
name: string;
|
|
1298
|
-
email: string;
|
|
1299
|
-
tier: 'free' | 'pro' | 'enterprise';
|
|
1300
|
-
credits: number;
|
|
1301
|
-
}
|
|
966
|
+
---
|
|
1302
967
|
|
|
1303
|
-
|
|
1304
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1305
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1306
|
-
storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
|
|
1307
|
-
});
|
|
968
|
+
### Raw Query
|
|
1308
969
|
|
|
1309
|
-
|
|
1310
|
-
const orders = db.records<Order>('orders');
|
|
1311
|
-
const customers = db.records<Customer>('customers');
|
|
970
|
+
Escape hatch for query types not covered by the typed helpers.
|
|
1312
971
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
{ queryableFields: ['status', 'country', 'customerId'] },
|
|
1316
|
-
);
|
|
972
|
+
```ts
|
|
973
|
+
import type { AnalyticsQuery, AnalyticsResult } from 'hydrousdb';
|
|
1317
974
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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',
|
|
983
|
+
});
|
|
1321
984
|
```
|
|
1322
985
|
|
|
1323
986
|
---
|
|
1324
987
|
|
|
1325
988
|
## Error Handling
|
|
1326
989
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
```typescript
|
|
990
|
+
```ts
|
|
1330
991
|
import {
|
|
1331
992
|
HydrousError,
|
|
1332
993
|
AuthError,
|
|
@@ -1338,215 +999,269 @@ import {
|
|
|
1338
999
|
} from 'hydrousdb';
|
|
1339
1000
|
|
|
1340
1001
|
try {
|
|
1341
|
-
|
|
1002
|
+
await db.auth().login({ email: 'x@x.com', password: 'wrong' });
|
|
1342
1003
|
} catch (err) {
|
|
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
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1343
1015
|
if (err instanceof NetworkError) {
|
|
1344
|
-
|
|
1345
|
-
console.
|
|
1346
|
-
|
|
1347
|
-
} else if (err instanceof AuthError) {
|
|
1348
|
-
// Bad key, expired session, insufficient permissions
|
|
1349
|
-
console.error(`Auth failed [${err.code}]:`, err.message);
|
|
1350
|
-
// err.code: INVALID_CREDENTIALS | ACCOUNT_LOCKED | INVALID_SESSION | FORBIDDEN | …
|
|
1351
|
-
|
|
1352
|
-
} else if (err instanceof ValidationError) {
|
|
1353
|
-
// Invalid input — check details array
|
|
1354
|
-
console.error('Validation failed:', err.details?.join(', '));
|
|
1355
|
-
|
|
1356
|
-
} else if (err instanceof RecordError) {
|
|
1357
|
-
// Record-specific API error
|
|
1358
|
-
console.error(`Record error [${err.code}]:`, err.message);
|
|
1359
|
-
|
|
1360
|
-
} else if (err instanceof StorageError) {
|
|
1361
|
-
// Storage-specific error
|
|
1362
|
-
console.error(`Storage error [${err.code}]:`, err.message);
|
|
1363
|
-
|
|
1364
|
-
} else if (err instanceof HydrousError) {
|
|
1365
|
-
// Any other API error
|
|
1366
|
-
console.error(`API error [${err.code}] ${err.status}:`, err.message);
|
|
1367
|
-
console.error('Request ID:', err.requestId); // include in support tickets
|
|
1016
|
+
console.log('No internet or server unreachable');
|
|
1017
|
+
console.log(err.cause); // original error
|
|
1368
1018
|
}
|
|
1369
1019
|
}
|
|
1370
1020
|
```
|
|
1371
1021
|
|
|
1372
|
-
|
|
1022
|
+
| Error class | When it's thrown |
|
|
1023
|
+
|---|---|
|
|
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 |
|
|
1027
|
+
| `StorageError` | Upload/download failures, file not found |
|
|
1028
|
+
| `AnalyticsError` | Invalid query, bucket not found |
|
|
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`. |
|
|
1373
1031
|
|
|
1374
|
-
|
|
1375
|
-
|---|---|---|
|
|
1376
|
-
| `message` | `string` | Human-readable description |
|
|
1377
|
-
| `code` | `string` | Machine-readable error code |
|
|
1378
|
-
| `status` | `number` | HTTP status code |
|
|
1379
|
-
| `requestId` | `string \| undefined` | Server request ID — include in support tickets |
|
|
1380
|
-
| `details` | `string[] \| undefined` | Validation error details |
|
|
1032
|
+
---
|
|
1381
1033
|
|
|
1382
|
-
|
|
1034
|
+
## TypeScript
|
|
1383
1035
|
|
|
1384
|
-
|
|
1385
|
-
|---|---|---|
|
|
1386
|
-
| `RECORD_NOT_FOUND` | 404 | The requested record ID does not exist |
|
|
1387
|
-
| `INVALID_CREDENTIALS` | 401 | Wrong email or password |
|
|
1388
|
-
| `ACCOUNT_LOCKED` | 403 | Account is temporarily locked |
|
|
1389
|
-
| `INVALID_SESSION` | 401 | Session expired or revoked — re-authenticate |
|
|
1390
|
-
| `MISSING_API_KEY` | 401 | Key not provided in headers |
|
|
1391
|
-
| `INVALID_SECURITY_KEY` | 401 | Key is wrong or has been revoked |
|
|
1392
|
-
| `FORBIDDEN` | 403 | Insufficient permissions for this operation |
|
|
1393
|
-
| `FILE_EXISTS` | 409 | File already exists — use `overwrite: true` |
|
|
1394
|
-
| `SYSTEM_BUCKET_FORBIDDEN` | 403 | Cannot query system buckets via analytics |
|
|
1395
|
-
| `CROSS_BUCKET_FORBIDDEN` | 403 | Key lacks read access to one of the requested buckets |
|
|
1396
|
-
| `VALIDATION_ERROR` | 400 | Invalid input — check `err.details` |
|
|
1397
|
-
| `NETWORK_ERROR` | — | Failed to reach the API |
|
|
1036
|
+
The SDK is written in TypeScript. Type your records for full autocomplete and safety:
|
|
1398
1037
|
|
|
1399
|
-
|
|
1038
|
+
```ts
|
|
1039
|
+
interface Order {
|
|
1040
|
+
customerId: string;
|
|
1041
|
+
amount: number;
|
|
1042
|
+
status: 'pending' | 'paid' | 'refunded';
|
|
1043
|
+
items: { sku: string; qty: number }[];
|
|
1044
|
+
}
|
|
1400
1045
|
|
|
1401
|
-
|
|
1046
|
+
const orders = db.records<Order>('orders');
|
|
1402
1047
|
|
|
1403
|
-
|
|
1048
|
+
const order = await orders.create({
|
|
1049
|
+
customerId: 'cust_123',
|
|
1050
|
+
amount: 49.99,
|
|
1051
|
+
status: 'pending',
|
|
1052
|
+
items: [{ sku: 'SHOE-42', qty: 1 }],
|
|
1053
|
+
});
|
|
1054
|
+
// order.status → 'pending' | 'paid' | 'refunded' ✓
|
|
1055
|
+
// order.id, order.createdAt, order.updatedAt are automatically added ✓
|
|
1404
1056
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
HYDROUS_STORAGE_MAIN=ssk_xxxxxxxxxxxxxxxxxxxx
|
|
1057
|
+
const result = await orders.query({
|
|
1058
|
+
filters: [{ field: 'status', op: '==', value: 'paid' }],
|
|
1059
|
+
});
|
|
1060
|
+
// result.records → (Order & RecordResult)[] ✓
|
|
1410
1061
|
```
|
|
1411
1062
|
|
|
1412
|
-
|
|
1413
|
-
|
|
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';
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## Security
|
|
1081
|
+
|
|
1082
|
+
- **Never commit API keys.** Use environment variables (`process.env.…`).
|
|
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.
|
|
1085
|
+
- **Files are private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for temporary external sharing.
|
|
1086
|
+
- **Use scoped storage** (`storage.scope('prefix/')`) to isolate files per user and prevent path-traversal bugs.
|
|
1087
|
+
|
|
1088
|
+
---
|
|
1089
|
+
|
|
1090
|
+
## Framework Examples
|
|
1091
|
+
|
|
1092
|
+
**Next.js (App Router)**
|
|
1093
|
+
```ts
|
|
1094
|
+
// lib/db.ts
|
|
1095
|
+
import { createClient } from 'hydrousdb';
|
|
1096
|
+
export const db = createClient({
|
|
1414
1097
|
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1415
1098
|
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1416
|
-
storageKeys:
|
|
1099
|
+
storageKeys: { main: process.env.HYDROUS_STORAGE_KEY! },
|
|
1417
1100
|
});
|
|
1418
|
-
```
|
|
1419
1101
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1102
|
+
// app/api/posts/route.ts
|
|
1103
|
+
import { db } from '@/lib/db';
|
|
1104
|
+
export async function GET() {
|
|
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
|
+
});
|
|
1111
|
+
return Response.json(records);
|
|
1112
|
+
}
|
|
1113
|
+
```
|
|
1423
1114
|
|
|
1424
|
-
**
|
|
1115
|
+
**React (client-side via your own API)**
|
|
1116
|
+
```ts
|
|
1117
|
+
// Always go through your backend — never put keys in React components
|
|
1118
|
+
const res = await fetch('/api/posts');
|
|
1119
|
+
const data = await res.json();
|
|
1120
|
+
```
|
|
1425
1121
|
|
|
1426
|
-
**
|
|
1122
|
+
**Vue / Nuxt**
|
|
1123
|
+
```ts
|
|
1124
|
+
// composables/useDb.ts
|
|
1125
|
+
import { createClient } from 'hydrousdb';
|
|
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
|
+
});
|
|
1131
|
+
```
|
|
1427
1132
|
|
|
1428
|
-
**
|
|
1133
|
+
**React Native**
|
|
1134
|
+
```ts
|
|
1135
|
+
import { createClient } from 'hydrousdb';
|
|
1136
|
+
// Works out of the box — uses the global fetch available in React Native
|
|
1137
|
+
const db = createClient({
|
|
1138
|
+
authKey: HYDROUS_AUTH_KEY,
|
|
1139
|
+
bucketSecurityKey: HYDROUS_BUCKET_KEY,
|
|
1140
|
+
storageKeys: { main: HYDROUS_STORAGE_KEY },
|
|
1141
|
+
});
|
|
1142
|
+
```
|
|
1429
1143
|
|
|
1430
1144
|
---
|
|
1431
1145
|
|
|
1432
|
-
##
|
|
1146
|
+
## API Reference
|
|
1433
1147
|
|
|
1434
|
-
### `createClient(config)`
|
|
1148
|
+
### `createClient(config)`
|
|
1435
1149
|
|
|
1436
|
-
|
|
|
1150
|
+
| Field | Type | Required | Description |
|
|
1437
1151
|
|---|---|---|---|
|
|
1438
|
-
| `authKey` | `string` |
|
|
1439
|
-
| `bucketSecurityKey` | `string` |
|
|
1440
|
-
| `storageKeys` | `
|
|
1441
|
-
| `baseUrl` | `string` | — | Override the API endpoint (
|
|
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) |
|
|
1442
1156
|
|
|
1443
1157
|
---
|
|
1444
1158
|
|
|
1445
|
-
### `db.records<T>(
|
|
1159
|
+
### `db.records<T>(bucket)` — all methods
|
|
1446
1160
|
|
|
1447
|
-
| Method |
|
|
1161
|
+
| Method | Returns | Description |
|
|
1448
1162
|
|---|---|---|
|
|
1449
|
-
| `create
|
|
1450
|
-
| `get` | `
|
|
1451
|
-
| `
|
|
1452
|
-
| `
|
|
1453
|
-
| `
|
|
1454
|
-
| `query` | `
|
|
1455
|
-
| `getAll
|
|
1456
|
-
| `
|
|
1457
|
-
| `
|
|
1458
|
-
| `batchDelete
|
|
1459
|
-
| `getHistory` | `
|
|
1460
|
-
| `
|
|
1461
|
-
|
|
1462
|
-
**`QueryOptions
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
```
|
|
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
|
+
|
|
1178
|
+
| Field | Type | Description |
|
|
1179
|
+
|---|---|---|
|
|
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` |
|
|
1478
1191
|
|
|
1479
1192
|
---
|
|
1480
1193
|
|
|
1481
|
-
### `db.auth(
|
|
1194
|
+
### `db.auth()` — all methods
|
|
1482
1195
|
|
|
1483
|
-
| Method |
|
|
1484
|
-
|
|
1485
|
-
| `signup
|
|
1486
|
-
| `login
|
|
1487
|
-
| `logout
|
|
1488
|
-
| `
|
|
1489
|
-
| `
|
|
1490
|
-
| `getUser
|
|
1491
|
-
| `updateUser
|
|
1492
|
-
| `changePassword
|
|
1493
|
-
| `requestPasswordReset` |
|
|
1494
|
-
| `confirmPasswordReset
|
|
1495
|
-
| `requestEmailVerification
|
|
1496
|
-
| `confirmEmailVerification` |
|
|
1497
|
-
| `
|
|
1498
|
-
| `
|
|
1499
|
-
| `
|
|
1500
|
-
| `
|
|
1501
|
-
| `hardDeleteUser
|
|
1502
|
-
| `bulkDeleteUsers` |
|
|
1196
|
+
| Method | Description |
|
|
1197
|
+
|---|---|
|
|
1198
|
+
| `signup(opts)` | Register + create session. Extra fields on `opts` are stored on the user. |
|
|
1199
|
+
| `login(opts)` | Authenticate + create session. |
|
|
1200
|
+
| `logout({ sessionId, allDevices? })` | Revoke one session or all sessions. |
|
|
1201
|
+
| `validateSession(sessionId)` | Check if a session is active; returns current user. |
|
|
1202
|
+
| `refreshSession(refreshToken)` | Get a new session from a refresh token. |
|
|
1203
|
+
| `getUser(userId)` | Fetch a user by ID. |
|
|
1204
|
+
| `updateUser(opts)` | Update profile fields. |
|
|
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. |
|
|
1503
1216
|
|
|
1504
1217
|
---
|
|
1505
1218
|
|
|
1506
|
-
### `db.storage(keyName)`
|
|
1219
|
+
### `db.storage(keyName)` — all methods
|
|
1507
1220
|
|
|
1508
|
-
| Method |
|
|
1509
|
-
|
|
1510
|
-
| `upload
|
|
1511
|
-
| `uploadRaw
|
|
1512
|
-
| `getUploadUrl
|
|
1513
|
-
| `uploadToSignedUrl
|
|
1514
|
-
| `confirmUpload
|
|
1515
|
-
| `getBatchUploadUrls` |
|
|
1516
|
-
| `batchConfirmUploads
|
|
1517
|
-
| `download
|
|
1518
|
-
| `batchDownload
|
|
1519
|
-
| `list
|
|
1520
|
-
| `getMetadata
|
|
1521
|
-
| `getSignedUrl
|
|
1522
|
-
| `setVisibility
|
|
1523
|
-
| `createFolder
|
|
1524
|
-
| `
|
|
1525
|
-
| `
|
|
1526
|
-
| `
|
|
1527
|
-
| `
|
|
1528
|
-
| `getStats
|
|
1529
|
-
| `info
|
|
1530
|
-
| `scope
|
|
1221
|
+
| Method | Description |
|
|
1222
|
+
|---|---|
|
|
1223
|
+
| `upload(data, path, opts?)` | Server-buffered upload (up to 500 MB). |
|
|
1224
|
+
| `uploadRaw(data, path, opts?)` | Upload a JS object or string as a file. |
|
|
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.
|
|
1531
1246
|
|
|
1532
1247
|
---
|
|
1533
1248
|
|
|
1534
|
-
### `db.analytics(
|
|
1249
|
+
### `db.analytics(bucket)` — all methods
|
|
1535
1250
|
|
|
1536
|
-
| Method |
|
|
1251
|
+
| Method | Returns | Description |
|
|
1537
1252
|
|---|---|---|
|
|
1538
|
-
| `count` | `
|
|
1539
|
-
| `distribution` | `
|
|
1540
|
-
| `sum` | `
|
|
1541
|
-
| `timeSeries
|
|
1542
|
-
| `fieldTimeSeries` | `
|
|
1543
|
-
| `topN` | `
|
|
1544
|
-
| `stats` | `
|
|
1545
|
-
| `records
|
|
1546
|
-
| `multiMetric` | `
|
|
1547
|
-
| `storageStats
|
|
1548
|
-
| `crossBucket` | `
|
|
1549
|
-
| `query` | `
|
|
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`. |
|
|
1550
1265
|
|
|
1551
1266
|
---
|
|
1552
1267
|
|
|
@@ -1555,24 +1270,17 @@ const db = createClient({
|
|
|
1555
1270
|
```bash
|
|
1556
1271
|
git clone https://github.com/hydrousdb/hydrousdb-js.git
|
|
1557
1272
|
cd hydrousdb-js
|
|
1558
|
-
|
|
1559
|
-
npm
|
|
1560
|
-
npm run build
|
|
1561
|
-
npm test # run the full test suite (68 tests)
|
|
1562
|
-
npm run test:watch # watch mode
|
|
1563
|
-
npm run lint # TypeScript type check
|
|
1273
|
+
npm install
|
|
1274
|
+
npm test # run tests
|
|
1275
|
+
npm run build # compile
|
|
1564
1276
|
```
|
|
1565
1277
|
|
|
1566
|
-
---
|
|
1567
|
-
|
|
1568
1278
|
## License
|
|
1569
1279
|
|
|
1570
|
-
MIT —
|
|
1280
|
+
MIT — [LICENSE](./LICENSE)
|
|
1571
1281
|
|
|
1572
1282
|
---
|
|
1573
1283
|
|
|
1574
1284
|
<p align="center">
|
|
1575
|
-
|
|
1576
|
-
Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
|
|
1577
|
-
<a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
|
|
1285
|
+
Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> · <a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
|
|
1578
1286
|
</p>
|