hydrousdb 3.2.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -1334
- package/dist/index.cjs +617 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +500 -222
- package/dist/index.d.ts +500 -222
- package/dist/index.mjs +612 -322
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,347 +1,68 @@
|
|
|
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>
|
|
7
|
-
|
|
8
|
-
<p align="center">
|
|
9
|
-
<a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/v/hydrousdb.svg" alt="npm version"></a>
|
|
10
|
-
<a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/dm/hydrousdb.svg" alt="npm downloads"></a>
|
|
11
|
-
<a href="https://github.com/hydrousdb/hydrousdb-js/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hydrousdb.svg" alt="MIT License"></a>
|
|
12
|
-
<a href="https://hydrousdb.com"><img src="https://img.shields.io/badge/docs-hydrousdb.com-blue" alt="Documentation"></a>
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Table of Contents
|
|
18
|
-
|
|
19
|
-
- [What is HydrousDB?](#what-is-hydrousdb)
|
|
20
|
-
- [How It Works](#how-it-works)
|
|
21
|
-
- [Quick Start](#quick-start)
|
|
22
|
-
- [Installation](#installation)
|
|
23
|
-
- [Module Formats — ESM & CommonJS](#module-formats--esm--commonjs)
|
|
24
|
-
- [Records](#records)
|
|
25
|
-
- [Create](#create-a-record)
|
|
26
|
-
- [Read](#read-a-record)
|
|
27
|
-
- [Update — patch vs set](#update-a-record)
|
|
28
|
-
- [Delete](#delete-a-record)
|
|
29
|
-
- [Query](#query-records)
|
|
30
|
-
- [Count](#count-records)
|
|
31
|
-
- [Batch Create](#batch-create)
|
|
32
|
-
- [Batch Delete](#batch-delete)
|
|
33
|
-
- [Version History](#version-history)
|
|
34
|
-
- [Write-Filter Sentinels](#write-filter-sentinels)
|
|
35
|
-
- [Custom Record IDs](#custom-record-ids)
|
|
36
|
-
- [Authentication](#authentication)
|
|
37
|
-
- [Sign Up](#sign-up)
|
|
38
|
-
- [Log In / Log Out](#log-in--log-out)
|
|
39
|
-
- [Session Management](#session-management)
|
|
40
|
-
- [Validate a Session](#validate-a-session)
|
|
41
|
-
- [Update Profile](#update-profile)
|
|
42
|
-
- [Change Password](#change-password)
|
|
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)
|
|
48
|
-
- [File Storage](#file-storage)
|
|
49
|
-
- [Simple Upload](#simple-upload)
|
|
50
|
-
- [Upload Raw JSON or Text](#upload-raw-json-or-text)
|
|
51
|
-
- [Large File Upload with Progress](#large-file-upload-with-progress)
|
|
52
|
-
- [Batch Upload](#batch-upload)
|
|
53
|
-
- [Download](#download)
|
|
54
|
-
- [Batch Download](#batch-download)
|
|
55
|
-
- [List Files](#list-files)
|
|
56
|
-
- [Scoped Storage](#scoped-storage)
|
|
57
|
-
- [File Metadata](#file-metadata)
|
|
58
|
-
- [Signed Share URLs](#signed-share-urls)
|
|
59
|
-
- [Visibility](#visibility)
|
|
60
|
-
- [Move, Copy, Delete](#move-copy-delete)
|
|
61
|
-
- [Storage Stats](#storage-stats)
|
|
62
|
-
- [Analytics](#analytics)
|
|
63
|
-
- [Count](#count-1)
|
|
64
|
-
- [Distribution](#distribution)
|
|
65
|
-
- [Sum](#sum)
|
|
66
|
-
- [Time Series](#time-series)
|
|
67
|
-
- [Field Time Series](#field-time-series)
|
|
68
|
-
- [Top N](#top-n)
|
|
69
|
-
- [Field Stats](#field-stats)
|
|
70
|
-
- [Multi-Metric Dashboard](#multi-metric-dashboard)
|
|
71
|
-
- [Filtered Records via BigQuery](#filtered-records-via-bigquery)
|
|
72
|
-
- [Cross-Bucket Comparison](#cross-bucket-comparison)
|
|
73
|
-
- [Storage Stats](#storage-stats-1)
|
|
74
|
-
- [Raw Query](#raw-query)
|
|
75
|
-
- [TypeScript](#typescript)
|
|
76
|
-
- [Error Handling](#error-handling)
|
|
77
|
-
- [Security Best Practices](#security-best-practices)
|
|
78
|
-
- [Full API Reference](#full-api-reference)
|
|
79
|
-
- [Contributing](#contributing)
|
|
80
|
-
- [License](#license)
|
|
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.
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
## Quick Start
|
|
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
|
|
3
|
+
**A database that doesn't choke on big JSON.** Store, query, and analyse massive records — with auth and file storage built in.
|
|
147
4
|
|
|
148
5
|
```bash
|
|
149
6
|
npm install hydrousdb
|
|
150
|
-
# or: yarn add hydrousdb / pnpm add hydrousdb
|
|
151
7
|
```
|
|
152
8
|
|
|
153
|
-
|
|
9
|
+
→ [Get your free API keys at hydrousdb.com](https://hydrousdb.com/dashboard)
|
|
154
10
|
|
|
155
|
-
|
|
11
|
+
---
|
|
156
12
|
|
|
157
|
-
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
158
16
|
import { createClient } from 'hydrousdb';
|
|
159
17
|
|
|
160
|
-
// Create once — reuse everywhere in your app
|
|
161
18
|
const db = createClient({
|
|
162
19
|
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
163
20
|
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
164
21
|
storageKeys: {
|
|
165
|
-
main: process.env.
|
|
22
|
+
main: process.env.HYDROUS_STORAGE_KEY!,
|
|
166
23
|
},
|
|
167
24
|
});
|
|
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
|
-
```
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Installation
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
npm install hydrousdb
|
|
194
25
|
```
|
|
195
26
|
|
|
196
|
-
|
|
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
|
-
```
|
|
27
|
+
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.
|
|
204
28
|
|
|
205
|
-
|
|
206
|
-
// CommonJS — legacy Node, Jest without transform, older tooling
|
|
207
|
-
const { createClient } = require('hydrousdb');
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Both exports are listed explicitly in `package.json` under the `exports` field:
|
|
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
|
-
```
|
|
29
|
+
Works in **React, Next.js, Vue, React Native, Node.js** — anywhere that runs modern JavaScript.
|
|
234
30
|
|
|
235
31
|
---
|
|
236
32
|
|
|
237
33
|
## Records
|
|
238
34
|
|
|
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
|
|
35
|
+
JSON objects stored in named buckets. Every record gets `id`, `createdAt`, and `updatedAt` automatically.
|
|
244
36
|
|
|
245
|
-
```
|
|
37
|
+
```ts
|
|
246
38
|
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
|
-
```
|
|
264
|
-
|
|
265
|
-
**With queryable fields** — fields you want to filter on server-side must be declared at write time:
|
|
266
39
|
|
|
267
|
-
|
|
40
|
+
// Create
|
|
268
41
|
const post = await posts.create(
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
status: 'draft',
|
|
272
|
-
authorId: 'usr_abc',
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
queryableFields: ['status', 'authorId'], // index these for filtering
|
|
276
|
-
userEmail: 'alice@example.com', // optional audit trail
|
|
277
|
-
},
|
|
42
|
+
{ title: 'Hello', status: 'draft', views: 0 },
|
|
43
|
+
{ queryableFields: ['status'] }, // declare fields you want to filter on
|
|
278
44
|
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
> 💡 **Why declare queryable fields?** HydrousDB stores records as compressed blobs. Fields you want to filter or sort by need to be registered in a lightweight index at write time. You only pay index overhead for the fields you actually query.
|
|
282
|
-
|
|
283
|
-
### Read a Record
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
|
-
// Path computed from the ID in memory — zero index reads
|
|
287
|
-
const post = await posts.get('260601-rec_01JA2XYZ');
|
|
288
|
-
|
|
289
|
-
// Throws HydrousError (code: RECORD_NOT_FOUND) if the ID doesn't exist
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Update a Record
|
|
293
|
-
|
|
294
|
-
**`patch(id, data)` — merge update.** Only the fields you provide are changed. All other fields on the record are left untouched.
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
const updated = await posts.patch('260601-rec_01JA2XYZ', {
|
|
298
|
-
status: 'published',
|
|
299
|
-
views: 1,
|
|
300
|
-
});
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**`set(id, data)` — full replace.** The entire record is replaced with the new data.
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
const replaced = await posts.set('260601-rec_01JA2XYZ', {
|
|
307
|
-
title: 'Updated Title',
|
|
308
|
-
body: 'New content.',
|
|
309
|
-
status: 'published',
|
|
310
|
-
views: 42,
|
|
311
|
-
});
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
**Disable merge** (force field removal):
|
|
45
|
+
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
315
46
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
await posts.patch('260601-rec_01JA2XYZ', { status: 'archived' }, { merge: false });
|
|
319
|
-
```
|
|
47
|
+
// Read (path computed from ID — zero index lookups)
|
|
48
|
+
const found = await posts.get(post.id);
|
|
320
49
|
|
|
321
|
-
|
|
50
|
+
// Update (only the fields you pass are changed)
|
|
51
|
+
await posts.patch(post.id, { status: 'published' });
|
|
322
52
|
|
|
323
|
-
|
|
324
|
-
await posts.delete(
|
|
53
|
+
// Delete
|
|
54
|
+
await posts.delete(post.id);
|
|
325
55
|
```
|
|
326
56
|
|
|
327
|
-
|
|
57
|
+
> **Why `queryableFields`?** Records are stored as compressed blobs. Fields you want to filter or sort by must be registered at write time. You only pay overhead for what you actually query.
|
|
328
58
|
|
|
329
|
-
|
|
330
|
-
// All records (up to 100 by default)
|
|
331
|
-
const { records } = await posts.query();
|
|
59
|
+
### Query
|
|
332
60
|
|
|
333
|
-
|
|
334
|
-
const { records: published } = await posts.query({
|
|
335
|
-
filters: [
|
|
336
|
-
{ field: 'status', op: '==', value: 'published' },
|
|
337
|
-
],
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Multiple filters, sort, limit
|
|
61
|
+
```ts
|
|
341
62
|
const { records, hasMore, nextCursor } = await posts.query({
|
|
342
63
|
filters: [
|
|
343
|
-
{ field: 'status',
|
|
344
|
-
{ field: 'views',
|
|
64
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
65
|
+
{ field: 'views', op: '>', value: 100 },
|
|
345
66
|
],
|
|
346
67
|
orderBy: 'createdAt',
|
|
347
68
|
order: 'desc',
|
|
@@ -350,374 +71,167 @@ const { records, hasMore, nextCursor } = await posts.query({
|
|
|
350
71
|
|
|
351
72
|
// Next page
|
|
352
73
|
if (hasMore) {
|
|
353
|
-
const page2 = await posts.query({
|
|
354
|
-
orderBy: 'createdAt',
|
|
355
|
-
order: 'desc',
|
|
356
|
-
limit: 20,
|
|
357
|
-
startAfter: nextCursor,
|
|
358
|
-
});
|
|
74
|
+
const page2 = await posts.query({ limit: 20, startAfter: nextCursor });
|
|
359
75
|
}
|
|
360
|
-
|
|
361
|
-
// Select only specific fields (reduces payload size)
|
|
362
|
-
const { records: light } = await posts.query({
|
|
363
|
-
fields: 'id,title,status,createdAt',
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Date range
|
|
367
|
-
const { records: thisWeek } = await posts.query({
|
|
368
|
-
dateRange: {
|
|
369
|
-
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
370
|
-
end: Date.now(),
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
76
|
```
|
|
374
77
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
| Operator | Meaning |
|
|
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 |
|
|
386
|
-
|
|
387
|
-
> ⚠️ You can only filter on fields declared as `queryableFields` when the record was created. Filtering on an un-indexed field returns no results.
|
|
78
|
+
Filter operators: `==` `!=` `>` `<` `>=` `<=` `CONTAINS`
|
|
388
79
|
|
|
389
|
-
###
|
|
80
|
+
### Atomic field updates
|
|
390
81
|
|
|
391
|
-
|
|
392
|
-
// Total records in the bucket
|
|
393
|
-
const total = await posts.count();
|
|
82
|
+
Avoid race conditions with server-side operations inside `patch()`:
|
|
394
83
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
|
|
84
|
+
```ts
|
|
85
|
+
await posts.patch(post.id, {
|
|
86
|
+
views: { __op: 'increment', delta: 1 }, // add 1
|
|
87
|
+
credits: { __op: 'decrement', delta: 5 }, // subtract 5
|
|
88
|
+
slug: { __op: 'setOnce', value: 'my-post' }, // set only if empty
|
|
89
|
+
tags: { __op: 'appendUnique', item: 'featured' }, // add to array, no dupes
|
|
90
|
+
tags: { __op: 'removeFromArray', item: 'draft' }, // remove from array
|
|
91
|
+
rating: { __op: 'clamp', value: 6, min: 0, max: 5 },
|
|
92
|
+
price: { __op: 'multiplyBy', factor: 1.1 },
|
|
93
|
+
active: { __op: 'toggleBool' },
|
|
94
|
+
syncedAt: { __op: 'serverTimestamp' },
|
|
95
|
+
} as any);
|
|
399
96
|
```
|
|
400
97
|
|
|
401
|
-
### Batch
|
|
402
|
-
|
|
403
|
-
Up to 500 records per call.
|
|
98
|
+
### Batch operations
|
|
404
99
|
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
{ title: 'Post C', status: 'published' },
|
|
411
|
-
],
|
|
412
|
-
{
|
|
413
|
-
queryableFields: ['status'],
|
|
414
|
-
userEmail: 'alice@example.com',
|
|
415
|
-
},
|
|
100
|
+
```ts
|
|
101
|
+
// Create up to 500 records at once
|
|
102
|
+
const { results, errors } = await posts.batchCreate(
|
|
103
|
+
[{ title: 'A' }, { title: 'B' }],
|
|
104
|
+
{ queryableFields: ['title'] },
|
|
416
105
|
);
|
|
417
|
-
// → [{ id: '…', title: 'Post A', … }, { id: '…', title: 'Post B', … }, …]
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Each record in the batch can optionally carry a `_customRecordId` for upsert behaviour:
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
await posts.batchCreate([
|
|
424
|
-
{ _customRecordId: '260601-post_welcome', title: 'Welcome', status: 'published' },
|
|
425
|
-
{ title: 'Auto-ID post', status: 'draft' },
|
|
426
|
-
]);
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
### Batch Delete
|
|
430
|
-
|
|
431
|
-
Up to 500 records per call.
|
|
432
106
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
'
|
|
436
|
-
'
|
|
437
|
-
'260601-rec_03JC',
|
|
107
|
+
// Update up to 500 records at once
|
|
108
|
+
await posts.batchUpdate([
|
|
109
|
+
{ recordId: 'id1', values: { status: 'archived' } },
|
|
110
|
+
{ recordId: 'id2', values: { status: 'archived' } },
|
|
438
111
|
]);
|
|
439
112
|
|
|
440
|
-
|
|
113
|
+
// Delete up to 500 records at once
|
|
114
|
+
const { successful, failed } = await posts.batchDelete(['id1', 'id2']);
|
|
441
115
|
```
|
|
442
116
|
|
|
443
|
-
### Version
|
|
117
|
+
### Version history
|
|
444
118
|
|
|
445
|
-
Every write
|
|
119
|
+
Every write is automatically versioned.
|
|
446
120
|
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// [{ id, version: 3, createdAt, data }, { version: 2, … }, { version: 1, … }]
|
|
121
|
+
```ts
|
|
122
|
+
const history = await posts.getHistory(post.id);
|
|
123
|
+
// [{ generation, savedAt, savedBy, sizeBytes }, …]
|
|
451
124
|
|
|
452
|
-
//
|
|
453
|
-
const
|
|
125
|
+
// Read a specific past version
|
|
126
|
+
const v1 = await posts.getVersion(post.id, history[0].generation!);
|
|
454
127
|
```
|
|
455
128
|
|
|
456
|
-
###
|
|
129
|
+
### Custom record IDs
|
|
457
130
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
await posts.patch('260601-rec_01JA', {
|
|
462
|
-
// Increment / decrement a numeric field atomically
|
|
463
|
-
views: { __op: 'increment', delta: 1 },
|
|
464
|
-
credits: { __op: 'decrement', delta: 5 },
|
|
465
|
-
|
|
466
|
-
// Set a field only if it doesn't already have a value
|
|
467
|
-
slug: { __op: 'setOnce', value: 'my-first-post' },
|
|
468
|
-
|
|
469
|
-
// Set a field only if a condition is met
|
|
470
|
-
discount: { __op: 'setIf', value: 10, cond: { op: '>=', value: 100 } },
|
|
471
|
-
|
|
472
|
-
// Add to an array (no duplicates)
|
|
473
|
-
tags: { __op: 'appendUnique', item: 'featured' },
|
|
474
|
-
|
|
475
|
-
// Remove from an array
|
|
476
|
-
tags: { __op: 'removeFromArray', item: 'draft' },
|
|
477
|
-
|
|
478
|
-
// Clamp a numeric value between min and max
|
|
479
|
-
rating: { __op: 'clamp', value: 6, min: 0, max: 5 },
|
|
480
|
-
|
|
481
|
-
// Multiply a numeric field
|
|
482
|
-
price: { __op: 'multiplyBy', factor: 1.1 },
|
|
483
|
-
|
|
484
|
-
// Flip a boolean
|
|
485
|
-
active: { __op: 'toggleBool' },
|
|
486
|
-
|
|
487
|
-
// Set field to the server's current timestamp
|
|
488
|
-
lastSeen: { __op: 'serverTimestamp' },
|
|
489
|
-
} as any);
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### Custom Record IDs
|
|
493
|
-
|
|
494
|
-
Provide your own ID instead of using an auto-generated one. If the ID already exists, the record is upserted.
|
|
495
|
-
|
|
496
|
-
```typescript
|
|
497
|
-
// Single record
|
|
131
|
+
```ts
|
|
132
|
+
// If the ID already exists, the record is upserted
|
|
498
133
|
const post = await posts.create(
|
|
499
|
-
{ title: 'Welcome'
|
|
134
|
+
{ title: 'Welcome' },
|
|
500
135
|
{ customRecordId: '260601-post_welcome' },
|
|
501
136
|
);
|
|
502
|
-
|
|
503
|
-
// Batch — set _customRecordId on individual items
|
|
504
|
-
await posts.batchCreate([
|
|
505
|
-
{ _customRecordId: '260601-post_welcome', title: 'Welcome' },
|
|
506
|
-
{ title: 'Auto-ID post' },
|
|
507
|
-
]);
|
|
508
137
|
```
|
|
509
138
|
|
|
510
|
-
Custom IDs must match `^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$`.
|
|
511
|
-
|
|
512
139
|
---
|
|
513
140
|
|
|
514
|
-
##
|
|
141
|
+
## Auth
|
|
515
142
|
|
|
516
|
-
|
|
143
|
+
A complete user system — signup, login, sessions, password reset, email verification, and admin controls.
|
|
517
144
|
|
|
518
|
-
```
|
|
519
|
-
const auth = db.auth(
|
|
145
|
+
```ts
|
|
146
|
+
const auth = db.auth();
|
|
520
147
|
```
|
|
521
148
|
|
|
522
|
-
### Sign
|
|
149
|
+
### Sign up & log in
|
|
523
150
|
|
|
524
|
-
```
|
|
151
|
+
```ts
|
|
152
|
+
// Sign up — extra fields beyond email/password are stored on the user
|
|
525
153
|
const { user, session } = await auth.signup({
|
|
526
154
|
email: 'alice@example.com',
|
|
527
|
-
password: 'hunter2',
|
|
528
|
-
fullName: 'Alice
|
|
529
|
-
//
|
|
530
|
-
plan: 'pro',
|
|
531
|
-
referral: 'friend123',
|
|
155
|
+
password: 'hunter2',
|
|
156
|
+
fullName: 'Alice Smith',
|
|
157
|
+
plan: 'pro', // any custom fields you want
|
|
532
158
|
});
|
|
533
159
|
|
|
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
160
|
// Log in
|
|
547
161
|
const { user, session } = await auth.login({
|
|
548
162
|
email: 'alice@example.com',
|
|
549
163
|
password: 'hunter2',
|
|
550
164
|
});
|
|
551
165
|
|
|
552
|
-
// Log out
|
|
166
|
+
// Log out (pass allDevices: true to log out everywhere)
|
|
553
167
|
await auth.logout({ sessionId: session.sessionId });
|
|
554
|
-
|
|
555
|
-
// Log out from all devices at once
|
|
556
|
-
await auth.logout({ sessionId: session.sessionId, allDevices: true });
|
|
557
168
|
```
|
|
558
169
|
|
|
559
|
-
|
|
170
|
+
Store `session.sessionId` and `session.refreshToken` in your app. Sessions expire after **24 hours**, refresh tokens after **30 days**.
|
|
560
171
|
|
|
561
|
-
|
|
172
|
+
### Session management
|
|
562
173
|
|
|
563
|
-
```
|
|
564
|
-
//
|
|
565
|
-
const
|
|
566
|
-
refreshToken: session.refreshToken,
|
|
567
|
-
});
|
|
568
|
-
// Store newSession.sessionId and newSession.refreshToken
|
|
174
|
+
```ts
|
|
175
|
+
// Check if a session is still valid (use on your backend)
|
|
176
|
+
const { user } = await auth.validateSession(session.sessionId);
|
|
569
177
|
|
|
570
|
-
// Get a
|
|
571
|
-
const
|
|
178
|
+
// Get a new session using the refresh token
|
|
179
|
+
const newSession = await auth.refreshSession(session.refreshToken);
|
|
572
180
|
```
|
|
573
181
|
|
|
574
|
-
###
|
|
182
|
+
### User profile
|
|
575
183
|
|
|
576
|
-
|
|
184
|
+
```ts
|
|
185
|
+
const user = await auth.getUser(session.userId);
|
|
577
186
|
|
|
578
|
-
|
|
579
|
-
const { user, session: activeSession } = await auth.validateSession({
|
|
580
|
-
sessionId: session.sessionId,
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
console.log(user.id); // "usr_xxxxxxxxxxxx"
|
|
584
|
-
console.log(activeSession.expiresAt); // timestamp
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Update Profile
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
const updated = await auth.updateUser({
|
|
187
|
+
await auth.updateUser({
|
|
591
188
|
sessionId: session.sessionId,
|
|
592
189
|
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
|
-
},
|
|
190
|
+
updates: { fullName: 'Alice Johnson', plan: 'enterprise' },
|
|
599
191
|
});
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
> ⚠️ The `updates` key is required — it wraps the fields to change. Fields not included in `updates` are left untouched.
|
|
603
192
|
|
|
604
|
-
|
|
193
|
+
await auth.deleteUser(session.sessionId, user.id);
|
|
194
|
+
```
|
|
605
195
|
|
|
606
|
-
|
|
196
|
+
### Password & email
|
|
607
197
|
|
|
608
|
-
```
|
|
198
|
+
```ts
|
|
199
|
+
// Change password (requires active session)
|
|
609
200
|
await auth.changePassword({
|
|
610
201
|
sessionId: session.sessionId,
|
|
611
202
|
userId: user.id,
|
|
612
203
|
currentPassword: 'hunter2',
|
|
613
204
|
newPassword: 'correcthorsebatterystaple',
|
|
614
205
|
});
|
|
615
|
-
// All existing sessions for this user are automatically revoked
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Password Reset Flow
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
// 1. User requests a reset — always returns success (prevents email enumeration)
|
|
622
|
-
await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
623
206
|
|
|
624
|
-
//
|
|
207
|
+
// Forgot password flow
|
|
208
|
+
await auth.requestPasswordReset('alice@example.com');
|
|
209
|
+
await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');
|
|
625
210
|
|
|
626
|
-
//
|
|
627
|
-
await auth.
|
|
628
|
-
|
|
629
|
-
newPassword: 'correcthorsebatterystaple',
|
|
630
|
-
});
|
|
631
|
-
// All existing sessions are automatically revoked
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### Email Verification
|
|
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' });
|
|
211
|
+
// Email verification
|
|
212
|
+
await auth.requestEmailVerification(user.id);
|
|
213
|
+
await auth.confirmEmailVerification(tokenFromEmail);
|
|
644
214
|
```
|
|
645
215
|
|
|
646
|
-
### Admin
|
|
216
|
+
### Admin
|
|
647
217
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
```typescript
|
|
651
|
-
// Paginated list — uses cursor-based pagination
|
|
218
|
+
```ts
|
|
219
|
+
// List all users (paginated)
|
|
652
220
|
const { users, hasMore, nextCursor } = await auth.listUsers({
|
|
653
221
|
sessionId: adminSession.sessionId,
|
|
654
222
|
limit: 50,
|
|
655
223
|
});
|
|
656
224
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
limit: 50,
|
|
661
|
-
cursor: nextCursor!,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
```
|
|
225
|
+
// Lock / unlock an account
|
|
226
|
+
await auth.lockAccount({ sessionId: adminSession.sessionId, userId: user.id });
|
|
227
|
+
await auth.unlockAccount(adminSession.sessionId, user.id);
|
|
665
228
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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)
|
|
686
|
-
const { lockedUntil, unlockTime } = await auth.lockAccount({
|
|
687
|
-
sessionId: adminSession.sessionId,
|
|
688
|
-
userId: 'usr_abc123',
|
|
689
|
-
duration: 60 * 60 * 1000, // 1 hour in ms (default: 15 minutes)
|
|
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',
|
|
698
|
-
});
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
### Admin — Delete Users
|
|
702
|
-
|
|
703
|
-
```typescript
|
|
704
|
-
// Soft-delete — marks the account as deleted, keeps the data
|
|
705
|
-
await auth.deleteUser({
|
|
706
|
-
sessionId: adminSession.sessionId,
|
|
707
|
-
userId: 'usr_abc123',
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
// Hard-delete — permanent, irreversible
|
|
711
|
-
await auth.hardDeleteUser({
|
|
712
|
-
sessionId: adminSession.sessionId,
|
|
713
|
-
userId: 'usr_abc123',
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Bulk delete — up to 500 users, soft or hard
|
|
717
|
-
const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
229
|
+
// Delete users
|
|
230
|
+
await auth.hardDeleteUser(adminSession.sessionId, user.id);
|
|
231
|
+
await auth.bulkDeleteUsers({
|
|
718
232
|
sessionId: adminSession.sessionId,
|
|
719
|
-
userIds: ['
|
|
720
|
-
hard:
|
|
233
|
+
userIds: ['id1', 'id2'],
|
|
234
|
+
hard: true,
|
|
721
235
|
});
|
|
722
236
|
```
|
|
723
237
|
|
|
@@ -725,828 +239,318 @@ const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
|
725
239
|
|
|
726
240
|
## File Storage
|
|
727
241
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
const main = db.storage('main');
|
|
732
|
-
const avatars = db.storage('avatars');
|
|
733
|
-
const documents = db.storage('documents');
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### Simple Upload
|
|
737
|
-
|
|
738
|
-
Server-buffered upload for files up to **500 MB**. No progress bar — use [Large File Upload](#large-file-upload-with-progress) if you need one.
|
|
739
|
-
|
|
740
|
-
```typescript
|
|
741
|
-
// Browser — from a file input
|
|
742
|
-
const file = document.querySelector('input[type="file"]').files[0];
|
|
743
|
-
const result = await db.storage('main').upload(file, `uploads/${file.name}`, {
|
|
744
|
-
isPublic: true, // publicly accessible without auth (default: false)
|
|
745
|
-
overwrite: false, // throw if file already exists (default: false)
|
|
746
|
-
mimeType: 'image/jpeg', // optional — auto-detected from extension if omitted
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
console.log(result.publicUrl); // CDN URL — use anywhere
|
|
750
|
-
console.log(result.downloadUrl); // null when isPublic: true
|
|
751
|
-
console.log(result.size); // bytes
|
|
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
|
|
242
|
+
```ts
|
|
243
|
+
const storage = db.storage('main');
|
|
759
244
|
```
|
|
760
245
|
|
|
761
|
-
### Upload
|
|
246
|
+
### Upload
|
|
762
247
|
|
|
763
|
-
```
|
|
764
|
-
//
|
|
765
|
-
const result = await
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
{ isPublic: false },
|
|
769
|
-
);
|
|
248
|
+
```ts
|
|
249
|
+
// Simple upload (up to 500 MB)
|
|
250
|
+
const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
|
|
251
|
+
console.log(result.publicUrl); // permanent CDN URL
|
|
252
|
+
console.log(result.downloadUrl); // authenticated download URL (if private)
|
|
770
253
|
|
|
771
|
-
// Upload a
|
|
772
|
-
await
|
|
773
|
-
'<html><body>Hello</body></html>',
|
|
774
|
-
'exports/page.html',
|
|
775
|
-
{ mimeType: 'text/html', isPublic: true },
|
|
776
|
-
);
|
|
254
|
+
// Upload a JSON object or string directly
|
|
255
|
+
await storage.uploadRaw({ theme: 'dark' }, 'config.json');
|
|
777
256
|
```
|
|
778
257
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
For files over ~10 MB or when you need a progress bar. The file goes **directly to GCS** — your server never buffers the bytes.
|
|
782
|
-
|
|
783
|
-
```typescript
|
|
784
|
-
const storage = db.storage('main');
|
|
258
|
+
**Large files with progress tracking** (recommended for anything > 10 MB):
|
|
785
259
|
|
|
786
|
-
|
|
260
|
+
```ts
|
|
261
|
+
// Step 1 — get a signed upload URL
|
|
787
262
|
const { uploadUrl, path } = await storage.getUploadUrl({
|
|
788
|
-
path:
|
|
789
|
-
mimeType: 'video/mp4',
|
|
790
|
-
size: file.size,
|
|
791
|
-
isPublic: true,
|
|
792
|
-
expiresInSeconds: 900, // how long the signed URL is valid (default: 900 = 15 min)
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Step 2 — Upload directly to GCS with real progress
|
|
796
|
-
await storage.uploadToSignedUrl(
|
|
797
|
-
uploadUrl,
|
|
798
|
-
file,
|
|
799
|
-
'video/mp4',
|
|
800
|
-
(percent) => {
|
|
801
|
-
progressBar.style.width = `${percent}%`;
|
|
802
|
-
console.log(`${percent}% uploaded`);
|
|
803
|
-
},
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
// Step 3 — Confirm the upload (registers metadata on the server)
|
|
807
|
-
const result = await storage.confirmUpload({
|
|
808
|
-
path: path,
|
|
263
|
+
path: 'videos/intro.mp4',
|
|
809
264
|
mimeType: 'video/mp4',
|
|
265
|
+
size: file.size,
|
|
810
266
|
isPublic: true,
|
|
811
267
|
});
|
|
812
268
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
### Batch Upload
|
|
817
|
-
|
|
818
|
-
Upload up to 50 files at once.
|
|
819
|
-
|
|
820
|
-
```typescript
|
|
821
|
-
const storage = db.storage('main');
|
|
822
|
-
|
|
823
|
-
// Step 1 — Get signed URLs for all files
|
|
824
|
-
const { files } = await storage.getBatchUploadUrls([
|
|
825
|
-
{ path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
|
|
826
|
-
{ path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
|
|
827
|
-
{ path: 'gallery/photo3.png', mimeType: 'image/png', size: 98304, isPublic: true },
|
|
828
|
-
]);
|
|
829
|
-
|
|
830
|
-
// Step 2 — Upload each directly to GCS
|
|
831
|
-
for (const f of files) {
|
|
832
|
-
await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
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 })),
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
console.log(`${succeeded.length} uploaded, ${failed.length} failed`);
|
|
841
|
-
for (const f of succeeded) {
|
|
842
|
-
console.log(f.publicUrl);
|
|
843
|
-
}
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
### Download
|
|
847
|
-
|
|
848
|
-
```typescript
|
|
849
|
-
// Private files require authentication — returns ArrayBuffer
|
|
850
|
-
const buffer = await db.storage('documents').download('reports/q3.pdf');
|
|
851
|
-
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
852
|
-
|
|
853
|
-
// Trigger browser download
|
|
854
|
-
const url = URL.createObjectURL(blob);
|
|
855
|
-
const a = document.createElement('a');
|
|
856
|
-
a.href = url;
|
|
857
|
-
a.download = 'q3.pdf';
|
|
858
|
-
a.click();
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
> 💡 **Public files:** Use `result.publicUrl` directly — no SDK call needed. `<img src={result.publicUrl} />` just works.
|
|
862
|
-
|
|
863
|
-
### Batch Download
|
|
864
|
-
|
|
865
|
-
Download up to 20 files in one call. Content is returned as base64 strings.
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
const { succeeded, failed } = await db.storage('documents').batchDownload([
|
|
869
|
-
'reports/q1.pdf',
|
|
870
|
-
'reports/q2.pdf',
|
|
871
|
-
'reports/q3.pdf',
|
|
872
|
-
]);
|
|
873
|
-
|
|
874
|
-
for (const f of succeeded) {
|
|
875
|
-
console.log(f.path); // 'reports/q1.pdf'
|
|
876
|
-
console.log(f.mimeType); // 'application/pdf'
|
|
877
|
-
console.log(f.size); // bytes
|
|
878
|
-
const bytes = Buffer.from(f.content, 'base64');
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
for (const f of failed) {
|
|
882
|
-
console.error(`${f.path}: ${f.error}`);
|
|
883
|
-
}
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### List Files
|
|
887
|
-
|
|
888
|
-
```typescript
|
|
889
|
-
const storage = db.storage('main');
|
|
890
|
-
|
|
891
|
-
// List everything at the root
|
|
892
|
-
const { files, folders, hasMore, nextCursor } = await storage.list();
|
|
893
|
-
|
|
894
|
-
// List a specific folder
|
|
895
|
-
const result = await storage.list({
|
|
896
|
-
prefix: 'gallery/',
|
|
897
|
-
limit: 50,
|
|
269
|
+
// Step 2 — upload directly to GCS (supports onProgress in browsers)
|
|
270
|
+
await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => {
|
|
271
|
+
console.log(`${pct}% uploaded`);
|
|
898
272
|
});
|
|
899
273
|
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
const page2 = await storage.list({
|
|
903
|
-
prefix: 'gallery/',
|
|
904
|
-
cursor: result.nextCursor,
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
```
|
|
908
|
-
|
|
909
|
-
Each file entry:
|
|
910
|
-
|
|
911
|
-
```typescript
|
|
912
|
-
{
|
|
913
|
-
name: 'photo1.jpg',
|
|
914
|
-
path: 'gallery/photo1.jpg',
|
|
915
|
-
size: 204800,
|
|
916
|
-
mimeType: 'image/jpeg',
|
|
917
|
-
isPublic: true,
|
|
918
|
-
publicUrl: 'https://storage.googleapis.com/…',
|
|
919
|
-
downloadUrl: null,
|
|
920
|
-
updatedAt: '2025-06-01T12:00:00.000Z',
|
|
921
|
-
}
|
|
274
|
+
// Step 3 — confirm and get the final result
|
|
275
|
+
const result = await storage.confirmUpload({ path, mimeType: 'video/mp4', isPublic: true });
|
|
922
276
|
```
|
|
923
277
|
|
|
924
|
-
###
|
|
925
|
-
|
|
926
|
-
Pre-fix all operations to a folder — great for per-user isolation.
|
|
278
|
+
### Download, list, manage
|
|
927
279
|
|
|
928
|
-
```
|
|
929
|
-
|
|
280
|
+
```ts
|
|
281
|
+
// Download a private file
|
|
282
|
+
const buffer = await storage.download('private/doc.pdf');
|
|
930
283
|
|
|
931
|
-
//
|
|
932
|
-
await
|
|
284
|
+
// List files
|
|
285
|
+
const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/' });
|
|
933
286
|
|
|
934
|
-
//
|
|
935
|
-
const
|
|
287
|
+
// Get file metadata
|
|
288
|
+
const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
936
289
|
|
|
937
|
-
//
|
|
938
|
-
await
|
|
290
|
+
// Time-limited share link (no login needed to access)
|
|
291
|
+
const { signedUrl } = await storage.getSignedUrl('private/report.pdf', 3600);
|
|
939
292
|
|
|
940
|
-
//
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
`ScopedStorage` has the same full API as `StorageManager` — every method listed in the reference is available.
|
|
946
|
-
|
|
947
|
-
### File Metadata
|
|
948
|
-
|
|
949
|
-
```typescript
|
|
950
|
-
const meta = await db.storage('documents').getMetadata('reports/q3.pdf');
|
|
951
|
-
|
|
952
|
-
console.log(meta.size); // bytes
|
|
953
|
-
console.log(meta.mimeType); // 'application/pdf'
|
|
954
|
-
console.log(meta.isPublic); // false
|
|
955
|
-
console.log(meta.downloadUrl); // auth-required URL
|
|
956
|
-
console.log(meta.createdAt); // ISO string
|
|
957
|
-
console.log(meta.updatedAt); // ISO string
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
### Signed Share URLs
|
|
961
|
-
|
|
962
|
-
Generate a time-limited link for a private file — no `X-Storage-Key` required to use it.
|
|
963
|
-
|
|
964
|
-
```typescript
|
|
965
|
-
const { signedUrl, expiresAt, expiresIn } = await db.storage('documents')
|
|
966
|
-
.getSignedUrl('reports/q3.pdf', 3600); // expires in 1 hour (default)
|
|
967
|
-
|
|
968
|
-
// Share signedUrl with the recipient — it expires automatically
|
|
969
|
-
console.log(`Link valid until: ${new Date(expiresAt).toLocaleString()}`);
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
> ⚠️ Downloads via signed URLs bypass the server — **download stats are not tracked** for those requests. Use `downloadUrl` for tracked downloads.
|
|
973
|
-
|
|
974
|
-
### Visibility
|
|
975
|
-
|
|
976
|
-
```typescript
|
|
977
|
-
// Make a private file public
|
|
978
|
-
const result = await db.storage('main').setVisibility('docs/report.pdf', true);
|
|
979
|
-
console.log(result.publicUrl); // now has a CDN URL
|
|
293
|
+
// Move, copy, delete
|
|
294
|
+
await storage.move('old/path.jpg', 'new/path.jpg');
|
|
295
|
+
await storage.copy('template.html', 'pages/home.html');
|
|
296
|
+
await storage.deleteFile('avatars/old.jpg');
|
|
297
|
+
await storage.deleteFolder('temp/');
|
|
980
298
|
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
console.log(result2.downloadUrl); // now requires auth
|
|
299
|
+
// Change visibility after upload
|
|
300
|
+
await storage.setVisibility('alice.jpg', false); // make private
|
|
984
301
|
```
|
|
985
302
|
|
|
986
|
-
###
|
|
987
|
-
|
|
988
|
-
```typescript
|
|
989
|
-
const storage = db.storage('main');
|
|
990
|
-
|
|
991
|
-
// Rename a file
|
|
992
|
-
await storage.move('drafts/report.pdf', 'published/report-2025.pdf');
|
|
993
|
-
|
|
994
|
-
// Move to a different folder
|
|
995
|
-
await storage.move('inbox/data.csv', 'archive/2025/data.csv');
|
|
303
|
+
### Scoped storage
|
|
996
304
|
|
|
997
|
-
|
|
998
|
-
await storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
305
|
+
Automatically prefix every path — useful for per-user file isolation:
|
|
999
306
|
|
|
1000
|
-
|
|
1001
|
-
|
|
307
|
+
```ts
|
|
308
|
+
const userFiles = db.storage('main').scope(`users/${userId}/`);
|
|
1002
309
|
|
|
1003
|
-
|
|
1004
|
-
await
|
|
310
|
+
await userFiles.upload(pdf, 'contract.pdf'); // → users/{userId}/contract.pdf
|
|
311
|
+
const { files } = await userFiles.list(); // → only lists users/{userId}/
|
|
1005
312
|
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
|
-
### Storage Stats
|
|
1011
|
-
|
|
1012
|
-
```typescript
|
|
1013
|
-
const stats = await db.storage('main').getStats();
|
|
1014
|
-
// {
|
|
1015
|
-
// totalFiles: 842,
|
|
1016
|
-
// totalBytes: 1073741824,
|
|
1017
|
-
// uploadCount: 1200,
|
|
1018
|
-
// downloadCount: 4830,
|
|
1019
|
-
// deleteCount: 58,
|
|
1020
|
-
// }
|
|
1021
|
-
|
|
1022
|
-
// Ping — no auth required
|
|
1023
|
-
const { ok, storageRoot } = await db.storage('main').info();
|
|
313
|
+
// Nest deeper
|
|
314
|
+
const reports = userFiles.scope('reports/'); // → users/{userId}/reports/
|
|
1024
315
|
```
|
|
1025
316
|
|
|
1026
317
|
---
|
|
1027
318
|
|
|
1028
319
|
## Analytics
|
|
1029
320
|
|
|
1030
|
-
|
|
321
|
+
BigQuery-powered aggregations. No ETL, no pipelines — just query.
|
|
1031
322
|
|
|
1032
|
-
```
|
|
323
|
+
```ts
|
|
1033
324
|
const analytics = db.analytics('orders');
|
|
1034
|
-
```
|
|
1035
325
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
### Count
|
|
1039
|
-
|
|
1040
|
-
```typescript
|
|
1041
|
-
// Total records
|
|
326
|
+
// Total record count
|
|
1042
327
|
const { count } = await analytics.count();
|
|
1043
328
|
|
|
1044
|
-
//
|
|
1045
|
-
const
|
|
1046
|
-
dateRange: {
|
|
1047
|
-
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
1048
|
-
end: Date.now(),
|
|
1049
|
-
},
|
|
1050
|
-
});
|
|
1051
|
-
```
|
|
1052
|
-
|
|
1053
|
-
### Distribution
|
|
1054
|
-
|
|
1055
|
-
How many records have each unique value for a field?
|
|
1056
|
-
|
|
1057
|
-
```typescript
|
|
1058
|
-
const rows = await analytics.distribution({
|
|
1059
|
-
field: 'status',
|
|
1060
|
-
limit: 10,
|
|
1061
|
-
order: 'desc', // 'asc' | 'desc'
|
|
1062
|
-
});
|
|
1063
|
-
// [
|
|
1064
|
-
// { value: 'completed', count: 8234 },
|
|
1065
|
-
// { value: 'pending', count: 1203 },
|
|
1066
|
-
// { value: 'refunded', count: 412 },
|
|
1067
|
-
// ]
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
### Sum
|
|
1071
|
-
|
|
1072
|
-
```typescript
|
|
1073
|
-
// Total revenue
|
|
1074
|
-
const [{ sum: total }] = await analytics.sum({ field: 'amount' });
|
|
1075
|
-
|
|
1076
|
-
// Revenue by country
|
|
1077
|
-
const byCountry = await analytics.sum({
|
|
1078
|
-
field: 'amount',
|
|
1079
|
-
groupBy: 'country',
|
|
1080
|
-
limit: 10,
|
|
1081
|
-
});
|
|
1082
|
-
// [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, …]
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
### Time Series
|
|
1086
|
-
|
|
1087
|
-
Record counts bucketed over time — ideal for activity and growth charts.
|
|
1088
|
-
|
|
1089
|
-
```typescript
|
|
1090
|
-
const rows = await analytics.timeSeries({
|
|
1091
|
-
granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
|
|
1092
|
-
dateRange: {
|
|
1093
|
-
start: new Date('2025-01-01').getTime(),
|
|
1094
|
-
end: new Date('2025-06-01').getTime(),
|
|
1095
|
-
},
|
|
1096
|
-
});
|
|
1097
|
-
// [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, …]
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
### Field Time Series
|
|
1101
|
-
|
|
1102
|
-
Aggregate a numeric field over time.
|
|
1103
|
-
|
|
1104
|
-
```typescript
|
|
1105
|
-
const revenue = await analytics.fieldTimeSeries({
|
|
1106
|
-
field: 'amount',
|
|
1107
|
-
aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
1108
|
-
granularity: 'week',
|
|
1109
|
-
dateRange: {
|
|
1110
|
-
start: new Date('2025-01-01').getTime(),
|
|
1111
|
-
end: Date.now(),
|
|
1112
|
-
},
|
|
1113
|
-
});
|
|
1114
|
-
// [{ date: '2025-W01', value: 12340.50 }, { date: '2025-W02', value: 9872.00 }, …]
|
|
1115
|
-
```
|
|
329
|
+
// How many records have each value of `status`
|
|
330
|
+
const dist = await analytics.distribution({ field: 'status' });
|
|
1116
331
|
|
|
1117
|
-
|
|
332
|
+
// Sum of `amount`, grouped by `region`
|
|
333
|
+
const sums = await analytics.sum({ field: 'amount', groupBy: 'region' });
|
|
1118
334
|
|
|
1119
|
-
|
|
335
|
+
// Records over time
|
|
336
|
+
const daily = await analytics.timeSeries({ granularity: 'day' });
|
|
1120
337
|
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
field: 'productId',
|
|
1124
|
-
labelField: 'productName', // optional human-readable label alongside the value
|
|
1125
|
-
n: 5,
|
|
1126
|
-
order: 'desc',
|
|
1127
|
-
});
|
|
1128
|
-
// [
|
|
1129
|
-
// { value: 'prod_123', label: 'Widget Pro', count: 892 },
|
|
1130
|
-
// { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
|
|
1131
|
-
// ]
|
|
1132
|
-
```
|
|
338
|
+
// How `revenue` changes over time
|
|
339
|
+
const trend = await analytics.fieldTimeSeries({ field: 'revenue', aggregation: 'sum' });
|
|
1133
340
|
|
|
1134
|
-
|
|
341
|
+
// Top 10 countries by record count
|
|
342
|
+
const top10 = await analytics.topN({ field: 'country', n: 10 });
|
|
1135
343
|
|
|
1136
|
-
|
|
344
|
+
// Min / max / avg / sum / stddev for a numeric field
|
|
345
|
+
const stats = await analytics.stats({ field: 'price' });
|
|
1137
346
|
|
|
1138
|
-
|
|
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
|
-
```
|
|
1149
|
-
|
|
1150
|
-
### Multi-Metric Dashboard
|
|
1151
|
-
|
|
1152
|
-
Run several aggregations in a single BigQuery query — one network call.
|
|
1153
|
-
|
|
1154
|
-
```typescript
|
|
347
|
+
// Multiple aggregations in one round-trip
|
|
1155
348
|
const dashboard = await analytics.multiMetric({
|
|
1156
349
|
metrics: [
|
|
1157
|
-
{ field: 'amount',
|
|
1158
|
-
{ field: 'amount',
|
|
1159
|
-
{ field: '
|
|
1160
|
-
{ field: 'userId', name: 'uniqueOrders', aggregation: 'count' },
|
|
350
|
+
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
351
|
+
{ field: 'amount', name: 'avgOrder', aggregation: 'avg' },
|
|
352
|
+
{ field: 'recordId', name: 'orderCount', aggregation: 'count' },
|
|
1161
353
|
],
|
|
1162
|
-
dateRange: {
|
|
1163
|
-
start: new Date('2025-01-01').getTime(),
|
|
1164
|
-
end: Date.now(),
|
|
1165
|
-
},
|
|
1166
354
|
});
|
|
1167
|
-
// {
|
|
1168
|
-
// totalRevenue: 198432.50,
|
|
1169
|
-
// avgOrderValue: 87.23,
|
|
1170
|
-
// maxOrder: 9999.99,
|
|
1171
|
-
// uniqueOrders: 2275,
|
|
1172
|
-
// }
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
### Filtered Records via BigQuery
|
|
355
|
+
// → { totalRevenue: 48200, avgOrder: 96.4, orderCount: 500 }
|
|
1176
356
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const records = await analytics.records({
|
|
1181
|
-
filters: [
|
|
1182
|
-
{ field: 'status', op: '==', value: 'refunded' },
|
|
1183
|
-
{ field: 'amount', op: '>', value: 100 },
|
|
1184
|
-
],
|
|
1185
|
-
selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
|
|
1186
|
-
orderBy: 'amount',
|
|
1187
|
-
order: 'desc',
|
|
1188
|
-
limit: 50,
|
|
1189
|
-
offset: 0,
|
|
1190
|
-
dateRange: {
|
|
1191
|
-
start: new Date('2025-01-01').getTime(),
|
|
1192
|
-
end: Date.now(),
|
|
1193
|
-
},
|
|
1194
|
-
});
|
|
1195
|
-
```
|
|
1196
|
-
|
|
1197
|
-
### Cross-Bucket Comparison
|
|
1198
|
-
|
|
1199
|
-
Compare the same metric across multiple buckets in one query.
|
|
1200
|
-
|
|
1201
|
-
```typescript
|
|
1202
|
-
const comparison = await analytics.crossBucket({
|
|
1203
|
-
bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
|
|
357
|
+
// Compare a metric across buckets
|
|
358
|
+
const compare = await analytics.crossBucket({
|
|
359
|
+
bucketKeys: ['orders-2024', 'orders-2025'],
|
|
1204
360
|
field: 'amount',
|
|
1205
361
|
aggregation: 'sum',
|
|
1206
362
|
});
|
|
1207
|
-
// [
|
|
1208
|
-
// { bucket: 'orders-us', value: 120000 },
|
|
1209
|
-
// { bucket: 'orders-eu', value: 45000 },
|
|
1210
|
-
// { bucket: 'orders-apac', value: 33000 },
|
|
1211
|
-
// ]
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
> ⚠️ Your Bucket Security Key must have read access to **every** bucket listed in `bucketKeys`.
|
|
1215
|
-
|
|
1216
|
-
### Storage Stats
|
|
1217
363
|
|
|
1218
|
-
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
// totalRecords: 48210,
|
|
1222
|
-
// totalBytes: 921600000,
|
|
1223
|
-
// avgBytes: 19112,
|
|
1224
|
-
// minBytes: 128,
|
|
1225
|
-
// maxBytes: 5242880,
|
|
1226
|
-
// }
|
|
1227
|
-
```
|
|
1228
|
-
|
|
1229
|
-
### Raw Query
|
|
1230
|
-
|
|
1231
|
-
Use the `query()` method when none of the typed helpers cover your use case.
|
|
1232
|
-
|
|
1233
|
-
```typescript
|
|
1234
|
-
import type { AnalyticsQuery } from 'hydrousdb';
|
|
1235
|
-
|
|
1236
|
-
const result = await analytics.query<{ count: number }>({
|
|
1237
|
-
queryType: 'count',
|
|
1238
|
-
dateRange: { start: Date.now() - 86400000, end: Date.now() },
|
|
364
|
+
// All queries accept an optional date range
|
|
365
|
+
const { count } = await analytics.count({
|
|
366
|
+
dateRange: { start: Date.now() - 7 * 24 * 60 * 60 * 1000, end: Date.now() },
|
|
1239
367
|
});
|
|
1240
|
-
|
|
1241
|
-
console.log(result.queryType); // 'count'
|
|
1242
|
-
console.log(result.data); // { count: 142 }
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
---
|
|
1246
|
-
|
|
1247
|
-
## TypeScript
|
|
1248
|
-
|
|
1249
|
-
The SDK is written entirely in TypeScript and ships full type definitions. Use generics for end-to-end type safety.
|
|
1250
|
-
|
|
1251
|
-
```typescript
|
|
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';
|
|
1286
|
-
|
|
1287
|
-
// Define your domain models
|
|
1288
|
-
interface Order {
|
|
1289
|
-
customerId: string;
|
|
1290
|
-
items: Array<{ productId: string; qty: number; price: number }>;
|
|
1291
|
-
total: number;
|
|
1292
|
-
status: 'pending' | 'processing' | 'completed' | 'refunded';
|
|
1293
|
-
country: string;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
interface Customer {
|
|
1297
|
-
name: string;
|
|
1298
|
-
email: string;
|
|
1299
|
-
tier: 'free' | 'pro' | 'enterprise';
|
|
1300
|
-
credits: number;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const db = createClient({
|
|
1304
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1305
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1306
|
-
storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
// Fully typed — autocomplete, compile-time safety throughout
|
|
1310
|
-
const orders = db.records<Order>('orders');
|
|
1311
|
-
const customers = db.records<Customer>('customers');
|
|
1312
|
-
|
|
1313
|
-
const order = await orders.create(
|
|
1314
|
-
{ customerId: 'cust_abc', items: [{ productId: 'prod_1', qty: 2, price: 29.99 }], total: 59.98, status: 'pending', country: 'US' },
|
|
1315
|
-
{ queryableFields: ['status', 'country', 'customerId'] },
|
|
1316
|
-
);
|
|
1317
|
-
|
|
1318
|
-
// TypeScript catches mistakes at compile time:
|
|
1319
|
-
// order.nonExistentField ← TS error ✓
|
|
1320
|
-
// orders.create({ bad: 1 }) ← TS error ✓
|
|
1321
368
|
```
|
|
1322
369
|
|
|
1323
370
|
---
|
|
1324
371
|
|
|
1325
372
|
## Error Handling
|
|
1326
373
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
```typescript
|
|
1330
|
-
import {
|
|
1331
|
-
HydrousError,
|
|
1332
|
-
AuthError,
|
|
1333
|
-
RecordError,
|
|
1334
|
-
StorageError,
|
|
1335
|
-
AnalyticsError,
|
|
1336
|
-
ValidationError,
|
|
1337
|
-
NetworkError,
|
|
1338
|
-
} from 'hydrousdb';
|
|
374
|
+
```ts
|
|
375
|
+
import { HydrousError, NetworkError } from 'hydrousdb';
|
|
1339
376
|
|
|
1340
377
|
try {
|
|
1341
|
-
|
|
378
|
+
await db.auth().login({ email: 'x@x.com', password: 'wrong' });
|
|
1342
379
|
} catch (err) {
|
|
380
|
+
if (err instanceof HydrousError) {
|
|
381
|
+
console.log(err.code); // e.g. 'INVALID_CREDENTIALS'
|
|
382
|
+
console.log(err.status); // e.g. 401
|
|
383
|
+
console.log(err.message); // human-readable
|
|
384
|
+
}
|
|
1343
385
|
if (err instanceof NetworkError) {
|
|
1344
|
-
|
|
1345
|
-
console.error('Cannot reach HydrousDB:', err.message);
|
|
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
|
|
386
|
+
console.log('No internet or server unreachable');
|
|
1368
387
|
}
|
|
1369
388
|
}
|
|
1370
389
|
```
|
|
1371
390
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
|
1375
|
-
|
|
1376
|
-
| `
|
|
1377
|
-
| `
|
|
1378
|
-
| `
|
|
1379
|
-
| `
|
|
1380
|
-
| `
|
|
1381
|
-
|
|
1382
|
-
**Common error codes:**
|
|
1383
|
-
|
|
1384
|
-
| Code | Status | Meaning |
|
|
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 |
|
|
391
|
+
| Error class | When it's thrown |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `HydrousError` | Base class — all SDK errors extend this |
|
|
394
|
+
| `AuthError` | Login failures, invalid sessions, permission denied |
|
|
395
|
+
| `RecordError` | Record not found, validation failures |
|
|
396
|
+
| `StorageError` | Upload/download failures, file not found |
|
|
397
|
+
| `AnalyticsError` | Invalid query, bucket not found |
|
|
398
|
+
| `NetworkError` | No network, request timed out |
|
|
399
|
+
| `ValidationError` | Bad input caught client-side before the request is sent |
|
|
1398
400
|
|
|
1399
401
|
---
|
|
1400
402
|
|
|
1401
|
-
##
|
|
403
|
+
## TypeScript
|
|
1402
404
|
|
|
1403
|
-
|
|
405
|
+
The SDK is written in TypeScript. Type your records for full autocomplete:
|
|
1404
406
|
|
|
1405
|
-
```
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
407
|
+
```ts
|
|
408
|
+
interface Order {
|
|
409
|
+
customerId: string;
|
|
410
|
+
amount: number;
|
|
411
|
+
status: 'pending' | 'paid' | 'refunded';
|
|
412
|
+
items: { sku: string; qty: number }[];
|
|
413
|
+
}
|
|
1411
414
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
415
|
+
const orders = db.records<Order>('orders');
|
|
416
|
+
|
|
417
|
+
const order = await orders.create({
|
|
418
|
+
customerId: 'cust_123',
|
|
419
|
+
amount: 49.99,
|
|
420
|
+
status: 'pending',
|
|
421
|
+
items: [{ sku: 'SHOE-42', qty: 1 }],
|
|
1417
422
|
});
|
|
423
|
+
// order.status → 'pending' | 'paid' | 'refunded' ✓
|
|
1418
424
|
```
|
|
1419
425
|
|
|
1420
|
-
**2. Never expose keys to browsers.** For browser-side apps, route requests through your own backend, or use per-user sessions from `auth.login()`.
|
|
1421
|
-
|
|
1422
|
-
**3. Keys travel in headers — never in URLs.** The SDK enforces this automatically. Keys never appear in server access logs, GCP audit trails, or browser history.
|
|
1423
|
-
|
|
1424
|
-
**4. Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
|
|
1425
|
-
|
|
1426
|
-
**5. Use scoped storage** (`db.storage('key').scope('prefix/')`) to isolate access per user or feature, reducing the blast radius of any misconfiguration.
|
|
1427
|
-
|
|
1428
|
-
**6. Keep files private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for time-limited external sharing rather than making files permanently public.
|
|
1429
|
-
|
|
1430
426
|
---
|
|
1431
427
|
|
|
1432
|
-
##
|
|
1433
|
-
|
|
1434
|
-
### `createClient(config)` → `HydrousClient`
|
|
428
|
+
## Security
|
|
1435
429
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
| `baseUrl` | `string` | — | Override the API endpoint (for self-hosting or testing) |
|
|
430
|
+
- **Never commit API keys.** Use environment variables (`process.env.…`).
|
|
431
|
+
- **Never expose keys in the browser.** For client-side apps, proxy requests through your own backend.
|
|
432
|
+
- **Keys travel in headers only** — the SDK enforces this. They never appear in URLs, access logs, or browser history.
|
|
433
|
+
- **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.
|
|
1442
435
|
|
|
1443
436
|
---
|
|
1444
437
|
|
|
1445
|
-
|
|
438
|
+
## Framework examples
|
|
1446
439
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
| `batchDelete` | `(ids: string[]) → { deleted, failed }` | Up to 500 records. |
|
|
1459
|
-
| `getHistory` | `(id: string) → RecordHistoryEntry[]` | Full version list, most recent first. |
|
|
1460
|
-
| `restoreVersion` | `(id: string, version: number) → T & RecordResult` | Roll back to any version. |
|
|
1461
|
-
|
|
1462
|
-
**`QueryOptions`:**
|
|
1463
|
-
|
|
1464
|
-
```typescript
|
|
1465
|
-
{
|
|
1466
|
-
filters?: QueryFilter[]; // [{ field, op, value }, …]
|
|
1467
|
-
fields?: string; // comma-separated field names
|
|
1468
|
-
orderBy?: string;
|
|
1469
|
-
order?: 'asc' | 'desc';
|
|
1470
|
-
limit?: number; // default 100, max 1000
|
|
1471
|
-
offset?: number;
|
|
1472
|
-
startAfter?: string; // cursor for next page
|
|
1473
|
-
startAt?: string;
|
|
1474
|
-
endAt?: string;
|
|
1475
|
-
dateRange?: DateRange; // { start?: number, end?: number }
|
|
440
|
+
**Next.js (App Router)**
|
|
441
|
+
```ts
|
|
442
|
+
// lib/db.ts
|
|
443
|
+
import { createClient } from 'hydrousdb';
|
|
444
|
+
export const db = createClient({ … });
|
|
445
|
+
|
|
446
|
+
// app/api/posts/route.ts
|
|
447
|
+
import { db } from '@/lib/db';
|
|
448
|
+
export async function GET() {
|
|
449
|
+
const { records } = await db.records('posts').query({ limit: 10 });
|
|
450
|
+
return Response.json(records);
|
|
1476
451
|
}
|
|
1477
452
|
```
|
|
1478
453
|
|
|
1479
|
-
|
|
454
|
+
**React (client-side via your own API)**
|
|
455
|
+
```ts
|
|
456
|
+
// Always go through your backend — never put keys in React components
|
|
457
|
+
const res = await fetch('/api/posts');
|
|
458
|
+
const data = await res.json();
|
|
459
|
+
```
|
|
1480
460
|
|
|
1481
|
-
|
|
461
|
+
**Vue / Nuxt**
|
|
462
|
+
```ts
|
|
463
|
+
// composables/useDb.ts
|
|
464
|
+
import { createClient } from 'hydrousdb';
|
|
465
|
+
export const db = createClient({ … });
|
|
466
|
+
```
|
|
1482
467
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
| `validateSession` | `({ sessionId }) → { user, session }` | Check if a session is still active. |
|
|
1490
|
-
| `getUser` | `({ userId }) → UserRecord` | Fetch a user by ID. |
|
|
1491
|
-
| `updateUser` | `(opts: UpdateUserOptions) → UserRecord` | Update user fields — must wrap changes in `updates: { … }`. |
|
|
1492
|
-
| `changePassword` | `(opts: ChangePasswordOptions) → void` | Authenticated password change. Field is `currentPassword` in the SDK (maps to `oldPassword` on the wire). |
|
|
1493
|
-
| `requestPasswordReset` | `({ email }) → void` | Trigger reset email. |
|
|
1494
|
-
| `confirmPasswordReset` | `({ resetToken, newPassword }) → void` | Apply new password. |
|
|
1495
|
-
| `requestEmailVerification` | `({ userId }) → void` | Send verification email. |
|
|
1496
|
-
| `confirmEmailVerification` | `({ verifyToken }) → void` | Complete verification. |
|
|
1497
|
-
| `listUsers` | `(opts: ListUsersOptions) → ListUsersResult` | Paginated user list. Uses cursor-based pagination — `nextCursor` replaces `offset`. |
|
|
1498
|
-
| `lockAccount` | `({ sessionId, userId, duration? }) → { lockedUntil, unlockTime }` | Admin only. |
|
|
1499
|
-
| `unlockAccount` | `({ sessionId, userId }) → void` | Admin only. |
|
|
1500
|
-
| `deleteUser` | `({ sessionId, userId }) → void` | Soft delete. Admin required unless deleting own account. |
|
|
1501
|
-
| `hardDeleteUser` | `({ sessionId, userId }) → void` | Permanent. Admin required unless deleting own account. |
|
|
1502
|
-
| `bulkDeleteUsers` | `({ sessionId, userIds, hard? }) → { succeeded, failed }` | Up to 500 users. Admin only. |
|
|
468
|
+
**React Native**
|
|
469
|
+
```ts
|
|
470
|
+
import { createClient } from 'hydrousdb';
|
|
471
|
+
// Works out of the box — uses the global fetch available in React Native
|
|
472
|
+
const db = createClient({ … });
|
|
473
|
+
```
|
|
1503
474
|
|
|
1504
475
|
---
|
|
1505
476
|
|
|
1506
|
-
|
|
477
|
+
## API Reference
|
|
1507
478
|
|
|
1508
|
-
|
|
1509
|
-
|---|---|---|
|
|
1510
|
-
| `upload` | `(data, path, opts?) → UploadResult` | Server-buffered upload. Up to 500 MB. |
|
|
1511
|
-
| `uploadRaw` | `(data, path, opts?) → UploadResult` | Upload a JS object or string as a file. |
|
|
1512
|
-
| `getUploadUrl` | `(opts) → UploadUrlResult` | Step 1 of direct upload. `expiresInSeconds` controls URL TTL. |
|
|
1513
|
-
| `uploadToSignedUrl` | `(signedUrl, data, mimeType, onProgress?) → void` | Step 2 — upload to GCS directly. `onProgress` callback fires with 0–100. |
|
|
1514
|
-
| `confirmUpload` | `(opts) → UploadResult` | Step 3 — register metadata. |
|
|
1515
|
-
| `getBatchUploadUrls` | `(files: BatchUploadItem[]) → BatchUploadUrlResult` | Up to 50 files. Returns `{ files: [...] }` — only succeeded items included. |
|
|
1516
|
-
| `batchConfirmUploads` | `(items) → { succeeded, failed }` | Confirm multiple uploads. Both arrays are returned. |
|
|
1517
|
-
| `download` | `(path) → ArrayBuffer` | Download private file. |
|
|
1518
|
-
| `batchDownload` | `(paths) → BatchDownloadResult` | Up to 20 files. Returns `{ succeeded, failed }` — content is base64. |
|
|
1519
|
-
| `list` | `(opts?) → ListResult` | Returns `{ files, folders, hasMore, nextCursor }`. |
|
|
1520
|
-
| `getMetadata` | `(path) → FileMetadata` | Size, MIME type, visibility, URLs. |
|
|
1521
|
-
| `getSignedUrl` | `(path, expiresIn?) → SignedUrlResult` | Time-limited share link. Default: 3600s. |
|
|
1522
|
-
| `setVisibility` | `(path, isPublic) → { path, isPublic, publicUrl, downloadUrl }` | Toggle public/private. |
|
|
1523
|
-
| `createFolder` | `(path) → { path }` | Create a GCS prefix placeholder. |
|
|
1524
|
-
| `deleteFile` | `(path) → void` | Delete a single file. |
|
|
1525
|
-
| `deleteFolder` | `(path) → void` | Delete folder + all contents. |
|
|
1526
|
-
| `move` | `(from, to) → { from, to }` | Move or rename. |
|
|
1527
|
-
| `copy` | `(from, to) → { from, to }` | Copy to a new path. |
|
|
1528
|
-
| `getStats` | `() → StorageStats` | Key-level totals: files, bytes, upload/download/delete counts. |
|
|
1529
|
-
| `info` | `() → { ok, storageRoot }` | Healthcheck — no auth required. |
|
|
1530
|
-
| `scope` | `(prefix) → ScopedStorage` | Create a path-prefixed sub-client with the full StorageManager API. |
|
|
479
|
+
### `createClient(config)`
|
|
1531
480
|
|
|
1532
|
-
|
|
481
|
+
| Field | Type | Description |
|
|
482
|
+
|---|---|---|
|
|
483
|
+
| `authKey` | `string` | `hk_auth_…` — for auth routes |
|
|
484
|
+
| `bucketSecurityKey` | `string` | `hk_bucket_…` — for records & analytics |
|
|
485
|
+
| `storageKeys` | `{ [name]: string }` | One or more `ssk_…` keys for storage |
|
|
486
|
+
| `baseUrl` | `string` | Override the endpoint (optional) |
|
|
1533
487
|
|
|
1534
|
-
### `db.
|
|
488
|
+
### `db.records(bucket)` — key methods
|
|
1535
489
|
|
|
1536
|
-
| Method |
|
|
1537
|
-
|
|
1538
|
-
| `
|
|
1539
|
-
| `
|
|
1540
|
-
| `
|
|
1541
|
-
| `
|
|
1542
|
-
| `
|
|
1543
|
-
| `
|
|
1544
|
-
| `
|
|
1545
|
-
| `
|
|
1546
|
-
| `
|
|
1547
|
-
| `
|
|
1548
|
-
| `
|
|
1549
|
-
|
|
490
|
+
| Method | What it does |
|
|
491
|
+
|---|---|
|
|
492
|
+
| `create(data, opts?)` | Create a record. Use `opts.queryableFields` to enable filtering. |
|
|
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 |
|
|
507
|
+
|---|---|
|
|
508
|
+
| `signup(opts)` | Register + create session. |
|
|
509
|
+
| `login(opts)` | Authenticate + create session. |
|
|
510
|
+
| `logout({ sessionId })` | Revoke session(s). |
|
|
511
|
+
| `validateSession(sessionId)` | Check if a session is active. |
|
|
512
|
+
| `refreshSession(refreshToken)` | Get a new session from a refresh token. |
|
|
513
|
+
| `getUser(userId)` | Fetch a user by ID. |
|
|
514
|
+
| `updateUser(opts)` | Update profile fields. |
|
|
515
|
+
| `changePassword(opts)` | Authenticated password change. |
|
|
516
|
+
| `requestPasswordReset(email)` | Trigger reset email. |
|
|
517
|
+
| `confirmPasswordReset(token, newPw)` | Apply new password. |
|
|
518
|
+
| `listUsers(opts)` | Paginated user list (admin). |
|
|
519
|
+
| `lockAccount(opts)` / `unlockAccount(…)` | Admin account controls. |
|
|
520
|
+
| `hardDeleteUser(sessionId, userId)` | Permanent delete (admin). |
|
|
521
|
+
|
|
522
|
+
### `db.storage(keyName)` — key methods
|
|
523
|
+
|
|
524
|
+
| Method | What it does |
|
|
525
|
+
|---|---|
|
|
526
|
+
| `upload(data, path, opts?)` | Server-buffered upload (up to 500 MB). |
|
|
527
|
+
| `uploadRaw(data, path, opts?)` | Upload a JS object or string as a file. |
|
|
528
|
+
| `getUploadUrl(opts)` | Step 1 of direct-to-GCS upload (for progress tracking). |
|
|
529
|
+
| `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2 — upload directly to GCS. |
|
|
530
|
+
| `confirmUpload(opts)` | Step 3 — register metadata. |
|
|
531
|
+
| `download(path)` | Download a private file as ArrayBuffer. |
|
|
532
|
+
| `list(opts?)` | List files and folders. |
|
|
533
|
+
| `getMetadata(path)` | File size, MIME, visibility, URLs. |
|
|
534
|
+
| `getSignedUrl(path, expiresIn?)` | Time-limited share link. |
|
|
535
|
+
| `setVisibility(path, isPublic)` | Toggle public/private. |
|
|
536
|
+
| `move(from, to)` / `copy(from, to)` | Move or copy files. |
|
|
537
|
+
| `deleteFile(path)` / `deleteFolder(path)` | Delete files or folders. |
|
|
538
|
+
| `scope(prefix)` | Get a path-prefixed sub-client. |
|
|
539
|
+
|
|
540
|
+
### `db.analytics(bucket)` — key methods
|
|
541
|
+
|
|
542
|
+
| Method | What it does |
|
|
543
|
+
|---|---|
|
|
544
|
+
| `count(opts?)` | Total record count. |
|
|
545
|
+
| `distribution(opts)` | Per-value counts for a field. |
|
|
546
|
+
| `sum(opts)` | Sum a numeric field, optional `groupBy`. |
|
|
547
|
+
| `timeSeries(opts?)` | Record counts over time. |
|
|
548
|
+
| `fieldTimeSeries(opts)` | Field aggregation over time. |
|
|
549
|
+
| `topN(opts)` | Most frequent field values. |
|
|
550
|
+
| `stats(opts)` | min / max / avg / sum / stddev. |
|
|
551
|
+
| `multiMetric(opts)` | Multiple aggregations in one request. |
|
|
552
|
+
| `crossBucket(opts)` | Compare a metric across buckets. |
|
|
553
|
+
| `records(opts?)` | Filtered records via BigQuery. |
|
|
1550
554
|
|
|
1551
555
|
---
|
|
1552
556
|
|
|
@@ -1555,24 +559,17 @@ const db = createClient({
|
|
|
1555
559
|
```bash
|
|
1556
560
|
git clone https://github.com/hydrousdb/hydrousdb-js.git
|
|
1557
561
|
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
|
|
562
|
+
npm install
|
|
563
|
+
npm test # run tests
|
|
564
|
+
npm run build # compile
|
|
1564
565
|
```
|
|
1565
566
|
|
|
1566
|
-
---
|
|
1567
|
-
|
|
1568
567
|
## License
|
|
1569
568
|
|
|
1570
|
-
MIT —
|
|
569
|
+
MIT — [LICENSE](./LICENSE)
|
|
1571
570
|
|
|
1572
571
|
---
|
|
1573
572
|
|
|
1574
573
|
<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>
|
|
574
|
+
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
575
|
</p>
|