hydrousdb 3.2.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,347 +1,68 @@
1
- # HydrousDB JavaScript / TypeScript SDK
1
+ # HydrousDB JS/TS SDK
2
2
 
3
- <p align="center">
4
- <strong>Store, retrieve, and query massive JSON records in milliseconds — with auth, file storage, and analytics built in.</strong><br><br>
5
- <a href="https://hydrousdb.com/dashboard"><strong>→ Create a free account and run your first query in 5 minutes</strong></a>
6
- </p>
7
-
8
- <p align="center">
9
- <a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/v/hydrousdb.svg" alt="npm version"></a>
10
- <a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/dm/hydrousdb.svg" alt="npm downloads"></a>
11
- <a href="https://github.com/hydrousdb/hydrousdb-js/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hydrousdb.svg" alt="MIT License"></a>
12
- <a href="https://hydrousdb.com"><img src="https://img.shields.io/badge/docs-hydrousdb.com-blue" alt="Documentation"></a>
13
- </p>
14
-
15
- ---
16
-
17
- ## Table of Contents
18
-
19
- - [What is HydrousDB?](#what-is-hydrousdb)
20
- - [How It Works](#how-it-works)
21
- - [Quick Start](#quick-start)
22
- - [Installation](#installation)
23
- - [Module Formats — ESM & CommonJS](#module-formats--esm--commonjs)
24
- - [Records](#records)
25
- - [Create](#create-a-record)
26
- - [Read](#read-a-record)
27
- - [Update — patch vs set](#update-a-record)
28
- - [Delete](#delete-a-record)
29
- - [Query](#query-records)
30
- - [Count](#count-records)
31
- - [Batch Create](#batch-create)
32
- - [Batch Delete](#batch-delete)
33
- - [Version History](#version-history)
34
- - [Write-Filter Sentinels](#write-filter-sentinels)
35
- - [Custom Record IDs](#custom-record-ids)
36
- - [Authentication](#authentication)
37
- - [Sign Up](#sign-up)
38
- - [Log In / Log Out](#log-in--log-out)
39
- - [Session Management](#session-management)
40
- - [Validate a Session](#validate-a-session)
41
- - [Update Profile](#update-profile)
42
- - [Change Password](#change-password)
43
- - [Password Reset Flow](#password-reset-flow)
44
- - [Email Verification](#email-verification)
45
- - [Admin — List Users](#admin--list-users)
46
- - [Admin — Lock / Unlock](#admin--lock--unlock)
47
- - [Admin — Delete Users](#admin--delete-users)
48
- - [File Storage](#file-storage)
49
- - [Simple Upload](#simple-upload)
50
- - [Upload Raw JSON or Text](#upload-raw-json-or-text)
51
- - [Large File Upload with Progress](#large-file-upload-with-progress)
52
- - [Batch Upload](#batch-upload)
53
- - [Download](#download)
54
- - [Batch Download](#batch-download)
55
- - [List Files](#list-files)
56
- - [Scoped Storage](#scoped-storage)
57
- - [File Metadata](#file-metadata)
58
- - [Signed Share URLs](#signed-share-urls)
59
- - [Visibility](#visibility)
60
- - [Move, Copy, Delete](#move-copy-delete)
61
- - [Storage Stats](#storage-stats)
62
- - [Analytics](#analytics)
63
- - [Count](#count-1)
64
- - [Distribution](#distribution)
65
- - [Sum](#sum)
66
- - [Time Series](#time-series)
67
- - [Field Time Series](#field-time-series)
68
- - [Top N](#top-n)
69
- - [Field Stats](#field-stats)
70
- - [Multi-Metric Dashboard](#multi-metric-dashboard)
71
- - [Filtered Records via BigQuery](#filtered-records-via-bigquery)
72
- - [Cross-Bucket Comparison](#cross-bucket-comparison)
73
- - [Storage Stats](#storage-stats-1)
74
- - [Raw Query](#raw-query)
75
- - [TypeScript](#typescript)
76
- - [Error Handling](#error-handling)
77
- - [Security Best Practices](#security-best-practices)
78
- - [Full API Reference](#full-api-reference)
79
- - [Contributing](#contributing)
80
- - [License](#license)
81
-
82
- ---
83
-
84
- ## What is HydrousDB?
85
-
86
- Traditional databases start choking when your JSON records get large. Postgres hits row-size limits. Firestore charges per field read. MongoDB buckles under millions of 500 KB+ documents. They were built for structured rows and small payloads — not the deeply nested, real-world JSON that modern apps actually produce.
87
-
88
- HydrousDB is built specifically for that problem. It stores every record as a compressed GCS blob, retrieves any record in a single network call (the storage path is computed directly from the record ID — no index lookups), and runs analytics at BigQuery scale without ETL. The bigger and messier your JSON, the more it outperforms traditional databases.
89
-
90
- | Domain | Example records | Why traditional DBs struggle |
91
- |---|---|---|
92
- | 🏥 **Hospital / EMR** | Full patient charts — vitals, medications, notes, imaging | 850 KB+ per chart, millions of patients, strict audit trails |
93
- | 🎓 **School management** | Student portfolios — grades, assessments, teacher notes | Deep nesting, bursty writes at term-end, long-term archival |
94
- | 🏭 **IoT / Industrial** | Sensor telemetry — readings, device state, calibration | Billions of records, append-heavy, rarely updated |
95
- | 🛒 **E-commerce** | Orders — line items, fulfilment events, return history | Variable shape, fast analytics across date ranges |
96
- | ⚖️ **Legal / compliance** | Case files — filings, correspondence, version history | 1 MB+ records, immutable audit log, cross-case analytics |
97
- | 🎮 **Gaming** | Player save states — inventory, quest progress, replays | Large payloads, millions of users, burst writes |
98
-
99
- **What you get out of the box:**
100
-
101
- | Feature | What it does |
102
- |---|---|
103
- | **Records** | Schemaless JSON store. Billion-scale, gzip-compressed, date-encoded IDs for zero-lookup retrieval. |
104
- | **Auth** | Full user system — signup, login, sessions, password reset, email verification, admin controls. |
105
- | **Storage** | File uploads to GCS. Direct uploads, public/private visibility, signed share URLs. |
106
- | **Analytics** | BigQuery-powered — counts, distributions, time series, top-N, cross-bucket. Zero ETL. |
107
-
108
- ---
109
-
110
- ## How It Works
111
-
112
- Every record ID encodes its creation date as a prefix (e.g. `260203-rec_01JA2XYZ`). This means the full GCS storage path to any record can be computed in memory — no index lookup needed.
113
-
114
- ```
115
- 260203-rec_01JA2XYZ
116
- ↓ parse date prefix
117
- YY=26 MM=02 DD=03
118
- ↓ compute path in memory
119
- projects/pid/buckets/bk/records/26/02/03/rec_01JA.json.gz
120
- ↓ fetch from GCS directly
121
- 0 index reads ✓
122
- ```
123
-
124
- Records are gzip-compressed on write (60–80% size reduction). An 850 KB hospital chart becomes ~255 KB on disk, automatically, every time.
125
-
126
- ---
127
-
128
- ## Quick Start
129
-
130
- ### 1. Create your account
131
-
132
- Sign up at [https://hydrousdb.com](https://hydrousdb.com).
133
-
134
- ### 2. Get your API keys
135
-
136
- From the dashboard → **Settings → API Keys**, create three key types:
137
-
138
- | Key | Prefix | Used for |
139
- |---|---|---|
140
- | **Auth Key** | `hk_auth_…` | All auth routes — signup, login, sessions |
141
- | **Bucket Security Key** | `hk_bucket_…` | Records and analytics |
142
- | **Storage Key(s)** | `ssk_…` | File storage — one key per storage bucket |
143
-
144
- > ⚠️ Never commit these to Git. Store them in environment variables.
145
-
146
- ### 3. Install
3
+ **A database that doesn't choke on big JSON.** Store, query, and analyse massive records — with auth and file storage built in.
147
4
 
148
5
  ```bash
149
6
  npm install hydrousdb
150
- # or: yarn add hydrousdb / pnpm add hydrousdb
151
7
  ```
152
8
 
153
- **Requirements:** Node.js 18+ (uses the native `fetch` API).
9
+ [Get your free API keys at hydrousdb.com](https://hydrousdb.com/dashboard)
154
10
 
155
- ### 4. Create the client and write your first record
11
+ ---
156
12
 
157
- ```typescript
13
+ ## Setup
14
+
15
+ ```ts
158
16
  import { createClient } from 'hydrousdb';
159
17
 
160
- // Create once — reuse everywhere in your app
161
18
  const db = createClient({
162
19
  authKey: process.env.HYDROUS_AUTH_KEY!,
163
20
  bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
164
21
  storageKeys: {
165
- main: process.env.HYDROUS_STORAGE_MAIN!,
22
+ main: process.env.HYDROUS_STORAGE_KEY!,
166
23
  },
167
24
  });
168
-
169
- // Write
170
- const post = await db.records('my-bucket').create({
171
- title: 'Hello, HydrousDB!',
172
- published: false,
173
- });
174
-
175
- console.log(post.id); // "260601-rec_01JA2XYZ"
176
- console.log(post.createdAt); // 1717200000000
177
-
178
- // Read back — zero database reads, path computed from ID
179
- const fetched = await db.records('my-bucket').get(post.id);
180
-
181
- // Update
182
- await db.records('my-bucket').patch(post.id, { published: true });
183
-
184
- // Delete
185
- await db.records('my-bucket').delete(post.id);
186
- ```
187
-
188
- ---
189
-
190
- ## Installation
191
-
192
- ```bash
193
- npm install hydrousdb
194
25
  ```
195
26
 
196
- ### Module FormatsESM & 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
- ```
27
+ You get three key types from the dashboard one for auth, one for records/analytics, one (or more) for file storage. Keep them in environment variables, never in your code.
204
28
 
205
- ```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
- ```
29
+ Works in **React, Next.js, Vue, React Native, Node.js** — anywhere that runs modern JavaScript.
234
30
 
235
31
  ---
236
32
 
237
33
  ## Records
238
34
 
239
- Records are JSON objects stored in named buckets. Every record automatically gets:
240
-
241
- - `id` — date-prefixed unique identifier (`"260601-rec_01JA2XYZ"`) — encodes the GCS path
242
- - `createdAt` — Unix timestamp in milliseconds
243
- - `updatedAt` — Unix timestamp in milliseconds
35
+ JSON objects stored in named buckets. Every record gets `id`, `createdAt`, and `updatedAt` automatically.
244
36
 
245
- ```typescript
37
+ ```ts
246
38
  const posts = db.records('blog-posts');
247
- // or typed:
248
- const orders = db.records<Order>('orders');
249
- ```
250
-
251
- ### Create a Record
252
-
253
- ```typescript
254
- const post = await posts.create({
255
- title: 'My First Post',
256
- body: 'Hello world.',
257
- status: 'draft',
258
- views: 0,
259
- });
260
-
261
- // post.id, post.createdAt, post.updatedAt are added automatically
262
- console.log(post.id); // "260601-rec_01JA2XYZ"
263
- ```
264
-
265
- **With queryable fields** — fields you want to filter on server-side must be declared at write time:
266
39
 
267
- ```typescript
40
+ // Create
268
41
  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
- },
42
+ { title: 'Hello', status: 'draft', views: 0 },
43
+ { queryableFields: ['status'] }, // declare fields you want to filter on
278
44
  );
279
- ```
280
-
281
- > 💡 **Why declare queryable fields?** HydrousDB stores records as compressed blobs. Fields you want to filter or sort by need to be registered in a lightweight index at write time. You only pay index overhead for the fields you actually query.
282
-
283
- ### Read a Record
284
-
285
- ```typescript
286
- // Path computed from the ID in memory — zero index reads
287
- const post = await posts.get('260601-rec_01JA2XYZ');
288
-
289
- // Throws HydrousError (code: RECORD_NOT_FOUND) if the ID doesn't exist
290
- ```
291
-
292
- ### Update a Record
293
-
294
- **`patch(id, data)` — merge update.** Only the fields you provide are changed. All other fields on the record are left untouched.
295
-
296
- ```typescript
297
- const updated = await posts.patch('260601-rec_01JA2XYZ', {
298
- status: 'published',
299
- views: 1,
300
- });
301
- ```
302
-
303
- **`set(id, data)` — full replace.** The entire record is replaced with the new data.
304
-
305
- ```typescript
306
- const replaced = await posts.set('260601-rec_01JA2XYZ', {
307
- title: 'Updated Title',
308
- body: 'New content.',
309
- status: 'published',
310
- views: 42,
311
- });
312
- ```
313
-
314
- **Disable merge** (force field removal):
45
+ console.log(post.id); // "260601-rec_01JA2XYZ"
315
46
 
316
- ```typescript
317
- // merge: false means fields not in `data` are removed
318
- await posts.patch('260601-rec_01JA2XYZ', { status: 'archived' }, { merge: false });
319
- ```
47
+ // Read (path computed from ID — zero index lookups)
48
+ const found = await posts.get(post.id);
320
49
 
321
- ### Delete a Record
50
+ // Update (only the fields you pass are changed)
51
+ await posts.patch(post.id, { status: 'published' });
322
52
 
323
- ```typescript
324
- await posts.delete('260601-rec_01JA2XYZ');
53
+ // Delete
54
+ await posts.delete(post.id);
325
55
  ```
326
56
 
327
- ### Query Records
57
+ > **Why `queryableFields`?** Records are stored as compressed blobs. Fields you want to filter or sort by must be registered at write time. You only pay overhead for what you actually query.
328
58
 
329
- ```typescript
330
- // All records (up to 100 by default)
331
- const { records } = await posts.query();
59
+ ### Query
332
60
 
333
- // With filters
334
- const { records: published } = await posts.query({
335
- filters: [
336
- { field: 'status', op: '==', value: 'published' },
337
- ],
338
- });
339
-
340
- // Multiple filters, sort, limit
61
+ ```ts
341
62
  const { records, hasMore, nextCursor } = await posts.query({
342
63
  filters: [
343
- { field: 'status', op: '==', value: 'published' },
344
- { field: 'views', op: '>', value: 100 },
64
+ { field: 'status', op: '==', value: 'published' },
65
+ { field: 'views', op: '>', value: 100 },
345
66
  ],
346
67
  orderBy: 'createdAt',
347
68
  order: 'desc',
@@ -350,374 +71,167 @@ const { records, hasMore, nextCursor } = await posts.query({
350
71
 
351
72
  // Next page
352
73
  if (hasMore) {
353
- const page2 = await posts.query({
354
- orderBy: 'createdAt',
355
- order: 'desc',
356
- limit: 20,
357
- startAfter: nextCursor,
358
- });
74
+ const page2 = await posts.query({ limit: 20, startAfter: nextCursor });
359
75
  }
360
-
361
- // Select only specific fields (reduces payload size)
362
- const { records: light } = await posts.query({
363
- fields: 'id,title,status,createdAt',
364
- });
365
-
366
- // Date range
367
- const { records: thisWeek } = await posts.query({
368
- dateRange: {
369
- start: Date.now() - 7 * 24 * 60 * 60 * 1000,
370
- end: Date.now(),
371
- },
372
- });
373
76
  ```
374
77
 
375
- **Filter operators:**
376
-
377
- | Operator | Meaning |
378
- |---|---|
379
- | `==` | Equal |
380
- | `!=` | Not equal |
381
- | `>` | Greater than |
382
- | `<` | Less than |
383
- | `>=` | Greater than or equal |
384
- | `<=` | Less than or equal |
385
- | `CONTAINS` | String contains |
386
-
387
- > ⚠️ You can only filter on fields declared as `queryableFields` when the record was created. Filtering on an un-indexed field returns no results.
78
+ Filter operators: `==` `!=` `>` `<` `>=` `<=` `CONTAINS`
388
79
 
389
- ### Count Records
80
+ ### Atomic field updates
390
81
 
391
- ```typescript
392
- // Total records in the bucket
393
- const total = await posts.count();
82
+ Avoid race conditions with server-side operations inside `patch()`:
394
83
 
395
- // Records matching filters
396
- const publishedCount = await posts.count([
397
- { field: 'status', op: '==', value: 'published' },
398
- ]);
84
+ ```ts
85
+ await posts.patch(post.id, {
86
+ views: { __op: 'increment', delta: 1 }, // add 1
87
+ credits: { __op: 'decrement', delta: 5 }, // subtract 5
88
+ slug: { __op: 'setOnce', value: 'my-post' }, // set only if empty
89
+ tags: { __op: 'appendUnique', item: 'featured' }, // add to array, no dupes
90
+ tags: { __op: 'removeFromArray', item: 'draft' }, // remove from array
91
+ rating: { __op: 'clamp', value: 6, min: 0, max: 5 },
92
+ price: { __op: 'multiplyBy', factor: 1.1 },
93
+ active: { __op: 'toggleBool' },
94
+ syncedAt: { __op: 'serverTimestamp' },
95
+ } as any);
399
96
  ```
400
97
 
401
- ### Batch Create
402
-
403
- Up to 500 records per call.
98
+ ### Batch operations
404
99
 
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
- },
100
+ ```ts
101
+ // Create up to 500 records at once
102
+ const { results, errors } = await posts.batchCreate(
103
+ [{ title: 'A' }, { title: 'B' }],
104
+ { queryableFields: ['title'] },
416
105
  );
417
- // → [{ id: '…', title: 'Post A', … }, { id: '…', title: 'Post B', … }, …]
418
- ```
419
-
420
- Each record in the batch can optionally carry a `_customRecordId` for upsert behaviour:
421
-
422
- ```typescript
423
- await posts.batchCreate([
424
- { _customRecordId: '260601-post_welcome', title: 'Welcome', status: 'published' },
425
- { title: 'Auto-ID post', status: 'draft' },
426
- ]);
427
- ```
428
-
429
- ### Batch Delete
430
-
431
- Up to 500 records per call.
432
106
 
433
- ```typescript
434
- const { deleted, failed } = await posts.batchDelete([
435
- '260601-rec_01JA',
436
- '260601-rec_02JB',
437
- '260601-rec_03JC',
107
+ // Update up to 500 records at once
108
+ await posts.batchUpdate([
109
+ { recordId: 'id1', values: { status: 'archived' } },
110
+ { recordId: 'id2', values: { status: 'archived' } },
438
111
  ]);
439
112
 
440
- console.log(`Deleted: ${deleted}, Failed: ${failed.length}`);
113
+ // Delete up to 500 records at once
114
+ const { successful, failed } = await posts.batchDelete(['id1', 'id2']);
441
115
  ```
442
116
 
443
- ### Version History
117
+ ### Version history
444
118
 
445
- Every write creates a new version — you can restore any record to any previous state.
119
+ Every write is automatically versioned.
446
120
 
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, … }]
121
+ ```ts
122
+ const history = await posts.getHistory(post.id);
123
+ // [{ generation, savedAt, savedBy, sizeBytes }, …]
451
124
 
452
- // Restore to version 1 (the original)
453
- const restored = await posts.restoreVersion('260601-rec_01JA2XYZ', history[2]!.version);
125
+ // Read a specific past version
126
+ const v1 = await posts.getVersion(post.id, history[0].generation!);
454
127
  ```
455
128
 
456
- ### Write-Filter Sentinels
129
+ ### Custom record IDs
457
130
 
458
- For atomic server-side field operations — use these inside `patch()` to avoid race conditions.
459
-
460
- ```typescript
461
- await posts.patch('260601-rec_01JA', {
462
- // Increment / decrement a numeric field atomically
463
- views: { __op: 'increment', delta: 1 },
464
- credits: { __op: 'decrement', delta: 5 },
465
-
466
- // Set a field only if it doesn't already have a value
467
- slug: { __op: 'setOnce', value: 'my-first-post' },
468
-
469
- // Set a field only if a condition is met
470
- discount: { __op: 'setIf', value: 10, cond: { op: '>=', value: 100 } },
471
-
472
- // Add to an array (no duplicates)
473
- tags: { __op: 'appendUnique', item: 'featured' },
474
-
475
- // Remove from an array
476
- tags: { __op: 'removeFromArray', item: 'draft' },
477
-
478
- // Clamp a numeric value between min and max
479
- rating: { __op: 'clamp', value: 6, min: 0, max: 5 },
480
-
481
- // Multiply a numeric field
482
- price: { __op: 'multiplyBy', factor: 1.1 },
483
-
484
- // Flip a boolean
485
- active: { __op: 'toggleBool' },
486
-
487
- // Set field to the server's current timestamp
488
- lastSeen: { __op: 'serverTimestamp' },
489
- } as any);
490
- ```
491
-
492
- ### Custom Record IDs
493
-
494
- Provide your own ID instead of using an auto-generated one. If the ID already exists, the record is upserted.
495
-
496
- ```typescript
497
- // Single record
131
+ ```ts
132
+ // If the ID already exists, the record is upserted
498
133
  const post = await posts.create(
499
- { title: 'Welcome', status: 'published' },
134
+ { title: 'Welcome' },
500
135
  { customRecordId: '260601-post_welcome' },
501
136
  );
502
-
503
- // Batch — set _customRecordId on individual items
504
- await posts.batchCreate([
505
- { _customRecordId: '260601-post_welcome', title: 'Welcome' },
506
- { title: 'Auto-ID post' },
507
- ]);
508
137
  ```
509
138
 
510
- Custom IDs must match `^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$`.
511
-
512
139
  ---
513
140
 
514
- ## Authentication
141
+ ## Auth
515
142
 
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.
143
+ A complete user system signup, login, sessions, password reset, email verification, and admin controls.
517
144
 
518
- ```typescript
519
- const auth = db.auth('app-users');
145
+ ```ts
146
+ const auth = db.auth();
520
147
  ```
521
148
 
522
- ### Sign Up
149
+ ### Sign up & log in
523
150
 
524
- ```typescript
151
+ ```ts
152
+ // Sign up — extra fields beyond email/password are stored on the user
525
153
  const { user, session } = await auth.signup({
526
154
  email: 'alice@example.com',
527
- password: 'hunter2', // validated server-side
528
- fullName: 'Alice Wonderland',
529
- // Any extra fields are stored on the user record:
530
- plan: 'pro',
531
- referral: 'friend123',
155
+ password: 'hunter2',
156
+ fullName: 'Alice Smith',
157
+ plan: 'pro', // any custom fields you want
532
158
  });
533
159
 
534
- // Persist these in your app / session store:
535
- // session.sessionId
536
- // session.refreshToken
537
- // session.expiresAt
538
-
539
- console.log(user.id); // "usr_xxxxxxxxxxxx"
540
- console.log(user.emailVerified); // false — send a verification email
541
- ```
542
-
543
- ### Log In / Log Out
544
-
545
- ```typescript
546
160
  // Log in
547
161
  const { user, session } = await auth.login({
548
162
  email: 'alice@example.com',
549
163
  password: 'hunter2',
550
164
  });
551
165
 
552
- // Log out invalidates the session server-side
166
+ // Log out (pass allDevices: true to log out everywhere)
553
167
  await auth.logout({ sessionId: session.sessionId });
554
-
555
- // Log out from all devices at once
556
- await auth.logout({ sessionId: session.sessionId, allDevices: true });
557
168
  ```
558
169
 
559
- ### Session Management
170
+ Store `session.sessionId` and `session.refreshToken` in your app. Sessions expire after **24 hours**, refresh tokens after **30 days**.
560
171
 
561
- Sessions expire after **24 hours**. Refresh tokens last **30 days**.
172
+ ### Session management
562
173
 
563
- ```typescript
564
- // Refresh before expiry to get a new session
565
- const newSession = await auth.refreshSession({
566
- refreshToken: session.refreshToken,
567
- });
568
- // Store newSession.sessionId and newSession.refreshToken
174
+ ```ts
175
+ // Check if a session is still valid (use on your backend)
176
+ const { user } = await auth.validateSession(session.sessionId);
569
177
 
570
- // Get a user by ID
571
- const user = await auth.getUser({ userId: session.userId });
178
+ // Get a new session using the refresh token
179
+ const newSession = await auth.refreshSession(session.refreshToken);
572
180
  ```
573
181
 
574
- ### Validate a Session
182
+ ### User profile
575
183
 
576
- Use this on your backend to verify an incoming session is still active.
184
+ ```ts
185
+ const user = await auth.getUser(session.userId);
577
186
 
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
588
-
589
- ```typescript
590
- const updated = await auth.updateUser({
187
+ await auth.updateUser({
591
188
  sessionId: session.sessionId,
592
189
  userId: user.id,
593
- updates: {
594
- fullName: 'Alice Smith',
595
- plan: 'enterprise',
596
- // Any field on the user record can be updated here
597
- avatar: 'https://example.com/avatar.jpg',
598
- },
190
+ updates: { fullName: 'Alice Johnson', plan: 'enterprise' },
599
191
  });
600
- ```
601
-
602
- > ⚠️ The `updates` key is required — it wraps the fields to change. Fields not included in `updates` are left untouched.
603
192
 
604
- ### Change Password
193
+ await auth.deleteUser(session.sessionId, user.id);
194
+ ```
605
195
 
606
- Requires an active session — so a stolen old password alone is not enough.
196
+ ### Password & email
607
197
 
608
- ```typescript
198
+ ```ts
199
+ // Change password (requires active session)
609
200
  await auth.changePassword({
610
201
  sessionId: session.sessionId,
611
202
  userId: user.id,
612
203
  currentPassword: 'hunter2',
613
204
  newPassword: 'correcthorsebatterystaple',
614
205
  });
615
- // All existing sessions for this user are automatically revoked
616
- ```
617
-
618
- ### Password Reset Flow
619
-
620
- ```typescript
621
- // 1. User requests a reset — always returns success (prevents email enumeration)
622
- await auth.requestPasswordReset({ email: 'alice@example.com' });
623
206
 
624
- // 2. User receives the reset token via email (handled by your email provider)
207
+ // Forgot password flow
208
+ await auth.requestPasswordReset('alice@example.com');
209
+ await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');
625
210
 
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
632
- ```
633
-
634
- ### Email Verification
635
-
636
- ```typescript
637
- // 1. Send the verification email
638
- await auth.requestEmailVerification({ userId: user.id });
639
-
640
- // 2. User clicks link in their inbox — your app extracts the token from the URL
641
-
642
- // 3. Confirm the token
643
- await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
211
+ // Email verification
212
+ await auth.requestEmailVerification(user.id);
213
+ await auth.confirmEmailVerification(tokenFromEmail);
644
214
  ```
645
215
 
646
- ### Admin — List Users
216
+ ### Admin
647
217
 
648
- Admin operations require a valid session from a user with `role: 'admin'`.
649
-
650
- ```typescript
651
- // Paginated list — uses cursor-based pagination
218
+ ```ts
219
+ // List all users (paginated)
652
220
  const { users, hasMore, nextCursor } = await auth.listUsers({
653
221
  sessionId: adminSession.sessionId,
654
222
  limit: 50,
655
223
  });
656
224
 
657
- if (hasMore) {
658
- const page2 = await auth.listUsers({
659
- sessionId: adminSession.sessionId,
660
- limit: 50,
661
- cursor: nextCursor!,
662
- });
663
- }
664
- ```
225
+ // Lock / unlock an account
226
+ await auth.lockAccount({ sessionId: adminSession.sessionId, userId: user.id });
227
+ await auth.unlockAccount(adminSession.sessionId, user.id);
665
228
 
666
- Each user in the list includes:
667
-
668
- ```typescript
669
- {
670
- id: 'usr_xxxxxxxxxxxx',
671
- email: 'alice@example.com',
672
- fullName: 'Alice Wonderland',
673
- emailVerified: true,
674
- accountStatus: 'active', // 'active' | 'locked' | 'suspended'
675
- role: 'user', // 'user' | 'admin'
676
- createdAt: 1717200000000,
677
- updatedAt: 1717200000000,
678
- // ...any extra fields stored at signup
679
- }
680
- ```
681
-
682
- ### Admin — Lock / Unlock
683
-
684
- ```typescript
685
- // Lock an account (prevents login)
686
- const { lockedUntil, unlockTime } = await auth.lockAccount({
687
- sessionId: adminSession.sessionId,
688
- userId: 'usr_abc123',
689
- duration: 60 * 60 * 1000, // 1 hour in ms (default: 15 minutes)
690
- });
691
-
692
- console.log(`Account locked until ${unlockTime}`);
693
-
694
- // Unlock manually
695
- await auth.unlockAccount({
696
- sessionId: adminSession.sessionId,
697
- userId: 'usr_abc123',
698
- });
699
- ```
700
-
701
- ### Admin — Delete Users
702
-
703
- ```typescript
704
- // Soft-delete — marks the account as deleted, keeps the data
705
- await auth.deleteUser({
706
- sessionId: adminSession.sessionId,
707
- userId: 'usr_abc123',
708
- });
709
-
710
- // Hard-delete — permanent, irreversible
711
- await auth.hardDeleteUser({
712
- sessionId: adminSession.sessionId,
713
- userId: 'usr_abc123',
714
- });
715
-
716
- // Bulk delete — up to 500 users, soft or hard
717
- const { succeeded, failed } = await auth.bulkDeleteUsers({
229
+ // Delete users
230
+ await auth.hardDeleteUser(adminSession.sessionId, user.id);
231
+ await auth.bulkDeleteUsers({
718
232
  sessionId: adminSession.sessionId,
719
- userIds: ['usr_a', 'usr_b', 'usr_c'],
720
- hard: false, // set true for permanent deletion
233
+ userIds: ['id1', 'id2'],
234
+ hard: true,
721
235
  });
722
236
  ```
723
237
 
@@ -725,828 +239,318 @@ const { succeeded, failed } = await auth.bulkDeleteUsers({
725
239
 
726
240
  ## File Storage
727
241
 
728
- 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');
734
- ```
735
-
736
- ### Simple Upload
737
-
738
- Server-buffered upload for files up to **500 MB**. No progress bar — use [Large File Upload](#large-file-upload-with-progress) if you need one.
739
-
740
- ```typescript
741
- // Browser — from a file input
742
- const file = document.querySelector('input[type="file"]').files[0];
743
- const result = await db.storage('main').upload(file, `uploads/${file.name}`, {
744
- isPublic: true, // publicly accessible without auth (default: false)
745
- overwrite: false, // throw if file already exists (default: false)
746
- mimeType: 'image/jpeg', // optional — auto-detected from extension if omitted
747
- });
748
-
749
- console.log(result.publicUrl); // CDN URL — use anywhere
750
- console.log(result.downloadUrl); // null when isPublic: true
751
- console.log(result.size); // bytes
752
- console.log(result.mimeType); // 'image/jpeg'
753
-
754
- // Node.js — from a Buffer or file path
755
- import { readFileSync } from 'fs';
756
- const buf = readFileSync('./report.pdf');
757
- const result = await db.storage('documents').upload(buf, 'reports/q3.pdf');
758
- console.log(result.downloadUrl); // auth-required download URL
242
+ ```ts
243
+ const storage = db.storage('main');
759
244
  ```
760
245
 
761
- ### Upload Raw JSON or Text
246
+ ### Upload
762
247
 
763
- ```typescript
764
- // Upload a JS object as JSON
765
- const result = await db.storage('main').uploadRaw(
766
- { theme: 'dark', language: 'en', version: 3 },
767
- 'settings/alice.json',
768
- { isPublic: false },
769
- );
248
+ ```ts
249
+ // Simple upload (up to 500 MB)
250
+ const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
251
+ console.log(result.publicUrl); // permanent CDN URL
252
+ console.log(result.downloadUrl); // authenticated download URL (if private)
770
253
 
771
- // Upload a 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
- );
254
+ // Upload a JSON object or string directly
255
+ await storage.uploadRaw({ theme: 'dark' }, 'config.json');
777
256
  ```
778
257
 
779
- ### Large File Upload with Progress
780
-
781
- For files over ~10 MB or when you need a progress bar. The file goes **directly to GCS** — your server never buffers the bytes.
782
-
783
- ```typescript
784
- const storage = db.storage('main');
258
+ **Large files with progress tracking** (recommended for anything > 10 MB):
785
259
 
786
- // Step 1 — Get a signed GCS upload URL
260
+ ```ts
261
+ // Step 1 — get a signed upload URL
787
262
  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)
793
- });
794
-
795
- // Step 2 — Upload directly to GCS with real progress
796
- await storage.uploadToSignedUrl(
797
- uploadUrl,
798
- file,
799
- 'video/mp4',
800
- (percent) => {
801
- progressBar.style.width = `${percent}%`;
802
- console.log(`${percent}% uploaded`);
803
- },
804
- );
805
-
806
- // Step 3 — Confirm the upload (registers metadata on the server)
807
- const result = await storage.confirmUpload({
808
- path: path,
263
+ path: 'videos/intro.mp4',
809
264
  mimeType: 'video/mp4',
265
+ size: file.size,
810
266
  isPublic: true,
811
267
  });
812
268
 
813
- console.log(result.publicUrl); // live CDN URL
814
- ```
815
-
816
- ### Batch Upload
817
-
818
- Upload up to 50 files at once.
819
-
820
- ```typescript
821
- const storage = db.storage('main');
822
-
823
- // Step 1 — Get signed URLs for all files
824
- const { files } = await storage.getBatchUploadUrls([
825
- { path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
826
- { path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
827
- { path: 'gallery/photo3.png', mimeType: 'image/png', size: 98304, isPublic: true },
828
- ]);
829
-
830
- // Step 2 — Upload each directly to GCS
831
- for (const f of files) {
832
- await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
833
- }
834
-
835
- // Step 3 — Confirm all at once
836
- const { succeeded, failed } = await storage.batchConfirmUploads(
837
- files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true })),
838
- );
839
-
840
- console.log(`${succeeded.length} uploaded, ${failed.length} failed`);
841
- for (const f of succeeded) {
842
- console.log(f.publicUrl);
843
- }
844
- ```
845
-
846
- ### Download
847
-
848
- ```typescript
849
- // Private files require authentication — returns ArrayBuffer
850
- const buffer = await db.storage('documents').download('reports/q3.pdf');
851
- const blob = new Blob([buffer], { type: 'application/pdf' });
852
-
853
- // Trigger browser download
854
- const url = URL.createObjectURL(blob);
855
- const a = document.createElement('a');
856
- a.href = url;
857
- a.download = 'q3.pdf';
858
- a.click();
859
- ```
860
-
861
- > 💡 **Public files:** Use `result.publicUrl` directly — no SDK call needed. `<img src={result.publicUrl} />` just works.
862
-
863
- ### Batch Download
864
-
865
- Download up to 20 files in one call. Content is returned as base64 strings.
866
-
867
- ```typescript
868
- const { succeeded, failed } = await db.storage('documents').batchDownload([
869
- 'reports/q1.pdf',
870
- 'reports/q2.pdf',
871
- 'reports/q3.pdf',
872
- ]);
873
-
874
- for (const f of succeeded) {
875
- console.log(f.path); // 'reports/q1.pdf'
876
- console.log(f.mimeType); // 'application/pdf'
877
- console.log(f.size); // bytes
878
- const bytes = Buffer.from(f.content, 'base64');
879
- }
880
-
881
- for (const f of failed) {
882
- console.error(`${f.path}: ${f.error}`);
883
- }
884
- ```
885
-
886
- ### List Files
887
-
888
- ```typescript
889
- const storage = db.storage('main');
890
-
891
- // List everything at the root
892
- const { files, folders, hasMore, nextCursor } = await storage.list();
893
-
894
- // List a specific folder
895
- const result = await storage.list({
896
- prefix: 'gallery/',
897
- limit: 50,
269
+ // Step 2 — upload directly to GCS (supports onProgress in browsers)
270
+ await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => {
271
+ console.log(`${pct}% uploaded`);
898
272
  });
899
273
 
900
- // Paginate
901
- if (result.hasMore) {
902
- const page2 = await storage.list({
903
- prefix: 'gallery/',
904
- cursor: result.nextCursor,
905
- });
906
- }
907
- ```
908
-
909
- Each file entry:
910
-
911
- ```typescript
912
- {
913
- name: 'photo1.jpg',
914
- path: 'gallery/photo1.jpg',
915
- size: 204800,
916
- mimeType: 'image/jpeg',
917
- isPublic: true,
918
- publicUrl: 'https://storage.googleapis.com/…',
919
- downloadUrl: null,
920
- updatedAt: '2025-06-01T12:00:00.000Z',
921
- }
274
+ // Step 3 — confirm and get the final result
275
+ const result = await storage.confirmUpload({ path, mimeType: 'video/mp4', isPublic: true });
922
276
  ```
923
277
 
924
- ### Scoped Storage
925
-
926
- Pre-fix all operations to a folder — great for per-user isolation.
278
+ ### Download, list, manage
927
279
 
928
- ```typescript
929
- const userDocs = db.storage('documents').scope(`users/${userId}/`);
280
+ ```ts
281
+ // Download a private file
282
+ const buffer = await storage.download('private/doc.pdf');
930
283
 
931
- // Uploads to: users/{userId}/contract.pdf
932
- await userDocs.upload(pdfBuffer, 'contract.pdf');
284
+ // List files
285
+ const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/' });
933
286
 
934
- // Lists: users/{userId}/
935
- const { files } = await userDocs.list();
287
+ // Get file metadata
288
+ const meta = await storage.getMetadata('avatars/alice.jpg');
936
289
 
937
- // Deletes: users/{userId}/contract.pdf
938
- await userDocs.deleteFile('contract.pdf');
290
+ // Time-limited share link (no login needed to access)
291
+ const { signedUrl } = await storage.getSignedUrl('private/report.pdf', 3600);
939
292
 
940
- // Nest scopes
941
- const userThumbs = userDocs.scope('thumbnails/');
942
- // All ops under: users/{userId}/thumbnails/
943
- ```
944
-
945
- `ScopedStorage` has the same full API as `StorageManager` — every method listed in the reference is available.
946
-
947
- ### File Metadata
948
-
949
- ```typescript
950
- const meta = await db.storage('documents').getMetadata('reports/q3.pdf');
951
-
952
- console.log(meta.size); // bytes
953
- console.log(meta.mimeType); // 'application/pdf'
954
- console.log(meta.isPublic); // false
955
- console.log(meta.downloadUrl); // auth-required URL
956
- console.log(meta.createdAt); // ISO string
957
- console.log(meta.updatedAt); // ISO string
958
- ```
959
-
960
- ### Signed Share URLs
961
-
962
- Generate a time-limited link for a private file — no `X-Storage-Key` required to use it.
963
-
964
- ```typescript
965
- const { signedUrl, expiresAt, expiresIn } = await db.storage('documents')
966
- .getSignedUrl('reports/q3.pdf', 3600); // expires in 1 hour (default)
967
-
968
- // Share signedUrl with the recipient — it expires automatically
969
- console.log(`Link valid until: ${new Date(expiresAt).toLocaleString()}`);
970
- ```
971
-
972
- > ⚠️ Downloads via signed URLs bypass the server — **download stats are not tracked** for those requests. Use `downloadUrl` for tracked downloads.
973
-
974
- ### Visibility
975
-
976
- ```typescript
977
- // Make a private file public
978
- const result = await db.storage('main').setVisibility('docs/report.pdf', true);
979
- console.log(result.publicUrl); // now has a CDN URL
293
+ // Move, copy, delete
294
+ await storage.move('old/path.jpg', 'new/path.jpg');
295
+ await storage.copy('template.html', 'pages/home.html');
296
+ await storage.deleteFile('avatars/old.jpg');
297
+ await storage.deleteFolder('temp/');
980
298
 
981
- // 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
299
+ // Change visibility after upload
300
+ await storage.setVisibility('alice.jpg', false); // make private
984
301
  ```
985
302
 
986
- ### Move, Copy, Delete
987
-
988
- ```typescript
989
- const storage = db.storage('main');
990
-
991
- // Rename a file
992
- await storage.move('drafts/report.pdf', 'published/report-2025.pdf');
993
-
994
- // Move to a different folder
995
- await storage.move('inbox/data.csv', 'archive/2025/data.csv');
303
+ ### Scoped storage
996
304
 
997
- // Copy
998
- await storage.copy('templates/invoice.html', 'invoices/inv-001.html');
305
+ Automatically prefix every path — useful for per-user file isolation:
999
306
 
1000
- // Create a folder placeholder
1001
- await storage.createFolder('archive/2025/');
307
+ ```ts
308
+ const userFiles = db.storage('main').scope(`users/${userId}/`);
1002
309
 
1003
- // Delete a single file
1004
- await storage.deleteFile('temp/scratch.txt');
310
+ await userFiles.upload(pdf, 'contract.pdf'); // users/{userId}/contract.pdf
311
+ const { files } = await userFiles.list(); // → only lists users/{userId}/
1005
312
 
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();
313
+ // Nest deeper
314
+ const reports = userFiles.scope('reports/'); // → users/{userId}/reports/
1024
315
  ```
1025
316
 
1026
317
  ---
1027
318
 
1028
319
  ## Analytics
1029
320
 
1030
- HydrousDB Analytics runs directly against BigQuery on your GCS data — zero ETL, no duplication, live results. Fast even on billions of records.
321
+ BigQuery-powered aggregations. No ETL, no pipelines just query.
1031
322
 
1032
- ```typescript
323
+ ```ts
1033
324
  const analytics = db.analytics('orders');
1034
- ```
1035
325
 
1036
- All `dateRange` values are Unix timestamps in milliseconds: `{ start?: number, end?: number }`.
1037
-
1038
- ### Count
1039
-
1040
- ```typescript
1041
- // Total records
326
+ // Total record count
1042
327
  const { count } = await analytics.count();
1043
328
 
1044
- // In a date range
1045
- const { count: lastWeek } = await analytics.count({
1046
- dateRange: {
1047
- start: Date.now() - 7 * 24 * 60 * 60 * 1000,
1048
- end: Date.now(),
1049
- },
1050
- });
1051
- ```
1052
-
1053
- ### Distribution
1054
-
1055
- How many records have each unique value for a field?
1056
-
1057
- ```typescript
1058
- const rows = await analytics.distribution({
1059
- field: 'status',
1060
- limit: 10,
1061
- order: 'desc', // 'asc' | 'desc'
1062
- });
1063
- // [
1064
- // { value: 'completed', count: 8234 },
1065
- // { value: 'pending', count: 1203 },
1066
- // { value: 'refunded', count: 412 },
1067
- // ]
1068
- ```
1069
-
1070
- ### Sum
1071
-
1072
- ```typescript
1073
- // Total revenue
1074
- const [{ sum: total }] = await analytics.sum({ field: 'amount' });
1075
-
1076
- // Revenue by country
1077
- const byCountry = await analytics.sum({
1078
- field: 'amount',
1079
- groupBy: 'country',
1080
- limit: 10,
1081
- });
1082
- // [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, …]
1083
- ```
1084
-
1085
- ### Time Series
1086
-
1087
- Record counts bucketed over time — ideal for activity and growth charts.
1088
-
1089
- ```typescript
1090
- const rows = await analytics.timeSeries({
1091
- granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
1092
- dateRange: {
1093
- start: new Date('2025-01-01').getTime(),
1094
- end: new Date('2025-06-01').getTime(),
1095
- },
1096
- });
1097
- // [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, …]
1098
- ```
1099
-
1100
- ### Field Time Series
1101
-
1102
- Aggregate a numeric field over time.
1103
-
1104
- ```typescript
1105
- const revenue = await analytics.fieldTimeSeries({
1106
- field: 'amount',
1107
- aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
1108
- granularity: 'week',
1109
- dateRange: {
1110
- start: new Date('2025-01-01').getTime(),
1111
- end: Date.now(),
1112
- },
1113
- });
1114
- // [{ date: '2025-W01', value: 12340.50 }, { date: '2025-W02', value: 9872.00 }, …]
1115
- ```
329
+ // How many records have each value of `status`
330
+ const dist = await analytics.distribution({ field: 'status' });
1116
331
 
1117
- ### Top N
332
+ // Sum of `amount`, grouped by `region`
333
+ const sums = await analytics.sum({ field: 'amount', groupBy: 'region' });
1118
334
 
1119
- Most common values for a field.
335
+ // Records over time
336
+ const daily = await analytics.timeSeries({ granularity: 'day' });
1120
337
 
1121
- ```typescript
1122
- const topProducts = await analytics.topN({
1123
- field: 'productId',
1124
- labelField: 'productName', // optional human-readable label alongside the value
1125
- n: 5,
1126
- order: 'desc',
1127
- });
1128
- // [
1129
- // { value: 'prod_123', label: 'Widget Pro', count: 892 },
1130
- // { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
1131
- // ]
1132
- ```
338
+ // How `revenue` changes over time
339
+ const trend = await analytics.fieldTimeSeries({ field: 'revenue', aggregation: 'sum' });
1133
340
 
1134
- ### Field Stats
341
+ // Top 10 countries by record count
342
+ const top10 = await analytics.topN({ field: 'country', n: 10 });
1135
343
 
1136
- Statistical summary for any numeric field.
344
+ // Min / max / avg / sum / stddev for a numeric field
345
+ const stats = await analytics.stats({ field: 'price' });
1137
346
 
1138
- ```typescript
1139
- const stats = await analytics.stats({ field: 'orderValue' });
1140
- // {
1141
- // min: 4.99,
1142
- // max: 9999.99,
1143
- // avg: 87.23,
1144
- // sum: 420948.27,
1145
- // count: 4823,
1146
- // stddev: 143.2,
1147
- // }
1148
- ```
1149
-
1150
- ### Multi-Metric Dashboard
1151
-
1152
- Run several aggregations in a single BigQuery query — one network call.
1153
-
1154
- ```typescript
347
+ // Multiple aggregations in one round-trip
1155
348
  const dashboard = await analytics.multiMetric({
1156
349
  metrics: [
1157
- { field: 'amount', 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' },
350
+ { field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
351
+ { field: 'amount', name: 'avgOrder', aggregation: 'avg' },
352
+ { field: 'recordId', name: 'orderCount', aggregation: 'count' },
1161
353
  ],
1162
- dateRange: {
1163
- start: new Date('2025-01-01').getTime(),
1164
- end: Date.now(),
1165
- },
1166
354
  });
1167
- // {
1168
- // totalRevenue: 198432.50,
1169
- // avgOrderValue: 87.23,
1170
- // maxOrder: 9999.99,
1171
- // uniqueOrders: 2275,
1172
- // }
1173
- ```
1174
-
1175
- ### Filtered Records via BigQuery
355
+ // { totalRevenue: 48200, avgOrder: 96.4, orderCount: 500 }
1176
356
 
1177
- Fetch raw records using the BigQuery engine — useful for large-scale filtered exports.
1178
-
1179
- ```typescript
1180
- const records = await analytics.records({
1181
- filters: [
1182
- { field: 'status', op: '==', value: 'refunded' },
1183
- { field: 'amount', op: '>', value: 100 },
1184
- ],
1185
- selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
1186
- orderBy: 'amount',
1187
- order: 'desc',
1188
- limit: 50,
1189
- offset: 0,
1190
- dateRange: {
1191
- start: new Date('2025-01-01').getTime(),
1192
- end: Date.now(),
1193
- },
1194
- });
1195
- ```
1196
-
1197
- ### Cross-Bucket Comparison
1198
-
1199
- Compare the same metric across multiple buckets in one query.
1200
-
1201
- ```typescript
1202
- const comparison = await analytics.crossBucket({
1203
- bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
357
+ // Compare a metric across buckets
358
+ const compare = await analytics.crossBucket({
359
+ bucketKeys: ['orders-2024', 'orders-2025'],
1204
360
  field: 'amount',
1205
361
  aggregation: 'sum',
1206
362
  });
1207
- // [
1208
- // { bucket: 'orders-us', value: 120000 },
1209
- // { bucket: 'orders-eu', value: 45000 },
1210
- // { bucket: 'orders-apac', value: 33000 },
1211
- // ]
1212
- ```
1213
-
1214
- > ⚠️ Your Bucket Security Key must have read access to **every** bucket listed in `bucketKeys`.
1215
-
1216
- ### Storage Stats
1217
363
 
1218
- ```typescript
1219
- const stats = await analytics.storageStats();
1220
- // {
1221
- // totalRecords: 48210,
1222
- // totalBytes: 921600000,
1223
- // avgBytes: 19112,
1224
- // minBytes: 128,
1225
- // maxBytes: 5242880,
1226
- // }
1227
- ```
1228
-
1229
- ### Raw Query
1230
-
1231
- Use the `query()` method when none of the typed helpers cover your use case.
1232
-
1233
- ```typescript
1234
- import type { AnalyticsQuery } from 'hydrousdb';
1235
-
1236
- const result = await analytics.query<{ count: number }>({
1237
- queryType: 'count',
1238
- dateRange: { start: Date.now() - 86400000, end: Date.now() },
364
+ // All queries accept an optional date range
365
+ const { count } = await analytics.count({
366
+ dateRange: { start: Date.now() - 7 * 24 * 60 * 60 * 1000, end: Date.now() },
1239
367
  });
1240
-
1241
- console.log(result.queryType); // 'count'
1242
- console.log(result.data); // { count: 142 }
1243
- ```
1244
-
1245
- ---
1246
-
1247
- ## TypeScript
1248
-
1249
- The SDK is written entirely in TypeScript and ships full type definitions. Use generics for end-to-end type safety.
1250
-
1251
- ```typescript
1252
- import { createClient } from 'hydrousdb';
1253
- import type {
1254
- HydrousConfig,
1255
- RecordResult,
1256
- QueryFilter,
1257
- QueryOptions,
1258
- QueryResult,
1259
- CreateRecordOptions,
1260
- BatchCreateOptions,
1261
- UploadResult,
1262
- UploadUrlResult,
1263
- ListResult,
1264
- FileMetadata,
1265
- SignedUrlResult,
1266
- BatchDownloadResult,
1267
- StorageStats,
1268
- DateRange,
1269
- AnalyticsQuery,
1270
- AnalyticsResult,
1271
- CountResult,
1272
- DistributionRow,
1273
- SumRow,
1274
- TimeSeriesRow,
1275
- FieldTimeSeriesRow,
1276
- TopNRow,
1277
- FieldStats,
1278
- MultiMetricResult,
1279
- StorageStatsResult,
1280
- CrossBucketRow,
1281
- UserRecord,
1282
- Session,
1283
- AuthResult,
1284
- ListUsersResult,
1285
- } from 'hydrousdb';
1286
-
1287
- // Define your domain models
1288
- interface Order {
1289
- customerId: string;
1290
- items: Array<{ productId: string; qty: number; price: number }>;
1291
- total: number;
1292
- status: 'pending' | 'processing' | 'completed' | 'refunded';
1293
- country: string;
1294
- }
1295
-
1296
- interface Customer {
1297
- name: string;
1298
- email: string;
1299
- tier: 'free' | 'pro' | 'enterprise';
1300
- credits: number;
1301
- }
1302
-
1303
- const db = createClient({
1304
- authKey: process.env.HYDROUS_AUTH_KEY!,
1305
- bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
1306
- storageKeys: { main: process.env.HYDROUS_STORAGE_MAIN! },
1307
- });
1308
-
1309
- // Fully typed — autocomplete, compile-time safety throughout
1310
- const orders = db.records<Order>('orders');
1311
- const customers = db.records<Customer>('customers');
1312
-
1313
- const order = await orders.create(
1314
- { customerId: 'cust_abc', items: [{ productId: 'prod_1', qty: 2, price: 29.99 }], total: 59.98, status: 'pending', country: 'US' },
1315
- { queryableFields: ['status', 'country', 'customerId'] },
1316
- );
1317
-
1318
- // TypeScript catches mistakes at compile time:
1319
- // order.nonExistentField ← TS error ✓
1320
- // orders.create({ bad: 1 }) ← TS error ✓
1321
368
  ```
1322
369
 
1323
370
  ---
1324
371
 
1325
372
  ## Error Handling
1326
373
 
1327
- All SDK errors extend `HydrousError`. Specific sub-classes let you handle different failure modes precisely.
1328
-
1329
- ```typescript
1330
- import {
1331
- HydrousError,
1332
- AuthError,
1333
- RecordError,
1334
- StorageError,
1335
- AnalyticsError,
1336
- ValidationError,
1337
- NetworkError,
1338
- } from 'hydrousdb';
374
+ ```ts
375
+ import { HydrousError, NetworkError } from 'hydrousdb';
1339
376
 
1340
377
  try {
1341
- const record = await db.records('orders').get('bad-id');
378
+ await db.auth().login({ email: 'x@x.com', password: 'wrong' });
1342
379
  } catch (err) {
380
+ if (err instanceof HydrousError) {
381
+ console.log(err.code); // e.g. 'INVALID_CREDENTIALS'
382
+ console.log(err.status); // e.g. 401
383
+ console.log(err.message); // human-readable
384
+ }
1343
385
  if (err instanceof NetworkError) {
1344
- // No internet / server unreachable
1345
- console.error('Cannot reach HydrousDB:', err.message);
1346
-
1347
- } else if (err instanceof AuthError) {
1348
- // Bad key, expired session, insufficient permissions
1349
- console.error(`Auth failed [${err.code}]:`, err.message);
1350
- // err.code: INVALID_CREDENTIALS | ACCOUNT_LOCKED | INVALID_SESSION | FORBIDDEN | …
1351
-
1352
- } else if (err instanceof ValidationError) {
1353
- // Invalid input — check details array
1354
- console.error('Validation failed:', err.details?.join(', '));
1355
-
1356
- } else if (err instanceof RecordError) {
1357
- // Record-specific API error
1358
- console.error(`Record error [${err.code}]:`, err.message);
1359
-
1360
- } else if (err instanceof StorageError) {
1361
- // Storage-specific error
1362
- console.error(`Storage error [${err.code}]:`, err.message);
1363
-
1364
- } else if (err instanceof HydrousError) {
1365
- // Any other API error
1366
- console.error(`API error [${err.code}] ${err.status}:`, err.message);
1367
- console.error('Request ID:', err.requestId); // include in support tickets
386
+ console.log('No internet or server unreachable');
1368
387
  }
1369
388
  }
1370
389
  ```
1371
390
 
1372
- **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
-
1382
- **Common error codes:**
1383
-
1384
- | Code | Status | Meaning |
1385
- |---|---|---|
1386
- | `RECORD_NOT_FOUND` | 404 | The requested record ID does not exist |
1387
- | `INVALID_CREDENTIALS` | 401 | Wrong email or password |
1388
- | `ACCOUNT_LOCKED` | 403 | Account is temporarily locked |
1389
- | `INVALID_SESSION` | 401 | Session expired or revoked — re-authenticate |
1390
- | `MISSING_API_KEY` | 401 | Key not provided in headers |
1391
- | `INVALID_SECURITY_KEY` | 401 | Key is wrong or has been revoked |
1392
- | `FORBIDDEN` | 403 | Insufficient permissions for this operation |
1393
- | `FILE_EXISTS` | 409 | File already exists — use `overwrite: true` |
1394
- | `SYSTEM_BUCKET_FORBIDDEN` | 403 | Cannot query system buckets via analytics |
1395
- | `CROSS_BUCKET_FORBIDDEN` | 403 | Key lacks read access to one of the requested buckets |
1396
- | `VALIDATION_ERROR` | 400 | Invalid input — check `err.details` |
1397
- | `NETWORK_ERROR` | — | Failed to reach the API |
391
+ | Error class | When it's thrown |
392
+ |---|---|
393
+ | `HydrousError` | Base class all SDK errors extend this |
394
+ | `AuthError` | Login failures, invalid sessions, permission denied |
395
+ | `RecordError` | Record not found, validation failures |
396
+ | `StorageError` | Upload/download failures, file not found |
397
+ | `AnalyticsError` | Invalid query, bucket not found |
398
+ | `NetworkError` | No network, request timed out |
399
+ | `ValidationError` | Bad input caught client-side before the request is sent |
1398
400
 
1399
401
  ---
1400
402
 
1401
- ## Security Best Practices
403
+ ## TypeScript
1402
404
 
1403
- **1. Never hard-code keys use environment variables.**
405
+ The SDK is written in TypeScript. Type your records for full autocomplete:
1404
406
 
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
- ```
407
+ ```ts
408
+ interface Order {
409
+ customerId: string;
410
+ amount: number;
411
+ status: 'pending' | 'paid' | 'refunded';
412
+ items: { sku: string; qty: number }[];
413
+ }
1411
414
 
1412
- ```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! },
415
+ const orders = db.records<Order>('orders');
416
+
417
+ const order = await orders.create({
418
+ customerId: 'cust_123',
419
+ amount: 49.99,
420
+ status: 'pending',
421
+ items: [{ sku: 'SHOE-42', qty: 1 }],
1417
422
  });
423
+ // order.status → 'pending' | 'paid' | 'refunded' ✓
1418
424
  ```
1419
425
 
1420
- **2. Never expose keys to browsers.** For browser-side apps, route requests through your own backend, or use per-user sessions from `auth.login()`.
1421
-
1422
- **3. Keys travel in headers — never in URLs.** The SDK enforces this automatically. Keys never appear in server access logs, GCP audit trails, or browser history.
1423
-
1424
- **4. Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
1425
-
1426
- **5. Use scoped storage** (`db.storage('key').scope('prefix/')`) to isolate access per user or feature, reducing the blast radius of any misconfiguration.
1427
-
1428
- **6. Keep files private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for time-limited external sharing rather than making files permanently public.
1429
-
1430
426
  ---
1431
427
 
1432
- ## Full API Reference
1433
-
1434
- ### `createClient(config)` → `HydrousClient`
428
+ ## Security
1435
429
 
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) |
430
+ - **Never commit API keys.** Use environment variables (`process.env.…`).
431
+ - **Never expose keys in the browser.** For client-side apps, proxy requests through your own backend.
432
+ - **Keys travel in headers only** the SDK enforces this. They never appear in URLs, access logs, or browser history.
433
+ - **Files are private by default.** `isPublic` defaults to `false`. Use `getSignedUrl()` for temporary external sharing.
434
+ - **Use scoped storage** (`storage.scope('prefix/')`) to isolate files per user.
1442
435
 
1443
436
  ---
1444
437
 
1445
- ### `db.records<T>(bucketKey)` → `RecordsClient<T>`
438
+ ## Framework examples
1446
439
 
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`:**
1463
-
1464
- ```typescript
1465
- {
1466
- filters?: QueryFilter[]; // [{ field, op, value }, …]
1467
- fields?: string; // comma-separated field names
1468
- orderBy?: string;
1469
- order?: 'asc' | 'desc';
1470
- limit?: number; // default 100, max 1000
1471
- offset?: number;
1472
- startAfter?: string; // cursor for next page
1473
- startAt?: string;
1474
- endAt?: string;
1475
- dateRange?: DateRange; // { start?: number, end?: number }
440
+ **Next.js (App Router)**
441
+ ```ts
442
+ // lib/db.ts
443
+ import { createClient } from 'hydrousdb';
444
+ export const db = createClient({ });
445
+
446
+ // app/api/posts/route.ts
447
+ import { db } from '@/lib/db';
448
+ export async function GET() {
449
+ const { records } = await db.records('posts').query({ limit: 10 });
450
+ return Response.json(records);
1476
451
  }
1477
452
  ```
1478
453
 
1479
- ---
454
+ **React (client-side via your own API)**
455
+ ```ts
456
+ // Always go through your backend — never put keys in React components
457
+ const res = await fetch('/api/posts');
458
+ const data = await res.json();
459
+ ```
1480
460
 
1481
- ### `db.auth(bucketKey)` → `AuthClient`
461
+ **Vue / Nuxt**
462
+ ```ts
463
+ // composables/useDb.ts
464
+ import { createClient } from 'hydrousdb';
465
+ export const db = createClient({ … });
466
+ ```
1482
467
 
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. |
468
+ **React Native**
469
+ ```ts
470
+ import { createClient } from 'hydrousdb';
471
+ // Works out of the box uses the global fetch available in React Native
472
+ const db = createClient({ });
473
+ ```
1503
474
 
1504
475
  ---
1505
476
 
1506
- ### `db.storage(keyName)` → `StorageManager`
477
+ ## API Reference
1507
478
 
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. |
479
+ ### `createClient(config)`
1531
480
 
1532
- ---
481
+ | Field | Type | Description |
482
+ |---|---|---|
483
+ | `authKey` | `string` | `hk_auth_…` — for auth routes |
484
+ | `bucketSecurityKey` | `string` | `hk_bucket_…` — for records & analytics |
485
+ | `storageKeys` | `{ [name]: string }` | One or more `ssk_…` keys for storage |
486
+ | `baseUrl` | `string` | Override the endpoint (optional) |
1533
487
 
1534
- ### `db.analytics(bucketKey)` `AnalyticsClient`
488
+ ### `db.records(bucket)` key methods
1535
489
 
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. |
490
+ | Method | What it does |
491
+ |---|---|
492
+ | `create(data, opts?)` | Create a record. Use `opts.queryableFields` to enable filtering. |
493
+ | `get(id)` | Fetch by ID. |
494
+ | `patch(id, data)` | Partial update only the fields you pass are changed. |
495
+ | `delete(id)` | Permanent delete. |
496
+ | `query(opts?)` | Filter, sort, paginate. |
497
+ | `batchCreate(items, opts?)` | Up to 500 at once. |
498
+ | `batchUpdate(updates)` | Up to 500 at once. |
499
+ | `batchDelete(ids)` | Up to 500 at once. |
500
+ | `getHistory(id)` | Full version list. |
501
+ | `getVersion(id, generation)` | Read a past version. |
502
+ | `exists(id)` | Lightweight existence check (HEAD request). |
503
+
504
+ ### `db.auth()` — key methods
505
+
506
+ | Method | What it does |
507
+ |---|---|
508
+ | `signup(opts)` | Register + create session. |
509
+ | `login(opts)` | Authenticate + create session. |
510
+ | `logout({ sessionId })` | Revoke session(s). |
511
+ | `validateSession(sessionId)` | Check if a session is active. |
512
+ | `refreshSession(refreshToken)` | Get a new session from a refresh token. |
513
+ | `getUser(userId)` | Fetch a user by ID. |
514
+ | `updateUser(opts)` | Update profile fields. |
515
+ | `changePassword(opts)` | Authenticated password change. |
516
+ | `requestPasswordReset(email)` | Trigger reset email. |
517
+ | `confirmPasswordReset(token, newPw)` | Apply new password. |
518
+ | `listUsers(opts)` | Paginated user list (admin). |
519
+ | `lockAccount(opts)` / `unlockAccount(…)` | Admin account controls. |
520
+ | `hardDeleteUser(sessionId, userId)` | Permanent delete (admin). |
521
+
522
+ ### `db.storage(keyName)` — key methods
523
+
524
+ | Method | What it does |
525
+ |---|---|
526
+ | `upload(data, path, opts?)` | Server-buffered upload (up to 500 MB). |
527
+ | `uploadRaw(data, path, opts?)` | Upload a JS object or string as a file. |
528
+ | `getUploadUrl(opts)` | Step 1 of direct-to-GCS upload (for progress tracking). |
529
+ | `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2 — upload directly to GCS. |
530
+ | `confirmUpload(opts)` | Step 3 — register metadata. |
531
+ | `download(path)` | Download a private file as ArrayBuffer. |
532
+ | `list(opts?)` | List files and folders. |
533
+ | `getMetadata(path)` | File size, MIME, visibility, URLs. |
534
+ | `getSignedUrl(path, expiresIn?)` | Time-limited share link. |
535
+ | `setVisibility(path, isPublic)` | Toggle public/private. |
536
+ | `move(from, to)` / `copy(from, to)` | Move or copy files. |
537
+ | `deleteFile(path)` / `deleteFolder(path)` | Delete files or folders. |
538
+ | `scope(prefix)` | Get a path-prefixed sub-client. |
539
+
540
+ ### `db.analytics(bucket)` — key methods
541
+
542
+ | Method | What it does |
543
+ |---|---|
544
+ | `count(opts?)` | Total record count. |
545
+ | `distribution(opts)` | Per-value counts for a field. |
546
+ | `sum(opts)` | Sum a numeric field, optional `groupBy`. |
547
+ | `timeSeries(opts?)` | Record counts over time. |
548
+ | `fieldTimeSeries(opts)` | Field aggregation over time. |
549
+ | `topN(opts)` | Most frequent field values. |
550
+ | `stats(opts)` | min / max / avg / sum / stddev. |
551
+ | `multiMetric(opts)` | Multiple aggregations in one request. |
552
+ | `crossBucket(opts)` | Compare a metric across buckets. |
553
+ | `records(opts?)` | Filtered records via BigQuery. |
1550
554
 
1551
555
  ---
1552
556
 
@@ -1555,24 +559,17 @@ const db = createClient({
1555
559
  ```bash
1556
560
  git clone https://github.com/hydrousdb/hydrousdb-js.git
1557
561
  cd hydrousdb-js
1558
-
1559
- npm 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
562
+ npm install
563
+ npm test # run tests
564
+ npm run build # compile
1564
565
  ```
1565
566
 
1566
- ---
1567
-
1568
567
  ## License
1569
568
 
1570
- MIT — see [LICENSE](./LICENSE) for details.
569
+ MIT — [LICENSE](./LICENSE)
1571
570
 
1572
571
  ---
1573
572
 
1574
573
  <p align="center">
1575
- Built with ❤️ by the <a href="https://hydrousdb.com">HydrousDB</a> team.<br>
1576
- Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
1577
- <a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
574
+ Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> · <a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
1578
575
  </p>