hydrousdb 2.0.1 → 3.0.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,730 +1,1126 @@
1
- # Hydrous SDK
1
+ # HydrousDB JavaScript / TypeScript SDK
2
2
 
3
- Official JavaScript / TypeScript SDK for the **Hydrous** platform.
4
- One packageauth, records, analytics, and storage.
3
+ <p align="center">
4
+ <strong>The official SDK for <a href="https://hydrousdb.com">HydrousDB</a> records, auth, file storage, and analytics in one package.</strong>
5
+ </p>
5
6
 
6
- ```bash
7
- npm install hydrous
8
- ```
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/v/hydrousdb.svg" alt="npm version"></a>
9
+ <a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/dm/hydrousdb.svg" alt="npm downloads"></a>
10
+ <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>
11
+ <a href="https://hydrousdb.com"><img src="https://img.shields.io/badge/docs-hydrousdb.com-blue" alt="Documentation"></a>
12
+ </p>
9
13
 
10
14
  ---
11
15
 
12
16
  ## Table of Contents
13
17
 
14
- 1. [Quick Start](#1-quick-start)
15
- 2. [Configuration](#2-configuration)
16
- 3. [Auth](#3-auth)
17
- - [Sign Up](#sign-up)
18
- - [Sign In](#sign-in)
19
- - [Sign Out](#sign-out)
20
- - [Get Current User](#get-current-user)
21
- 4. [Records](#4-records)
22
- - [Insert](#insert)
23
- - [Select / Query](#select--query)
24
- - [Get by ID](#get-by-id)
25
- - [Update](#update)
26
- - [Delete](#delete)
27
- - [Query Helpers](#query-helpers)
28
- 5. [Analytics](#5-analytics)
29
- - [Track an Event](#track-an-event)
30
- - [Batch Track](#batch-track)
31
- - [Query Events](#query-events)
32
- 6. [Storage](#6-storage)
33
- - [How Bucket Keys Work](#how-bucket-keys-work)
34
- - [Upload a File](#upload-a-file)
35
- - [Upload Raw Text / JSON](#upload-raw-text--json)
36
- - [Track Upload Progress](#track-upload-progress)
37
- - [Batch Upload](#batch-upload)
38
- - [Download a File](#download-a-file)
39
- - [Batch Download](#batch-download)
40
- - [List Files & Folders](#list-files--folders)
41
- - [File Metadata](#file-metadata)
42
- - [Delete a File](#delete-a-file)
43
- - [Delete a Folder](#delete-a-folder)
44
- - [Create a Folder](#create-a-folder)
45
- - [Move a File](#move-a-file)
46
- - [Copy a File](#copy-a-file)
47
- - [Signed URLs](#signed-urls)
48
- - [Bucket Stats](#bucket-stats)
49
- 7. [Error Handling](#7-error-handling)
50
- 8. [TypeScript Types Reference](#8-typescript-types-reference)
18
+ - [What is HydrousDB?](#what-is-hydrousdb)
19
+ - [Quick Start (5 minutes)](#quick-start-5-minutes)
20
+ - [Step 1 — Create your account](#step-1--create-your-account)
21
+ - [Step 2 — Create your first bucket](#step-2--create-your-first-bucket)
22
+ - [Step 3 — Grab your Security Key](#step-3--grab-your-security-key)
23
+ - [Step 4 — Install the SDK](#step-4--install-the-sdk)
24
+ - [Step 5 — Your first record](#step-5--your-first-record)
25
+ - [Records](#records)
26
+ - [Create](#create-a-record)
27
+ - [Read](#read-a-record)
28
+ - [Update](#update-a-record)
29
+ - [Delete](#delete-a-record)
30
+ - [Query](#query-records)
31
+ - [Batch Operations](#batch-operations)
32
+ - [Version History](#version-history)
33
+ - [Authentication](#authentication)
34
+ - [Sign Up](#sign-up-users)
35
+ - [Log In / Log Out](#log-in--log-out)
36
+ - [Session Management](#session-management)
37
+ - [Password Reset](#password-reset-flow)
38
+ - [Email Verification](#email-verification)
39
+ - [Admin Operations](#admin-operations)
40
+ - [File Storage](#file-storage)
41
+ - [Simple Upload](#simple-upload)
42
+ - [Large File Upload (with progress)](#large-file-upload-with-progress)
43
+ - [Download](#download-files)
44
+ - [List Files](#list-files)
45
+ - [Scoped Storage](#scoped-storage)
46
+ - [Share & Visibility](#share--visibility)
47
+ - [File Operations](#file-operations)
48
+ - [Analytics](#analytics)
49
+ - [Count](#count)
50
+ - [Distribution](#distribution)
51
+ - [Time Series](#time-series)
52
+ - [Top N](#top-n)
53
+ - [Field Stats](#field-stats)
54
+ - [Multi-Metric Dashboard](#multi-metric-dashboard)
55
+ - [Cross-Bucket Comparison](#cross-bucket-comparison)
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)
51
62
 
52
63
  ---
53
64
 
54
- ## 1. Quick Start
65
+ ## What is HydrousDB?
55
66
 
56
- ```ts
57
- import { createClient } from 'hydrous';
67
+ HydrousDB is a **backend-as-a-service** platform that gives your app a fully managed backend in minutes. Instead of spinning up servers, databases, and storage buckets yourself, you call an API.
58
68
 
59
- const hydrous = createClient({
60
- url: 'https://api.yourapp.hydrous.app',
61
- apiKey: 'hk_live_…',
62
- });
69
+ | Feature | What it does |
70
+ |---|---|
71
+ | **Records** | Schemaless JSON document store. Create, read, update, delete and query records in named *buckets*. |
72
+ | **Auth** | Full user authentication — signup, login, sessions, password reset, email verification, and admin controls. |
73
+ | **Storage** | File uploads and downloads backed by Google Cloud Storage. Public and private files, signed share URLs. |
74
+ | **Analytics** | BigQuery-powered aggregations — counts, distributions, time series, top-N, multi-metric dashboards, and cross-bucket comparisons. |
63
75
 
64
- // Upload a file
65
- const { data, error } = await hydrous.storage.upload(
66
- 'ssk_my_bucket_key', // bucket key always comes first
67
- file,
68
- {
69
- path: 'avatars/alice.jpg',
70
- onProgress: (p) => console.log(`${p.percent}%`),
71
- }
72
- );
73
- ```
76
+ Everything is organised around two concepts:
77
+
78
+ - **Security Key (`sk_...`)** your master credential. Authenticate all API calls. Keep it secret.
79
+ - **Bucket Key** — just the name of your bucket (e.g. `"blog-posts"`, `"app-users"`). Not a secret.
74
80
 
75
81
  ---
76
82
 
77
- ## 2. Configuration
83
+ ## Quick Start (5 minutes)
78
84
 
79
- ```ts
80
- import { HydrousClient } from 'hydrous';
85
+ ### Step 1 — Create your account
81
86
 
82
- const hydrous = new HydrousClient({
83
- url: 'https://api.yourapp.hydrous.app', // your project URL
84
- apiKey: 'hk_live_…', // your project API key
85
- timeout: 30_000, // optional — ms (default 30s)
86
- });
87
- ```
87
+ Go to **[https://hydrousdb.com](https://hydrousdb.com)** and sign up for a free account.
88
88
 
89
- | Option | Type | Required | Description |
90
- |-----------|----------|----------|--------------------------------------|
91
- | `url` | `string` | ✅ | Your Hydrous project base URL |
92
- | `apiKey` | `string` | ✅ | Your project API key |
93
- | `timeout` | `number` | ✗ | Request timeout in ms (default 30000) |
89
+ ### Step 2 Create your first bucket
94
90
 
95
- ---
91
+ 1. Log in to your dashboard at **[https://hydrousdb.com/dashboard](https://hydrousdb.com/dashboard)**.
92
+ 2. Click **"New Bucket"**.
93
+ 3. Give it a name — use lowercase letters, numbers, hyphens, or underscores (e.g. `my-first-bucket`).
94
+ 4. Click **"Create"**.
96
95
 
97
- ## 3. Auth
96
+ > 💡 **What is a bucket?** A bucket is a named collection of JSON records — similar to a table in SQL or a collection in MongoDB.
98
97
 
99
- ### Sign Up
98
+ ### Step 3 — Grab your Security Key
100
99
 
101
- ```ts
102
- const { data, error } = await hydrous.auth.signUp({
103
- email: 'user@example.com',
104
- password: 'supersecret',
105
- metadata: { plan: 'pro' }, // optional
106
- });
100
+ 1. In the dashboard, go to **Settings → API Keys**.
101
+ 2. Click **"Generate Security Key"**.
102
+ 3. Copy the key — it looks like `sk_live_xxxxxxxxxxxxxxxxxxxx`.
107
103
 
108
- if (data) {
109
- console.log('New user:', data.user.id);
110
- console.log('Access token:', data.accessToken);
111
- }
104
+ > ⚠️ **Your Security Key is your most important credential.** Treat it like a password. Never commit it to Git. Use environment variables.
105
+
106
+ ### Step 4 — Install the SDK
107
+
108
+ ```bash
109
+ npm install hydrousdb
110
+ # or
111
+ yarn add hydrousdb
112
+ # or
113
+ pnpm add hydrousdb
112
114
  ```
113
115
 
114
- ### Sign In
116
+ **Requirements:** Node.js 18+ (uses the native `fetch` API).
115
117
 
116
- ```ts
117
- const { data, error } = await hydrous.auth.signIn({
118
- email: 'user@example.com',
119
- password: 'supersecret',
120
- });
118
+ ### Step 5 — Your first record
121
119
 
122
- if (data) {
123
- console.log('Welcome back,', data.user.email);
124
- }
125
- ```
120
+ ```typescript
121
+ import { createClient } from 'hydrousdb';
126
122
 
127
- ### Sign Out
123
+ // Create the client once — reuse it everywhere
124
+ const db = createClient({
125
+ securityKey: process.env.HYDROUS_SECURITY_KEY!, // from Step 3
126
+ });
128
127
 
129
- ```ts
130
- await hydrous.auth.signOut();
131
- ```
128
+ // Write a record to your bucket
129
+ const post = await db.records('my-first-bucket').create({
130
+ title: 'Hello, HydrousDB!',
131
+ body: 'My first record.',
132
+ published: false,
133
+ });
132
134
 
133
- ### Get Current User
135
+ console.log(post.id); // "rec_a1b2c3d4"
136
+ console.log(post.createdAt); // 1717200000000
134
137
 
135
- ```ts
136
- const { data: user } = await hydrous.auth.getUser();
137
- console.log(user?.email);
138
- ```
138
+ // Read it back
139
+ const fetched = await db.records('my-first-bucket').get(post.id);
140
+ console.log(fetched.title); // "Hello, HydrousDB!"
139
141
 
140
- ### Refresh Session
142
+ // Update it
143
+ const updated = await db.records('my-first-bucket').patch(post.id, { published: true });
141
144
 
142
- ```ts
143
- const { data: session } = await hydrous.auth.refreshSession();
145
+ // Delete it
146
+ await db.records('my-first-bucket').delete(post.id);
144
147
  ```
145
148
 
149
+ 🎉 **That's it!** You're live with zero configuration beyond your Security Key.
150
+
146
151
  ---
147
152
 
148
- ## 4. Records
153
+ ## Records
154
+
155
+ Records are JSON objects stored in named buckets. Every record automatically gets:
156
+ - `id` — unique record identifier (e.g. `"rec_a1b2c3d4"`)
157
+ - `createdAt` — Unix timestamp in milliseconds
158
+ - `updatedAt` — Unix timestamp in milliseconds (updated on every write)
149
159
 
150
- ### Insert
160
+ ### Create a Record
151
161
 
152
- Single record:
162
+ ```typescript
163
+ const products = db.records('products');
153
164
 
154
- ```ts
155
- const { data, error } = await hydrous.records.insert('users', {
156
- name: 'Alice',
157
- email: 'alice@example.com',
158
- role: 'admin',
165
+ const product = await products.create({
166
+ name: 'Wireless Headphones',
167
+ price: 79.99,
168
+ inStock: true,
169
+ tags: ['audio', 'wireless'],
159
170
  });
171
+
172
+ // product.id, product.createdAt, product.updatedAt are added automatically
160
173
  ```
161
174
 
162
- Bulk insert (array):
175
+ ### Read a Record
163
176
 
164
- ```ts
165
- const { data } = await hydrous.records.insert('products', [
166
- { name: 'Widget A', price: 9.99 },
167
- { name: 'Widget B', price: 14.99 },
168
- ]);
169
- console.log(`Inserted ${data.length} products`);
177
+ ```typescript
178
+ // Get by ID
179
+ const product = await products.get('rec_abc123');
180
+
181
+ // product is null-safe: throws HydrousError with code RECORD_NOT_FOUND if missing
170
182
  ```
171
183
 
172
- ### Select / Query
184
+ ### Update a Record
173
185
 
174
- ```ts
175
- const { data, count } = await hydrous.records.select('users', {
176
- where: { field: 'role', operator: 'eq', value: 'admin' },
177
- orderBy: { field: 'createdAt', direction: 'desc' },
178
- limit: 20,
179
- offset: 0,
180
- select: ['id', 'name', 'email'], // optional column projection
186
+ ```typescript
187
+ // Patch (merge) only the listed fields are changed
188
+ const updated = await products.patch('rec_abc123', {
189
+ price: 69.99,
190
+ inStock: false,
191
+ });
192
+
193
+ // Set (full replace) — the entire record is replaced
194
+ const replaced = await products.set('rec_abc123', {
195
+ name: 'Wireless Headphones v2',
196
+ price: 89.99,
197
+ inStock: true,
198
+ tags: ['audio', 'wireless', 'premium'],
181
199
  });
182
200
  ```
183
201
 
184
- Multiple filters:
202
+ ### Delete a Record
203
+
204
+ ```typescript
205
+ await products.delete('rec_abc123');
206
+ ```
185
207
 
186
- ```ts
187
- import { eq, gt, inArray } from 'hydrous';
208
+ ### Query Records
188
209
 
189
- const { data } = await hydrous.records.select('orders', {
190
- where: [
191
- eq('status', 'shipped'),
192
- gt('total', 100),
193
- inArray('tag', ['vip', 'priority']),
210
+ ```typescript
211
+ // Get all records (up to 100 by default)
212
+ const { records } = await products.query();
213
+
214
+ // With filters
215
+ const { records: affordableStock } = await products.query({
216
+ filters: [
217
+ { field: 'inStock', op: '==', value: true },
218
+ { field: 'price', op: '<', value: 100 },
194
219
  ],
195
220
  });
196
- ```
197
221
 
198
- ### Get by ID
222
+ // Sort and paginate
223
+ const { records, hasMore, nextCursor } = await products.query({
224
+ orderBy: 'price',
225
+ order: 'asc',
226
+ limit: 20,
227
+ });
199
228
 
200
- ```ts
201
- const { data: user } = await hydrous.records.get('users', 'user_abc123');
202
- ```
229
+ // Next page
230
+ if (hasMore) {
231
+ const page2 = await products.query({
232
+ orderBy: 'price',
233
+ order: 'asc',
234
+ limit: 20,
235
+ startAfter: nextCursor,
236
+ });
237
+ }
203
238
 
204
- ### Update
239
+ // Select only specific fields
240
+ const { records: lightRecords } = await products.query({
241
+ fields: 'name,price,inStock',
242
+ });
205
243
 
206
- ```ts
207
- const { data } = await hydrous.records.update('users', 'user_abc123', {
208
- name: 'Alice Smith',
244
+ // Filter by date range
245
+ const { records: recent } = await products.query({
246
+ dateRange: {
247
+ start: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
248
+ end: Date.now(),
249
+ },
209
250
  });
210
251
  ```
211
252
 
212
- ### Delete
253
+ **Available filter operators:**
254
+
255
+ | Operator | Meaning |
256
+ |---|---|
257
+ | `==` | Equal |
258
+ | `!=` | Not equal |
259
+ | `>` | Greater than |
260
+ | `<` | Less than |
261
+ | `>=` | Greater than or equal |
262
+ | `<=` | Less than or equal |
263
+ | `CONTAINS` | String contains (case-sensitive) |
264
+
265
+ ### Batch Operations
266
+
267
+ ```typescript
268
+ // Create multiple records at once
269
+ const created = await products.batchCreate([
270
+ { name: 'Item A', price: 10.00, inStock: true },
271
+ { name: 'Item B', price: 20.00, inStock: false },
272
+ { name: 'Item C', price: 30.00, inStock: true },
273
+ ]);
274
+ // → [{ id: 'rec_1', ... }, { id: 'rec_2', ... }, { id: 'rec_3', ... }]
275
+
276
+ // Count records
277
+ const total = await products.count();
278
+ const inStock = await products.count([{ field: 'inStock', op: '==', value: true }]);
213
279
 
214
- ```ts
215
- const { error } = await hydrous.records.delete('users', 'user_abc123');
280
+ // Delete multiple records
281
+ const { deleted, failed } = await products.batchDelete(['rec_1', 'rec_2', 'rec_3']);
216
282
  ```
217
283
 
218
- ### Query Helpers
284
+ ### Version History
285
+
286
+ Every write to a record creates a new version stored in GCS, so you can travel back in time.
219
287
 
220
- | Helper | SQL equivalent |
221
- |---------------------|------------------------|
222
- | `eq(field, val)` | `field = val` |
223
- | `neq(field, val)` | `field != val` |
224
- | `gt(field, val)` | `field > val` |
225
- | `lt(field, val)` | `field < val` |
226
- | `inArray(field, [])` | `field IN (…)` |
288
+ ```typescript
289
+ // Get the full version history of a record
290
+ const history = await products.getHistory('rec_abc123');
291
+ // history[0] is the latest version, history[1] is one write before, etc.
227
292
 
228
- ```ts
229
- import { eq, gt, inArray } from 'hydrous';
293
+ // Restore to a specific version
294
+ const restored = await products.restoreVersion('rec_abc123', history[2]!.version);
230
295
  ```
231
296
 
232
297
  ---
233
298
 
234
- ## 5. Analytics
299
+ ## Authentication
300
+
301
+ HydrousDB has a built-in user auth system. Your users live in a bucket you create
302
+ (e.g. `"app-users"`). You get sessions, refresh tokens, password reset, email
303
+ verification, and admin controls out of the box.
235
304
 
236
- ### Track an Event
305
+ ```typescript
306
+ const auth = db.auth('app-users');
307
+ ```
308
+
309
+ ### Sign Up Users
237
310
 
238
- ```ts
239
- await hydrous.analytics.track({
240
- event: 'page_view',
241
- properties: { page: '/home', referrer: 'google.com' },
242
- userId: 'user_abc123',
243
- sessionId: 'sess_xyz',
311
+ ```typescript
312
+ const { user, session } = await auth.signup({
313
+ email: 'alice@example.com',
314
+ password: 'hunter2', // min 8 characters, validated server-side
315
+ fullName: 'Alice Wonderland',
316
+ // Any extra fields are stored on the user record:
317
+ plan: 'pro',
318
+ referral: 'friend123',
244
319
  });
320
+
321
+ // user.id → "usr_xxxxxxxxxx"
322
+ // session.sessionId → persist this in your app
323
+ // session.refreshToken → persist this for long-lived sessions
245
324
  ```
246
325
 
247
- ### Batch Track
326
+ ### Log In / Log Out
248
327
 
249
- More efficient than calling `track()` in a loop:
328
+ ```typescript
329
+ // Log in
330
+ const { user, session } = await auth.login({
331
+ email: 'alice@example.com',
332
+ password: 'hunter2',
333
+ });
250
334
 
251
- ```ts
252
- await hydrous.analytics.trackBatch([
253
- { event: 'signup', userId: 'u1' },
254
- { event: 'onboarded', userId: 'u1', properties: { step: 'profile' } },
255
- ]);
335
+ // Log out (invalidates the session server-side)
336
+ await auth.logout({ sessionId: session.sessionId });
337
+ ```
338
+
339
+ ### Session Management
340
+
341
+ Sessions expire after **24 hours**. Use the refresh token to get a new session (refresh tokens last **30 days**).
342
+
343
+ ```typescript
344
+ // Refresh the session before it expires
345
+ const newSession = await auth.refreshSession({
346
+ refreshToken: session.refreshToken,
347
+ });
348
+ // Store newSession.sessionId and newSession.refreshToken
349
+
350
+ // Get the current user
351
+ const user = await auth.getUser({ userId: session.userId });
256
352
  ```
257
353
 
258
- ### Query Events
354
+ ### Update User Profile
355
+
356
+ ```typescript
357
+ const updated = await auth.updateUser({
358
+ sessionId: session.sessionId,
359
+ userId: user.id,
360
+ data: {
361
+ fullName: 'Alice Smith',
362
+ plan: 'enterprise',
363
+ avatar: 'https://example.com/avatar.jpg',
364
+ },
365
+ });
366
+ ```
259
367
 
260
- ```ts
261
- const { data, count } = await hydrous.analytics.query({
262
- event: 'page_view',
263
- from: '2024-01-01',
264
- to: '2024-01-31',
265
- limit: 500,
266
- groupBy: 'properties.page',
368
+ ### Password Reset Flow
369
+
370
+ ```typescript
371
+ // 1. User requests a reset (always returns success — prevents email enumeration)
372
+ await auth.requestPasswordReset({ email: 'alice@example.com' });
373
+
374
+ // 2. User receives an email with a reset token (your app handles the email sending)
375
+
376
+ // 3. User submits the new password
377
+ await auth.confirmPasswordReset({
378
+ resetToken: 'tok_from_email',
379
+ newPassword: 'correcthorsebatterystaple',
267
380
  });
381
+ // All existing sessions for this user are automatically revoked
268
382
  ```
269
383
 
270
- ---
384
+ ### Change Password (authenticated)
271
385
 
272
- ## 6. Storage
386
+ ```typescript
387
+ await auth.changePassword({
388
+ sessionId: session.sessionId,
389
+ userId: user.id,
390
+ currentPassword: 'hunter2',
391
+ newPassword: 'correcthorsebatterystaple',
392
+ });
393
+ ```
273
394
 
274
- The storage module handles all file operations. Every method takes a **bucket key** as its **first argument** — a string that begins with `ssk_`.
395
+ ### Email Verification
275
396
 
276
- ### How Bucket Keys Work
397
+ ```typescript
398
+ // 1. Send verification email
399
+ await auth.requestEmailVerification({ userId: user.id });
277
400
 
278
- A bucket key (`ssk_…`) is a scoped credential that grants specific permissions (read / write / delete) to a bucket. You create them in the Hydrous dashboard.
401
+ // 2. User clicks link in email, your app extracts the token
279
402
 
403
+ // 3. Confirm the token
404
+ await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
280
405
  ```
281
- hydrous.storage.<method>(
282
- 'ssk_your_bucket_key', // first, always a string
283
- ...args
284
- )
406
+
407
+ ### Admin Operations
408
+
409
+ Admin operations require a valid session from a user with `role: 'admin'`.
410
+
411
+ ```typescript
412
+ // List all users
413
+ const { users, total } = await auth.listUsers({
414
+ sessionId: adminSession.sessionId,
415
+ limit: 50,
416
+ offset: 0,
417
+ });
418
+
419
+ // Lock an account (prevents login)
420
+ await auth.lockAccount({
421
+ sessionId: adminSession.sessionId,
422
+ userId: 'usr_abc123',
423
+ duration: 60 * 60 * 1000, // lock for 1 hour (default: 15 minutes)
424
+ });
425
+
426
+ // Unlock an account
427
+ await auth.unlockAccount({
428
+ sessionId: adminSession.sessionId,
429
+ userId: 'usr_abc123',
430
+ });
431
+
432
+ // Soft-delete a user (marks as deleted, keeps data)
433
+ await auth.deleteUser({
434
+ sessionId: adminSession.sessionId,
435
+ userId: 'usr_abc123',
436
+ });
437
+
438
+ // Hard-delete a user (permanent — irreversible)
439
+ await auth.hardDeleteUser({
440
+ sessionId: adminSession.sessionId,
441
+ userId: 'usr_abc123',
442
+ });
443
+
444
+ // Bulk delete multiple users
445
+ const { deleted, failed } = await auth.bulkDeleteUsers({
446
+ sessionId: adminSession.sessionId,
447
+ userIds: ['usr_a', 'usr_b', 'usr_c'],
448
+ });
285
449
  ```
286
450
 
287
451
  ---
288
452
 
289
- ### Upload a File
290
-
291
- ```ts
292
- const { data, error } = await hydrous.storage.upload(
293
- 'ssk_my_bucket_key',
294
- file, // File | Blob | Uint8Array | ArrayBuffer
295
- {
296
- path: 'avatars/alice.jpg', // destination path in your bucket
297
- overwrite: true, // replace if exists (default: false)
298
- onProgress: (progress) => {
299
- console.log(progress.stage, progress.percent + '%');
300
- },
301
- }
302
- );
453
+ ## File Storage
303
454
 
304
- if (data) {
305
- console.log('Stored at:', data.path);
306
- console.log('Space saved:', data.spaceSaved, 'bytes');
307
- }
455
+ HydrousDB Storage is backed by Google Cloud Storage. Your files live at:
308
456
  ```
457
+ hydrous-storage/{your-owner-id}/{your-path}
458
+ ```
459
+ You never see or specify the owner prefix — the SDK handles it transparently.
309
460
 
310
- ---
461
+ ### Simple Upload
311
462
 
312
- ### Upload Raw Text / JSON
463
+ For files up to **500 MB** when you don't need upload progress:
313
464
 
314
- No `File` object needed — pass any string directly.
465
+ ```typescript
466
+ // Browser: upload from a file input
467
+ const fileInput = document.querySelector('input[type="file"]');
468
+ const file = fileInput.files[0];
315
469
 
316
- ```ts
317
- // Upload a plain text file
318
- await hydrous.storage.uploadText(
319
- 'ssk_my_bucket_key',
320
- 'reports/summary.txt',
321
- 'Monthly report content…',
322
- { mimeType: 'text/plain' }
323
- );
470
+ const result = await db.storage.upload(file, `uploads/${file.name}`, {
471
+ isPublic: true, // publicly accessible without auth
472
+ overwrite: false, // throw if the file already exists
473
+ });
474
+
475
+ console.log(result.publicUrl); // CDN URL — usable anywhere
476
+ console.log(result.downloadUrl); // null (it's public)
477
+ console.log(result.size); // bytes
478
+ console.log(result.mimeType); // auto-detected from extension
479
+
480
+ // Node.js: upload from a Buffer
481
+ import { readFileSync } from 'fs';
482
+ const buffer = readFileSync('./report.pdf');
483
+ const result = await db.storage.upload(buffer, 'reports/q3.pdf');
484
+ console.log(result.downloadUrl); // requires X-Storage-Key to access
485
+ ```
486
+
487
+ ### Upload Raw JSON or Text
324
488
 
325
- // Upload JSON
326
- await hydrous.storage.uploadText(
327
- 'ssk_my_bucket_key',
328
- 'data/config.json',
329
- JSON.stringify({ theme: 'dark', lang: 'en' }),
330
- { mimeType: 'application/json' }
489
+ ```typescript
490
+ const result = await db.storage.uploadRaw(
491
+ { theme: 'dark', language: 'en' },
492
+ 'user-config/alice.json',
493
+ { isPublic: false },
331
494
  );
332
495
  ```
333
496
 
334
- ---
497
+ ### Large File Upload (with progress)
335
498
 
336
- ### Track Upload Progress
499
+ For files over 10 MB or when you need a progress bar. The file goes directly
500
+ to GCS — your server never buffers it.
337
501
 
338
- `onProgress` is called on every progress tick with a rich `UploadProgress` object.
502
+ ```typescript
503
+ // Step 1: Get a signed upload URL
504
+ const { uploadUrl, path } = await db.storage.getUploadUrl({
505
+ path: 'videos/product-demo.mp4',
506
+ mimeType: 'video/mp4',
507
+ size: file.size,
508
+ isPublic: true,
509
+ });
339
510
 
340
- ```ts
341
- await hydrous.storage.upload(
342
- 'ssk_my_bucket_key',
511
+ // Step 2: Upload directly to GCS with progress
512
+ await db.storage.uploadToSignedUrl(
513
+ uploadUrl,
343
514
  file,
344
- {
345
- onProgress: (p) => {
346
- // p.stage — 'pending' | 'compressing' | 'uploading' | 'done' | 'error'
347
- // p.percent 0–100 integer
348
- // p.bytesUploaded — bytes sent so far
349
- // p.totalBytes — total bytes to send
350
- // p.bytesPerSecond — current speed (null before first tick)
351
- // p.eta — estimated seconds remaining (null until speed is known)
352
- // p.index — file index (always 0 for single uploads)
353
- // p.total — total files (always 1 for single uploads)
354
-
355
- switch (p.stage) {
356
- case 'pending':
357
- console.log('Queued');
358
- break;
359
- case 'compressing':
360
- console.log('Compressing…');
361
- break;
362
- case 'uploading':
363
- console.log(`Uploading ${p.percent}% — ${formatSpeed(p.bytesPerSecond)} — ETA ${p.eta}s`);
364
- break;
365
- case 'done':
366
- console.log('✅ Done!', p.result);
367
- break;
368
- case 'error':
369
- console.error('❌ Error:', p.error);
370
- break;
371
- }
372
- },
373
- }
515
+ 'video/mp4',
516
+ (percent) => {
517
+ progressBar.style.width = `${percent}%`;
518
+ console.log(`${percent}% uploaded`);
519
+ },
374
520
  );
375
521
 
376
- function formatSpeed(bps: number | null): string {
377
- if (!bps) return '—';
378
- if (bps > 1_000_000) return `${(bps / 1_000_000).toFixed(1)} MB/s`;
379
- if (bps > 1_000) return `${(bps / 1_000).toFixed(0)} KB/s`;
380
- return `${bps} B/s`;
381
- }
382
- ```
383
-
384
- **Stage lifecycle:**
522
+ // Step 3: Confirm the upload (registers metadata)
523
+ const result = await db.storage.confirmUpload({
524
+ path: path,
525
+ mimeType: 'video/mp4',
526
+ isPublic: true,
527
+ });
385
528
 
386
- ```
387
- pending → compressing → uploading → done
388
- ↘ error
529
+ console.log(result.publicUrl); // ready to use
389
530
  ```
390
531
 
391
- > **Note:** In browsers, upload progress (bytes leaving the NIC) is tracked via
392
- > `XMLHttpRequest`. The final `done` stage fires only after the server confirms
393
- > the write to cloud storage — so `100%` means the file is truly saved.
532
+ ### Batch Upload
394
533
 
395
- ---
534
+ ```typescript
535
+ // Get signed URLs for multiple files
536
+ const { files } = await db.storage.getBatchUploadUrls([
537
+ { path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
538
+ { path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
539
+ ]);
396
540
 
397
- ### Batch Upload
541
+ // Upload each one
542
+ for (const f of files) {
543
+ await db.storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
544
+ }
398
545
 
399
- Upload many files in one request. Progress fires per-file so you can render
400
- individual progress bars.
401
-
402
- ```ts
403
- const inputFiles = Array.from(fileInput.files); // FileList → array
404
-
405
- const { data, error } = await hydrous.storage.batchUpload(
406
- 'ssk_my_bucket_key',
407
- inputFiles,
408
- {
409
- prefix: 'uploads/2024/', // prepended to each filename
410
- overwrite: false,
411
- concurrency: 5, // max parallel uploads on the server (1–10)
412
- onProgress: (p) => {
413
- // p.index identifies WHICH file this event is for (0-based)
414
- console.log(`File ${p.index} (${p.path}): ${p.stage} ${p.percent}%`);
415
- },
416
- }
546
+ // Confirm all at once
547
+ const results = await db.storage.batchConfirmUploads(
548
+ files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true }))
417
549
  );
550
+ ```
551
+
552
+ ### Download Files
553
+
554
+ ```typescript
555
+ // Private files require authentication — download as ArrayBuffer
556
+ const buffer = await db.storage.download('reports/q3.pdf');
557
+ const blob = new Blob([buffer], { type: 'application/pdf' });
418
558
 
419
- console.log('Succeeded:', data.succeeded.length);
420
- console.log('Failed:', data.failed.length);
559
+ // In a browser: trigger a file download
560
+ const url = URL.createObjectURL(blob);
561
+ const a = document.createElement('a');
562
+ a.href = url;
563
+ a.download = 'q3.pdf';
564
+ a.click();
565
+
566
+ // Public files: just use the publicUrl directly (no SDK needed)
567
+ // <img src={result.publicUrl} />
421
568
  ```
422
569
 
423
- Per-file paths override:
570
+ ### List Files
424
571
 
425
- ```ts
426
- await hydrous.storage.batchUpload('ssk_key', files, {
427
- paths: [
428
- 'documents/report-q1.pdf',
429
- 'documents/report-q2.pdf',
430
- ],
572
+ ```typescript
573
+ // List everything at the root
574
+ const { files, folders } = await db.storage.list();
575
+
576
+ // List a specific folder
577
+ const { files, folders, hasMore, nextCursor } = await db.storage.list({
578
+ prefix: 'gallery/',
579
+ limit: 50,
580
+ recursive: false,
431
581
  });
582
+
583
+ // Paginate
584
+ if (hasMore) {
585
+ const page2 = await db.storage.list({ prefix: 'gallery/', cursor: nextCursor });
586
+ }
432
587
  ```
433
588
 
434
- > All files are validated upfront before any uploads begin — if your quota
435
- > would be exceeded the entire batch is rejected cleanly with no partial writes.
589
+ Each file entry includes:
590
+ ```typescript
591
+ {
592
+ name: 'photo1.jpg',
593
+ path: 'gallery/photo1.jpg',
594
+ size: 204800,
595
+ mimeType: 'image/jpeg',
596
+ isPublic: true,
597
+ publicUrl: 'https://storage.googleapis.com/...',
598
+ downloadUrl: null,
599
+ updatedAt: '2025-06-01T12:00:00.000Z',
600
+ }
601
+ ```
436
602
 
437
- ---
603
+ ### Scoped Storage
438
604
 
439
- ### Download a File
605
+ Working within a specific folder? Use `.scope()` to avoid typing the prefix on every call.
440
606
 
441
- Returns the file as an `ArrayBuffer`.
607
+ ```typescript
608
+ // All operations in the "user-avatars/" folder
609
+ const avatars = db.storage.scope('user-avatars');
442
610
 
443
- ```ts
444
- const { data: buffer, error } = await hydrous.storage.download(
445
- 'ssk_my_bucket_key',
446
- 'avatars/alice.jpg'
447
- );
611
+ await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
612
+ // uploads to "user-avatars/{userId}.jpg"
448
613
 
449
- if (buffer) {
450
- // Browser: display image
451
- const blob = new Blob([buffer], { type: 'image/jpeg' });
452
- img.src = URL.createObjectURL(blob);
614
+ const { files } = await avatars.list();
615
+ // lists files under "user-avatars/"
453
616
 
454
- // Node: write to disk
455
- import { writeFileSync } from 'fs';
456
- writeFileSync('alice.jpg', Buffer.from(buffer));
457
- }
617
+ await avatars.deleteFile(`${userId}.jpg`);
618
+ // deletes "user-avatars/{userId}.jpg"
619
+
620
+ // Nest scopes
621
+ const thumbnails = avatars.scope('thumbnails');
622
+ // → all operations under "user-avatars/thumbnails/"
458
623
  ```
459
624
 
460
- ---
625
+ ### Share & Visibility
461
626
 
462
- ### Batch Download
463
-
464
- ```ts
465
- const { data: files } = await hydrous.storage.batchDownload(
466
- 'ssk_my_bucket_key',
467
- ['reports/jan.pdf', 'reports/feb.pdf', 'reports/mar.pdf'],
468
- {
469
- concurrency: 3,
470
- autoSave: true, // browser: auto-triggers Save dialog per file
471
- onProgress: (p) => {
472
- console.log(`${p.path}: ${p.status}`); // 'pending' | 'success' | 'error'
473
- },
474
- }
627
+ ```typescript
628
+ // Get file metadata (sizes, URLs, visibility)
629
+ const meta = await db.storage.getMetadata('reports/q3.pdf');
630
+
631
+ // Generate a time-limited share link for a private file
632
+ // (no auth key needed to use the link)
633
+ const { signedUrl, expiresAt } = await db.storage.getSignedUrl(
634
+ 'reports/q3.pdf',
635
+ 3600, // expires in 1 hour (default)
475
636
  );
637
+ // Share signedUrl with whoever needs it
476
638
 
477
- // files[n].content — ArrayBuffer
478
- // files[n].mimeType string
479
- // files[n].path — original path
480
- // files[n].size — bytes
639
+ // Toggle visibility after upload
640
+ const result = await db.storage.setVisibility('reports/q3.pdf', true); // make public
641
+ const result2 = await db.storage.setVisibility('reports/q3.pdf', false); // make private
481
642
  ```
482
643
 
483
- ---
644
+ ### File Operations
484
645
 
485
- ### List Files & Folders
646
+ ```typescript
647
+ // Rename / move a file
648
+ await db.storage.move('drafts/report.pdf', 'published/report-2025.pdf');
486
649
 
487
- ```ts
488
- const { data } = await hydrous.storage.list('ssk_my_bucket_key', {
489
- prefix: 'avatars/', // list inside a folder — omit for root
490
- limit: 50, // max items per page (1–100)
491
- });
650
+ // Copy a file
651
+ await db.storage.copy('templates/invoice.html', 'invoices/inv-001.html');
492
652
 
493
- for (const item of data.items) {
494
- if (item.type === 'folder') {
495
- console.log('📁', item.path);
496
- } else {
497
- console.log('📄', item.path, item.size, 'bytes');
498
- }
499
- }
653
+ // Create a folder
654
+ await db.storage.createFolder('archive/2025/');
500
655
 
501
- // Paginate
502
- if (data.pagination.hasNextPage) {
503
- const page2 = await hydrous.storage.list('ssk_my_bucket_key', {
504
- prefix: 'avatars/',
505
- cursor: data.pagination.nextCursor,
506
- });
507
- }
656
+ // Delete a file
657
+ await db.storage.deleteFile('temp/scratch.txt');
658
+
659
+ // Delete a folder and all its contents
660
+ await db.storage.deleteFolder('temp/');
661
+
662
+ // Get key-level stats
663
+ const stats = await db.storage.getStats();
664
+ // → { totalFiles: 842, totalBytes: 1073741824, uploadCount: 1200, ... }
508
665
  ```
509
666
 
510
667
  ---
511
668
 
512
- ### File Metadata
669
+ ## Analytics
513
670
 
514
- ```ts
515
- const { data: meta } = await hydrous.storage.metadata(
516
- 'ssk_my_bucket_key',
517
- 'avatars/alice.jpg'
518
- );
671
+ HydrousDB Analytics runs your queries against BigQuery, so they're fast even
672
+ on millions of records. All queries accept an optional `dateRange` filter.
519
673
 
520
- console.log(meta.size); // stored bytes
521
- console.log(meta.originalSize); // bytes before compression
522
- console.log(meta.mimeType);
523
- console.log(meta.isCompressed);
524
- console.log(meta.updatedAt);
674
+ ```typescript
675
+ const analytics = db.analytics('orders');
525
676
  ```
526
677
 
527
- ---
678
+ ### Count
528
679
 
529
- ### Delete a File
680
+ ```typescript
681
+ // Total records
682
+ const { count } = await analytics.count();
530
683
 
531
- ```ts
532
- const { error } = await hydrous.storage.deleteFile(
533
- 'ssk_my_bucket_key',
534
- 'avatars/old-photo.jpg'
535
- );
684
+ // Records in a date range
685
+ const { count: lastWeek } = await analytics.count({
686
+ dateRange: {
687
+ start: Date.now() - 7 * 24 * 60 * 60 * 1000,
688
+ end: Date.now(),
689
+ },
690
+ });
536
691
  ```
537
692
 
538
- ---
693
+ ### Distribution
539
694
 
540
- ### Delete a Folder
695
+ How many records have each unique value for a field?
541
696
 
542
- Recursively deletes the folder and everything inside it.
697
+ ```typescript
698
+ const rows = await analytics.distribution({ field: 'status', limit: 10, order: 'desc' });
699
+ // → [
700
+ // { value: 'completed', count: 8234 },
701
+ // { value: 'pending', count: 1203 },
702
+ // { value: 'refunded', count: 412 },
703
+ // ]
704
+ ```
705
+
706
+ ### Sum
707
+
708
+ ```typescript
709
+ // Total revenue
710
+ const rows = await analytics.sum({ field: 'amount' });
711
+ // → [{ sum: 198432.50 }]
543
712
 
544
- ```ts
545
- await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
713
+ // Revenue by country
714
+ const byCountry = await analytics.sum({
715
+ field: 'amount',
716
+ groupBy: 'country',
717
+ limit: 10,
718
+ });
719
+ // → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
546
720
  ```
547
721
 
548
- ---
722
+ ### Time Series
549
723
 
550
- ### Create a Folder
724
+ Record counts over time — ideal for activity charts.
551
725
 
552
- ```ts
553
- await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
726
+ ```typescript
727
+ const rows = await analytics.timeSeries({
728
+ granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
729
+ dateRange: {
730
+ start: new Date('2025-01-01').getTime(),
731
+ end: new Date('2025-06-01').getTime(),
732
+ },
733
+ });
734
+ // → [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, ...]
554
735
  ```
555
736
 
556
- ---
737
+ Aggregate a field over time:
738
+
739
+ ```typescript
740
+ const revenue = await analytics.fieldTimeSeries({
741
+ field: 'amount',
742
+ aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
743
+ granularity: 'week',
744
+ });
745
+ // → [{ date: '2025-W01', value: 12340.50 }, ...]
746
+ ```
557
747
 
558
- ### Move a File
748
+ ### Top N
559
749
 
560
- ```ts
561
- await hydrous.storage.move(
562
- 'ssk_my_bucket_key',
563
- 'drafts/report.pdf',
564
- 'published/report.pdf'
565
- );
750
+ Most common values for a field:
751
+
752
+ ```typescript
753
+ const topProducts = await analytics.topN({
754
+ field: 'productId',
755
+ labelField: 'productName', // optional: include a human-readable label
756
+ n: 5,
757
+ order: 'desc',
758
+ });
759
+ // → [
760
+ // { value: 'prod_123', label: 'Widget Pro', count: 892 },
761
+ // { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
762
+ // ]
566
763
  ```
567
764
 
568
- ---
765
+ ### Field Stats
569
766
 
570
- ### Copy a File
767
+ Statistical summary for any numeric field:
571
768
 
572
- ```ts
573
- await hydrous.storage.copy(
574
- 'ssk_my_bucket_key',
575
- 'templates/invoice.pdf',
576
- 'invoices/invoice-001.pdf'
577
- );
769
+ ```typescript
770
+ const stats = await analytics.stats({ field: 'orderValue' });
771
+ // → {
772
+ // min: 4.99, max: 9999.99, avg: 87.23,
773
+ // sum: 420948.27, count: 4823, stddev: 143.2
774
+ // }
578
775
  ```
579
776
 
580
- ---
777
+ ### Multi-Metric Dashboard
581
778
 
582
- ### Signed URLs
779
+ Calculate several aggregations in a single BigQuery query:
583
780
 
584
- Generate a time-limited public URL for a private file.
781
+ ```typescript
782
+ const dashboard = await analytics.multiMetric({
783
+ metrics: [
784
+ { field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
785
+ { field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
786
+ { field: 'amount', name: 'maxOrder', aggregation: 'max' },
787
+ { field: 'userId', name: 'totalOrders', aggregation: 'count' },
788
+ ],
789
+ dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
790
+ });
791
+ // → {
792
+ // totalRevenue: 198432.50,
793
+ // avgOrderValue: 87.23,
794
+ // maxOrder: 9999.99,
795
+ // totalOrders: 2275,
796
+ // }
797
+ ```
585
798
 
586
- ```ts
587
- const { data } = await hydrous.storage.signedUrl(
588
- 'ssk_my_bucket_key',
589
- 'contracts/agreement.pdf',
590
- { expiresIn: 300 } // 5 minutes (default: 3600 = 1 hour)
591
- );
799
+ ### Filtered Records (BigQuery)
592
800
 
593
- console.log(data.signedUrl); // share this expires at data.expiresAt
801
+ Query raw records with full BigQuery speed:
802
+
803
+ ```typescript
804
+ const records = await analytics.records({
805
+ filters: [
806
+ { field: 'status', op: '==', value: 'refunded' },
807
+ { field: 'amount', op: '>', value: 100 },
808
+ ],
809
+ selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
810
+ orderBy: 'amount',
811
+ order: 'desc',
812
+ limit: 50,
813
+ });
594
814
  ```
595
815
 
596
- ---
816
+ ### Cross-Bucket Comparison
597
817
 
598
- ### Bucket Stats
818
+ Compare the same metric across multiple buckets in one query:
599
819
 
600
- ```ts
601
- const { data: stats } = await hydrous.storage.stats('ssk_my_bucket_key');
820
+ ```typescript
821
+ const comparison = await analytics.crossBucket({
822
+ bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
823
+ field: 'amount',
824
+ aggregation: 'sum',
825
+ });
826
+ // → [
827
+ // { bucket: 'orders-us', value: 120000 },
828
+ // { bucket: 'orders-eu', value: 45000 },
829
+ // { bucket: 'orders-apac', value: 33000 },
830
+ // ]
831
+ ```
832
+
833
+ > ⚠️ Your Security Key must have read access to **all** buckets in the list.
834
+
835
+ ### Storage Stats
602
836
 
603
- console.log('Files:', stats.totalFiles);
604
- console.log('Stored:', stats.totalSizeBytes, 'bytes');
605
- console.log('Space saved:', stats.spaceSavedBytes, 'bytes');
606
- console.log('Credits used:', stats.creditsTotalUsed);
607
- console.log('Compression:', stats.compressionRatio);
837
+ ```typescript
838
+ const stats = await analytics.storageStats();
839
+ // → { totalRecords: 48210, totalBytes: 921600000, avgBytes: 19112, minBytes: 128, maxBytes: 5242880 }
608
840
  ```
609
841
 
610
842
  ---
611
843
 
612
- ## 7. Error Handling
844
+ ## TypeScript Support
613
845
 
614
- Every method returns `{ data, error }`. `error` is `null` on success.
846
+ The SDK is written in TypeScript and ships with full type definitions. Use generic
847
+ type parameters to describe the shape of your records and get autocomplete throughout.
615
848
 
616
- ```ts
617
- const { data, error } = await hydrous.storage.upload('ssk_key', file);
849
+ ```typescript
850
+ import { createClient } from 'hydrousdb';
618
851
 
619
- if (error) {
620
- console.error(error.message); // human-readable message
621
- console.error(error.code); // machine-readable code e.g. 'QUOTA_EXCEEDED'
622
- console.error(error.status); // HTTP status code (if applicable)
852
+ // Define your data models
853
+ interface Order {
854
+ customerId: string;
855
+ items: Array<{ productId: string; qty: number; price: number }>;
856
+ total: number;
857
+ status: 'pending' | 'processing' | 'completed' | 'refunded';
858
+ country: string;
623
859
  }
860
+
861
+ interface Customer {
862
+ name: string;
863
+ email: string;
864
+ tier: 'free' | 'pro' | 'enterprise';
865
+ credits: number;
866
+ }
867
+
868
+ const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
869
+
870
+ // Fully typed clients
871
+ const orders = db.records<Order>('orders');
872
+ const customers = db.records<Customer>('customers');
873
+
874
+ // order.total, order.status, etc. are all type-safe
875
+ const order = await orders.create({
876
+ customerId: 'cust_abc',
877
+ items: [{ productId: 'prod_1', qty: 2, price: 29.99 }],
878
+ total: 59.98,
879
+ status: 'pending',
880
+ country: 'US',
881
+ });
882
+
883
+ // TypeScript catches mistakes at compile time:
884
+ // order.nonExistentField // ← TS error ✓
885
+ // order.status = 'invalid' // ← TS error ✓
886
+ ```
887
+
888
+ All exported types are available for import:
889
+
890
+ ```typescript
891
+ import type {
892
+ HydrousConfig,
893
+ RecordResult,
894
+ QueryFilter,
895
+ QueryOptions,
896
+ UploadResult,
897
+ AnalyticsQuery,
898
+ DateRange,
899
+ // ... and many more
900
+ } from 'hydrousdb';
624
901
  ```
625
902
 
626
- ### Catching thrown errors
903
+ ---
904
+
905
+ ## Error Handling
906
+
907
+ All errors thrown by the SDK extend `HydrousError`, which carries:
627
908
 
628
- If you prefer `try/catch`, import `isHydrousError`:
909
+ | Property | Type | Description |
910
+ |---|---|---|
911
+ | `message` | `string` | Human-readable description |
912
+ | `code` | `string` | Machine-readable error code (e.g. `"RECORD_NOT_FOUND"`) |
913
+ | `status` | `number` | HTTP status code |
914
+ | `requestId` | `string` | Server request ID (for support tracing) |
915
+ | `details` | `string[]` | Validation error details |
629
916
 
630
- ```ts
631
- import { isHydrousError } from 'hydrous';
917
+ ```typescript
918
+ import { HydrousError, NetworkError, AuthError } from 'hydrousdb';
632
919
 
633
920
  try {
634
- // ...
921
+ const user = await auth.login({ email: 'a@b.com', password: 'wrong' });
635
922
  } catch (err) {
636
- if (isHydrousError(err)) {
637
- console.error(err.code, err.message);
923
+ if (err instanceof AuthError) {
924
+ // Authentication-specific error
925
+ console.error(`Auth failed: ${err.code}`);
926
+ // err.code might be: INVALID_CREDENTIALS, ACCOUNT_LOCKED, EMAIL_NOT_VERIFIED, etc.
927
+ } else if (err instanceof NetworkError) {
928
+ // No internet / server unreachable
929
+ console.error('Cannot reach HydrousDB — check your internet connection');
930
+ } else if (err instanceof HydrousError) {
931
+ // Any other API error
932
+ console.error(`API error [${err.code}]: ${err.message}`);
933
+ console.error(`Request ID: ${err.requestId}`); // include this in support tickets
638
934
  }
639
935
  }
640
936
  ```
641
937
 
642
- ### Common error codes
938
+ **Common error codes:**
939
+
940
+ | Code | Meaning |
941
+ |---|---|
942
+ | `RECORD_NOT_FOUND` | The requested record ID does not exist |
943
+ | `INVALID_CREDENTIALS` | Wrong email or password |
944
+ | `ACCOUNT_LOCKED` | The account is temporarily locked |
945
+ | `INVALID_SESSION` | Session expired or revoked — re-authenticate |
946
+ | `MISSING_API_KEY` | Security key not provided |
947
+ | `INVALID_SECURITY_KEY` | Security key is wrong or revoked |
948
+ | `FORBIDDEN` | Insufficient permissions |
949
+ | `FILE_EXISTS` | File already exists at path (use `overwrite: true`) |
950
+ | `LIMIT_EXCEEDED` | Storage quota or file size limit reached |
951
+ | `SYSTEM_BUCKET_FORBIDDEN` | Cannot query system buckets via analytics |
952
+ | `VALIDATION_ERROR` | Invalid input — check `err.details` |
953
+ | `NETWORK_ERROR` | Failed to reach the API |
954
+
955
+ ---
956
+
957
+ ## Security Best Practices
643
958
 
644
- | Code | Meaning |
645
- |---------------------|----------------------------------------------|
646
- | `HTTP_ERROR` | Non-2xx HTTP response from the server |
647
- | `NETWORK_ERROR` | Failed to reach the server |
648
- | `QUOTA_EXCEEDED` | Storage quota has been reached |
649
- | `FILE_TOO_LARGE` | File exceeds the per-file size limit (50 MB) |
650
- | `INVALID_MIME` | MIME type not permitted |
651
- | `FILE_EXISTS` | File already exists and `overwrite` is false |
652
- | `UPLOAD_ABORTED` | Upload was cancelled by the client |
653
- | `UPLOAD_TIMEOUT` | Upload timed out |
654
- | `NO_SESSION` | Auth operation requires a session |
655
- | `UNKNOWN_ERROR` | An unexpected error occurred |
959
+ 1. **Never hard-code your Security Key.** Use environment variables:
960
+
961
+ ```bash
962
+ # .env (add to .gitignore)
963
+ HYDROUS_SECURITY_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
964
+ ```
965
+
966
+ ```typescript
967
+ const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
968
+ ```
969
+
970
+ 2. **Never expose your Security Key to browsers.** For browser-side apps,
971
+ route requests through your own backend, or use per-user session tokens.
972
+
973
+ 3. **Your Security Key is sent via the `X-Api-Key` header — never in URLs.**
974
+ The SDK enforces this automatically, so keys never appear in server logs or
975
+ browser history.
976
+
977
+ 4. **Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
978
+
979
+ 5. **Use scoped storage** (`db.storage.scope(...)`) to isolate access by user or
980
+ feature, reducing the blast radius of any misconfiguration.
981
+
982
+ 6. **Use `isPublic: false` (the default) for sensitive files.** Use signed URLs
983
+ for time-limited sharing instead of making files permanently public.
656
984
 
657
985
  ---
658
986
 
659
- ## 8. TypeScript Types Reference
660
-
661
- ### `UploadProgress`
662
-
663
- ```ts
664
- interface UploadProgress {
665
- index: number; // file index (0-based)
666
- total: number; // total files in the operation
667
- path: string; // destination path
668
- stage: UploadStage; // see below
669
- bytesUploaded: number;
670
- totalBytes: number;
671
- percent: number; // 0–100
672
- bytesPerSecond: number | null; // null before first tick
673
- eta: number | null; // seconds remaining, null until speed known
674
- result?: UploadResult; // set when stage === 'done'
675
- error?: string; // set when stage === 'error'
676
- code?: string;
677
- }
987
+ ## API Reference
678
988
 
679
- type UploadStage = 'pending' | 'compressing' | 'uploading' | 'done' | 'error';
680
- ```
989
+ ### `createClient(config)`
681
990
 
682
- ### `UploadResult`
991
+ Creates and returns a `HydrousClient` instance. Call this once and reuse the instance.
683
992
 
684
- ```ts
685
- interface UploadResult {
686
- path: string;
687
- compressed: boolean;
688
- originalSize: number;
689
- storedSize: number;
690
- spaceSaved: number;
691
- mimeType: string;
692
- }
993
+ ```typescript
994
+ const db = createClient({
995
+ securityKey: 'sk_live_...', // Required — from https://hydrousdb.com/dashboard
996
+ baseUrl: 'https://...', // Optional — defaults to official HydrousDB endpoint
997
+ });
693
998
  ```
694
999
 
695
- ### `StorageItem` (from `list()`)
1000
+ ### `db.records<T>(bucketKey)`
1001
+
1002
+ Returns a `RecordsClient<T>` for the named bucket.
1003
+
1004
+ | Method | Description |
1005
+ |---|---|
1006
+ | `create(data)` | Create a new record |
1007
+ | `get(id)` | Get a record by ID |
1008
+ | `set(id, data)` | Full replace |
1009
+ | `patch(id, data, opts?)` | Partial update (merge by default) |
1010
+ | `delete(id)` | Delete a record |
1011
+ | `query(opts?)` | Query with filters, sort, pagination |
1012
+ | `getAll(opts?)` | Shortcut for query without filters |
1013
+ | `count(filters?)` | Count matching records |
1014
+ | `batchCreate(items)` | Create multiple records |
1015
+ | `batchDelete(ids)` | Delete multiple records |
1016
+ | `getHistory(id)` | Get version history |
1017
+ | `restoreVersion(id, version)` | Restore to a version |
1018
+
1019
+ ### `db.auth(bucketKey)`
1020
+
1021
+ Returns an `AuthClient` for the named user bucket.
1022
+
1023
+ | Method | Description |
1024
+ |---|---|
1025
+ | `signup(opts)` | Register a new user |
1026
+ | `login(opts)` | Authenticate and create session |
1027
+ | `logout({ sessionId })` | Invalidate session |
1028
+ | `refreshSession({ refreshToken })` | Extend a session |
1029
+ | `getUser({ userId })` | Get user by ID |
1030
+ | `updateUser(opts)` | Update user fields |
1031
+ | `deleteUser(opts)` | Soft-delete a user |
1032
+ | `hardDeleteUser(opts)` | Permanently delete a user (admin) |
1033
+ | `listUsers(opts)` | List all users (admin) |
1034
+ | `bulkDeleteUsers(opts)` | Bulk delete (admin) |
1035
+ | `lockAccount(opts)` | Lock a user (admin) |
1036
+ | `unlockAccount(opts)` | Unlock a user (admin) |
1037
+ | `changePassword(opts)` | Change password (authenticated) |
1038
+ | `requestPasswordReset(opts)` | Send reset email |
1039
+ | `confirmPasswordReset(opts)` | Apply new password |
1040
+ | `requestEmailVerification(opts)` | Send verification email |
1041
+ | `confirmEmailVerification(opts)` | Verify email with token |
1042
+
1043
+ ### `db.analytics(bucketKey)`
1044
+
1045
+ Returns an `AnalyticsClient` for the named bucket.
1046
+
1047
+ | Method | Description |
1048
+ |---|---|
1049
+ | `count(opts?)` | Count records |
1050
+ | `distribution(opts)` | Value distribution for a field |
1051
+ | `sum(opts)` | Sum (with optional groupBy) |
1052
+ | `timeSeries(opts?)` | Records over time |
1053
+ | `fieldTimeSeries(opts)` | Field aggregation over time |
1054
+ | `topN(opts)` | Top N values for a field |
1055
+ | `stats(opts)` | Statistical summary for a field |
1056
+ | `records(opts?)` | Filtered raw records (BigQuery) |
1057
+ | `multiMetric(opts)` | Multiple aggregations in one query |
1058
+ | `storageStats(opts?)` | Bucket storage statistics |
1059
+ | `crossBucket(opts)` | Compare across multiple buckets |
1060
+ | `query(query)` | Raw analytics query |
1061
+
1062
+ ### `db.storage`
1063
+
1064
+ The `StorageManager` instance. Also has `.scope(prefix)` for folder-scoped access.
1065
+
1066
+ | Method | Description |
1067
+ |---|---|
1068
+ | `upload(data, path, opts?)` | Simple server-buffered upload |
1069
+ | `uploadRaw(data, path, opts?)` | Upload JSON/text data |
1070
+ | `getUploadUrl(opts)` | Step 1: Get signed GCS upload URL |
1071
+ | `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2: Upload to GCS directly |
1072
+ | `confirmUpload(opts)` | Step 3: Register upload metadata |
1073
+ | `getBatchUploadUrls(files)` | Batch signed upload URLs |
1074
+ | `batchConfirmUploads(items)` | Confirm batch uploads |
1075
+ | `download(path)` | Download private file |
1076
+ | `batchDownload(paths)` | Batch download |
1077
+ | `list(opts?)` | List files and folders |
1078
+ | `getMetadata(path)` | File metadata |
1079
+ | `getSignedUrl(path, expiresIn?)` | Time-limited share URL |
1080
+ | `setVisibility(path, isPublic)` | Toggle public/private |
1081
+ | `createFolder(path)` | Create a folder |
1082
+ | `deleteFile(path)` | Delete a file |
1083
+ | `deleteFolder(path)` | Delete a folder recursively |
1084
+ | `move(from, to)` | Move/rename |
1085
+ | `copy(from, to)` | Copy |
1086
+ | `getStats()` | Key-level stats |
1087
+ | `info()` | Server ping (no auth) |
1088
+ | `scope(prefix)` | Get a ScopedStorage instance |
696
1089
 
697
- ```ts
698
- interface StorageItem {
699
- name: string;
700
- path: string;
701
- type: 'file' | 'folder';
702
- size?: number | null;
703
- mimeType?: string | null;
704
- isCompressed?: boolean;
705
- updatedAt?: string | null;
706
- }
707
- ```
1090
+ ---
708
1091
 
709
- ### `StorageStats`
710
-
711
- ```ts
712
- interface StorageStats {
713
- totalFiles: number;
714
- totalSizeBytes: number;
715
- totalOriginalSizeBytes: number;
716
- spaceSavedBytes: number;
717
- uploadsCount: number;
718
- downloadsCount: number;
719
- creditsUsedUpload: number;
720
- creditsUsedDownload: number;
721
- creditsTotalUsed: number;
722
- compressionRatio: string; // e.g. "34.2%"
723
- }
1092
+ ## Contributing
1093
+
1094
+ We love contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
1095
+
1096
+ ```bash
1097
+ # Clone the repo
1098
+ git clone https://github.com/hydrousdb/hydrousdb-js.git
1099
+ cd hydrousdb-js
1100
+
1101
+ # Install dependencies
1102
+ npm install
1103
+
1104
+ # Run tests
1105
+ npm test
1106
+
1107
+ # Build
1108
+ npm run build
1109
+
1110
+ # Run tests in watch mode
1111
+ npm run test:watch
724
1112
  ```
725
1113
 
726
1114
  ---
727
1115
 
728
1116
  ## License
729
1117
 
730
- MIT © Hydrous
1118
+ MIT see [LICENSE](./LICENSE) for details.
1119
+
1120
+ ---
1121
+
1122
+ <p align="center">
1123
+ Built with ❤️ by the <a href="https://hydrousdb.com">HydrousDB</a> team.<br>
1124
+ Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
1125
+ <a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
1126
+ </p>