hydrousdb 3.0.2 → 3.5.0

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