hydrousdb 3.0.2 → 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 +350 -975
- package/dist/index.cjs +942 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +643 -398
- package/dist/index.d.ts +643 -398
- package/dist/index.mjs +1581 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -6
- package/dist/index.js +0 -1179
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,500 +1,237 @@
|
|
|
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 (5 minutes)](#quick-start-5-minutes)
|
|
22
|
-
- [Records](#records)
|
|
23
|
-
- [Create](#create-a-record)
|
|
24
|
-
- [Read](#read-a-record)
|
|
25
|
-
- [Update](#update-a-record)
|
|
26
|
-
- [Delete](#delete-a-record)
|
|
27
|
-
- [Query](#query-records)
|
|
28
|
-
- [Batch Operations](#batch-operations)
|
|
29
|
-
- [Version History](#version-history)
|
|
30
|
-
- [Authentication](#authentication)
|
|
31
|
-
- [Sign Up](#sign-up-users)
|
|
32
|
-
- [Log In / Log Out](#log-in--log-out)
|
|
33
|
-
- [Session Management](#session-management)
|
|
34
|
-
- [Password Reset](#password-reset-flow)
|
|
35
|
-
- [Email Verification](#email-verification)
|
|
36
|
-
- [Admin Operations](#admin-operations)
|
|
37
|
-
- [File Storage](#file-storage)
|
|
38
|
-
- [Simple Upload](#simple-upload)
|
|
39
|
-
- [Large File Upload (with progress)](#large-file-upload-with-progress)
|
|
40
|
-
- [Download](#download-files)
|
|
41
|
-
- [List Files](#list-files)
|
|
42
|
-
- [Scoped Storage](#scoped-storage)
|
|
43
|
-
- [Share & Visibility](#share--visibility)
|
|
44
|
-
- [File Operations](#file-operations)
|
|
45
|
-
- [Analytics](#analytics)
|
|
46
|
-
- [Count](#count)
|
|
47
|
-
- [Distribution](#distribution)
|
|
48
|
-
- [Sum](#sum)
|
|
49
|
-
- [Time Series](#time-series)
|
|
50
|
-
- [Top N](#top-n)
|
|
51
|
-
- [Field Stats](#field-stats)
|
|
52
|
-
- [Multi-Metric Dashboard](#multi-metric-dashboard)
|
|
53
|
-
- [Filtered Records](#filtered-records-bigquery)
|
|
54
|
-
- [Cross-Bucket Comparison](#cross-bucket-comparison)
|
|
55
|
-
- [Storage Stats](#storage-stats)
|
|
56
|
-
- [TypeScript Support](#typescript-support)
|
|
57
|
-
- [Error Handling](#error-handling)
|
|
58
|
-
- [Security Best Practices](#security-best-practices)
|
|
59
|
-
- [API Reference](#api-reference)
|
|
60
|
-
- [Contributing](#contributing)
|
|
61
|
-
- [License](#license)
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## What is HydrousDB?
|
|
66
|
-
|
|
67
|
-
Traditional databases start choking when your JSON records get large. Postgres hits row-size limits. Firestore charges per field read. MongoDB Atlas buckles under millions of 500 KB+ documents. They were designed for structured rows and small payloads — not the kind of deeply nested, real-world JSON that modern applications actually produce.
|
|
68
|
-
|
|
69
|
-
HydrousDB is built specifically for that problem. It stores every record as a compressed GCS blob, retrieves any record in a single network call (no index lookups — the storage path is computed directly from the record ID), and runs analytics at BigQuery scale without ETL. The bigger and messier your JSON, the more it outperforms traditional databases.
|
|
70
|
-
|
|
71
|
-
**Systems that benefit immediately:**
|
|
72
|
-
|
|
73
|
-
| Domain | Example records | Why traditional DBs struggle |
|
|
74
|
-
|---|---|---|
|
|
75
|
-
| 🏥 **Hospital / EMR** | Full patient charts — vitals history, medication lists, clinical notes, imaging metadata | 850 KB+ per chart, millions of patients, strict audit trails |
|
|
76
|
-
| 🎓 **School management** | Student portfolios — all grades, attendance, assessments, teacher notes across years | Deep nesting, bursty writes at term-end, long-term archival |
|
|
77
|
-
| 🏭 **IoT / Industrial** | Sensor telemetry — time-stamped readings, device state, calibration metadata | Billions of records, append-heavy, rarely updated |
|
|
78
|
-
| 🛒 **E-commerce** | Order records — line items, fulfilment events, return history, custom attributes | Highly variable shape, needs fast analytics across date ranges |
|
|
79
|
-
| ⚖️ **Legal / compliance** | Case files — filings, correspondence, version history, linked documents | 1 MB+ records, immutable audit log, cross-case analytics |
|
|
80
|
-
| 🎮 **Gaming** | Player save states — inventory, quest progress, achievement history, replay data | Large payloads, millions of concurrent users, burst writes |
|
|
81
|
-
| 📡 **Logistics / tracking** | Shipment records — full event timeline, customs data, carrier metadata | Append-only events, heavy querying by date range and status |
|
|
82
|
-
|
|
83
|
-
**What you get out of the box:**
|
|
84
|
-
|
|
85
|
-
| Feature | What it does |
|
|
86
|
-
|---|---|
|
|
87
|
-
| **Records** | Schemaless JSON store. Billion-scale, gzip-compressed, date-encoded IDs for zero-lookup retrieval. Up to 1 MB per record. |
|
|
88
|
-
| **Auth** | Full user authentication — signup, login, sessions, password reset, email verification, and admin controls. |
|
|
89
|
-
| **Storage** | File uploads backed by Google Cloud Storage. Direct-to-GCS uploads, public/private visibility, signed share URLs. |
|
|
90
|
-
| **Analytics** | BigQuery-powered aggregations — counts, distributions, time series, top-N, multi-metric dashboards, cross-bucket comparisons. Zero ETL. |
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## How It Works
|
|
95
|
-
|
|
96
|
-
Every HydrousDB record ID encodes its creation date as a prefix (e.g. `260203-rec_01JA2XYZ`). This means the full storage path to any record can be computed in memory — no index lookup, no pointer chase. Just math.
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
260203-rec_01JA2XYZ
|
|
100
|
-
↓ parse date prefix
|
|
101
|
-
YY=26 MM=02 DD=03
|
|
102
|
-
↓ compute path in memory
|
|
103
|
-
projects/pid/buckets/bk/records/26/02/03/rec_01JA.json.gz
|
|
104
|
-
↓ fetch from GCS directly
|
|
105
|
-
0 index reads ✓
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Records are gzip-compressed on write (typically 60–80% size reduction). A full 850 KB hospital patient chart compresses to ~255 KB on disk — automatically, every time. Records age through storage tiers (Standard → Nearline → Coldline → Archive) as they get older, keeping historical data accessible without manual lifecycle management.
|
|
109
|
-
|
|
110
|
-
This architecture means HydrousDB handles what breaks other databases:
|
|
111
|
-
- **Huge records** — up to 1 MB per document, compressed
|
|
112
|
-
- **Append-heavy workloads** — IoT telemetry, audit logs, event streams
|
|
113
|
-
- **Date-range queries at scale** — the ID prefix enables efficient folder scans without a full table scan
|
|
114
|
-
- **Long-term retention** — billions of records stay queryable via BigQuery without any migration
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Quick Start (5 minutes)
|
|
119
|
-
|
|
120
|
-
### Step 1 — Create your account
|
|
121
|
-
|
|
122
|
-
Go to **[https://hydrousdb.com](https://hydrousdb.com)** and sign up for a free account.
|
|
123
|
-
|
|
124
|
-
### Step 2 — Create your first bucket
|
|
125
|
-
|
|
126
|
-
1. Log in to your dashboard at **[https://hydrousdb.com/dashboard](https://hydrousdb.com/dashboard)**.
|
|
127
|
-
2. Click **"New Bucket"**.
|
|
128
|
-
3. Give it a name — use lowercase letters, numbers, hyphens, or underscores (e.g. `my-first-bucket`).
|
|
129
|
-
4. Click **"Create"**.
|
|
130
|
-
|
|
131
|
-
> 💡 **What is a bucket?** A bucket is a named collection of JSON records — similar to a table in SQL or a collection in MongoDB.
|
|
132
|
-
|
|
133
|
-
### Step 3 — Grab your API Keys
|
|
134
|
-
|
|
135
|
-
HydrousDB uses three separate keys, each scoped to a service:
|
|
136
|
-
|
|
137
|
-
| Key | Prefix | Used for |
|
|
138
|
-
|---|---|---|
|
|
139
|
-
| **Auth Key** | `hk_auth_…` | All `/auth/*` routes — signup, login, sessions |
|
|
140
|
-
| **Bucket Security Key** | `hk_bucket_…` | Records and analytics |
|
|
141
|
-
| **Storage Key(s)** | `ssk_…` | File storage — one key per storage bucket |
|
|
142
|
-
|
|
143
|
-
1. In the dashboard go to **Settings → API Keys**.
|
|
144
|
-
2. Generate each key type you need.
|
|
145
|
-
3. Copy them — you'll use all three when initialising the client.
|
|
146
|
-
|
|
147
|
-
> ⚠️ **These keys are your credentials.** Treat them like passwords. Never commit them to Git. Use environment variables.
|
|
148
|
-
|
|
149
|
-
### Step 4 — Install the SDK
|
|
3
|
+
**A database that doesn't choke on big JSON.** Store, query, and analyse massive records — with auth and file storage built in.
|
|
150
4
|
|
|
151
5
|
```bash
|
|
152
6
|
npm install hydrousdb
|
|
153
|
-
# or
|
|
154
|
-
yarn add hydrousdb
|
|
155
|
-
# or
|
|
156
|
-
pnpm add hydrousdb
|
|
157
7
|
```
|
|
158
8
|
|
|
159
|
-
|
|
9
|
+
→ [Get your free API keys at hydrousdb.com](https://hydrousdb.com/dashboard)
|
|
10
|
+
|
|
11
|
+
---
|
|
160
12
|
|
|
161
|
-
|
|
13
|
+
## Setup
|
|
162
14
|
|
|
163
|
-
```
|
|
15
|
+
```ts
|
|
164
16
|
import { createClient } from 'hydrousdb';
|
|
165
17
|
|
|
166
|
-
// Create the client once — reuse it everywhere
|
|
167
18
|
const db = createClient({
|
|
168
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
169
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
19
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
20
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
170
21
|
storageKeys: {
|
|
171
|
-
main: process.env.
|
|
22
|
+
main: process.env.HYDROUS_STORAGE_KEY!,
|
|
172
23
|
},
|
|
173
24
|
});
|
|
174
|
-
|
|
175
|
-
// Write a record to your bucket
|
|
176
|
-
const post = await db.records('my-first-bucket').create({
|
|
177
|
-
title: 'Hello, HydrousDB!',
|
|
178
|
-
body: 'My first record.',
|
|
179
|
-
published: false,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
183
|
-
console.log(post.createdAt); // 1717200000000
|
|
184
|
-
|
|
185
|
-
// Read it back — zero database reads, path computed from ID
|
|
186
|
-
const fetched = await db.records('my-first-bucket').get(post.id);
|
|
187
|
-
console.log(fetched.title); // "Hello, HydrousDB!"
|
|
188
|
-
|
|
189
|
-
// Update it
|
|
190
|
-
await db.records('my-first-bucket').patch(post.id, { published: true });
|
|
191
|
-
|
|
192
|
-
// Delete it
|
|
193
|
-
await db.records('my-first-bucket').delete(post.id);
|
|
194
25
|
```
|
|
195
26
|
|
|
196
|
-
|
|
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.
|
|
28
|
+
|
|
29
|
+
Works in **React, Next.js, Vue, React Native, Node.js** — anywhere that runs modern JavaScript.
|
|
197
30
|
|
|
198
31
|
---
|
|
199
32
|
|
|
200
33
|
## Records
|
|
201
34
|
|
|
202
|
-
|
|
203
|
-
- `id` — date-prefixed unique identifier (e.g. `"260601-rec_01JA2XYZ"`) — encodes storage path
|
|
204
|
-
- `createdAt` — Unix timestamp in milliseconds
|
|
205
|
-
- `updatedAt` — Unix timestamp in milliseconds (updated on every write)
|
|
206
|
-
|
|
207
|
-
Records are gzip-compressed before storage. A 850 KB EMR chart becomes ~255 KB on disk. You never manage this — it's always on.
|
|
208
|
-
|
|
209
|
-
### Create a Record
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
const products = db.records('products');
|
|
213
|
-
|
|
214
|
-
const product = await products.create({
|
|
215
|
-
name: 'Wireless Headphones',
|
|
216
|
-
price: 79.99,
|
|
217
|
-
inStock: true,
|
|
218
|
-
tags: ['audio', 'wireless'],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// product.id, product.createdAt, product.updatedAt are added automatically
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Read a Record
|
|
35
|
+
JSON objects stored in named buckets. Every record gets `id`, `createdAt`, and `updatedAt` automatically.
|
|
225
36
|
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
const product = await products.get('rec_abc123');
|
|
37
|
+
```ts
|
|
38
|
+
const posts = db.records('blog-posts');
|
|
229
39
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Patch (merge) — only the specified fields are changed
|
|
237
|
-
const updated = await products.patch('rec_abc123', {
|
|
238
|
-
price: 69.99,
|
|
239
|
-
inStock: false,
|
|
240
|
-
});
|
|
40
|
+
// Create
|
|
41
|
+
const post = await posts.create(
|
|
42
|
+
{ title: 'Hello', status: 'draft', views: 0 },
|
|
43
|
+
{ queryableFields: ['status'] }, // declare fields you want to filter on
|
|
44
|
+
);
|
|
45
|
+
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
241
46
|
|
|
242
|
-
//
|
|
243
|
-
const
|
|
244
|
-
name: 'Wireless Headphones v2',
|
|
245
|
-
price: 89.99,
|
|
246
|
-
inStock: true,
|
|
247
|
-
tags: ['audio', 'wireless', 'premium'],
|
|
248
|
-
});
|
|
249
|
-
```
|
|
47
|
+
// Read (path computed from ID — zero index lookups)
|
|
48
|
+
const found = await posts.get(post.id);
|
|
250
49
|
|
|
251
|
-
|
|
50
|
+
// Update (only the fields you pass are changed)
|
|
51
|
+
await posts.patch(post.id, { status: 'published' });
|
|
252
52
|
|
|
253
|
-
|
|
254
|
-
await
|
|
53
|
+
// Delete
|
|
54
|
+
await posts.delete(post.id);
|
|
255
55
|
```
|
|
256
56
|
|
|
257
|
-
|
|
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.
|
|
258
58
|
|
|
259
|
-
|
|
260
|
-
// Get all records (up to 100 by default)
|
|
261
|
-
const { records } = await products.query();
|
|
59
|
+
### Query
|
|
262
60
|
|
|
263
|
-
|
|
264
|
-
const { records
|
|
61
|
+
```ts
|
|
62
|
+
const { records, hasMore, nextCursor } = await posts.query({
|
|
265
63
|
filters: [
|
|
266
|
-
{ field: '
|
|
267
|
-
{ field: '
|
|
64
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
65
|
+
{ field: 'views', op: '>', value: 100 },
|
|
268
66
|
],
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// Sort and paginate
|
|
272
|
-
const { records, hasMore, nextCursor } = await products.query({
|
|
273
|
-
orderBy: 'price',
|
|
274
|
-
order: 'asc',
|
|
67
|
+
orderBy: 'createdAt',
|
|
68
|
+
order: 'desc',
|
|
275
69
|
limit: 20,
|
|
276
70
|
});
|
|
277
71
|
|
|
278
72
|
// Next page
|
|
279
73
|
if (hasMore) {
|
|
280
|
-
const page2 = await
|
|
281
|
-
orderBy: 'price',
|
|
282
|
-
order: 'asc',
|
|
283
|
-
limit: 20,
|
|
284
|
-
startAfter: nextCursor,
|
|
285
|
-
});
|
|
74
|
+
const page2 = await posts.query({ limit: 20, startAfter: nextCursor });
|
|
286
75
|
}
|
|
76
|
+
```
|
|
287
77
|
|
|
288
|
-
|
|
289
|
-
const { records: lightRecords } = await products.query({
|
|
290
|
-
fields: 'name,price,inStock',
|
|
291
|
-
});
|
|
78
|
+
Filter operators: `==` `!=` `>` `<` `>=` `<=` `CONTAINS`
|
|
292
79
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
80
|
+
### Atomic field updates
|
|
81
|
+
|
|
82
|
+
Avoid race conditions with server-side operations inside `patch()`:
|
|
83
|
+
|
|
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);
|
|
300
96
|
```
|
|
301
97
|
|
|
302
|
-
|
|
98
|
+
### Batch operations
|
|
303
99
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
// Create multiple records at once
|
|
318
|
-
const created = await products.batchCreate([
|
|
319
|
-
{ name: 'Item A', price: 10.00, inStock: true },
|
|
320
|
-
{ name: 'Item B', price: 20.00, inStock: false },
|
|
321
|
-
{ name: 'Item C', price: 30.00, inStock: true },
|
|
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'] },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Update up to 500 records at once
|
|
108
|
+
await posts.batchUpdate([
|
|
109
|
+
{ recordId: 'id1', values: { status: 'archived' } },
|
|
110
|
+
{ recordId: 'id2', values: { status: 'archived' } },
|
|
322
111
|
]);
|
|
323
|
-
// → [{ id: '260601-rec_1', ... }, { id: '260601-rec_2', ... }, ...]
|
|
324
112
|
|
|
325
|
-
//
|
|
326
|
-
const
|
|
327
|
-
|
|
113
|
+
// Delete up to 500 records at once
|
|
114
|
+
const { successful, failed } = await posts.batchDelete(['id1', 'id2']);
|
|
115
|
+
```
|
|
328
116
|
|
|
329
|
-
|
|
330
|
-
const all = await products.getAll({ orderBy: 'price', order: 'asc' });
|
|
117
|
+
### Version history
|
|
331
118
|
|
|
332
|
-
|
|
333
|
-
const { deleted, failed } = await products.batchDelete(['rec_1', 'rec_2', 'rec_3']);
|
|
334
|
-
```
|
|
119
|
+
Every write is automatically versioned.
|
|
335
120
|
|
|
336
|
-
|
|
121
|
+
```ts
|
|
122
|
+
const history = await posts.getHistory(post.id);
|
|
123
|
+
// [{ generation, savedAt, savedBy, sizeBytes }, …]
|
|
337
124
|
|
|
338
|
-
|
|
125
|
+
// Read a specific past version
|
|
126
|
+
const v1 = await posts.getVersion(post.id, history[0].generation!);
|
|
127
|
+
```
|
|
339
128
|
|
|
340
|
-
|
|
341
|
-
// Get the full version history of a record
|
|
342
|
-
const history = await products.getHistory('rec_abc123');
|
|
343
|
-
// history[0] is the latest version, history[1] is one write before, etc.
|
|
129
|
+
### Custom record IDs
|
|
344
130
|
|
|
345
|
-
|
|
346
|
-
|
|
131
|
+
```ts
|
|
132
|
+
// If the ID already exists, the record is upserted
|
|
133
|
+
const post = await posts.create(
|
|
134
|
+
{ title: 'Welcome' },
|
|
135
|
+
{ customRecordId: '260601-post_welcome' },
|
|
136
|
+
);
|
|
347
137
|
```
|
|
348
138
|
|
|
349
139
|
---
|
|
350
140
|
|
|
351
|
-
##
|
|
141
|
+
## Auth
|
|
352
142
|
|
|
353
|
-
|
|
143
|
+
A complete user system — signup, login, sessions, password reset, email verification, and admin controls.
|
|
354
144
|
|
|
355
|
-
```
|
|
356
|
-
const auth = db.auth(
|
|
145
|
+
```ts
|
|
146
|
+
const auth = db.auth();
|
|
357
147
|
```
|
|
358
148
|
|
|
359
|
-
### Sign
|
|
149
|
+
### Sign up & log in
|
|
360
150
|
|
|
361
|
-
```
|
|
151
|
+
```ts
|
|
152
|
+
// Sign up — extra fields beyond email/password are stored on the user
|
|
362
153
|
const { user, session } = await auth.signup({
|
|
363
154
|
email: 'alice@example.com',
|
|
364
|
-
password: 'hunter2',
|
|
365
|
-
fullName: 'Alice
|
|
366
|
-
//
|
|
367
|
-
plan: 'pro',
|
|
368
|
-
referral: 'friend123',
|
|
155
|
+
password: 'hunter2',
|
|
156
|
+
fullName: 'Alice Smith',
|
|
157
|
+
plan: 'pro', // any custom fields you want
|
|
369
158
|
});
|
|
370
159
|
|
|
371
|
-
// user.id → "usr_xxxxxxxxxx"
|
|
372
|
-
// session.sessionId → persist this in your app
|
|
373
|
-
// session.refreshToken → persist this for long-lived sessions
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Log In / Log Out
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
160
|
// Log in
|
|
380
161
|
const { user, session } = await auth.login({
|
|
381
162
|
email: 'alice@example.com',
|
|
382
163
|
password: 'hunter2',
|
|
383
164
|
});
|
|
384
165
|
|
|
385
|
-
// Log out (
|
|
166
|
+
// Log out (pass allDevices: true to log out everywhere)
|
|
386
167
|
await auth.logout({ sessionId: session.sessionId });
|
|
387
168
|
```
|
|
388
169
|
|
|
389
|
-
|
|
170
|
+
Store `session.sessionId` and `session.refreshToken` in your app. Sessions expire after **24 hours**, refresh tokens after **30 days**.
|
|
390
171
|
|
|
391
|
-
|
|
172
|
+
### Session management
|
|
392
173
|
|
|
393
|
-
```
|
|
394
|
-
//
|
|
395
|
-
const
|
|
396
|
-
refreshToken: session.refreshToken,
|
|
397
|
-
});
|
|
398
|
-
// 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);
|
|
399
177
|
|
|
400
|
-
// Get the
|
|
401
|
-
const
|
|
178
|
+
// Get a new session using the refresh token
|
|
179
|
+
const newSession = await auth.refreshSession(session.refreshToken);
|
|
402
180
|
```
|
|
403
181
|
|
|
404
|
-
###
|
|
182
|
+
### User profile
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const user = await auth.getUser(session.userId);
|
|
405
186
|
|
|
406
|
-
|
|
407
|
-
const updated = await auth.updateUser({
|
|
187
|
+
await auth.updateUser({
|
|
408
188
|
sessionId: session.sessionId,
|
|
409
189
|
userId: user.id,
|
|
410
|
-
|
|
411
|
-
fullName: 'Alice Smith',
|
|
412
|
-
plan: 'enterprise',
|
|
413
|
-
avatar: 'https://example.com/avatar.jpg',
|
|
414
|
-
},
|
|
190
|
+
updates: { fullName: 'Alice Johnson', plan: 'enterprise' },
|
|
415
191
|
});
|
|
416
|
-
```
|
|
417
192
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
// 1. User requests a reset (always returns success — prevents email enumeration)
|
|
422
|
-
await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
423
|
-
|
|
424
|
-
// 2. User receives an email with a reset token
|
|
425
|
-
|
|
426
|
-
// 3. User submits the new password
|
|
427
|
-
await auth.confirmPasswordReset({
|
|
428
|
-
resetToken: 'tok_from_email',
|
|
429
|
-
newPassword: 'correcthorsebatterystaple',
|
|
430
|
-
});
|
|
431
|
-
// All existing sessions for this user are automatically revoked
|
|
193
|
+
await auth.deleteUser(session.sessionId, user.id);
|
|
432
194
|
```
|
|
433
195
|
|
|
434
|
-
###
|
|
196
|
+
### Password & email
|
|
435
197
|
|
|
436
|
-
```
|
|
198
|
+
```ts
|
|
199
|
+
// Change password (requires active session)
|
|
437
200
|
await auth.changePassword({
|
|
438
201
|
sessionId: session.sessionId,
|
|
439
202
|
userId: user.id,
|
|
440
203
|
currentPassword: 'hunter2',
|
|
441
204
|
newPassword: 'correcthorsebatterystaple',
|
|
442
205
|
});
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Email Verification
|
|
446
206
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
await auth.
|
|
207
|
+
// Forgot password flow
|
|
208
|
+
await auth.requestPasswordReset('alice@example.com');
|
|
209
|
+
await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');
|
|
450
210
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
|
|
211
|
+
// Email verification
|
|
212
|
+
await auth.requestEmailVerification(user.id);
|
|
213
|
+
await auth.confirmEmailVerification(tokenFromEmail);
|
|
455
214
|
```
|
|
456
215
|
|
|
457
|
-
### Admin
|
|
458
|
-
|
|
459
|
-
Admin operations require a valid session from a user with `role: 'admin'`.
|
|
216
|
+
### Admin
|
|
460
217
|
|
|
461
|
-
```
|
|
462
|
-
// List all users
|
|
463
|
-
const { users,
|
|
218
|
+
```ts
|
|
219
|
+
// List all users (paginated)
|
|
220
|
+
const { users, hasMore, nextCursor } = await auth.listUsers({
|
|
464
221
|
sessionId: adminSession.sessionId,
|
|
465
222
|
limit: 50,
|
|
466
|
-
offset: 0,
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Lock an account (prevents login)
|
|
470
|
-
await auth.lockAccount({
|
|
471
|
-
sessionId: adminSession.sessionId,
|
|
472
|
-
userId: 'usr_abc123',
|
|
473
|
-
duration: 60 * 60 * 1000, // lock for 1 hour (default: 15 minutes)
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Unlock an account
|
|
477
|
-
await auth.unlockAccount({
|
|
478
|
-
sessionId: adminSession.sessionId,
|
|
479
|
-
userId: 'usr_abc123',
|
|
480
223
|
});
|
|
481
224
|
|
|
482
|
-
//
|
|
483
|
-
await auth.
|
|
484
|
-
|
|
485
|
-
userId: 'usr_abc123',
|
|
486
|
-
});
|
|
225
|
+
// Lock / unlock an account
|
|
226
|
+
await auth.lockAccount({ sessionId: adminSession.sessionId, userId: user.id });
|
|
227
|
+
await auth.unlockAccount(adminSession.sessionId, user.id);
|
|
487
228
|
|
|
488
|
-
//
|
|
489
|
-
await auth.hardDeleteUser(
|
|
229
|
+
// Delete users
|
|
230
|
+
await auth.hardDeleteUser(adminSession.sessionId, user.id);
|
|
231
|
+
await auth.bulkDeleteUsers({
|
|
490
232
|
sessionId: adminSession.sessionId,
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// Bulk delete multiple users
|
|
495
|
-
const { deleted, failed } = await auth.bulkDeleteUsers({
|
|
496
|
-
sessionId: adminSession.sessionId,
|
|
497
|
-
userIds: ['usr_a', 'usr_b', 'usr_c'],
|
|
233
|
+
userIds: ['id1', 'id2'],
|
|
234
|
+
hard: true,
|
|
498
235
|
});
|
|
499
236
|
```
|
|
500
237
|
|
|
@@ -502,548 +239,238 @@ const { deleted, failed } = await auth.bulkDeleteUsers({
|
|
|
502
239
|
|
|
503
240
|
## File Storage
|
|
504
241
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
// Pick a storage key by the name you gave it in storageKeys
|
|
509
|
-
const files = db.storage('main');
|
|
510
|
-
const avatars = db.storage('avatars');
|
|
511
|
-
const documents = db.storage('documents');
|
|
242
|
+
```ts
|
|
243
|
+
const storage = db.storage('main');
|
|
512
244
|
```
|
|
513
245
|
|
|
514
|
-
###
|
|
246
|
+
### Upload
|
|
515
247
|
|
|
516
|
-
|
|
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)
|
|
517
253
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const file = document.querySelector('input[type="file"]').files[0];
|
|
521
|
-
|
|
522
|
-
const result = await db.storage('main').upload(file, `uploads/${file.name}`, {
|
|
523
|
-
isPublic: true, // publicly accessible without auth
|
|
524
|
-
overwrite: false, // throw if the file already exists
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
console.log(result.publicUrl); // CDN URL — usable anywhere
|
|
528
|
-
console.log(result.downloadUrl); // null (it's public)
|
|
529
|
-
console.log(result.size); // bytes
|
|
530
|
-
console.log(result.mimeType); // auto-detected from extension
|
|
531
|
-
|
|
532
|
-
// Node.js: upload from a Buffer
|
|
533
|
-
import { readFileSync } from 'fs';
|
|
534
|
-
const buffer = readFileSync('./report.pdf');
|
|
535
|
-
const result = await db.storage('documents').upload(buffer, 'reports/q3.pdf');
|
|
536
|
-
console.log(result.downloadUrl); // requires X-Storage-Key to access
|
|
254
|
+
// Upload a JSON object or string directly
|
|
255
|
+
await storage.uploadRaw({ theme: 'dark' }, 'config.json');
|
|
537
256
|
```
|
|
538
257
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
const result = await db.storage('main').uploadRaw(
|
|
543
|
-
{ theme: 'dark', language: 'en' },
|
|
544
|
-
'user-config/alice.json',
|
|
545
|
-
{ isPublic: false },
|
|
546
|
-
);
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### Large File Upload (with progress)
|
|
550
|
-
|
|
551
|
-
For files over 10 MB or when you need a progress bar. The file goes directly to GCS — your server never buffers it.
|
|
258
|
+
**Large files with progress tracking** (recommended for anything > 10 MB):
|
|
552
259
|
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
// Step 1: Get a signed upload URL
|
|
260
|
+
```ts
|
|
261
|
+
// Step 1 — get a signed upload URL
|
|
557
262
|
const { uploadUrl, path } = await storage.getUploadUrl({
|
|
558
|
-
path: 'videos/
|
|
263
|
+
path: 'videos/intro.mp4',
|
|
559
264
|
mimeType: 'video/mp4',
|
|
560
265
|
size: file.size,
|
|
561
266
|
isPublic: true,
|
|
562
267
|
});
|
|
563
268
|
|
|
564
|
-
// Step 2
|
|
565
|
-
await storage.uploadToSignedUrl(
|
|
566
|
-
|
|
567
|
-
file,
|
|
568
|
-
'video/mp4',
|
|
569
|
-
(percent) => {
|
|
570
|
-
progressBar.style.width = `${percent}%`;
|
|
571
|
-
console.log(`${percent}% uploaded`);
|
|
572
|
-
},
|
|
573
|
-
);
|
|
574
|
-
|
|
575
|
-
// Step 3: Confirm the upload (registers metadata server-side)
|
|
576
|
-
const result = await storage.confirmUpload({
|
|
577
|
-
path: path,
|
|
578
|
-
mimeType: 'video/mp4',
|
|
579
|
-
isPublic: true,
|
|
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`);
|
|
580
272
|
});
|
|
581
273
|
|
|
582
|
-
|
|
274
|
+
// Step 3 — confirm and get the final result
|
|
275
|
+
const result = await storage.confirmUpload({ path, mimeType: 'video/mp4', isPublic: true });
|
|
583
276
|
```
|
|
584
277
|
|
|
585
|
-
###
|
|
278
|
+
### Download, list, manage
|
|
586
279
|
|
|
587
|
-
```
|
|
588
|
-
|
|
280
|
+
```ts
|
|
281
|
+
// Download a private file
|
|
282
|
+
const buffer = await storage.download('private/doc.pdf');
|
|
589
283
|
|
|
590
|
-
//
|
|
591
|
-
const { files } = await storage.
|
|
592
|
-
{ path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
|
|
593
|
-
{ path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
|
|
594
|
-
]);
|
|
284
|
+
// List files
|
|
285
|
+
const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/' });
|
|
595
286
|
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Confirm all at once
|
|
602
|
-
const results = await storage.batchConfirmUploads(
|
|
603
|
-
files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true })),
|
|
604
|
-
);
|
|
605
|
-
```
|
|
287
|
+
// Get file metadata
|
|
288
|
+
const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
606
289
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
```typescript
|
|
610
|
-
// Private files require authentication — returns ArrayBuffer
|
|
611
|
-
const buffer = await db.storage('documents').download('reports/q3.pdf');
|
|
612
|
-
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
613
|
-
|
|
614
|
-
// Trigger a browser download
|
|
615
|
-
const url = URL.createObjectURL(blob);
|
|
616
|
-
const a = document.createElement('a');
|
|
617
|
-
a.href = url;
|
|
618
|
-
a.download = 'q3.pdf';
|
|
619
|
-
a.click();
|
|
620
|
-
|
|
621
|
-
// Public files: use publicUrl directly — no SDK needed
|
|
622
|
-
// <img src={result.publicUrl} />
|
|
623
|
-
```
|
|
290
|
+
// Time-limited share link (no login needed to access)
|
|
291
|
+
const { signedUrl } = await storage.getSignedUrl('private/report.pdf', 3600);
|
|
624
292
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// List everything at the root
|
|
631
|
-
const { files, folders } = await storage.list();
|
|
632
|
-
|
|
633
|
-
// List a specific folder
|
|
634
|
-
const { files, folders, hasMore, nextCursor } = await storage.list({
|
|
635
|
-
prefix: 'gallery/',
|
|
636
|
-
limit: 50,
|
|
637
|
-
recursive: false,
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
// Paginate
|
|
641
|
-
if (hasMore) {
|
|
642
|
-
const page2 = await storage.list({ prefix: 'gallery/', cursor: nextCursor });
|
|
643
|
-
}
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
Each file entry includes:
|
|
647
|
-
```typescript
|
|
648
|
-
{
|
|
649
|
-
name: 'photo1.jpg',
|
|
650
|
-
path: 'gallery/photo1.jpg',
|
|
651
|
-
size: 204800,
|
|
652
|
-
mimeType: 'image/jpeg',
|
|
653
|
-
isPublic: true,
|
|
654
|
-
publicUrl: 'https://storage.googleapis.com/...',
|
|
655
|
-
downloadUrl: null,
|
|
656
|
-
updatedAt: '2025-06-01T12:00:00.000Z',
|
|
657
|
-
}
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### Scoped Storage
|
|
661
|
-
|
|
662
|
-
Working within a specific folder? Use `.scope()` to avoid repeating the prefix on every call.
|
|
663
|
-
|
|
664
|
-
```typescript
|
|
665
|
-
// All operations in the "user-avatars/" folder
|
|
666
|
-
const avatars = db.storage('avatars').scope('user-avatars');
|
|
667
|
-
|
|
668
|
-
await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
|
|
669
|
-
// → uploads to "user-avatars/{userId}.jpg"
|
|
670
|
-
|
|
671
|
-
const { files } = await avatars.list();
|
|
672
|
-
// → lists files under "user-avatars/"
|
|
673
|
-
|
|
674
|
-
await avatars.deleteFile(`${userId}.jpg`);
|
|
675
|
-
// → deletes "user-avatars/{userId}.jpg"
|
|
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/');
|
|
676
298
|
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
// → all operations under "user-avatars/thumbnails/"
|
|
299
|
+
// Change visibility after upload
|
|
300
|
+
await storage.setVisibility('alice.jpg', false); // make private
|
|
680
301
|
```
|
|
681
302
|
|
|
682
|
-
###
|
|
303
|
+
### Scoped storage
|
|
683
304
|
|
|
684
|
-
|
|
685
|
-
const storage = db.storage('documents');
|
|
305
|
+
Automatically prefix every path — useful for per-user file isolation:
|
|
686
306
|
|
|
687
|
-
|
|
688
|
-
const
|
|
307
|
+
```ts
|
|
308
|
+
const userFiles = db.storage('main').scope(`users/${userId}/`);
|
|
689
309
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const { signedUrl, expiresAt } = await storage.getSignedUrl(
|
|
693
|
-
'reports/q3.pdf',
|
|
694
|
-
3600, // expires in 1 hour (default)
|
|
695
|
-
);
|
|
310
|
+
await userFiles.upload(pdf, 'contract.pdf'); // → users/{userId}/contract.pdf
|
|
311
|
+
const { files } = await userFiles.list(); // → only lists users/{userId}/
|
|
696
312
|
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
await storage.setVisibility('reports/q3.pdf', false); // make private
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### File Operations
|
|
703
|
-
|
|
704
|
-
```typescript
|
|
705
|
-
const storage = db.storage('main');
|
|
706
|
-
|
|
707
|
-
// Rename / move a file
|
|
708
|
-
await storage.move('drafts/report.pdf', 'published/report-2025.pdf');
|
|
709
|
-
|
|
710
|
-
// Copy a file
|
|
711
|
-
await storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
712
|
-
|
|
713
|
-
// Create a folder
|
|
714
|
-
await storage.createFolder('archive/2025/');
|
|
715
|
-
|
|
716
|
-
// Delete a file
|
|
717
|
-
await storage.deleteFile('temp/scratch.txt');
|
|
718
|
-
|
|
719
|
-
// Delete a folder and all its contents
|
|
720
|
-
await storage.deleteFolder('temp/');
|
|
721
|
-
|
|
722
|
-
// Get key-level stats
|
|
723
|
-
const stats = await storage.getStats();
|
|
724
|
-
// → { totalFiles: 842, totalBytes: 1073741824, uploadCount: 1200, ... }
|
|
313
|
+
// Nest deeper
|
|
314
|
+
const reports = userFiles.scope('reports/'); // → users/{userId}/reports/
|
|
725
315
|
```
|
|
726
316
|
|
|
727
317
|
---
|
|
728
318
|
|
|
729
319
|
## Analytics
|
|
730
320
|
|
|
731
|
-
|
|
321
|
+
BigQuery-powered aggregations. No ETL, no pipelines — just query.
|
|
732
322
|
|
|
733
|
-
```
|
|
323
|
+
```ts
|
|
734
324
|
const analytics = db.analytics('orders');
|
|
735
|
-
```
|
|
736
325
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
```typescript
|
|
740
|
-
// Total records
|
|
326
|
+
// Total record count
|
|
741
327
|
const { count } = await analytics.count();
|
|
742
328
|
|
|
743
|
-
//
|
|
744
|
-
const
|
|
745
|
-
dateRange: {
|
|
746
|
-
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
747
|
-
end: Date.now(),
|
|
748
|
-
},
|
|
749
|
-
});
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
### Distribution
|
|
753
|
-
|
|
754
|
-
How many records have each unique value for a field?
|
|
755
|
-
|
|
756
|
-
```typescript
|
|
757
|
-
const rows = await analytics.distribution({ field: 'status', limit: 10, order: 'desc' });
|
|
758
|
-
// → [
|
|
759
|
-
// { value: 'completed', count: 8234 },
|
|
760
|
-
// { value: 'pending', count: 1203 },
|
|
761
|
-
// { value: 'refunded', count: 412 },
|
|
762
|
-
// ]
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
### Sum
|
|
766
|
-
|
|
767
|
-
```typescript
|
|
768
|
-
// Total revenue
|
|
769
|
-
const rows = await analytics.sum({ field: 'amount' });
|
|
770
|
-
// → [{ sum: 198432.50 }]
|
|
771
|
-
|
|
772
|
-
// Revenue grouped by country
|
|
773
|
-
const byCountry = await analytics.sum({
|
|
774
|
-
field: 'amount',
|
|
775
|
-
groupBy: 'country',
|
|
776
|
-
limit: 10,
|
|
777
|
-
});
|
|
778
|
-
// → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
|
|
779
|
-
```
|
|
329
|
+
// How many records have each value of `status`
|
|
330
|
+
const dist = await analytics.distribution({ field: 'status' });
|
|
780
331
|
|
|
781
|
-
|
|
332
|
+
// Sum of `amount`, grouped by `region`
|
|
333
|
+
const sums = await analytics.sum({ field: 'amount', groupBy: 'region' });
|
|
782
334
|
|
|
783
|
-
|
|
335
|
+
// Records over time
|
|
336
|
+
const daily = await analytics.timeSeries({ granularity: 'day' });
|
|
784
337
|
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
|
|
788
|
-
dateRange: {
|
|
789
|
-
start: new Date('2025-01-01').getTime(),
|
|
790
|
-
end: new Date('2025-06-01').getTime(),
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
// → [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, ...]
|
|
794
|
-
```
|
|
338
|
+
// How `revenue` changes over time
|
|
339
|
+
const trend = await analytics.fieldTimeSeries({ field: 'revenue', aggregation: 'sum' });
|
|
795
340
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
const revenue = await analytics.fieldTimeSeries({
|
|
800
|
-
field: 'amount',
|
|
801
|
-
aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
802
|
-
granularity: 'week',
|
|
803
|
-
});
|
|
804
|
-
// → [{ date: '2025-W01', value: 12340.50 }, ...]
|
|
805
|
-
```
|
|
341
|
+
// Top 10 countries by record count
|
|
342
|
+
const top10 = await analytics.topN({ field: 'country', n: 10 });
|
|
806
343
|
|
|
807
|
-
|
|
344
|
+
// Min / max / avg / sum / stddev for a numeric field
|
|
345
|
+
const stats = await analytics.stats({ field: 'price' });
|
|
808
346
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
```typescript
|
|
812
|
-
const topProducts = await analytics.topN({
|
|
813
|
-
field: 'productId',
|
|
814
|
-
labelField: 'productName', // optional: include a human-readable label
|
|
815
|
-
n: 5,
|
|
816
|
-
order: 'desc',
|
|
817
|
-
});
|
|
818
|
-
// → [
|
|
819
|
-
// { value: 'prod_123', label: 'Widget Pro', count: 892 },
|
|
820
|
-
// { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
|
|
821
|
-
// ]
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
### Field Stats
|
|
825
|
-
|
|
826
|
-
Statistical summary for any numeric field:
|
|
827
|
-
|
|
828
|
-
```typescript
|
|
829
|
-
const stats = await analytics.stats({ field: 'orderValue' });
|
|
830
|
-
// → {
|
|
831
|
-
// min: 4.99, max: 9999.99, avg: 87.23,
|
|
832
|
-
// sum: 420948.27, count: 4823, stddev: 143.2
|
|
833
|
-
// }
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
### Multi-Metric Dashboard
|
|
837
|
-
|
|
838
|
-
Calculate several aggregations in a single BigQuery query:
|
|
839
|
-
|
|
840
|
-
```typescript
|
|
347
|
+
// Multiple aggregations in one round-trip
|
|
841
348
|
const dashboard = await analytics.multiMetric({
|
|
842
349
|
metrics: [
|
|
843
|
-
{ field: 'amount',
|
|
844
|
-
{ field: 'amount',
|
|
845
|
-
{ field: '
|
|
846
|
-
{ field: 'userId', name: 'totalOrders', aggregation: 'count' },
|
|
350
|
+
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
351
|
+
{ field: 'amount', name: 'avgOrder', aggregation: 'avg' },
|
|
352
|
+
{ field: 'recordId', name: 'orderCount', aggregation: 'count' },
|
|
847
353
|
],
|
|
848
|
-
dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
|
|
849
354
|
});
|
|
850
|
-
// → {
|
|
851
|
-
// totalRevenue: 198432.50,
|
|
852
|
-
// avgOrderValue: 87.23,
|
|
853
|
-
// maxOrder: 9999.99,
|
|
854
|
-
// totalOrders: 2275,
|
|
855
|
-
// }
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
### Filtered Records (BigQuery)
|
|
859
|
-
|
|
860
|
-
Query raw records at full BigQuery speed:
|
|
355
|
+
// → { totalRevenue: 48200, avgOrder: 96.4, orderCount: 500 }
|
|
861
356
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
{ field: 'status', op: '==', value: 'refunded' },
|
|
866
|
-
{ field: 'amount', op: '>', value: 100 },
|
|
867
|
-
],
|
|
868
|
-
selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
|
|
869
|
-
orderBy: 'amount',
|
|
870
|
-
order: 'desc',
|
|
871
|
-
limit: 50,
|
|
872
|
-
});
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
### Cross-Bucket Comparison
|
|
876
|
-
|
|
877
|
-
Compare the same metric across multiple buckets in one query:
|
|
878
|
-
|
|
879
|
-
```typescript
|
|
880
|
-
const comparison = await analytics.crossBucket({
|
|
881
|
-
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'],
|
|
882
360
|
field: 'amount',
|
|
883
361
|
aggregation: 'sum',
|
|
884
362
|
});
|
|
885
|
-
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
363
|
+
|
|
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() },
|
|
367
|
+
});
|
|
890
368
|
```
|
|
891
369
|
|
|
892
|
-
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Error Handling
|
|
893
373
|
|
|
894
|
-
|
|
374
|
+
```ts
|
|
375
|
+
import { HydrousError, NetworkError } from 'hydrousdb';
|
|
895
376
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
377
|
+
try {
|
|
378
|
+
await db.auth().login({ email: 'x@x.com', password: 'wrong' });
|
|
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
|
+
}
|
|
385
|
+
if (err instanceof NetworkError) {
|
|
386
|
+
console.log('No internet or server unreachable');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
899
389
|
```
|
|
900
390
|
|
|
901
|
-
|
|
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 |
|
|
902
400
|
|
|
903
|
-
|
|
401
|
+
---
|
|
904
402
|
|
|
905
|
-
|
|
403
|
+
## TypeScript
|
|
906
404
|
|
|
907
|
-
|
|
908
|
-
import { createClient } from 'hydrousdb';
|
|
405
|
+
The SDK is written in TypeScript. Type your records for full autocomplete:
|
|
909
406
|
|
|
910
|
-
|
|
407
|
+
```ts
|
|
911
408
|
interface Order {
|
|
912
409
|
customerId: string;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
country: string;
|
|
410
|
+
amount: number;
|
|
411
|
+
status: 'pending' | 'paid' | 'refunded';
|
|
412
|
+
items: { sku: string; qty: number }[];
|
|
917
413
|
}
|
|
918
414
|
|
|
919
|
-
|
|
920
|
-
name: string;
|
|
921
|
-
email: string;
|
|
922
|
-
tier: 'free' | 'pro' | 'enterprise';
|
|
923
|
-
credits: number;
|
|
924
|
-
}
|
|
415
|
+
const orders = db.records<Order>('orders');
|
|
925
416
|
|
|
926
|
-
const db = createClient({
|
|
927
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
928
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
929
|
-
storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
// Fully typed clients
|
|
933
|
-
const orders = db.records<Order>('orders');
|
|
934
|
-
const customers = db.records<Customer>('customers');
|
|
935
|
-
|
|
936
|
-
// order.total, order.status, etc. are all type-safe
|
|
937
417
|
const order = await orders.create({
|
|
938
|
-
customerId: '
|
|
939
|
-
|
|
940
|
-
total: 59.98,
|
|
418
|
+
customerId: 'cust_123',
|
|
419
|
+
amount: 49.99,
|
|
941
420
|
status: 'pending',
|
|
942
|
-
|
|
421
|
+
items: [{ sku: 'SHOE-42', qty: 1 }],
|
|
943
422
|
});
|
|
944
|
-
|
|
945
|
-
// TypeScript catches mistakes at compile time:
|
|
946
|
-
// order.nonExistentField // ← TS error ✓
|
|
947
|
-
// order.status = 'invalid' // ← TS error ✓
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
All exported types are available for import:
|
|
951
|
-
|
|
952
|
-
```typescript
|
|
953
|
-
import type {
|
|
954
|
-
HydrousConfig,
|
|
955
|
-
RecordResult,
|
|
956
|
-
QueryFilter,
|
|
957
|
-
QueryOptions,
|
|
958
|
-
UploadResult,
|
|
959
|
-
AnalyticsQuery,
|
|
960
|
-
DateRange,
|
|
961
|
-
// ... and many more
|
|
962
|
-
} from 'hydrousdb';
|
|
423
|
+
// order.status → 'pending' | 'paid' | 'refunded' ✓
|
|
963
424
|
```
|
|
964
425
|
|
|
965
426
|
---
|
|
966
427
|
|
|
967
|
-
##
|
|
968
|
-
|
|
969
|
-
All errors thrown by the SDK extend `HydrousError`, which carries:
|
|
970
|
-
|
|
971
|
-
| Property | Type | Description |
|
|
972
|
-
|---|---|---|
|
|
973
|
-
| `message` | `string` | Human-readable description |
|
|
974
|
-
| `code` | `string` | Machine-readable error code (e.g. `"RECORD_NOT_FOUND"`) |
|
|
975
|
-
| `status` | `number` | HTTP status code |
|
|
976
|
-
| `requestId` | `string` | Server request ID (for support tracing) |
|
|
977
|
-
| `details` | `string[]` | Validation error details |
|
|
428
|
+
## Security
|
|
978
429
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
} catch (err) {
|
|
985
|
-
if (err instanceof AuthError) {
|
|
986
|
-
// Authentication-specific error
|
|
987
|
-
console.error(`Auth failed: ${err.code}`);
|
|
988
|
-
// err.code might be: INVALID_CREDENTIALS, ACCOUNT_LOCKED, EMAIL_NOT_VERIFIED
|
|
989
|
-
} else if (err instanceof NetworkError) {
|
|
990
|
-
// No internet / server unreachable
|
|
991
|
-
console.error('Cannot reach HydrousDB — check your internet connection');
|
|
992
|
-
} else if (err instanceof HydrousError) {
|
|
993
|
-
// Any other API error
|
|
994
|
-
console.error(`API error [${err.code}]: ${err.message}`);
|
|
995
|
-
console.error(`Request ID: ${err.requestId}`); // include in support tickets
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
**Common error codes:**
|
|
1001
|
-
|
|
1002
|
-
| Code | Meaning |
|
|
1003
|
-
|---|---|
|
|
1004
|
-
| `RECORD_NOT_FOUND` | The requested record ID does not exist |
|
|
1005
|
-
| `INVALID_CREDENTIALS` | Wrong email or password |
|
|
1006
|
-
| `ACCOUNT_LOCKED` | The account is temporarily locked |
|
|
1007
|
-
| `INVALID_SESSION` | Session expired or revoked — re-authenticate |
|
|
1008
|
-
| `MISSING_API_KEY` | Key not provided |
|
|
1009
|
-
| `INVALID_SECURITY_KEY` | Key is wrong or revoked |
|
|
1010
|
-
| `FORBIDDEN` | Insufficient permissions |
|
|
1011
|
-
| `FILE_EXISTS` | File already exists at path (use `overwrite: true`) |
|
|
1012
|
-
| `LIMIT_EXCEEDED` | Storage quota or file size limit reached |
|
|
1013
|
-
| `SYSTEM_BUCKET_FORBIDDEN` | Cannot query system buckets via analytics |
|
|
1014
|
-
| `VALIDATION_ERROR` | Invalid input — check `err.details` |
|
|
1015
|
-
| `NETWORK_ERROR` | Failed to reach the API |
|
|
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.
|
|
1016
435
|
|
|
1017
436
|
---
|
|
1018
437
|
|
|
1019
|
-
##
|
|
1020
|
-
|
|
1021
|
-
1. **Never hard-code your keys.** Use environment variables:
|
|
1022
|
-
|
|
1023
|
-
```bash
|
|
1024
|
-
# .env (add to .gitignore)
|
|
1025
|
-
HYDROUS_AUTH_KEY=hk_auth_xxxxxxxxxxxxxxxxxxxx
|
|
1026
|
-
HYDROUS_BUCKET_KEY=hk_bucket_xxxxxxxxxxxxxxxxxxxx
|
|
1027
|
-
HYDROUS_STORAGE_MAIN=ssk_xxxxxxxxxxxxxxxxxxxx
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
```typescript
|
|
1031
|
-
const db = createClient({
|
|
1032
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1033
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1034
|
-
storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
|
|
1035
|
-
});
|
|
1036
|
-
```
|
|
438
|
+
## Framework examples
|
|
1037
439
|
|
|
1038
|
-
|
|
440
|
+
**Next.js (App Router)**
|
|
441
|
+
```ts
|
|
442
|
+
// lib/db.ts
|
|
443
|
+
import { createClient } from 'hydrousdb';
|
|
444
|
+
export const db = createClient({ … });
|
|
1039
445
|
|
|
1040
|
-
|
|
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);
|
|
451
|
+
}
|
|
452
|
+
```
|
|
1041
453
|
|
|
1042
|
-
|
|
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
|
+
```
|
|
1043
460
|
|
|
1044
|
-
|
|
461
|
+
**Vue / Nuxt**
|
|
462
|
+
```ts
|
|
463
|
+
// composables/useDb.ts
|
|
464
|
+
import { createClient } from 'hydrousdb';
|
|
465
|
+
export const db = createClient({ … });
|
|
466
|
+
```
|
|
1045
467
|
|
|
1046
|
-
|
|
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
|
+
```
|
|
1047
474
|
|
|
1048
475
|
---
|
|
1049
476
|
|
|
@@ -1051,150 +478,98 @@ try {
|
|
|
1051
478
|
|
|
1052
479
|
### `createClient(config)`
|
|
1053
480
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
storageKeys: { // Required — at least one entry
|
|
1061
|
-
main: 'ssk_main_…',
|
|
1062
|
-
avatars: 'ssk_avatars_…',
|
|
1063
|
-
documents: 'ssk_docs_…',
|
|
1064
|
-
},
|
|
1065
|
-
baseUrl: 'https://...', // Optional — defaults to official endpoint
|
|
1066
|
-
});
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
### `db.records<T>(bucketKey)`
|
|
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) |
|
|
1070
487
|
|
|
1071
|
-
|
|
488
|
+
### `db.records(bucket)` — key methods
|
|
1072
489
|
|
|
1073
|
-
| Method |
|
|
490
|
+
| Method | What it does |
|
|
1074
491
|
|---|---|
|
|
1075
|
-
| `create(data)` | Create a
|
|
1076
|
-
| `get(id)` |
|
|
1077
|
-
| `
|
|
1078
|
-
| `
|
|
1079
|
-
| `
|
|
1080
|
-
| `
|
|
1081
|
-
| `
|
|
1082
|
-
| `
|
|
1083
|
-
| `
|
|
1084
|
-
| `
|
|
1085
|
-
| `
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
Returns an `AuthClient` for the named user bucket. Uses `authKey` automatically.
|
|
1091
|
-
|
|
1092
|
-
| Method | Description |
|
|
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 |
|
|
1093
507
|
|---|---|
|
|
1094
|
-
| `signup(opts)` | Register
|
|
1095
|
-
| `login(opts)` | Authenticate
|
|
1096
|
-
| `logout({ sessionId })` |
|
|
1097
|
-
| `
|
|
1098
|
-
| `
|
|
1099
|
-
| `
|
|
1100
|
-
| `
|
|
1101
|
-
| `
|
|
1102
|
-
| `
|
|
1103
|
-
| `
|
|
1104
|
-
| `
|
|
1105
|
-
| `
|
|
1106
|
-
| `
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
|
1111
|
-
|
|
1112
|
-
### `db.analytics(bucketKey)`
|
|
1113
|
-
|
|
1114
|
-
Returns an `AnalyticsClient` for the named bucket. Uses `bucketSecurityKey` automatically.
|
|
1115
|
-
|
|
1116
|
-
| Method | Description |
|
|
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 |
|
|
1117
525
|
|---|---|
|
|
1118
|
-
| `
|
|
1119
|
-
| `
|
|
1120
|
-
| `
|
|
1121
|
-
| `
|
|
1122
|
-
| `
|
|
1123
|
-
| `
|
|
1124
|
-
| `
|
|
1125
|
-
| `
|
|
1126
|
-
| `
|
|
1127
|
-
| `
|
|
1128
|
-
| `
|
|
1129
|
-
| `
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
```typescript
|
|
1136
|
-
const storage = db.storage('avatars');
|
|
1137
|
-
const scoped = db.storage('avatars').scope('user-uploads/');
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
| Method | Description |
|
|
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 |
|
|
1141
543
|
|---|---|
|
|
1142
|
-
| `
|
|
1143
|
-
| `
|
|
1144
|
-
| `
|
|
1145
|
-
| `
|
|
1146
|
-
| `
|
|
1147
|
-
| `
|
|
1148
|
-
| `
|
|
1149
|
-
| `
|
|
1150
|
-
| `
|
|
1151
|
-
| `
|
|
1152
|
-
| `getMetadata(path)` | Get file metadata |
|
|
1153
|
-
| `getSignedUrl(path, expiresIn?)` | Generate a time-limited share URL |
|
|
1154
|
-
| `setVisibility(path, isPublic)` | Toggle public / private |
|
|
1155
|
-
| `createFolder(path)` | Create a folder |
|
|
1156
|
-
| `deleteFile(path)` | Delete a file |
|
|
1157
|
-
| `deleteFolder(path)` | Delete a folder and all its contents |
|
|
1158
|
-
| `move(from, to)` | Move or rename a file |
|
|
1159
|
-
| `copy(from, to)` | Copy a file |
|
|
1160
|
-
| `getStats()` | Key-level storage statistics |
|
|
1161
|
-
| `info()` | Ping the storage service (no auth required) |
|
|
1162
|
-
| `scope(prefix)` | Get a `ScopedStorage` instance pre-fixed to a folder |
|
|
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. |
|
|
1163
554
|
|
|
1164
555
|
---
|
|
1165
556
|
|
|
1166
557
|
## Contributing
|
|
1167
558
|
|
|
1168
|
-
We love contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
1169
|
-
|
|
1170
559
|
```bash
|
|
1171
|
-
# Clone the repo
|
|
1172
560
|
git clone https://github.com/hydrousdb/hydrousdb-js.git
|
|
1173
561
|
cd hydrousdb-js
|
|
1174
|
-
|
|
1175
|
-
# Install dependencies
|
|
1176
562
|
npm install
|
|
1177
|
-
|
|
1178
|
-
#
|
|
1179
|
-
npm test
|
|
1180
|
-
|
|
1181
|
-
# Build
|
|
1182
|
-
npm run build
|
|
1183
|
-
|
|
1184
|
-
# Run tests in watch mode
|
|
1185
|
-
npm run test:watch
|
|
563
|
+
npm test # run tests
|
|
564
|
+
npm run build # compile
|
|
1186
565
|
```
|
|
1187
566
|
|
|
1188
|
-
---
|
|
1189
|
-
|
|
1190
567
|
## License
|
|
1191
568
|
|
|
1192
|
-
MIT —
|
|
569
|
+
MIT — [LICENSE](./LICENSE)
|
|
1193
570
|
|
|
1194
571
|
---
|
|
1195
572
|
|
|
1196
573
|
<p align="center">
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
<a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
|
|
1200
|
-
</p>
|
|
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>
|
|
575
|
+
</p>
|