hydrousdb 3.0.1 → 3.2.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 +945 -493
- package/dist/index.cjs +539 -427
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -325
- package/dist/index.d.ts +292 -325
- package/dist/{index.js → index.mjs} +541 -429
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -6
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# HydrousDB JavaScript / TypeScript SDK
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
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>
|
|
5
6
|
</p>
|
|
6
7
|
|
|
7
8
|
<p align="center">
|
|
@@ -16,47 +17,65 @@
|
|
|
16
17
|
## Table of Contents
|
|
17
18
|
|
|
18
19
|
- [What is HydrousDB?](#what-is-hydrousdb)
|
|
19
|
-
- [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [Step 4 — Install the SDK](#step-4--install-the-sdk)
|
|
24
|
-
- [Step 5 — Your first record](#step-5--your-first-record)
|
|
20
|
+
- [How It Works](#how-it-works)
|
|
21
|
+
- [Quick Start](#quick-start)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Module Formats — ESM & CommonJS](#module-formats--esm--commonjs)
|
|
25
24
|
- [Records](#records)
|
|
26
25
|
- [Create](#create-a-record)
|
|
27
26
|
- [Read](#read-a-record)
|
|
28
|
-
- [Update](#update-a-record)
|
|
27
|
+
- [Update — patch vs set](#update-a-record)
|
|
29
28
|
- [Delete](#delete-a-record)
|
|
30
29
|
- [Query](#query-records)
|
|
31
|
-
- [
|
|
30
|
+
- [Count](#count-records)
|
|
31
|
+
- [Batch Create](#batch-create)
|
|
32
|
+
- [Batch Delete](#batch-delete)
|
|
32
33
|
- [Version History](#version-history)
|
|
34
|
+
- [Write-Filter Sentinels](#write-filter-sentinels)
|
|
35
|
+
- [Custom Record IDs](#custom-record-ids)
|
|
33
36
|
- [Authentication](#authentication)
|
|
34
|
-
- [Sign Up](#sign-up
|
|
37
|
+
- [Sign Up](#sign-up)
|
|
35
38
|
- [Log In / Log Out](#log-in--log-out)
|
|
36
39
|
- [Session Management](#session-management)
|
|
37
|
-
- [
|
|
40
|
+
- [Validate a Session](#validate-a-session)
|
|
41
|
+
- [Update Profile](#update-profile)
|
|
42
|
+
- [Change Password](#change-password)
|
|
43
|
+
- [Password Reset Flow](#password-reset-flow)
|
|
38
44
|
- [Email Verification](#email-verification)
|
|
39
|
-
- [Admin
|
|
45
|
+
- [Admin — List Users](#admin--list-users)
|
|
46
|
+
- [Admin — Lock / Unlock](#admin--lock--unlock)
|
|
47
|
+
- [Admin — Delete Users](#admin--delete-users)
|
|
40
48
|
- [File Storage](#file-storage)
|
|
41
49
|
- [Simple Upload](#simple-upload)
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
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)
|
|
44
55
|
- [List Files](#list-files)
|
|
45
56
|
- [Scoped Storage](#scoped-storage)
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
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)
|
|
48
62
|
- [Analytics](#analytics)
|
|
49
|
-
- [Count](#count)
|
|
63
|
+
- [Count](#count-1)
|
|
50
64
|
- [Distribution](#distribution)
|
|
65
|
+
- [Sum](#sum)
|
|
51
66
|
- [Time Series](#time-series)
|
|
67
|
+
- [Field Time Series](#field-time-series)
|
|
52
68
|
- [Top N](#top-n)
|
|
53
69
|
- [Field Stats](#field-stats)
|
|
54
70
|
- [Multi-Metric Dashboard](#multi-metric-dashboard)
|
|
71
|
+
- [Filtered Records via BigQuery](#filtered-records-via-bigquery)
|
|
55
72
|
- [Cross-Bucket Comparison](#cross-bucket-comparison)
|
|
56
|
-
- [
|
|
73
|
+
- [Storage Stats](#storage-stats-1)
|
|
74
|
+
- [Raw Query](#raw-query)
|
|
75
|
+
- [TypeScript](#typescript)
|
|
57
76
|
- [Error Handling](#error-handling)
|
|
58
77
|
- [Security Best Practices](#security-best-practices)
|
|
59
|
-
- [API Reference](#api-reference)
|
|
78
|
+
- [Full API Reference](#full-api-reference)
|
|
60
79
|
- [Contributing](#contributing)
|
|
61
80
|
- [License](#license)
|
|
62
81
|
|
|
@@ -64,193 +83,296 @@
|
|
|
64
83
|
|
|
65
84
|
## What is HydrousDB?
|
|
66
85
|
|
|
67
|
-
|
|
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:**
|
|
68
100
|
|
|
69
101
|
| Feature | What it does |
|
|
70
102
|
|---|---|
|
|
71
|
-
| **Records** | Schemaless JSON
|
|
72
|
-
| **Auth** | Full user
|
|
73
|
-
| **Storage** | File uploads
|
|
74
|
-
| **Analytics** | BigQuery-powered
|
|
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
|
+
---
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
## How It Works
|
|
77
111
|
|
|
78
|
-
|
|
79
|
-
- **Bucket Key** — just the name of your bucket (e.g. `"blog-posts"`, `"app-users"`). Not a secret.
|
|
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.
|
|
80
113
|
|
|
81
|
-
|
|
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
|
+
```
|
|
82
123
|
|
|
83
|
-
|
|
124
|
+
Records are gzip-compressed on write (60–80% size reduction). An 850 KB hospital chart becomes ~255 KB on disk, automatically, every time.
|
|
84
125
|
|
|
85
|
-
|
|
126
|
+
---
|
|
86
127
|
|
|
87
|
-
|
|
128
|
+
## Quick Start
|
|
88
129
|
|
|
89
|
-
###
|
|
130
|
+
### 1. Create your account
|
|
90
131
|
|
|
91
|
-
|
|
92
|
-
2. Click **"New Bucket"**.
|
|
93
|
-
3. Give it a name — use lowercase letters, numbers, hyphens, or underscores (e.g. `my-first-bucket`).
|
|
94
|
-
4. Click **"Create"**.
|
|
132
|
+
Sign up at [https://hydrousdb.com](https://hydrousdb.com).
|
|
95
133
|
|
|
96
|
-
|
|
134
|
+
### 2. Get your API keys
|
|
97
135
|
|
|
98
|
-
|
|
136
|
+
From the dashboard → **Settings → API Keys**, create three key types:
|
|
99
137
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 |
|
|
103
143
|
|
|
104
|
-
> ⚠️
|
|
144
|
+
> ⚠️ Never commit these to Git. Store them in environment variables.
|
|
105
145
|
|
|
106
|
-
###
|
|
146
|
+
### 3. Install
|
|
107
147
|
|
|
108
148
|
```bash
|
|
109
149
|
npm install hydrousdb
|
|
110
|
-
# or
|
|
111
|
-
yarn add hydrousdb
|
|
112
|
-
# or
|
|
113
|
-
pnpm add hydrousdb
|
|
150
|
+
# or: yarn add hydrousdb / pnpm add hydrousdb
|
|
114
151
|
```
|
|
115
152
|
|
|
116
153
|
**Requirements:** Node.js 18+ (uses the native `fetch` API).
|
|
117
154
|
|
|
118
|
-
###
|
|
155
|
+
### 4. Create the client and write your first record
|
|
119
156
|
|
|
120
157
|
```typescript
|
|
121
158
|
import { createClient } from 'hydrousdb';
|
|
122
159
|
|
|
123
|
-
// Create
|
|
160
|
+
// Create once — reuse everywhere in your app
|
|
124
161
|
const db = createClient({
|
|
125
|
-
|
|
162
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
163
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
164
|
+
storageKeys: {
|
|
165
|
+
main: process.env.HYDROUS_STORAGE_MAIN!,
|
|
166
|
+
},
|
|
126
167
|
});
|
|
127
168
|
|
|
128
|
-
// Write
|
|
129
|
-
const post = await db.records('my-
|
|
169
|
+
// Write
|
|
170
|
+
const post = await db.records('my-bucket').create({
|
|
130
171
|
title: 'Hello, HydrousDB!',
|
|
131
|
-
body: 'My first record.',
|
|
132
172
|
published: false,
|
|
133
173
|
});
|
|
134
174
|
|
|
135
|
-
console.log(post.id); // "
|
|
175
|
+
console.log(post.id); // "260601-rec_01JA2XYZ"
|
|
136
176
|
console.log(post.createdAt); // 1717200000000
|
|
137
177
|
|
|
138
|
-
// Read
|
|
139
|
-
const fetched = await db.records('my-
|
|
140
|
-
|
|
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
|
+
---
|
|
141
189
|
|
|
142
|
-
|
|
143
|
-
const updated = await db.records('my-first-bucket').patch(post.id, { published: true });
|
|
190
|
+
## Installation
|
|
144
191
|
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
```bash
|
|
193
|
+
npm install hydrousdb
|
|
147
194
|
```
|
|
148
195
|
|
|
149
|
-
|
|
196
|
+
### Module Formats — ESM & CommonJS
|
|
197
|
+
|
|
198
|
+
The package ships both ESM (`.mjs`) and CommonJS (`.cjs`) builds. Your toolchain picks the right one automatically based on your `import` or `require` call.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// ESM — Next.js, Vite, modern Node, TypeScript
|
|
202
|
+
import { createClient } from 'hydrousdb';
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
// CommonJS — legacy Node, Jest without transform, older tooling
|
|
207
|
+
const { createClient } = require('hydrousdb');
|
|
208
|
+
```
|
|
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
|
+
```
|
|
150
234
|
|
|
151
235
|
---
|
|
152
236
|
|
|
153
237
|
## Records
|
|
154
238
|
|
|
155
239
|
Records are JSON objects stored in named buckets. Every record automatically gets:
|
|
156
|
-
|
|
240
|
+
|
|
241
|
+
- `id` — date-prefixed unique identifier (`"260601-rec_01JA2XYZ"`) — encodes the GCS path
|
|
157
242
|
- `createdAt` — Unix timestamp in milliseconds
|
|
158
|
-
- `updatedAt` — Unix timestamp in milliseconds
|
|
243
|
+
- `updatedAt` — Unix timestamp in milliseconds
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const posts = db.records('blog-posts');
|
|
247
|
+
// or typed:
|
|
248
|
+
const orders = db.records<Order>('orders');
|
|
249
|
+
```
|
|
159
250
|
|
|
160
251
|
### Create a Record
|
|
161
252
|
|
|
162
253
|
```typescript
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
inStock: true,
|
|
169
|
-
tags: ['audio', 'wireless'],
|
|
254
|
+
const post = await posts.create({
|
|
255
|
+
title: 'My First Post',
|
|
256
|
+
body: 'Hello world.',
|
|
257
|
+
status: 'draft',
|
|
258
|
+
views: 0,
|
|
170
259
|
});
|
|
171
260
|
|
|
172
|
-
//
|
|
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
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const post = await posts.create(
|
|
269
|
+
{
|
|
270
|
+
title: 'My First Post',
|
|
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
|
+
},
|
|
278
|
+
);
|
|
173
279
|
```
|
|
174
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
|
+
|
|
175
283
|
### Read a Record
|
|
176
284
|
|
|
177
285
|
```typescript
|
|
178
|
-
//
|
|
179
|
-
const
|
|
286
|
+
// Path computed from the ID in memory — zero index reads
|
|
287
|
+
const post = await posts.get('260601-rec_01JA2XYZ');
|
|
180
288
|
|
|
181
|
-
//
|
|
289
|
+
// Throws HydrousError (code: RECORD_NOT_FOUND) if the ID doesn't exist
|
|
182
290
|
```
|
|
183
291
|
|
|
184
292
|
### Update a Record
|
|
185
293
|
|
|
294
|
+
**`patch(id, data)` — merge update.** Only the fields you provide are changed. All other fields on the record are left untouched.
|
|
295
|
+
|
|
186
296
|
```typescript
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
inStock: false,
|
|
297
|
+
const updated = await posts.patch('260601-rec_01JA2XYZ', {
|
|
298
|
+
status: 'published',
|
|
299
|
+
views: 1,
|
|
191
300
|
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**`set(id, data)` — full replace.** The entire record is replaced with the new data.
|
|
192
304
|
|
|
193
|
-
|
|
194
|
-
const replaced = await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
199
311
|
});
|
|
200
312
|
```
|
|
201
313
|
|
|
314
|
+
**Disable merge** (force field removal):
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// merge: false means fields not in `data` are removed
|
|
318
|
+
await posts.patch('260601-rec_01JA2XYZ', { status: 'archived' }, { merge: false });
|
|
319
|
+
```
|
|
320
|
+
|
|
202
321
|
### Delete a Record
|
|
203
322
|
|
|
204
323
|
```typescript
|
|
205
|
-
await
|
|
324
|
+
await posts.delete('260601-rec_01JA2XYZ');
|
|
206
325
|
```
|
|
207
326
|
|
|
208
327
|
### Query Records
|
|
209
328
|
|
|
210
329
|
```typescript
|
|
211
|
-
//
|
|
212
|
-
const { records } = await
|
|
330
|
+
// All records (up to 100 by default)
|
|
331
|
+
const { records } = await posts.query();
|
|
213
332
|
|
|
214
333
|
// With filters
|
|
215
|
-
const { records:
|
|
334
|
+
const { records: published } = await posts.query({
|
|
216
335
|
filters: [
|
|
217
|
-
{ field: '
|
|
218
|
-
{ field: 'price', op: '<', value: 100 },
|
|
336
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
219
337
|
],
|
|
220
338
|
});
|
|
221
339
|
|
|
222
|
-
//
|
|
223
|
-
const { records, hasMore, nextCursor } = await
|
|
224
|
-
|
|
225
|
-
|
|
340
|
+
// Multiple filters, sort, limit
|
|
341
|
+
const { records, hasMore, nextCursor } = await posts.query({
|
|
342
|
+
filters: [
|
|
343
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
344
|
+
{ field: 'views', op: '>', value: 100 },
|
|
345
|
+
],
|
|
346
|
+
orderBy: 'createdAt',
|
|
347
|
+
order: 'desc',
|
|
226
348
|
limit: 20,
|
|
227
349
|
});
|
|
228
350
|
|
|
229
351
|
// Next page
|
|
230
352
|
if (hasMore) {
|
|
231
|
-
const page2 = await
|
|
232
|
-
orderBy: '
|
|
233
|
-
order: '
|
|
353
|
+
const page2 = await posts.query({
|
|
354
|
+
orderBy: 'createdAt',
|
|
355
|
+
order: 'desc',
|
|
234
356
|
limit: 20,
|
|
235
357
|
startAfter: nextCursor,
|
|
236
358
|
});
|
|
237
359
|
}
|
|
238
360
|
|
|
239
|
-
// Select only specific fields
|
|
240
|
-
const { records:
|
|
241
|
-
fields: '
|
|
361
|
+
// Select only specific fields (reduces payload size)
|
|
362
|
+
const { records: light } = await posts.query({
|
|
363
|
+
fields: 'id,title,status,createdAt',
|
|
242
364
|
});
|
|
243
365
|
|
|
244
|
-
//
|
|
245
|
-
const { records:
|
|
366
|
+
// Date range
|
|
367
|
+
const { records: thisWeek } = await posts.query({
|
|
246
368
|
dateRange: {
|
|
247
|
-
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
369
|
+
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
248
370
|
end: Date.now(),
|
|
249
371
|
},
|
|
250
372
|
});
|
|
251
373
|
```
|
|
252
374
|
|
|
253
|
-
**
|
|
375
|
+
**Filter operators:**
|
|
254
376
|
|
|
255
377
|
| Operator | Meaning |
|
|
256
378
|
|---|---|
|
|
@@ -260,67 +382,162 @@ const { records: recent } = await products.query({
|
|
|
260
382
|
| `<` | Less than |
|
|
261
383
|
| `>=` | Greater than or equal |
|
|
262
384
|
| `<=` | Less than or equal |
|
|
263
|
-
| `CONTAINS` | String contains
|
|
385
|
+
| `CONTAINS` | String contains |
|
|
264
386
|
|
|
265
|
-
|
|
387
|
+
> ⚠️ You can only filter on fields declared as `queryableFields` when the record was created. Filtering on an un-indexed field returns no results.
|
|
388
|
+
|
|
389
|
+
### Count Records
|
|
266
390
|
|
|
267
391
|
```typescript
|
|
268
|
-
//
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
392
|
+
// Total records in the bucket
|
|
393
|
+
const total = await posts.count();
|
|
394
|
+
|
|
395
|
+
// Records matching filters
|
|
396
|
+
const publishedCount = await posts.count([
|
|
397
|
+
{ field: 'status', op: '==', value: 'published' },
|
|
273
398
|
]);
|
|
274
|
-
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Batch Create
|
|
402
|
+
|
|
403
|
+
Up to 500 records per call.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const created = await posts.batchCreate(
|
|
407
|
+
[
|
|
408
|
+
{ title: 'Post A', status: 'draft' },
|
|
409
|
+
{ title: 'Post B', status: 'draft' },
|
|
410
|
+
{ title: 'Post C', status: 'published' },
|
|
411
|
+
],
|
|
412
|
+
{
|
|
413
|
+
queryableFields: ['status'],
|
|
414
|
+
userEmail: 'alice@example.com',
|
|
415
|
+
},
|
|
416
|
+
);
|
|
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
|
+
```
|
|
275
428
|
|
|
276
|
-
|
|
277
|
-
const total = await products.count();
|
|
278
|
-
const inStock = await products.count([{ field: 'inStock', op: '==', value: true }]);
|
|
429
|
+
### Batch Delete
|
|
279
430
|
|
|
280
|
-
|
|
281
|
-
|
|
431
|
+
Up to 500 records per call.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const { deleted, failed } = await posts.batchDelete([
|
|
435
|
+
'260601-rec_01JA',
|
|
436
|
+
'260601-rec_02JB',
|
|
437
|
+
'260601-rec_03JC',
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
console.log(`Deleted: ${deleted}, Failed: ${failed.length}`);
|
|
282
441
|
```
|
|
283
442
|
|
|
284
443
|
### Version History
|
|
285
444
|
|
|
286
|
-
Every write
|
|
445
|
+
Every write creates a new version — you can restore any record to any previous state.
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// Get the full history list (most recent first)
|
|
449
|
+
const history = await posts.getHistory('260601-rec_01JA2XYZ');
|
|
450
|
+
// [{ id, version: 3, createdAt, data }, { version: 2, … }, { version: 1, … }]
|
|
451
|
+
|
|
452
|
+
// Restore to version 1 (the original)
|
|
453
|
+
const restored = await posts.restoreVersion('260601-rec_01JA2XYZ', history[2]!.version);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Write-Filter Sentinels
|
|
457
|
+
|
|
458
|
+
For atomic server-side field operations — use these inside `patch()` to avoid race conditions.
|
|
287
459
|
|
|
288
460
|
```typescript
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 },
|
|
292
465
|
|
|
293
|
-
//
|
|
294
|
-
|
|
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);
|
|
295
490
|
```
|
|
296
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
|
|
498
|
+
const post = await posts.create(
|
|
499
|
+
{ title: 'Welcome', status: 'published' },
|
|
500
|
+
{ customRecordId: '260601-post_welcome' },
|
|
501
|
+
);
|
|
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
|
+
```
|
|
509
|
+
|
|
510
|
+
Custom IDs must match `^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$`.
|
|
511
|
+
|
|
297
512
|
---
|
|
298
513
|
|
|
299
514
|
## Authentication
|
|
300
515
|
|
|
301
|
-
HydrousDB has a
|
|
302
|
-
(e.g. `"app-users"`). You get sessions, refresh tokens, password reset, email
|
|
303
|
-
verification, and admin controls out of the box.
|
|
516
|
+
HydrousDB has a complete user auth system. Your users live in a bucket you name (e.g. `"app-users"`). You get sessions, refresh tokens, password reset, email verification, and admin controls — all built in.
|
|
304
517
|
|
|
305
518
|
```typescript
|
|
306
519
|
const auth = db.auth('app-users');
|
|
307
520
|
```
|
|
308
521
|
|
|
309
|
-
### Sign Up
|
|
522
|
+
### Sign Up
|
|
310
523
|
|
|
311
524
|
```typescript
|
|
312
525
|
const { user, session } = await auth.signup({
|
|
313
526
|
email: 'alice@example.com',
|
|
314
|
-
password: 'hunter2',
|
|
527
|
+
password: 'hunter2', // validated server-side
|
|
315
528
|
fullName: 'Alice Wonderland',
|
|
316
529
|
// Any extra fields are stored on the user record:
|
|
317
530
|
plan: 'pro',
|
|
318
531
|
referral: 'friend123',
|
|
319
532
|
});
|
|
320
533
|
|
|
321
|
-
//
|
|
322
|
-
// session.sessionId
|
|
323
|
-
// session.refreshToken
|
|
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
|
|
324
541
|
```
|
|
325
542
|
|
|
326
543
|
### Log In / Log Out
|
|
@@ -332,56 +549,61 @@ const { user, session } = await auth.login({
|
|
|
332
549
|
password: 'hunter2',
|
|
333
550
|
});
|
|
334
551
|
|
|
335
|
-
// Log out
|
|
552
|
+
// Log out — invalidates the session server-side
|
|
336
553
|
await auth.logout({ sessionId: session.sessionId });
|
|
554
|
+
|
|
555
|
+
// Log out from all devices at once
|
|
556
|
+
await auth.logout({ sessionId: session.sessionId, allDevices: true });
|
|
337
557
|
```
|
|
338
558
|
|
|
339
559
|
### Session Management
|
|
340
560
|
|
|
341
|
-
Sessions expire after **24 hours**.
|
|
561
|
+
Sessions expire after **24 hours**. Refresh tokens last **30 days**.
|
|
342
562
|
|
|
343
563
|
```typescript
|
|
344
|
-
// Refresh
|
|
564
|
+
// Refresh before expiry to get a new session
|
|
345
565
|
const newSession = await auth.refreshSession({
|
|
346
566
|
refreshToken: session.refreshToken,
|
|
347
567
|
});
|
|
348
568
|
// Store newSession.sessionId and newSession.refreshToken
|
|
349
569
|
|
|
350
|
-
// Get
|
|
570
|
+
// Get a user by ID
|
|
351
571
|
const user = await auth.getUser({ userId: session.userId });
|
|
352
572
|
```
|
|
353
573
|
|
|
354
|
-
###
|
|
574
|
+
### Validate a Session
|
|
575
|
+
|
|
576
|
+
Use this on your backend to verify an incoming session is still active.
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
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
|
|
355
588
|
|
|
356
589
|
```typescript
|
|
357
590
|
const updated = await auth.updateUser({
|
|
358
591
|
sessionId: session.sessionId,
|
|
359
592
|
userId: user.id,
|
|
360
|
-
|
|
593
|
+
updates: {
|
|
361
594
|
fullName: 'Alice Smith',
|
|
362
595
|
plan: 'enterprise',
|
|
596
|
+
// Any field on the user record can be updated here
|
|
363
597
|
avatar: 'https://example.com/avatar.jpg',
|
|
364
598
|
},
|
|
365
599
|
});
|
|
366
600
|
```
|
|
367
601
|
|
|
368
|
-
|
|
602
|
+
> ⚠️ The `updates` key is required — it wraps the fields to change. Fields not included in `updates` are left untouched.
|
|
369
603
|
|
|
370
|
-
|
|
371
|
-
// 1. User requests a reset (always returns success — prevents email enumeration)
|
|
372
|
-
await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
604
|
+
### Change Password
|
|
373
605
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// 3. User submits the new password
|
|
377
|
-
await auth.confirmPasswordReset({
|
|
378
|
-
resetToken: 'tok_from_email',
|
|
379
|
-
newPassword: 'correcthorsebatterystaple',
|
|
380
|
-
});
|
|
381
|
-
// All existing sessions for this user are automatically revoked
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Change Password (authenticated)
|
|
606
|
+
Requires an active session — so a stolen old password alone is not enough.
|
|
385
607
|
|
|
386
608
|
```typescript
|
|
387
609
|
await auth.changePassword({
|
|
@@ -390,61 +612,112 @@ await auth.changePassword({
|
|
|
390
612
|
currentPassword: 'hunter2',
|
|
391
613
|
newPassword: 'correcthorsebatterystaple',
|
|
392
614
|
});
|
|
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
|
+
|
|
624
|
+
// 2. User receives the reset token via email (handled by your email provider)
|
|
625
|
+
|
|
626
|
+
// 3. User submits the token + new password
|
|
627
|
+
await auth.confirmPasswordReset({
|
|
628
|
+
resetToken: 'tok_from_email',
|
|
629
|
+
newPassword: 'correcthorsebatterystaple',
|
|
630
|
+
});
|
|
631
|
+
// All existing sessions are automatically revoked
|
|
393
632
|
```
|
|
394
633
|
|
|
395
634
|
### Email Verification
|
|
396
635
|
|
|
397
636
|
```typescript
|
|
398
|
-
// 1. Send verification email
|
|
637
|
+
// 1. Send the verification email
|
|
399
638
|
await auth.requestEmailVerification({ userId: user.id });
|
|
400
639
|
|
|
401
|
-
// 2. User clicks link in
|
|
640
|
+
// 2. User clicks link in their inbox — your app extracts the token from the URL
|
|
402
641
|
|
|
403
642
|
// 3. Confirm the token
|
|
404
643
|
await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
|
|
405
644
|
```
|
|
406
645
|
|
|
407
|
-
### Admin
|
|
646
|
+
### Admin — List Users
|
|
408
647
|
|
|
409
648
|
Admin operations require a valid session from a user with `role: 'admin'`.
|
|
410
649
|
|
|
411
650
|
```typescript
|
|
412
|
-
//
|
|
413
|
-
const { users,
|
|
651
|
+
// Paginated list — uses cursor-based pagination
|
|
652
|
+
const { users, hasMore, nextCursor } = await auth.listUsers({
|
|
414
653
|
sessionId: adminSession.sessionId,
|
|
415
654
|
limit: 50,
|
|
416
|
-
offset: 0,
|
|
417
655
|
});
|
|
418
656
|
|
|
657
|
+
if (hasMore) {
|
|
658
|
+
const page2 = await auth.listUsers({
|
|
659
|
+
sessionId: adminSession.sessionId,
|
|
660
|
+
limit: 50,
|
|
661
|
+
cursor: nextCursor!,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Each user in the list includes:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
{
|
|
670
|
+
id: 'usr_xxxxxxxxxxxx',
|
|
671
|
+
email: 'alice@example.com',
|
|
672
|
+
fullName: 'Alice Wonderland',
|
|
673
|
+
emailVerified: true,
|
|
674
|
+
accountStatus: 'active', // 'active' | 'locked' | 'suspended'
|
|
675
|
+
role: 'user', // 'user' | 'admin'
|
|
676
|
+
createdAt: 1717200000000,
|
|
677
|
+
updatedAt: 1717200000000,
|
|
678
|
+
// ...any extra fields stored at signup
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Admin — Lock / Unlock
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
419
685
|
// Lock an account (prevents login)
|
|
420
|
-
await auth.lockAccount({
|
|
686
|
+
const { lockedUntil, unlockTime } = await auth.lockAccount({
|
|
421
687
|
sessionId: adminSession.sessionId,
|
|
422
688
|
userId: 'usr_abc123',
|
|
423
|
-
duration: 60 * 60 * 1000, //
|
|
689
|
+
duration: 60 * 60 * 1000, // 1 hour in ms (default: 15 minutes)
|
|
424
690
|
});
|
|
425
691
|
|
|
426
|
-
|
|
692
|
+
console.log(`Account locked until ${unlockTime}`);
|
|
693
|
+
|
|
694
|
+
// Unlock manually
|
|
427
695
|
await auth.unlockAccount({
|
|
428
696
|
sessionId: adminSession.sessionId,
|
|
429
697
|
userId: 'usr_abc123',
|
|
430
698
|
});
|
|
699
|
+
```
|
|
431
700
|
|
|
432
|
-
|
|
701
|
+
### Admin — Delete Users
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// Soft-delete — marks the account as deleted, keeps the data
|
|
433
705
|
await auth.deleteUser({
|
|
434
706
|
sessionId: adminSession.sessionId,
|
|
435
707
|
userId: 'usr_abc123',
|
|
436
708
|
});
|
|
437
709
|
|
|
438
|
-
// Hard-delete
|
|
710
|
+
// Hard-delete — permanent, irreversible
|
|
439
711
|
await auth.hardDeleteUser({
|
|
440
712
|
sessionId: adminSession.sessionId,
|
|
441
713
|
userId: 'usr_abc123',
|
|
442
714
|
});
|
|
443
715
|
|
|
444
|
-
// Bulk delete
|
|
445
|
-
const {
|
|
716
|
+
// Bulk delete — up to 500 users, soft or hard
|
|
717
|
+
const { succeeded, failed } = await auth.bulkDeleteUsers({
|
|
446
718
|
sessionId: adminSession.sessionId,
|
|
447
719
|
userIds: ['usr_a', 'usr_b', 'usr_c'],
|
|
720
|
+
hard: false, // set true for permanent deletion
|
|
448
721
|
});
|
|
449
722
|
```
|
|
450
723
|
|
|
@@ -452,64 +725,75 @@ const { deleted, failed } = await auth.bulkDeleteUsers({
|
|
|
452
725
|
|
|
453
726
|
## File Storage
|
|
454
727
|
|
|
455
|
-
HydrousDB Storage is backed by Google Cloud Storage.
|
|
456
|
-
|
|
457
|
-
|
|
728
|
+
HydrousDB Storage is backed by Google Cloud Storage. Storage keys (`ssk_…`) are scoped per bucket — you can give different parts of your app different permissions.
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
const main = db.storage('main');
|
|
732
|
+
const avatars = db.storage('avatars');
|
|
733
|
+
const documents = db.storage('documents');
|
|
458
734
|
```
|
|
459
|
-
You never see or specify the owner prefix — the SDK handles it transparently.
|
|
460
735
|
|
|
461
736
|
### Simple Upload
|
|
462
737
|
|
|
463
|
-
|
|
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.
|
|
464
739
|
|
|
465
740
|
```typescript
|
|
466
|
-
// Browser
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
overwrite: false, // throw if the file already exists
|
|
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
|
|
473
747
|
});
|
|
474
748
|
|
|
475
|
-
console.log(result.publicUrl); // CDN URL —
|
|
476
|
-
console.log(result.downloadUrl); // null
|
|
749
|
+
console.log(result.publicUrl); // CDN URL — use anywhere
|
|
750
|
+
console.log(result.downloadUrl); // null when isPublic: true
|
|
477
751
|
console.log(result.size); // bytes
|
|
478
|
-
console.log(result.mimeType); //
|
|
752
|
+
console.log(result.mimeType); // 'image/jpeg'
|
|
479
753
|
|
|
480
|
-
// Node.js
|
|
754
|
+
// Node.js — from a Buffer or file path
|
|
481
755
|
import { readFileSync } from 'fs';
|
|
482
|
-
const
|
|
483
|
-
const result = await db.storage.upload(
|
|
484
|
-
console.log(result.downloadUrl); //
|
|
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
|
|
485
759
|
```
|
|
486
760
|
|
|
487
761
|
### Upload Raw JSON or Text
|
|
488
762
|
|
|
489
763
|
```typescript
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
'
|
|
764
|
+
// Upload a JS object as JSON
|
|
765
|
+
const result = await db.storage('main').uploadRaw(
|
|
766
|
+
{ theme: 'dark', language: 'en', version: 3 },
|
|
767
|
+
'settings/alice.json',
|
|
493
768
|
{ isPublic: false },
|
|
494
769
|
);
|
|
770
|
+
|
|
771
|
+
// Upload a plain string
|
|
772
|
+
await db.storage('main').uploadRaw(
|
|
773
|
+
'<html><body>Hello</body></html>',
|
|
774
|
+
'exports/page.html',
|
|
775
|
+
{ mimeType: 'text/html', isPublic: true },
|
|
776
|
+
);
|
|
495
777
|
```
|
|
496
778
|
|
|
497
|
-
### Large File Upload
|
|
779
|
+
### Large File Upload with Progress
|
|
498
780
|
|
|
499
|
-
For files over 10 MB or when you need a progress bar. The file goes directly
|
|
500
|
-
to GCS — your server never buffers it.
|
|
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.
|
|
501
782
|
|
|
502
783
|
```typescript
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
784
|
+
const storage = db.storage('main');
|
|
785
|
+
|
|
786
|
+
// Step 1 — Get a signed GCS upload URL
|
|
787
|
+
const { uploadUrl, path } = await storage.getUploadUrl({
|
|
788
|
+
path: 'videos/product-demo.mp4',
|
|
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)
|
|
509
793
|
});
|
|
510
794
|
|
|
511
|
-
// Step 2
|
|
512
|
-
await
|
|
795
|
+
// Step 2 — Upload directly to GCS with real progress
|
|
796
|
+
await storage.uploadToSignedUrl(
|
|
513
797
|
uploadUrl,
|
|
514
798
|
file,
|
|
515
799
|
'video/mp4',
|
|
@@ -519,74 +803,111 @@ await db.storage.uploadToSignedUrl(
|
|
|
519
803
|
},
|
|
520
804
|
);
|
|
521
805
|
|
|
522
|
-
// Step 3
|
|
523
|
-
const result = await
|
|
806
|
+
// Step 3 — Confirm the upload (registers metadata on the server)
|
|
807
|
+
const result = await storage.confirmUpload({
|
|
524
808
|
path: path,
|
|
525
809
|
mimeType: 'video/mp4',
|
|
526
810
|
isPublic: true,
|
|
527
811
|
});
|
|
528
812
|
|
|
529
|
-
console.log(result.publicUrl); //
|
|
813
|
+
console.log(result.publicUrl); // live CDN URL
|
|
530
814
|
```
|
|
531
815
|
|
|
532
816
|
### Batch Upload
|
|
533
817
|
|
|
818
|
+
Upload up to 50 files at once.
|
|
819
|
+
|
|
534
820
|
```typescript
|
|
535
|
-
|
|
536
|
-
|
|
821
|
+
const storage = db.storage('main');
|
|
822
|
+
|
|
823
|
+
// Step 1 — Get signed URLs for all files
|
|
824
|
+
const { files } = await storage.getBatchUploadUrls([
|
|
537
825
|
{ path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
|
|
538
826
|
{ path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
|
|
827
|
+
{ path: 'gallery/photo3.png', mimeType: 'image/png', size: 98304, isPublic: true },
|
|
539
828
|
]);
|
|
540
829
|
|
|
541
|
-
// Upload each
|
|
830
|
+
// Step 2 — Upload each directly to GCS
|
|
542
831
|
for (const f of files) {
|
|
543
|
-
await
|
|
832
|
+
await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
544
833
|
}
|
|
545
834
|
|
|
546
|
-
// Confirm all at once
|
|
547
|
-
const
|
|
548
|
-
files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true }))
|
|
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 })),
|
|
549
838
|
);
|
|
839
|
+
|
|
840
|
+
console.log(`${succeeded.length} uploaded, ${failed.length} failed`);
|
|
841
|
+
for (const f of succeeded) {
|
|
842
|
+
console.log(f.publicUrl);
|
|
843
|
+
}
|
|
550
844
|
```
|
|
551
845
|
|
|
552
|
-
### Download
|
|
846
|
+
### Download
|
|
553
847
|
|
|
554
848
|
```typescript
|
|
555
|
-
// Private files require authentication —
|
|
556
|
-
const buffer = await db.storage.download('reports/q3.pdf');
|
|
849
|
+
// Private files require authentication — returns ArrayBuffer
|
|
850
|
+
const buffer = await db.storage('documents').download('reports/q3.pdf');
|
|
557
851
|
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
558
852
|
|
|
559
|
-
//
|
|
560
|
-
const url
|
|
561
|
-
const a
|
|
562
|
-
a.href
|
|
853
|
+
// Trigger browser download
|
|
854
|
+
const url = URL.createObjectURL(blob);
|
|
855
|
+
const a = document.createElement('a');
|
|
856
|
+
a.href = url;
|
|
563
857
|
a.download = 'q3.pdf';
|
|
564
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
|
+
]);
|
|
565
873
|
|
|
566
|
-
|
|
567
|
-
//
|
|
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
|
+
}
|
|
568
884
|
```
|
|
569
885
|
|
|
570
886
|
### List Files
|
|
571
887
|
|
|
572
888
|
```typescript
|
|
889
|
+
const storage = db.storage('main');
|
|
890
|
+
|
|
573
891
|
// List everything at the root
|
|
574
|
-
const { files, folders } = await
|
|
892
|
+
const { files, folders, hasMore, nextCursor } = await storage.list();
|
|
575
893
|
|
|
576
894
|
// List a specific folder
|
|
577
|
-
const
|
|
578
|
-
prefix:
|
|
579
|
-
limit:
|
|
580
|
-
recursive: false,
|
|
895
|
+
const result = await storage.list({
|
|
896
|
+
prefix: 'gallery/',
|
|
897
|
+
limit: 50,
|
|
581
898
|
});
|
|
582
899
|
|
|
583
900
|
// Paginate
|
|
584
|
-
if (hasMore) {
|
|
585
|
-
const page2 = await
|
|
901
|
+
if (result.hasMore) {
|
|
902
|
+
const page2 = await storage.list({
|
|
903
|
+
prefix: 'gallery/',
|
|
904
|
+
cursor: result.nextCursor,
|
|
905
|
+
});
|
|
586
906
|
}
|
|
587
907
|
```
|
|
588
908
|
|
|
589
|
-
Each file entry
|
|
909
|
+
Each file entry:
|
|
910
|
+
|
|
590
911
|
```typescript
|
|
591
912
|
{
|
|
592
913
|
name: 'photo1.jpg',
|
|
@@ -594,7 +915,7 @@ Each file entry includes:
|
|
|
594
915
|
size: 204800,
|
|
595
916
|
mimeType: 'image/jpeg',
|
|
596
917
|
isPublic: true,
|
|
597
|
-
publicUrl: 'https://storage.googleapis.com
|
|
918
|
+
publicUrl: 'https://storage.googleapis.com/…',
|
|
598
919
|
downloadUrl: null,
|
|
599
920
|
updatedAt: '2025-06-01T12:00:00.000Z',
|
|
600
921
|
}
|
|
@@ -602,86 +923,125 @@ Each file entry includes:
|
|
|
602
923
|
|
|
603
924
|
### Scoped Storage
|
|
604
925
|
|
|
605
|
-
|
|
926
|
+
Pre-fix all operations to a folder — great for per-user isolation.
|
|
606
927
|
|
|
607
928
|
```typescript
|
|
608
|
-
|
|
609
|
-
const avatars = db.storage.scope('user-avatars');
|
|
929
|
+
const userDocs = db.storage('documents').scope(`users/${userId}/`);
|
|
610
930
|
|
|
611
|
-
|
|
612
|
-
|
|
931
|
+
// Uploads to: users/{userId}/contract.pdf
|
|
932
|
+
await userDocs.upload(pdfBuffer, 'contract.pdf');
|
|
613
933
|
|
|
614
|
-
|
|
615
|
-
|
|
934
|
+
// Lists: users/{userId}/
|
|
935
|
+
const { files } = await userDocs.list();
|
|
616
936
|
|
|
617
|
-
|
|
618
|
-
|
|
937
|
+
// Deletes: users/{userId}/contract.pdf
|
|
938
|
+
await userDocs.deleteFile('contract.pdf');
|
|
619
939
|
|
|
620
940
|
// Nest scopes
|
|
621
|
-
const
|
|
622
|
-
//
|
|
941
|
+
const userThumbs = userDocs.scope('thumbnails/');
|
|
942
|
+
// All ops under: users/{userId}/thumbnails/
|
|
623
943
|
```
|
|
624
944
|
|
|
625
|
-
|
|
945
|
+
`ScopedStorage` has the same full API as `StorageManager` — every method listed in the reference is available.
|
|
946
|
+
|
|
947
|
+
### File Metadata
|
|
626
948
|
|
|
627
949
|
```typescript
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
```
|
|
630
959
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
637
|
-
//
|
|
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)
|
|
638
967
|
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
const result2 = await db.storage.setVisibility('reports/q3.pdf', false); // make private
|
|
968
|
+
// Share signedUrl with the recipient — it expires automatically
|
|
969
|
+
console.log(`Link valid until: ${new Date(expiresAt).toLocaleString()}`);
|
|
642
970
|
```
|
|
643
971
|
|
|
644
|
-
|
|
972
|
+
> ⚠️ Downloads via signed URLs bypass the server — **download stats are not tracked** for those requests. Use `downloadUrl` for tracked downloads.
|
|
973
|
+
|
|
974
|
+
### Visibility
|
|
645
975
|
|
|
646
976
|
```typescript
|
|
647
|
-
//
|
|
648
|
-
await db.storage.
|
|
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
|
|
980
|
+
|
|
981
|
+
// Make a public file private
|
|
982
|
+
const result2 = await db.storage('main').setVisibility('docs/report.pdf', false);
|
|
983
|
+
console.log(result2.downloadUrl); // now requires auth
|
|
984
|
+
```
|
|
649
985
|
|
|
650
|
-
|
|
651
|
-
await db.storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
986
|
+
### Move, Copy, Delete
|
|
652
987
|
|
|
653
|
-
|
|
654
|
-
|
|
988
|
+
```typescript
|
|
989
|
+
const storage = db.storage('main');
|
|
655
990
|
|
|
656
|
-
//
|
|
657
|
-
await
|
|
991
|
+
// Rename a file
|
|
992
|
+
await storage.move('drafts/report.pdf', 'published/report-2025.pdf');
|
|
658
993
|
|
|
659
|
-
//
|
|
660
|
-
await
|
|
994
|
+
// Move to a different folder
|
|
995
|
+
await storage.move('inbox/data.csv', 'archive/2025/data.csv');
|
|
661
996
|
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
997
|
+
// Copy
|
|
998
|
+
await storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
999
|
+
|
|
1000
|
+
// Create a folder placeholder
|
|
1001
|
+
await storage.createFolder('archive/2025/');
|
|
1002
|
+
|
|
1003
|
+
// Delete a single file
|
|
1004
|
+
await storage.deleteFile('temp/scratch.txt');
|
|
1005
|
+
|
|
1006
|
+
// Delete a folder and all its contents recursively
|
|
1007
|
+
await storage.deleteFolder('temp/');
|
|
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();
|
|
665
1024
|
```
|
|
666
1025
|
|
|
667
1026
|
---
|
|
668
1027
|
|
|
669
1028
|
## Analytics
|
|
670
1029
|
|
|
671
|
-
HydrousDB Analytics runs your
|
|
672
|
-
on millions of records. All queries accept an optional `dateRange` filter.
|
|
1030
|
+
HydrousDB Analytics runs directly against BigQuery on your GCS data — zero ETL, no duplication, live results. Fast even on billions of records.
|
|
673
1031
|
|
|
674
1032
|
```typescript
|
|
675
1033
|
const analytics = db.analytics('orders');
|
|
676
1034
|
```
|
|
677
1035
|
|
|
1036
|
+
All `dateRange` values are Unix timestamps in milliseconds: `{ start?: number, end?: number }`.
|
|
1037
|
+
|
|
678
1038
|
### Count
|
|
679
1039
|
|
|
680
1040
|
```typescript
|
|
681
1041
|
// Total records
|
|
682
1042
|
const { count } = await analytics.count();
|
|
683
1043
|
|
|
684
|
-
//
|
|
1044
|
+
// In a date range
|
|
685
1045
|
const { count: lastWeek } = await analytics.count({
|
|
686
1046
|
dateRange: {
|
|
687
1047
|
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
@@ -695,8 +1055,12 @@ const { count: lastWeek } = await analytics.count({
|
|
|
695
1055
|
How many records have each unique value for a field?
|
|
696
1056
|
|
|
697
1057
|
```typescript
|
|
698
|
-
const rows = await analytics.distribution({
|
|
699
|
-
|
|
1058
|
+
const rows = await analytics.distribution({
|
|
1059
|
+
field: 'status',
|
|
1060
|
+
limit: 10,
|
|
1061
|
+
order: 'desc', // 'asc' | 'desc'
|
|
1062
|
+
});
|
|
1063
|
+
// [
|
|
700
1064
|
// { value: 'completed', count: 8234 },
|
|
701
1065
|
// { value: 'pending', count: 1203 },
|
|
702
1066
|
// { value: 'refunded', count: 412 },
|
|
@@ -707,8 +1071,7 @@ const rows = await analytics.distribution({ field: 'status', limit: 10, order: '
|
|
|
707
1071
|
|
|
708
1072
|
```typescript
|
|
709
1073
|
// Total revenue
|
|
710
|
-
const
|
|
711
|
-
// → [{ sum: 198432.50 }]
|
|
1074
|
+
const [{ sum: total }] = await analytics.sum({ field: 'amount' });
|
|
712
1075
|
|
|
713
1076
|
// Revenue by country
|
|
714
1077
|
const byCountry = await analytics.sum({
|
|
@@ -716,47 +1079,53 @@ const byCountry = await analytics.sum({
|
|
|
716
1079
|
groupBy: 'country',
|
|
717
1080
|
limit: 10,
|
|
718
1081
|
});
|
|
719
|
-
//
|
|
1082
|
+
// [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, …]
|
|
720
1083
|
```
|
|
721
1084
|
|
|
722
1085
|
### Time Series
|
|
723
1086
|
|
|
724
|
-
Record counts over time — ideal for activity charts.
|
|
1087
|
+
Record counts bucketed over time — ideal for activity and growth charts.
|
|
725
1088
|
|
|
726
1089
|
```typescript
|
|
727
1090
|
const rows = await analytics.timeSeries({
|
|
728
|
-
granularity: 'day',
|
|
1091
|
+
granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
|
|
729
1092
|
dateRange: {
|
|
730
1093
|
start: new Date('2025-01-01').getTime(),
|
|
731
1094
|
end: new Date('2025-06-01').getTime(),
|
|
732
1095
|
},
|
|
733
1096
|
});
|
|
734
|
-
//
|
|
1097
|
+
// [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, …]
|
|
735
1098
|
```
|
|
736
1099
|
|
|
737
|
-
|
|
1100
|
+
### Field Time Series
|
|
1101
|
+
|
|
1102
|
+
Aggregate a numeric field over time.
|
|
738
1103
|
|
|
739
1104
|
```typescript
|
|
740
1105
|
const revenue = await analytics.fieldTimeSeries({
|
|
741
1106
|
field: 'amount',
|
|
742
1107
|
aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
743
1108
|
granularity: 'week',
|
|
1109
|
+
dateRange: {
|
|
1110
|
+
start: new Date('2025-01-01').getTime(),
|
|
1111
|
+
end: Date.now(),
|
|
1112
|
+
},
|
|
744
1113
|
});
|
|
745
|
-
//
|
|
1114
|
+
// [{ date: '2025-W01', value: 12340.50 }, { date: '2025-W02', value: 9872.00 }, …]
|
|
746
1115
|
```
|
|
747
1116
|
|
|
748
1117
|
### Top N
|
|
749
1118
|
|
|
750
|
-
Most common values for a field
|
|
1119
|
+
Most common values for a field.
|
|
751
1120
|
|
|
752
1121
|
```typescript
|
|
753
1122
|
const topProducts = await analytics.topN({
|
|
754
1123
|
field: 'productId',
|
|
755
|
-
labelField: 'productName',
|
|
1124
|
+
labelField: 'productName', // optional human-readable label alongside the value
|
|
756
1125
|
n: 5,
|
|
757
1126
|
order: 'desc',
|
|
758
1127
|
});
|
|
759
|
-
//
|
|
1128
|
+
// [
|
|
760
1129
|
// { value: 'prod_123', label: 'Widget Pro', count: 892 },
|
|
761
1130
|
// { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
|
|
762
1131
|
// ]
|
|
@@ -764,41 +1133,48 @@ const topProducts = await analytics.topN({
|
|
|
764
1133
|
|
|
765
1134
|
### Field Stats
|
|
766
1135
|
|
|
767
|
-
Statistical summary for any numeric field
|
|
1136
|
+
Statistical summary for any numeric field.
|
|
768
1137
|
|
|
769
1138
|
```typescript
|
|
770
1139
|
const stats = await analytics.stats({ field: 'orderValue' });
|
|
771
|
-
//
|
|
772
|
-
// min:
|
|
773
|
-
//
|
|
1140
|
+
// {
|
|
1141
|
+
// min: 4.99,
|
|
1142
|
+
// max: 9999.99,
|
|
1143
|
+
// avg: 87.23,
|
|
1144
|
+
// sum: 420948.27,
|
|
1145
|
+
// count: 4823,
|
|
1146
|
+
// stddev: 143.2,
|
|
774
1147
|
// }
|
|
775
1148
|
```
|
|
776
1149
|
|
|
777
1150
|
### Multi-Metric Dashboard
|
|
778
1151
|
|
|
779
|
-
|
|
1152
|
+
Run several aggregations in a single BigQuery query — one network call.
|
|
780
1153
|
|
|
781
1154
|
```typescript
|
|
782
1155
|
const dashboard = await analytics.multiMetric({
|
|
783
1156
|
metrics: [
|
|
784
|
-
{ field: 'amount',
|
|
785
|
-
{ field: 'amount',
|
|
786
|
-
{ field: 'amount',
|
|
787
|
-
{ field: 'userId',
|
|
1157
|
+
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
1158
|
+
{ field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
|
|
1159
|
+
{ field: 'amount', name: 'maxOrder', aggregation: 'max' },
|
|
1160
|
+
{ field: 'userId', name: 'uniqueOrders', aggregation: 'count' },
|
|
788
1161
|
],
|
|
789
|
-
dateRange: {
|
|
1162
|
+
dateRange: {
|
|
1163
|
+
start: new Date('2025-01-01').getTime(),
|
|
1164
|
+
end: Date.now(),
|
|
1165
|
+
},
|
|
790
1166
|
});
|
|
791
|
-
//
|
|
1167
|
+
// {
|
|
792
1168
|
// totalRevenue: 198432.50,
|
|
793
1169
|
// avgOrderValue: 87.23,
|
|
794
1170
|
// maxOrder: 9999.99,
|
|
795
|
-
//
|
|
1171
|
+
// uniqueOrders: 2275,
|
|
796
1172
|
// }
|
|
797
1173
|
```
|
|
798
1174
|
|
|
799
|
-
### Filtered Records
|
|
1175
|
+
### Filtered Records via BigQuery
|
|
800
1176
|
|
|
801
|
-
|
|
1177
|
+
Fetch raw records using the BigQuery engine — useful for large-scale filtered exports.
|
|
802
1178
|
|
|
803
1179
|
```typescript
|
|
804
1180
|
const records = await analytics.records({
|
|
@@ -807,15 +1183,20 @@ const records = await analytics.records({
|
|
|
807
1183
|
{ field: 'amount', op: '>', value: 100 },
|
|
808
1184
|
],
|
|
809
1185
|
selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
|
|
810
|
-
orderBy:
|
|
811
|
-
order:
|
|
812
|
-
limit:
|
|
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
|
+
},
|
|
813
1194
|
});
|
|
814
1195
|
```
|
|
815
1196
|
|
|
816
1197
|
### Cross-Bucket Comparison
|
|
817
1198
|
|
|
818
|
-
Compare the same metric across multiple buckets in one query
|
|
1199
|
+
Compare the same metric across multiple buckets in one query.
|
|
819
1200
|
|
|
820
1201
|
```typescript
|
|
821
1202
|
const comparison = await analytics.crossBucket({
|
|
@@ -823,33 +1204,87 @@ const comparison = await analytics.crossBucket({
|
|
|
823
1204
|
field: 'amount',
|
|
824
1205
|
aggregation: 'sum',
|
|
825
1206
|
});
|
|
826
|
-
//
|
|
1207
|
+
// [
|
|
827
1208
|
// { bucket: 'orders-us', value: 120000 },
|
|
828
1209
|
// { bucket: 'orders-eu', value: 45000 },
|
|
829
1210
|
// { bucket: 'orders-apac', value: 33000 },
|
|
830
1211
|
// ]
|
|
831
1212
|
```
|
|
832
1213
|
|
|
833
|
-
> ⚠️ Your Security Key must have read access to **
|
|
1214
|
+
> ⚠️ Your Bucket Security Key must have read access to **every** bucket listed in `bucketKeys`.
|
|
834
1215
|
|
|
835
1216
|
### Storage Stats
|
|
836
1217
|
|
|
837
1218
|
```typescript
|
|
838
1219
|
const stats = await analytics.storageStats();
|
|
839
|
-
//
|
|
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() },
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
console.log(result.queryType); // 'count'
|
|
1242
|
+
console.log(result.data); // { count: 142 }
|
|
840
1243
|
```
|
|
841
1244
|
|
|
842
1245
|
---
|
|
843
1246
|
|
|
844
|
-
## TypeScript
|
|
1247
|
+
## TypeScript
|
|
845
1248
|
|
|
846
|
-
The SDK is written in TypeScript and ships
|
|
847
|
-
type parameters to describe the shape of your records and get autocomplete throughout.
|
|
1249
|
+
The SDK is written entirely in TypeScript and ships full type definitions. Use generics for end-to-end type safety.
|
|
848
1250
|
|
|
849
1251
|
```typescript
|
|
850
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';
|
|
851
1286
|
|
|
852
|
-
// Define your
|
|
1287
|
+
// Define your domain models
|
|
853
1288
|
interface Order {
|
|
854
1289
|
customerId: string;
|
|
855
1290
|
items: Array<{ productId: string; qty: number; price: number }>;
|
|
@@ -865,250 +1300,267 @@ interface Customer {
|
|
|
865
1300
|
credits: number;
|
|
866
1301
|
}
|
|
867
1302
|
|
|
868
|
-
const db = createClient({
|
|
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
|
+
});
|
|
869
1308
|
|
|
870
|
-
// Fully typed
|
|
1309
|
+
// Fully typed — autocomplete, compile-time safety throughout
|
|
871
1310
|
const orders = db.records<Order>('orders');
|
|
872
1311
|
const customers = db.records<Customer>('customers');
|
|
873
1312
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
total: 59.98,
|
|
879
|
-
status: 'pending',
|
|
880
|
-
country: 'US',
|
|
881
|
-
});
|
|
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
|
+
);
|
|
882
1317
|
|
|
883
1318
|
// TypeScript catches mistakes at compile time:
|
|
884
|
-
// order.nonExistentField
|
|
885
|
-
//
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
All exported types are available for import:
|
|
889
|
-
|
|
890
|
-
```typescript
|
|
891
|
-
import type {
|
|
892
|
-
HydrousConfig,
|
|
893
|
-
RecordResult,
|
|
894
|
-
QueryFilter,
|
|
895
|
-
QueryOptions,
|
|
896
|
-
UploadResult,
|
|
897
|
-
AnalyticsQuery,
|
|
898
|
-
DateRange,
|
|
899
|
-
// ... and many more
|
|
900
|
-
} from 'hydrousdb';
|
|
1319
|
+
// order.nonExistentField ← TS error ✓
|
|
1320
|
+
// orders.create({ bad: 1 }) ← TS error ✓
|
|
901
1321
|
```
|
|
902
1322
|
|
|
903
1323
|
---
|
|
904
1324
|
|
|
905
1325
|
## Error Handling
|
|
906
1326
|
|
|
907
|
-
All errors
|
|
908
|
-
|
|
909
|
-
| Property | Type | Description |
|
|
910
|
-
|---|---|---|
|
|
911
|
-
| `message` | `string` | Human-readable description |
|
|
912
|
-
| `code` | `string` | Machine-readable error code (e.g. `"RECORD_NOT_FOUND"`) |
|
|
913
|
-
| `status` | `number` | HTTP status code |
|
|
914
|
-
| `requestId` | `string` | Server request ID (for support tracing) |
|
|
915
|
-
| `details` | `string[]` | Validation error details |
|
|
1327
|
+
All SDK errors extend `HydrousError`. Specific sub-classes let you handle different failure modes precisely.
|
|
916
1328
|
|
|
917
1329
|
```typescript
|
|
918
|
-
import {
|
|
1330
|
+
import {
|
|
1331
|
+
HydrousError,
|
|
1332
|
+
AuthError,
|
|
1333
|
+
RecordError,
|
|
1334
|
+
StorageError,
|
|
1335
|
+
AnalyticsError,
|
|
1336
|
+
ValidationError,
|
|
1337
|
+
NetworkError,
|
|
1338
|
+
} from 'hydrousdb';
|
|
919
1339
|
|
|
920
1340
|
try {
|
|
921
|
-
const
|
|
1341
|
+
const record = await db.records('orders').get('bad-id');
|
|
922
1342
|
} catch (err) {
|
|
923
|
-
if (err instanceof
|
|
924
|
-
// Authentication-specific error
|
|
925
|
-
console.error(`Auth failed: ${err.code}`);
|
|
926
|
-
// err.code might be: INVALID_CREDENTIALS, ACCOUNT_LOCKED, EMAIL_NOT_VERIFIED, etc.
|
|
927
|
-
} else if (err instanceof NetworkError) {
|
|
1343
|
+
if (err instanceof NetworkError) {
|
|
928
1344
|
// No internet / server unreachable
|
|
929
|
-
console.error('Cannot reach HydrousDB
|
|
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
|
+
|
|
930
1364
|
} else if (err instanceof HydrousError) {
|
|
931
1365
|
// Any other API error
|
|
932
|
-
console.error(`API error [${err.code}]
|
|
933
|
-
console.error(
|
|
1366
|
+
console.error(`API error [${err.code}] ${err.status}:`, err.message);
|
|
1367
|
+
console.error('Request ID:', err.requestId); // include in support tickets
|
|
934
1368
|
}
|
|
935
1369
|
}
|
|
936
1370
|
```
|
|
937
1371
|
|
|
1372
|
+
**Error properties:**
|
|
1373
|
+
|
|
1374
|
+
| Property | Type | Description |
|
|
1375
|
+
|---|---|---|
|
|
1376
|
+
| `message` | `string` | Human-readable description |
|
|
1377
|
+
| `code` | `string` | Machine-readable error code |
|
|
1378
|
+
| `status` | `number` | HTTP status code |
|
|
1379
|
+
| `requestId` | `string \| undefined` | Server request ID — include in support tickets |
|
|
1380
|
+
| `details` | `string[] \| undefined` | Validation error details |
|
|
1381
|
+
|
|
938
1382
|
**Common error codes:**
|
|
939
1383
|
|
|
940
|
-
| Code | Meaning |
|
|
941
|
-
|
|
942
|
-
| `RECORD_NOT_FOUND` | The requested record ID does not exist |
|
|
943
|
-
| `INVALID_CREDENTIALS` | Wrong email or password |
|
|
944
|
-
| `ACCOUNT_LOCKED` |
|
|
945
|
-
| `INVALID_SESSION` | Session expired or revoked — re-authenticate |
|
|
946
|
-
| `MISSING_API_KEY` |
|
|
947
|
-
| `INVALID_SECURITY_KEY` |
|
|
948
|
-
| `FORBIDDEN` | Insufficient permissions |
|
|
949
|
-
| `FILE_EXISTS` | File already exists
|
|
950
|
-
| `
|
|
951
|
-
| `
|
|
952
|
-
| `VALIDATION_ERROR` | Invalid input — check `err.details` |
|
|
953
|
-
| `NETWORK_ERROR` | Failed to reach the API |
|
|
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 |
|
|
954
1398
|
|
|
955
1399
|
---
|
|
956
1400
|
|
|
957
1401
|
## Security Best Practices
|
|
958
1402
|
|
|
959
|
-
1.
|
|
1403
|
+
**1. Never hard-code keys — use environment variables.**
|
|
960
1404
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1405
|
+
```bash
|
|
1406
|
+
# .env (add .env to your .gitignore)
|
|
1407
|
+
HYDROUS_AUTH_KEY=hk_auth_xxxxxxxxxxxxxxxxxxxx
|
|
1408
|
+
HYDROUS_BUCKET_KEY=hk_bucket_xxxxxxxxxxxxxxxxxxxx
|
|
1409
|
+
HYDROUS_STORAGE_MAIN=ssk_xxxxxxxxxxxxxxxxxxxx
|
|
1410
|
+
```
|
|
965
1411
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1412
|
+
```typescript
|
|
1413
|
+
const db = createClient({
|
|
1414
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1415
|
+
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1416
|
+
storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
|
|
1417
|
+
});
|
|
1418
|
+
```
|
|
969
1419
|
|
|
970
|
-
2.
|
|
971
|
-
route requests through your own backend, or use per-user session tokens.
|
|
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()`.
|
|
972
1421
|
|
|
973
|
-
3.
|
|
974
|
-
The SDK enforces this automatically, so keys never appear in server logs or
|
|
975
|
-
browser history.
|
|
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.
|
|
976
1423
|
|
|
977
|
-
4.
|
|
1424
|
+
**4. Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
|
|
978
1425
|
|
|
979
|
-
5.
|
|
980
|
-
feature, reducing the blast radius of any misconfiguration.
|
|
1426
|
+
**5. Use scoped storage** (`db.storage('key').scope('prefix/')`) to isolate access per user or feature, reducing the blast radius of any misconfiguration.
|
|
981
1427
|
|
|
982
|
-
6.
|
|
983
|
-
for time-limited sharing instead of making files permanently public.
|
|
1428
|
+
**6. Keep files private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for time-limited external sharing rather than making files permanently public.
|
|
984
1429
|
|
|
985
1430
|
---
|
|
986
1431
|
|
|
987
|
-
## API Reference
|
|
1432
|
+
## Full API Reference
|
|
988
1433
|
|
|
989
|
-
### `createClient(config)`
|
|
1434
|
+
### `createClient(config)` → `HydrousClient`
|
|
990
1435
|
|
|
991
|
-
|
|
1436
|
+
| Config field | Type | Required | Description |
|
|
1437
|
+
|---|---|---|---|
|
|
1438
|
+
| `authKey` | `string` | ✅ | `hk_auth_…` — used for all auth routes |
|
|
1439
|
+
| `bucketSecurityKey` | `string` | ✅ | `hk_bucket_…` — used for records & analytics |
|
|
1440
|
+
| `storageKeys` | `Record<string, string>` | ✅ | Named `ssk_…` keys — at least one entry |
|
|
1441
|
+
| `baseUrl` | `string` | — | Override the API endpoint (for self-hosting or testing) |
|
|
1442
|
+
|
|
1443
|
+
---
|
|
1444
|
+
|
|
1445
|
+
### `db.records<T>(bucketKey)` → `RecordsClient<T>`
|
|
1446
|
+
|
|
1447
|
+
| Method | Signature | Description |
|
|
1448
|
+
|---|---|---|
|
|
1449
|
+
| `create` | `(data: T, opts?: CreateRecordOptions) → T & RecordResult` | Create a record. `opts.queryableFields` indexes fields for filtering. `opts.customRecordId` enables upsert. |
|
|
1450
|
+
| `get` | `(id: string) → T & RecordResult` | Fetch by ID. Zero index reads. |
|
|
1451
|
+
| `set` | `(id: string, data: T) → T & RecordResult` | Full replace. |
|
|
1452
|
+
| `patch` | `(id: string, data: Partial<T>, opts?) → T & RecordResult` | Partial update. `opts.merge` (default `true`) controls whether missing fields are removed. |
|
|
1453
|
+
| `delete` | `(id: string) → void` | Permanent delete. |
|
|
1454
|
+
| `query` | `(opts?: QueryOptions) → QueryResult<T>` | Filtered, sorted, paginated query. |
|
|
1455
|
+
| `getAll` | `(opts?) → (T & RecordResult)[]` | Query without filters — convenience shortcut. |
|
|
1456
|
+
| `count` | `(filters?) → number` | Count matching records. |
|
|
1457
|
+
| `batchCreate` | `(items: T[], opts?: BatchCreateOptions) → (T & RecordResult)[]` | Up to 500 records. |
|
|
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`:**
|
|
992
1463
|
|
|
993
1464
|
```typescript
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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 }
|
|
1476
|
+
}
|
|
998
1477
|
```
|
|
999
1478
|
|
|
1000
|
-
|
|
1479
|
+
---
|
|
1480
|
+
|
|
1481
|
+
### `db.auth(bucketKey)` → `AuthClient`
|
|
1001
1482
|
|
|
1002
|
-
|
|
1483
|
+
| Method | Signature | Description |
|
|
1484
|
+
|---|---|---|
|
|
1485
|
+
| `signup` | `(opts: SignupOptions) → AuthResult` | Register user + create session. Extra fields beyond `email/password/fullName` are stored on the user record. |
|
|
1486
|
+
| `login` | `(opts: LoginOptions) → AuthResult` | Authenticate + create session. |
|
|
1487
|
+
| `logout` | `({ sessionId, allDevices? }) → void` | Revoke one or all sessions. |
|
|
1488
|
+
| `refreshSession` | `({ refreshToken }) → Session` | Extend an expiring session. |
|
|
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. |
|
|
1003
1503
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
|
1009
|
-
|
|
1010
|
-
| `
|
|
1011
|
-
| `
|
|
1012
|
-
| `
|
|
1013
|
-
| `
|
|
1014
|
-
| `
|
|
1015
|
-
| `
|
|
1016
|
-
| `
|
|
1017
|
-
| `
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
|
1024
|
-
|
|
1025
|
-
| `
|
|
1026
|
-
| `
|
|
1027
|
-
| `
|
|
1028
|
-
| `
|
|
1029
|
-
| `
|
|
1030
|
-
| `
|
|
1031
|
-
| `deleteUser(opts)` | Soft-delete a user |
|
|
1032
|
-
| `hardDeleteUser(opts)` | Permanently delete a user (admin) |
|
|
1033
|
-
| `listUsers(opts)` | List all users (admin) |
|
|
1034
|
-
| `bulkDeleteUsers(opts)` | Bulk delete (admin) |
|
|
1035
|
-
| `lockAccount(opts)` | Lock a user (admin) |
|
|
1036
|
-
| `unlockAccount(opts)` | Unlock a user (admin) |
|
|
1037
|
-
| `changePassword(opts)` | Change password (authenticated) |
|
|
1038
|
-
| `requestPasswordReset(opts)` | Send reset email |
|
|
1039
|
-
| `confirmPasswordReset(opts)` | Apply new password |
|
|
1040
|
-
| `requestEmailVerification(opts)` | Send verification email |
|
|
1041
|
-
| `confirmEmailVerification(opts)` | Verify email with token |
|
|
1042
|
-
|
|
1043
|
-
### `db.analytics(bucketKey)`
|
|
1044
|
-
|
|
1045
|
-
Returns an `AnalyticsClient` for the named bucket.
|
|
1046
|
-
|
|
1047
|
-
| Method | Description |
|
|
1048
|
-
|---|---|
|
|
1049
|
-
| `count(opts?)` | Count records |
|
|
1050
|
-
| `distribution(opts)` | Value distribution for a field |
|
|
1051
|
-
| `sum(opts)` | Sum (with optional groupBy) |
|
|
1052
|
-
| `timeSeries(opts?)` | Records over time |
|
|
1053
|
-
| `fieldTimeSeries(opts)` | Field aggregation over time |
|
|
1054
|
-
| `topN(opts)` | Top N values for a field |
|
|
1055
|
-
| `stats(opts)` | Statistical summary for a field |
|
|
1056
|
-
| `records(opts?)` | Filtered raw records (BigQuery) |
|
|
1057
|
-
| `multiMetric(opts)` | Multiple aggregations in one query |
|
|
1058
|
-
| `storageStats(opts?)` | Bucket storage statistics |
|
|
1059
|
-
| `crossBucket(opts)` | Compare across multiple buckets |
|
|
1060
|
-
| `query(query)` | Raw analytics query |
|
|
1061
|
-
|
|
1062
|
-
### `db.storage`
|
|
1063
|
-
|
|
1064
|
-
The `StorageManager` instance. Also has `.scope(prefix)` for folder-scoped access.
|
|
1065
|
-
|
|
1066
|
-
| Method | Description |
|
|
1067
|
-
|---|---|
|
|
1068
|
-
| `upload(data, path, opts?)` | Simple server-buffered upload |
|
|
1069
|
-
| `uploadRaw(data, path, opts?)` | Upload JSON/text data |
|
|
1070
|
-
| `getUploadUrl(opts)` | Step 1: Get signed GCS upload URL |
|
|
1071
|
-
| `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2: Upload to GCS directly |
|
|
1072
|
-
| `confirmUpload(opts)` | Step 3: Register upload metadata |
|
|
1073
|
-
| `getBatchUploadUrls(files)` | Batch signed upload URLs |
|
|
1074
|
-
| `batchConfirmUploads(items)` | Confirm batch uploads |
|
|
1075
|
-
| `download(path)` | Download private file |
|
|
1076
|
-
| `batchDownload(paths)` | Batch download |
|
|
1077
|
-
| `list(opts?)` | List files and folders |
|
|
1078
|
-
| `getMetadata(path)` | File metadata |
|
|
1079
|
-
| `getSignedUrl(path, expiresIn?)` | Time-limited share URL |
|
|
1080
|
-
| `setVisibility(path, isPublic)` | Toggle public/private |
|
|
1081
|
-
| `createFolder(path)` | Create a folder |
|
|
1082
|
-
| `deleteFile(path)` | Delete a file |
|
|
1083
|
-
| `deleteFolder(path)` | Delete a folder recursively |
|
|
1084
|
-
| `move(from, to)` | Move/rename |
|
|
1085
|
-
| `copy(from, to)` | Copy |
|
|
1086
|
-
| `getStats()` | Key-level stats |
|
|
1087
|
-
| `info()` | Server ping (no auth) |
|
|
1088
|
-
| `scope(prefix)` | Get a ScopedStorage instance |
|
|
1504
|
+
---
|
|
1505
|
+
|
|
1506
|
+
### `db.storage(keyName)` → `StorageManager`
|
|
1507
|
+
|
|
1508
|
+
| Method | Signature | Description |
|
|
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. |
|
|
1089
1531
|
|
|
1090
1532
|
---
|
|
1091
1533
|
|
|
1092
|
-
|
|
1534
|
+
### `db.analytics(bucketKey)` → `AnalyticsClient`
|
|
1093
1535
|
|
|
1094
|
-
|
|
1536
|
+
| Method | Signature | Description |
|
|
1537
|
+
|---|---|---|
|
|
1538
|
+
| `count` | `(opts?) → CountResult` | Total record count. |
|
|
1539
|
+
| `distribution` | `(opts) → DistributionRow[]` | Per-value counts for a field. |
|
|
1540
|
+
| `sum` | `(opts) → SumRow[]` | Sum of a numeric field, optional groupBy. |
|
|
1541
|
+
| `timeSeries` | `(opts?) → TimeSeriesRow[]` | Record counts over time. |
|
|
1542
|
+
| `fieldTimeSeries` | `(opts) → FieldTimeSeriesRow[]` | Field aggregation over time. |
|
|
1543
|
+
| `topN` | `(opts) → TopNRow[]` | Most frequent values. `labelField` adds a human-readable label. |
|
|
1544
|
+
| `stats` | `(opts) → FieldStats` | min / max / avg / sum / count / stddev for a numeric field. |
|
|
1545
|
+
| `records` | `(opts?) → (T & RecordResult)[]` | Filtered raw records via BigQuery. |
|
|
1546
|
+
| `multiMetric` | `(opts) → MultiMetricResult` | Multiple aggregations in one query. Each metric gets a named alias. |
|
|
1547
|
+
| `storageStats` | `(opts?) → StorageStatsResult` | Record count + byte totals for the bucket. |
|
|
1548
|
+
| `crossBucket` | `(opts) → CrossBucketRow[]` | Compare a metric across multiple buckets. |
|
|
1549
|
+
| `query` | `(query: AnalyticsQuery) → AnalyticsResult<T>` | Raw query — for cases not covered by the typed methods above. |
|
|
1550
|
+
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
## Contributing
|
|
1095
1554
|
|
|
1096
1555
|
```bash
|
|
1097
|
-
# Clone the repo
|
|
1098
1556
|
git clone https://github.com/hydrousdb/hydrousdb-js.git
|
|
1099
1557
|
cd hydrousdb-js
|
|
1100
1558
|
|
|
1101
|
-
#
|
|
1102
|
-
npm
|
|
1103
|
-
|
|
1104
|
-
#
|
|
1105
|
-
npm
|
|
1106
|
-
|
|
1107
|
-
# Build
|
|
1108
|
-
npm run build
|
|
1109
|
-
|
|
1110
|
-
# Run tests in watch mode
|
|
1111
|
-
npm run test:watch
|
|
1559
|
+
npm install # install dependencies
|
|
1560
|
+
npm run build # compile ESM + CJS + type declarations
|
|
1561
|
+
npm test # run the full test suite (68 tests)
|
|
1562
|
+
npm run test:watch # watch mode
|
|
1563
|
+
npm run lint # TypeScript type check
|
|
1112
1564
|
```
|
|
1113
1565
|
|
|
1114
1566
|
---
|
|
@@ -1121,6 +1573,6 @@ MIT — see [LICENSE](./LICENSE) for details.
|
|
|
1121
1573
|
|
|
1122
1574
|
<p align="center">
|
|
1123
1575
|
Built with ❤️ by the <a href="https://hydrousdb.com">HydrousDB</a> team.<br>
|
|
1124
|
-
Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
|
|
1576
|
+
Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
|
|
1125
1577
|
<a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
|
|
1126
1578
|
</p>
|