hydrousdb 2.0.3 → 3.0.1

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,1285 +1,1126 @@
1
- # HydrousDB SDK
1
+ # HydrousDB JavaScript / TypeScript SDK
2
2
 
3
- Official JavaScript / TypeScript SDK for the **HydrousDB** platform.
4
- Auth · Records · Analytics · Storage one package, one client.
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 hydrousdb
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. [Setup & Client Configuration](#1-setup--client-configuration)
15
- 2. [Auth](#2-auth)
16
- 3. [Records](#3-records)
17
- 4. [Analytics](#4-analytics)
18
- 5. [Storage](#5-storage)
19
- - [How storage keys work](#how-storage-keys-work)
20
- - [Upload a file with progress](#upload-a-file-with-progress)
21
- - [Batch upload](#batch-upload)
22
- - [Download a file](#download-a-file)
23
- - [List files](#list-files)
24
- - [Delete, move, copy](#delete-move-copy)
25
- - [Signed URLs](#signed-urls)
26
- - [Upload raw text / JSON](#upload-raw-text--json)
27
- - [Stats](#stats)
28
- 6. [Error Handling](#6-error-handling)
29
- 7. [Real-World Examples — Next.js](#7-real-world-examples--nextjs)
30
- - [Auth: Login page with session handling](#auth-login-page-with-session-handling)
31
- - [Records: Product listing with filters](#records-product-listing-with-filters)
32
- - [Storage: Avatar upload with live progress bar](#storage-avatar-upload-with-live-progress-bar)
33
- - [Storage: Secure file download link](#storage-secure-file-download-link)
34
- - [Analytics: Page-view tracking](#analytics-page-view-tracking)
35
- 8. [Real-World Examples — Vue 3](#8-real-world-examples--vue-3)
36
- - [Auth: Login composable](#auth-login-composable)
37
- - [Storage: File upload with progress](#storage-file-upload-with-progress-1)
38
- - [Records: Data table with pagination](#records-data-table-with-pagination)
39
- 9. [Real-World Examples — SvelteKit](#9-real-world-examples--sveltekit)
40
- 10. [Real-World Examples — Express / Node API](#10-real-world-examples--express--node-api)
41
- 11. [TypeScript Types Reference](#11-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)
42
62
 
43
63
  ---
44
64
 
45
- ## 1. Setup & Client Configuration
65
+ ## What is HydrousDB?
46
66
 
47
- Create **one** client and reuse it across your app.
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.
48
68
 
49
- ```ts
50
- import { createClient } from 'hydrousdb';
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. |
51
75
 
52
- const db = createClient({
53
- url: 'https://api.myapp.hydrous.app', // your project URL
54
- authKey: 'hk_auth_…', // auth service key
55
- bucketSecurityKey: 'hk_bucket_…', // records + analytics key
56
- storageKeys: {
57
- main: 'ssk_main_…', // your default storage bucket
58
- avatars: 'ssk_avatars_…', // a dedicated bucket for user avatars
59
- documents: 'ssk_docs_…', // a bucket for user documents
60
- // …add as many as you need
61
- },
62
- });
63
- ```
76
+ Everything is organised around two concepts:
64
77
 
65
- ### Key types explained
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.
66
80
 
67
- | Config field | Used by | What it is |
68
- |---------------------|-------------------------|----------------------------------------------------------|
69
- | `authKey` | `db.auth.*` | One key — controls all auth operations |
70
- | `bucketSecurityKey` | `db.records.*` `db.analytics.*` | One key — controls data and event access |
71
- | `storageKeys` | `db.storage('name').*` | **Many keys** — one per storage bucket you use |
81
+ ---
72
82
 
73
- Because you can have many storage buckets (avatars, documents, exports, …),
74
- `storageKeys` is an object where **you choose the name**. When you call a
75
- storage method you pick which bucket to use by that name.
83
+ ## Quick Start (5 minutes)
76
84
 
77
- ### Environment variables (recommended)
85
+ ### Step 1 — Create your account
78
86
 
79
- ```bash
80
- # .env.local
81
- NEXT_PUBLIC_HYDROUS_URL=https://api.myapp.hydrous.app
82
- HYDROUS_AUTH_KEY=hk_auth_…
83
- HYDROUS_BUCKET_KEY=hk_bucket_…
84
- HYDROUS_STORAGE_MAIN=ssk_main_…
85
- HYDROUS_STORAGE_AVATARS=ssk_avatars_…
86
- HYDROUS_STORAGE_DOCS=ssk_docs_…
87
- ```
87
+ Go to **[https://hydrousdb.com](https://hydrousdb.com)** and sign up for a free account.
88
88
 
89
- ```ts
90
- // lib/db.ts (server-side only — never expose secret keys to the browser)
91
- import { createClient } from 'hydrousdb';
89
+ ### Step 2 — Create your first bucket
92
90
 
93
- export const db = createClient({
94
- url: process.env.NEXT_PUBLIC_HYDROUS_URL!,
95
- authKey: process.env.HYDROUS_AUTH_KEY!,
96
- bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
97
- storageKeys: {
98
- main: process.env.HYDROUS_STORAGE_MAIN!,
99
- avatars: process.env.HYDROUS_STORAGE_AVATARS!,
100
- documents: process.env.HYDROUS_STORAGE_DOCS!,
101
- },
102
- });
103
- ```
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"**.
104
95
 
105
- ---
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.
106
97
 
107
- ## 2. Auth
98
+ ### Step 3 — Grab your Security Key
108
99
 
109
- ### Sign Up
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`.
110
103
 
111
- ```ts
112
- const { data, error } = await db.auth.signUp({
113
- email: 'alice@example.com',
114
- password: 'supersecret',
115
- metadata: { plan: 'pro', referral: 'google' },
116
- });
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
117
107
 
118
- if (error) console.error(error.message);
119
- if (data) console.log('New user:', data.user.id);
108
+ ```bash
109
+ npm install hydrousdb
110
+ # or
111
+ yarn add hydrousdb
112
+ # or
113
+ pnpm add hydrousdb
120
114
  ```
121
115
 
122
- ### Sign In
116
+ **Requirements:** Node.js 18+ (uses the native `fetch` API).
123
117
 
124
- ```ts
125
- const { data, error } = await db.auth.signIn({
126
- email: 'alice@example.com',
127
- password: 'supersecret',
128
- });
118
+ ### Step 5 — Your first record
129
119
 
130
- if (data) {
131
- localStorage.setItem('token', data.accessToken);
132
- }
133
- ```
120
+ ```typescript
121
+ import { createClient } from 'hydrousdb';
134
122
 
135
- ### 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
+ });
136
127
 
137
- ```ts
138
- await db.auth.signOut();
139
- localStorage.removeItem('token');
140
- ```
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
+ });
141
134
 
142
- ### Get current user
135
+ console.log(post.id); // "rec_a1b2c3d4"
136
+ console.log(post.createdAt); // 1717200000000
143
137
 
144
- ```ts
145
- const { data: user } = await db.auth.getUser();
146
- console.log(user?.email);
147
- ```
138
+ // Read it back
139
+ const fetched = await db.records('my-first-bucket').get(post.id);
140
+ console.log(fetched.title); // "Hello, HydrousDB!"
148
141
 
149
- ### Refresh session
142
+ // Update it
143
+ const updated = await db.records('my-first-bucket').patch(post.id, { published: true });
150
144
 
151
- ```ts
152
- const { data: session } = await db.auth.refreshSession();
145
+ // Delete it
146
+ await db.records('my-first-bucket').delete(post.id);
153
147
  ```
154
148
 
149
+ 🎉 **That's it!** You're live with zero configuration beyond your Security Key.
150
+
155
151
  ---
156
152
 
157
- ## 3. 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)
158
159
 
159
- ### Insert
160
+ ### Create a Record
160
161
 
161
- ```ts
162
- // Single record
163
- const { data } = await db.records.insert('products', {
164
- name: 'Widget Pro',
165
- price: 49.99,
166
- category: 'hardware',
162
+ ```typescript
163
+ const products = db.records('products');
164
+
165
+ const product = await products.create({
166
+ name: 'Wireless Headphones',
167
+ price: 79.99,
167
168
  inStock: true,
169
+ tags: ['audio', 'wireless'],
168
170
  });
169
171
 
170
- // Bulk insert
171
- const { data, count } = await db.records.insert('products', [
172
- { name: 'Widget A', price: 9.99 },
173
- { name: 'Widget B', price: 19.99 },
174
- ]);
175
- console.log(`Inserted ${count} products`);
172
+ // product.id, product.createdAt, product.updatedAt are added automatically
176
173
  ```
177
174
 
178
- ### Select (query)
175
+ ### Read a Record
179
176
 
180
- ```ts
181
- import { eq, gt, inArray } from 'hydrousdb';
177
+ ```typescript
178
+ // Get by ID
179
+ const product = await products.get('rec_abc123');
182
180
 
183
- const { data, count } = await db.records.select('products', {
184
- where: [eq('category', 'hardware'), gt('price', 10)],
185
- orderBy: { field: 'price', direction: 'asc' },
186
- limit: 20,
187
- offset: 0,
188
- select: ['id', 'name', 'price'], // return only these fields
189
- });
181
+ // product is null-safe: throws HydrousError with code RECORD_NOT_FOUND if missing
190
182
  ```
191
183
 
192
- ### Get one by ID
184
+ ### Update a Record
193
185
 
194
- ```ts
195
- const { data: product } = await db.records.get('products', 'prod_abc123');
196
- ```
197
-
198
- ### Update
199
-
200
- ```ts
201
- const { data } = await db.records.update('products', 'prod_abc123', {
202
- price: 59.99,
186
+ ```typescript
187
+ // Patch (merge) only the listed fields are changed
188
+ const updated = await products.patch('rec_abc123', {
189
+ price: 69.99,
203
190
  inStock: false,
204
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'],
199
+ });
205
200
  ```
206
201
 
207
- ### Delete
202
+ ### Delete a Record
208
203
 
209
- ```ts
210
- const { error } = await db.records.delete('products', 'prod_abc123');
204
+ ```typescript
205
+ await products.delete('rec_abc123');
211
206
  ```
212
207
 
213
- ---
214
-
215
- ## 4. Analytics
208
+ ### Query Records
216
209
 
217
- ### Track an event
210
+ ```typescript
211
+ // Get all records (up to 100 by default)
212
+ const { records } = await products.query();
218
213
 
219
- ```ts
220
- await db.analytics.track({
221
- event: 'purchase_completed',
222
- userId: 'user_abc123',
223
- sessionId: 'sess_xyz',
224
- properties: {
225
- orderId: 'ord_001',
226
- total: 99.99,
227
- currency: 'USD',
228
- itemCount: 3,
229
- },
214
+ // With filters
215
+ const { records: affordableStock } = await products.query({
216
+ filters: [
217
+ { field: 'inStock', op: '==', value: true },
218
+ { field: 'price', op: '<', value: 100 },
219
+ ],
230
220
  });
231
- ```
232
221
 
233
- ### Track many events at once
222
+ // Sort and paginate
223
+ const { records, hasMore, nextCursor } = await products.query({
224
+ orderBy: 'price',
225
+ order: 'asc',
226
+ limit: 20,
227
+ });
234
228
 
235
- ```ts
236
- await db.analytics.trackBatch([
237
- { event: 'page_view', userId: 'u1', properties: { page: '/home' } },
238
- { event: 'button_click', userId: 'u1', properties: { button: 'buy' } },
239
- { event: 'add_to_cart', userId: 'u1', properties: { product: 'prod_abc' } },
240
- ]);
241
- ```
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
+ }
242
238
 
243
- ### Query events
239
+ // Select only specific fields
240
+ const { records: lightRecords } = await products.query({
241
+ fields: 'name,price,inStock',
242
+ });
244
243
 
245
- ```ts
246
- const { data } = await db.analytics.query({
247
- event: 'purchase_completed',
248
- from: '2024-01-01',
249
- to: '2024-01-31',
250
- limit: 500,
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
+ },
251
250
  });
252
251
  ```
253
252
 
254
- ---
255
-
256
- ## 5. Storage
257
-
258
- ### How storage keys work
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', ... }]
259
275
 
260
- `db.storage` is a function — call it with the **name** of the key you configured:
276
+ // Count records
277
+ const total = await products.count();
278
+ const inStock = await products.count([{ field: 'inStock', op: '==', value: true }]);
261
279
 
262
- ```ts
263
- db.storage('avatars') // returns a ScopedStorageClient for your avatars bucket
264
- db.storage('documents') // returns one for your documents bucket
265
- db.storage('main') // returns one for your main bucket
280
+ // Delete multiple records
281
+ const { deleted, failed } = await products.batchDelete(['rec_1', 'rec_2', 'rec_3']);
266
282
  ```
267
283
 
268
- You can cache the result if you use the same bucket a lot:
284
+ ### Version History
269
285
 
270
- ```ts
271
- const avatarStore = db.storage('avatars');
272
- const documentStore = db.storage('documents');
286
+ Every write to a record creates a new version stored in GCS, so you can travel back in time.
273
287
 
274
- await avatarStore.upload(file, { path: 'alice.jpg' });
275
- const list = await documentStore.list({ prefix: 'invoices/' });
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.
292
+
293
+ // Restore to a specific version
294
+ const restored = await products.restoreVersion('rec_abc123', history[2]!.version);
276
295
  ```
277
296
 
278
297
  ---
279
298
 
280
- ### Upload a file with progress
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.
281
304
 
282
- ```ts
283
- const { data, error } = await db.storage('avatars').upload(file, {
284
- path: 'users/alice.jpg',
285
- overwrite: true,
305
+ ```typescript
306
+ const auth = db.auth('app-users');
307
+ ```
286
308
 
287
- onProgress: (p) => {
288
- // p.stage — 'pending' | 'compressing' | 'uploading' | 'done' | 'error'
289
- // p.percent — 0–100 integer
290
- // p.bytesUploaded — bytes sent so far
291
- // p.totalBytes — total size
292
- // p.bytesPerSecond — current speed (null until first tick)
293
- // p.eta — estimated seconds remaining
309
+ ### Sign Up Users
294
310
 
295
- console.log(`[${p.stage}] ${p.percent}% — ${p.bytesPerSecond} B/s — ETA ${p.eta}s`);
296
- },
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',
297
319
  });
298
320
 
299
- if (data) {
300
- console.log('Path:', data.path);
301
- console.log('Space saved by compression:', data.spaceSaved, 'bytes');
302
- }
321
+ // user.id → "usr_xxxxxxxxxx"
322
+ // session.sessionId → persist this in your app
323
+ // session.refreshToken persist this for long-lived sessions
303
324
  ```
304
325
 
305
- ### Batch upload
306
-
307
- ```ts
308
- const files = Array.from(fileInput.files);
309
-
310
- const { data } = await db.storage('documents').batchUpload(files, {
311
- prefix: 'uploads/2024/',
312
- overwrite: false,
313
- concurrency: 5,
326
+ ### Log In / Log Out
314
327
 
315
- onProgress: (p) => {
316
- // p.index identifies WHICH file (0-based, same order as files array)
317
- console.log(`File ${p.index} "${p.path}": ${p.stage} ${p.percent}%`);
318
- },
328
+ ```typescript
329
+ // Log in
330
+ const { user, session } = await auth.login({
331
+ email: 'alice@example.com',
332
+ password: 'hunter2',
319
333
  });
320
334
 
321
- console.log('Succeeded:', data!.succeeded.length);
322
- console.log('Failed:', data!.failed.length);
323
- data!.failed.forEach(f => console.error(f.path, f.error));
335
+ // Log out (invalidates the session server-side)
336
+ await auth.logout({ sessionId: session.sessionId });
324
337
  ```
325
338
 
326
- ### Download a file
339
+ ### Session Management
327
340
 
328
- ```ts
329
- const { data: buffer, error } = await db.storage('documents').download('reports/q1.pdf');
341
+ Sessions expire after **24 hours**. Use the refresh token to get a new session (refresh tokens last **30 days**).
330
342
 
331
- if (buffer) {
332
- // In a browser trigger Save dialog
333
- const blob = new Blob([buffer], { type: 'application/pdf' });
334
- const url = URL.createObjectURL(blob);
335
- const a = document.createElement('a');
336
- a.href = url;
337
- a.download = 'q1.pdf';
338
- a.click();
339
- URL.revokeObjectURL(url);
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
340
349
 
341
- // In Node write to disk
342
- // import { writeFileSync } from 'fs';
343
- // writeFileSync('q1.pdf', Buffer.from(buffer));
344
- }
350
+ // Get the current user
351
+ const user = await auth.getUser({ userId: session.userId });
345
352
  ```
346
353
 
347
- ### List files
354
+ ### Update User Profile
348
355
 
349
- ```ts
350
- const { data } = await db.storage('documents').list({
351
- prefix: 'invoices/',
352
- limit: 50,
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
+ },
353
365
  });
354
-
355
- for (const item of data!.items) {
356
- if (item.type === 'folder') console.log('📁', item.path);
357
- else console.log('📄', item.path, item.size, 'bytes');
358
- }
359
-
360
- // Paginate
361
- if (data!.pagination.hasNextPage) {
362
- const page2 = await db.storage('documents').list({
363
- prefix: 'invoices/',
364
- cursor: data!.pagination.nextCursor!,
365
- });
366
- }
367
366
  ```
368
367
 
369
- ### Delete, move, copy
368
+ ### Password Reset Flow
370
369
 
371
- ```ts
372
- // Delete a single file
373
- await db.storage('avatars').deleteFile('users/old-photo.jpg');
374
-
375
- // Recursively delete a folder
376
- await db.storage('temp').deleteFolder('uploads/draft/');
377
-
378
- // Create a folder
379
- await db.storage('documents').createFolder('reports/2025/');
370
+ ```typescript
371
+ // 1. User requests a reset (always returns success — prevents email enumeration)
372
+ await auth.requestPasswordReset({ email: 'alice@example.com' });
380
373
 
381
- // Move (rename) a file
382
- await db.storage('documents').move('drafts/report.pdf', 'published/report.pdf');
374
+ // 2. User receives an email with a reset token (your app handles the email sending)
383
375
 
384
- // Copy a file (original kept)
385
- await db.storage('documents').copy('templates/invoice.pdf', 'invoices/inv-001.pdf');
376
+ // 3. User submits the new password
377
+ await auth.confirmPasswordReset({
378
+ resetToken: 'tok_from_email',
379
+ newPassword: 'correcthorsebatterystaple',
380
+ });
381
+ // All existing sessions for this user are automatically revoked
386
382
  ```
387
383
 
388
- ### Signed URLs
389
-
390
- ```ts
391
- // Generate a link that expires in 10 minutes
392
- const { data } = await db.storage('documents').signedUrl(
393
- 'contracts/nda.pdf',
394
- { expiresIn: 600 }
395
- );
384
+ ### Change Password (authenticated)
396
385
 
397
- console.log(data!.signedUrl); // share this URL
398
- console.log(data!.expiresAt); // ISO timestamp when it expires
386
+ ```typescript
387
+ await auth.changePassword({
388
+ sessionId: session.sessionId,
389
+ userId: user.id,
390
+ currentPassword: 'hunter2',
391
+ newPassword: 'correcthorsebatterystaple',
392
+ });
399
393
  ```
400
394
 
401
- ### Upload raw text / JSON
395
+ ### Email Verification
402
396
 
403
- ```ts
404
- // Great for saving generated content, configs, or processed data
405
- await db.storage('configs').uploadText(
406
- 'settings/app.json',
407
- JSON.stringify({ theme: 'dark', language: 'en' }, null, 2),
408
- { mimeType: 'application/json' }
409
- );
410
- ```
397
+ ```typescript
398
+ // 1. Send verification email
399
+ await auth.requestEmailVerification({ userId: user.id });
411
400
 
412
- ### Stats
401
+ // 2. User clicks link in email, your app extracts the token
413
402
 
414
- ```ts
415
- const { data: stats } = await db.storage('main').stats();
416
- console.log('Files:', stats!.totalFiles);
417
- console.log('Stored:', stats!.totalSizeBytes, 'bytes');
418
- console.log('Saved:', stats!.spaceSavedBytes, 'bytes via compression');
419
- console.log('Credits:', stats!.creditsTotalUsed);
403
+ // 3. Confirm the token
404
+ await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
420
405
  ```
421
406
 
422
- ---
407
+ ### Admin Operations
423
408
 
424
- ## 6. Error Handling
409
+ Admin operations require a valid session from a user with `role: 'admin'`.
425
410
 
426
- Every method returns `{ data, error }`. `error` is `null` on success.
411
+ ```typescript
412
+ // List all users
413
+ const { users, total } = await auth.listUsers({
414
+ sessionId: adminSession.sessionId,
415
+ limit: 50,
416
+ offset: 0,
417
+ });
427
418
 
428
- ```ts
429
- const { data, error } = await db.storage('avatars').upload(file);
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
+ });
430
425
 
431
- if (error) {
432
- console.error(error.message); // human-readable
433
- console.error(error.code); // machine-readable e.g. 'QUOTA_EXCEEDED'
434
- console.error(error.status); // HTTP status (if applicable)
435
- }
436
- ```
426
+ // Unlock an account
427
+ await auth.unlockAccount({
428
+ sessionId: adminSession.sessionId,
429
+ userId: 'usr_abc123',
430
+ });
437
431
 
438
- ### Catching thrown errors
432
+ // Soft-delete a user (marks as deleted, keeps data)
433
+ await auth.deleteUser({
434
+ sessionId: adminSession.sessionId,
435
+ userId: 'usr_abc123',
436
+ });
439
437
 
440
- ```ts
441
- import { isHydrousError } from 'hydrousdb';
438
+ // Hard-delete a user (permanent — irreversible)
439
+ await auth.hardDeleteUser({
440
+ sessionId: adminSession.sessionId,
441
+ userId: 'usr_abc123',
442
+ });
442
443
 
443
- try {
444
- await db.storage('avatars').upload(file);
445
- } catch (err) {
446
- if (isHydrousError(err)) {
447
- console.error(err.code, err.message);
448
- }
449
- }
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
+ });
450
449
  ```
451
450
 
452
- ### Common error codes
451
+ ---
453
452
 
454
- | Code | Meaning |
455
- |-----------------------|------------------------------------------------|
456
- | `HTTP_ERROR` | Non-2xx response from the server |
457
- | `NETWORK_ERROR` | Could not reach the server |
458
- | `QUOTA_EXCEEDED` | Storage quota reached |
459
- | `FILE_TOO_LARGE` | File exceeds the 50 MB per-file limit |
460
- | `FILE_EXISTS` | File exists and `overwrite` is false |
461
- | `UPLOAD_ABORTED` | Upload cancelled |
462
- | `UPLOAD_TIMEOUT` | Upload timed out |
463
- | `UNKNOWN_STORAGE_KEY` | Key name not in your `storageKeys` config |
464
- | `NO_SESSION` | Auth operation requires a signed-in session |
453
+ ## File Storage
465
454
 
466
- ---
455
+ HydrousDB Storage is backed by Google Cloud Storage. Your files live at:
456
+ ```
457
+ hydrous-storage/{your-owner-id}/{your-path}
458
+ ```
459
+ You never see or specify the owner prefix — the SDK handles it transparently.
467
460
 
468
- ## 7. Real-World Examples — Next.js
461
+ ### Simple Upload
469
462
 
470
- ### Shared client (lib/db.ts)
463
+ For files up to **500 MB** when you don't need upload progress:
471
464
 
472
- ```ts
473
- // lib/db.ts
474
- import { createClient } from 'hydrousdb';
465
+ ```typescript
466
+ // Browser: upload from a file input
467
+ const fileInput = document.querySelector('input[type="file"]');
468
+ const file = fileInput.files[0];
475
469
 
476
- export const db = createClient({
477
- url: process.env.NEXT_PUBLIC_HYDROUS_URL!,
478
- authKey: process.env.HYDROUS_AUTH_KEY!,
479
- bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
480
- storageKeys: {
481
- avatars: process.env.HYDROUS_STORAGE_AVATARS!,
482
- documents: process.env.HYDROUS_STORAGE_DOCS!,
483
- },
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
484
473
  });
485
- ```
486
474
 
487
- ---
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
488
479
 
489
- ### Auth: Login page with session handling
490
-
491
- ```tsx
492
- // app/login/page.tsx
493
- 'use client';
494
- import { useState } from 'react';
495
- import { useRouter } from 'next/navigation';
496
- import { db } from '@/lib/db';
497
-
498
- export default function LoginPage() {
499
- const router = useRouter();
500
- const [email, setEmail] = useState('');
501
- const [password, setPassword] = useState('');
502
- const [error, setError] = useState<string | null>(null);
503
- const [loading, setLoading] = useState(false);
504
-
505
- async function handleLogin(e: React.FormEvent) {
506
- e.preventDefault();
507
- setLoading(true);
508
- setError(null);
509
-
510
- const { data, error } = await db.auth.signIn({ email, password });
511
-
512
- if (error) {
513
- setError(error.message);
514
- setLoading(false);
515
- return;
516
- }
517
-
518
- // Persist session
519
- localStorage.setItem('hydrous_token', data!.accessToken);
520
- router.push('/dashboard');
521
- }
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
+ ```
522
486
 
523
- return (
524
- <form onSubmit={handleLogin}>
525
- <input value={email} onChange={e => setEmail(e.target.value)}
526
- type="email" placeholder="Email" required />
527
- <input value={password} onChange={e => setPassword(e.target.value)}
528
- type="password" placeholder="Password" required />
529
- {error && <p style={{ color: 'red' }}>{error}</p>}
530
- <button type="submit" disabled={loading}>
531
- {loading ? 'Signing in…' : 'Sign In'}
532
- </button>
533
- </form>
534
- );
535
- }
487
+ ### Upload Raw JSON or Text
488
+
489
+ ```typescript
490
+ const result = await db.storage.uploadRaw(
491
+ { theme: 'dark', language: 'en' },
492
+ 'user-config/alice.json',
493
+ { isPublic: false },
494
+ );
536
495
  ```
537
496
 
538
- ---
497
+ ### Large File Upload (with progress)
539
498
 
540
- ### Records: Product listing with filters
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.
541
501
 
542
- ```tsx
543
- // app/products/page.tsx
544
- import { db } from '@/lib/db';
545
- import { eq } from 'hydrousdb';
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
+ });
546
510
 
547
- type Product = { id: string; name: string; price: number; inStock: boolean };
511
+ // Step 2: Upload directly to GCS with progress
512
+ await db.storage.uploadToSignedUrl(
513
+ uploadUrl,
514
+ file,
515
+ 'video/mp4',
516
+ (percent) => {
517
+ progressBar.style.width = `${percent}%`;
518
+ console.log(`${percent}% uploaded`);
519
+ },
520
+ );
548
521
 
549
- export default async function ProductsPage() {
550
- const { data: products, error } = await db.records.select<Product>('products', {
551
- where: eq('inStock', true),
552
- orderBy: { field: 'price', direction: 'asc' },
553
- limit: 24,
554
- });
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
+ });
555
528
 
556
- if (error) return <p>Error: {error.message}</p>;
557
-
558
- return (
559
- <ul>
560
- {products.map(p => (
561
- <li key={p.id}>
562
- {p.name} — ${p.price}
563
- </li>
564
- ))}
565
- </ul>
566
- );
567
- }
529
+ console.log(result.publicUrl); // ready to use
568
530
  ```
569
531
 
570
- ---
532
+ ### Batch Upload
571
533
 
572
- ### Storage: Avatar upload with live progress bar
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
+ ]);
573
540
 
574
- ```tsx
575
- // components/AvatarUploader.tsx
576
- 'use client';
577
- import { useState } from 'react';
578
- import { db } from '@/lib/db';
579
- import type { UploadProgress } from 'hydrousdb';
541
+ // Upload each one
542
+ for (const f of files) {
543
+ await db.storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
544
+ }
580
545
 
581
- export default function AvatarUploader({ userId }: { userId: string }) {
582
- const [progress, setProgress] = useState<UploadProgress | null>(null);
583
- const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
584
- const [error, setError] = useState<string | null>(null);
546
+ // Confirm all at once
547
+ const results = await db.storage.batchConfirmUploads(
548
+ files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true }))
549
+ );
550
+ ```
585
551
 
586
- async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
587
- const file = e.target.files?.[0];
588
- if (!file) return;
552
+ ### Download Files
589
553
 
590
- setError(null);
591
- setProgress(null);
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' });
592
558
 
593
- const { data, error } = await db.storage('avatars').upload(file, {
594
- path: `users/${userId}/avatar.jpg`,
595
- overwrite: true,
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();
596
565
 
597
- onProgress: (p) => setProgress(p),
598
- });
566
+ // Public files: just use the publicUrl directly (no SDK needed)
567
+ // <img src={result.publicUrl} />
568
+ ```
599
569
 
600
- if (error) { setError(error.message); return; }
570
+ ### List Files
601
571
 
602
- // Generate a signed URL to display the uploaded avatar
603
- const { data: urlData } = await db.storage('avatars').signedUrl(
604
- `users/${userId}/avatar.jpg`,
605
- { expiresIn: 3600 }
606
- );
607
- setAvatarUrl(urlData?.signedUrl ?? null);
608
- setProgress(null);
609
- }
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,
581
+ });
610
582
 
611
- return (
612
- <div>
613
- <input type="file" accept="image/*" onChange={handleFileChange} />
614
-
615
- {progress && (
616
- <div>
617
- <p>{progress.stage} — {progress.percent}%</p>
618
- <progress value={progress.percent} max={100} />
619
- {progress.bytesPerSecond && (
620
- <p>
621
- {(progress.bytesPerSecond / 1024).toFixed(1)} KB/s
622
- {progress.eta != null && ` · ${progress.eta}s remaining`}
623
- </p>
624
- )}
625
- </div>
626
- )}
627
-
628
- {error && <p style={{ color: 'red' }}>{error}</p>}
629
- {avatarUrl && <img src={avatarUrl} alt="Avatar" width={100} />}
630
- </div>
631
- );
583
+ // Paginate
584
+ if (hasMore) {
585
+ const page2 = await db.storage.list({ prefix: 'gallery/', cursor: nextCursor });
632
586
  }
633
587
  ```
634
588
 
635
- ---
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
+ ```
636
602
 
637
- ### Storage: Secure file download link (API route)
603
+ ### Scoped Storage
638
604
 
639
- ```ts
640
- // app/api/download/route.ts
641
- import { NextRequest, NextResponse } from 'next/server';
642
- import { db } from '@/lib/db';
605
+ Working within a specific folder? Use `.scope()` to avoid typing the prefix on every call.
643
606
 
644
- export async function GET(req: NextRequest) {
645
- const filePath = req.nextUrl.searchParams.get('file');
646
- if (!filePath) return NextResponse.json({ error: 'file param required' }, { status: 400 });
607
+ ```typescript
608
+ // All operations in the "user-avatars/" folder
609
+ const avatars = db.storage.scope('user-avatars');
647
610
 
648
- // Generate a short-lived signed URL (2 minutes)
649
- const { data, error } = await db.storage('documents').signedUrl(filePath, {
650
- expiresIn: 120,
651
- });
611
+ await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
612
+ // uploads to "user-avatars/{userId}.jpg"
652
613
 
653
- if (error) return NextResponse.json({ error: error.message }, { status: 500 });
614
+ const { files } = await avatars.list();
615
+ // → lists files under "user-avatars/"
654
616
 
655
- return NextResponse.json({ url: data!.signedUrl, expiresAt: data!.expiresAt });
656
- }
657
- ```
617
+ await avatars.deleteFile(`${userId}.jpg`);
618
+ // → deletes "user-avatars/{userId}.jpg"
658
619
 
659
- ```tsx
660
- // Usage in a component:
661
- // const res = await fetch(`/api/download?file=contracts/nda.pdf`);
662
- // const { url } = await res.json();
663
- // window.open(url, '_blank');
620
+ // Nest scopes
621
+ const thumbnails = avatars.scope('thumbnails');
622
+ // all operations under "user-avatars/thumbnails/"
664
623
  ```
665
624
 
666
- ---
625
+ ### Share & Visibility
667
626
 
668
- ### Analytics: Page-view tracking
669
-
670
- ```tsx
671
- // components/Analytics.tsx
672
- 'use client';
673
- import { useEffect } from 'react';
674
- import { usePathname } from 'next/navigation';
675
- import { db } from '@/lib/db';
676
-
677
- export default function Analytics() {
678
- const pathname = usePathname();
679
-
680
- useEffect(() => {
681
- db.analytics.track({
682
- event: 'page_view',
683
- properties: {
684
- path: pathname,
685
- referrer: document.referrer,
686
- userAgent: navigator.userAgent,
687
- },
688
- });
689
- }, [pathname]);
690
-
691
- return null;
692
- }
693
- ```
627
+ ```typescript
628
+ // Get file metadata (sizes, URLs, visibility)
629
+ const meta = await db.storage.getMetadata('reports/q3.pdf');
694
630
 
695
- ```tsx
696
- // app/layout.tsx add <Analytics /> inside <body>
697
- import Analytics from '@/components/Analytics';
698
-
699
- export default function RootLayout({ children }: { children: React.ReactNode }) {
700
- return (
701
- <html>
702
- <body>
703
- <Analytics />
704
- {children}
705
- </body>
706
- </html>
707
- );
708
- }
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)
636
+ );
637
+ // Share signedUrl with whoever needs it
638
+
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
709
642
  ```
710
643
 
711
- ---
644
+ ### File Operations
712
645
 
713
- ### Records: Server Action — create a product
646
+ ```typescript
647
+ // Rename / move a file
648
+ await db.storage.move('drafts/report.pdf', 'published/report-2025.pdf');
714
649
 
715
- ```ts
716
- // app/products/actions.ts
717
- 'use server';
718
- import { db } from '@/lib/db';
719
- import { revalidatePath } from 'next/cache';
650
+ // Copy a file
651
+ await db.storage.copy('templates/invoice.html', 'invoices/inv-001.html');
720
652
 
721
- export async function createProduct(formData: FormData) {
722
- const name = formData.get('name') as string;
723
- const price = Number(formData.get('price'));
724
- const category = formData.get('category') as string;
653
+ // Create a folder
654
+ await db.storage.createFolder('archive/2025/');
725
655
 
726
- const { data, error } = await db.records.insert('products', {
727
- name, price, category, inStock: true,
728
- });
656
+ // Delete a file
657
+ await db.storage.deleteFile('temp/scratch.txt');
729
658
 
730
- if (error) throw new Error(error.message);
659
+ // Delete a folder and all its contents
660
+ await db.storage.deleteFolder('temp/');
731
661
 
732
- revalidatePath('/products');
733
- return data[0];
734
- }
662
+ // Get key-level stats
663
+ const stats = await db.storage.getStats();
664
+ // → { totalFiles: 842, totalBytes: 1073741824, uploadCount: 1200, ... }
735
665
  ```
736
666
 
737
667
  ---
738
668
 
739
- ## 8. Real-World Examples — Vue 3
669
+ ## Analytics
740
670
 
741
- ### Shared client (src/lib/db.ts)
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.
742
673
 
743
- ```ts
744
- // src/lib/db.ts
745
- import { createClient } from 'hydrousdb';
674
+ ```typescript
675
+ const analytics = db.analytics('orders');
676
+ ```
677
+
678
+ ### Count
679
+
680
+ ```typescript
681
+ // Total records
682
+ const { count } = await analytics.count();
746
683
 
747
- export const db = createClient({
748
- url: import.meta.env.VITE_HYDROUS_URL,
749
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
750
- bucketSecurityKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
751
- storageKeys: {
752
- avatars: import.meta.env.VITE_HYDROUS_STORAGE_AVATARS,
753
- documents: import.meta.env.VITE_HYDROUS_STORAGE_DOCS,
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(),
754
689
  },
755
690
  });
756
691
  ```
757
692
 
758
- ---
759
-
760
- ### Auth: Login composable
693
+ ### Distribution
761
694
 
762
- ```ts
763
- // composables/useAuth.ts
764
- import { ref } from 'vue';
765
- import { useRouter } from 'vue-router';
766
- import { db } from '@/lib/db';
695
+ How many records have each unique value for a field?
767
696
 
768
- export function useAuth() {
769
- const user = ref(null);
770
- const loading = ref(false);
771
- const error = ref<string | null>(null);
772
- const router = useRouter();
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
+ ```
773
705
 
774
- async function login(email: string, password: string) {
775
- loading.value = true;
776
- error.value = null;
706
+ ### Sum
777
707
 
778
- const { data, error: err } = await db.auth.signIn({ email, password });
708
+ ```typescript
709
+ // Total revenue
710
+ const rows = await analytics.sum({ field: 'amount' });
711
+ // → [{ sum: 198432.50 }]
779
712
 
780
- if (err) {
781
- error.value = err.message;
782
- } else {
783
- user.value = data!.user as any;
784
- localStorage.setItem('token', data!.accessToken);
785
- router.push('/dashboard');
786
- }
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 }, ...]
720
+ ```
787
721
 
788
- loading.value = false;
789
- }
722
+ ### Time Series
790
723
 
791
- async function logout() {
792
- await db.auth.signOut();
793
- user.value = null;
794
- localStorage.removeItem('token');
795
- router.push('/login');
796
- }
724
+ Record counts over time — ideal for activity charts.
797
725
 
798
- return { user, loading, error, login, logout };
799
- }
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 }, ...]
800
735
  ```
801
736
 
802
- ---
737
+ Aggregate a field over time:
803
738
 
804
- ### Storage: File upload with progress
805
-
806
- ```vue
807
- <!-- components/FileUploader.vue -->
808
- <script setup lang="ts">
809
- import { ref } from 'vue';
810
- import { db } from '@/lib/db';
811
- import type { UploadProgress } from 'hydrousdb';
812
-
813
- const progress = ref<UploadProgress | null>(null);
814
- const done = ref(false);
815
- const error = ref<string | null>(null);
816
-
817
- async function onFileChange(event: Event) {
818
- const file = (event.target as HTMLInputElement).files?.[0];
819
- if (!file) return;
820
-
821
- done.value = false;
822
- error.value = null;
823
- progress.value = null;
824
-
825
- const { data, error: err } = await db.storage('documents').upload(file, {
826
- path: `uploads/${Date.now()}-${file.name}`,
827
- overwrite: false,
828
- onProgress: (p) => {
829
- progress.value = p;
830
- },
831
- });
832
-
833
- if (err) {
834
- error.value = err.message;
835
- } else {
836
- done.value = true;
837
- progress.value = null;
838
- console.log('Uploaded to', data!.path);
839
- }
840
- }
841
- </script>
842
-
843
- <template>
844
- <div>
845
- <input type="file" @change="onFileChange" />
846
-
847
- <div v-if="progress">
848
- <p>{{ progress.stage }} — {{ progress.percent }}%</p>
849
- <div class="progress-bar">
850
- <div class="fill" :style="{ width: progress.percent + '%' }" />
851
- </div>
852
- <p v-if="progress.bytesPerSecond">
853
- {{ (progress.bytesPerSecond / 1024).toFixed(1) }} KB/s
854
- <span v-if="progress.eta != null"> · {{ progress.eta }}s left</span>
855
- </p>
856
- </div>
857
-
858
- <p v-if="done" style="color: green">✓ Upload complete!</p>
859
- <p v-if="error" style="color: red">{{ error }}</p>
860
- </div>
861
- </template>
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 }, ...]
862
746
  ```
863
747
 
864
- ---
748
+ ### Top N
749
+
750
+ Most common values for a field:
865
751
 
866
- ### Records: Data table with pagination
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
+ // ]
763
+ ```
867
764
 
868
- ```vue
869
- <!-- components/ProductTable.vue -->
870
- <script setup lang="ts">
871
- import { ref, onMounted } from 'vue';
872
- import { db } from '@/lib/db';
873
- import { eq } from 'hydrousdb';
765
+ ### Field Stats
874
766
 
875
- type Product = { id: string; name: string; price: number; inStock: boolean };
767
+ Statistical summary for any numeric field:
876
768
 
877
- const products = ref<Product[]>([]);
878
- const total = ref(0);
879
- const page = ref(1);
880
- const limit = 10;
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
+ // }
775
+ ```
881
776
 
882
- async function loadPage(p: number) {
883
- const { data, count, error } = await db.records.select<Product>('products', {
884
- where: eq('inStock', true),
885
- orderBy: { field: 'name', direction: 'asc' },
886
- limit,
887
- offset: (p - 1) * limit,
888
- });
777
+ ### Multi-Metric Dashboard
889
778
 
890
- if (error) { console.error(error.message); return; }
891
- products.value = data;
892
- total.value = count;
893
- page.value = p;
894
- }
779
+ Calculate several aggregations in a single BigQuery query:
895
780
 
896
- onMounted(() => loadPage(1));
897
- </script>
898
-
899
- <template>
900
- <table>
901
- <tr v-for="p in products" :key="p.id">
902
- <td>{{ p.name }}</td>
903
- <td>${{ p.price }}</td>
904
- <td>{{ p.inStock ? 'In Stock' : 'Out' }}</td>
905
- </tr>
906
- </table>
907
-
908
- <div class="pagination">
909
- <button :disabled="page === 1" @click="loadPage(page - 1)">← Prev</button>
910
- <span>Page {{ page }} of {{ Math.ceil(total / limit) }}</span>
911
- <button :disabled="page * limit >= total" @click="loadPage(page + 1)">Next →</button>
912
- </div>
913
- </template>
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
+ // }
914
797
  ```
915
798
 
916
- ---
799
+ ### Filtered Records (BigQuery)
917
800
 
918
- ### Vue: Batch file upload with per-file progress
801
+ Query raw records with full BigQuery speed:
919
802
 
920
- ```vue
921
- <!-- components/BatchUploader.vue -->
922
- <script setup lang="ts">
923
- import { ref } from 'vue';
924
- import { db } from '@/lib/db';
925
- import type { UploadProgress } from 'hydrousdb';
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
+ });
814
+ ```
926
815
 
927
- type FileState = { name: string; percent: number; stage: string; done: boolean; error?: string };
816
+ ### Cross-Bucket Comparison
928
817
 
929
- const fileStates = ref<FileState[]>([]);
818
+ Compare the same metric across multiple buckets in one query:
930
819
 
931
- async function onFilesChange(event: Event) {
932
- const files = Array.from((event.target as HTMLInputElement).files ?? []);
933
- if (!files.length) return;
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
+ ```
934
832
 
935
- fileStates.value = files.map(f => ({ name: f.name, percent: 0, stage: 'pending', done: false }));
833
+ > ⚠️ Your Security Key must have read access to **all** buckets in the list.
936
834
 
937
- await db.storage('documents').batchUpload(files, {
938
- prefix: 'batch-uploads/',
835
+ ### Storage Stats
939
836
 
940
- onProgress: (p: UploadProgress) => {
941
- const s = fileStates.value[p.index];
942
- if (!s) return;
943
- s.percent = p.percent;
944
- s.stage = p.stage;
945
- s.done = p.stage === 'done';
946
- s.error = p.error;
947
- },
948
- });
949
- }
950
- </script>
951
-
952
- <template>
953
- <input type="file" multiple @change="onFilesChange" />
954
-
955
- <ul>
956
- <li v-for="(f, i) in fileStates" :key="i">
957
- <span>{{ f.name }}</span>
958
- <progress :value="f.percent" max="100" />
959
- <span :style="{ color: f.done ? 'green' : f.error ? 'red' : 'inherit' }">
960
- {{ f.done ? '✓' : f.error ?? f.stage + ' ' + f.percent + '%' }}
961
- </span>
962
- </li>
963
- </ul>
964
- </template>
837
+ ```typescript
838
+ const stats = await analytics.storageStats();
839
+ // { totalRecords: 48210, totalBytes: 921600000, avgBytes: 19112, minBytes: 128, maxBytes: 5242880 }
965
840
  ```
966
841
 
967
842
  ---
968
843
 
969
- ## 9. Real-World Examples — SvelteKit
844
+ ## TypeScript Support
970
845
 
971
- ### Shared client
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.
972
848
 
973
- ```ts
974
- // src/lib/db.ts
849
+ ```typescript
975
850
  import { createClient } from 'hydrousdb';
976
- import {
977
- PUBLIC_HYDROUS_URL,
978
- } from '$env/static/public';
979
- import {
980
- HYDROUS_AUTH_KEY,
981
- HYDROUS_BUCKET_KEY,
982
- HYDROUS_STORAGE_AVATARS,
983
- HYDROUS_STORAGE_DOCS,
984
- } from '$env/static/private';
985
-
986
- export const db = createClient({
987
- url: PUBLIC_HYDROUS_URL,
988
- authKey: HYDROUS_AUTH_KEY,
989
- bucketSecurityKey: HYDROUS_BUCKET_KEY,
990
- storageKeys: {
991
- avatars: HYDROUS_STORAGE_AVATARS,
992
- documents: HYDROUS_STORAGE_DOCS,
993
- },
994
- });
995
- ```
996
-
997
- ### Server route — fetch records
998
851
 
999
- ```ts
1000
- // src/routes/api/products/+server.ts
1001
- import { json } from '@sveltejs/kit';
1002
- import { db } from '$lib/db';
1003
- import { eq } from 'hydrousdb';
1004
-
1005
- export async function GET() {
1006
- const { data, error } = await db.records.select('products', {
1007
- where: eq('inStock', true),
1008
- orderBy: { field: 'price', direction: 'asc' },
1009
- limit: 50,
1010
- });
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;
859
+ }
1011
860
 
1012
- if (error) return json({ error: error.message }, { status: 500 });
1013
- return json(data);
861
+ interface Customer {
862
+ name: string;
863
+ email: string;
864
+ tier: 'free' | 'pro' | 'enterprise';
865
+ credits: number;
1014
866
  }
1015
- ```
1016
867
 
1017
- ### Page load server-side
868
+ const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
1018
869
 
1019
- ```ts
1020
- // src/routes/products/+page.server.ts
1021
- import { db } from '$lib/db';
1022
- import type { PageServerLoad } from './$types';
870
+ // Fully typed clients
871
+ const orders = db.records<Order>('orders');
872
+ const customers = db.records<Customer>('customers');
1023
873
 
1024
- export const load: PageServerLoad = async () => {
1025
- const { data: products, error } = await db.records.select('products', {
1026
- limit: 24,
1027
- orderBy: { field: 'createdAt', direction: 'desc' },
1028
- });
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
+ });
1029
882
 
1030
- if (error) throw error;
1031
- return { products };
1032
- };
883
+ // TypeScript catches mistakes at compile time:
884
+ // order.nonExistentField // TS error ✓
885
+ // order.status = 'invalid' // ← TS error ✓
1033
886
  ```
1034
887
 
1035
- ### File upload component
1036
-
1037
- ```svelte
1038
- <!-- src/lib/components/Uploader.svelte -->
1039
- <script lang="ts">
1040
- import { db } from '$lib/db';
1041
- import type { UploadProgress } from 'hydrousdb';
1042
-
1043
- let progress: UploadProgress | null = null;
1044
- let uploadedPath = '';
1045
- let errorMsg = '';
1046
-
1047
- async function handleFile(event: Event) {
1048
- const file = (event.target as HTMLInputElement).files?.[0];
1049
- if (!file) return;
1050
-
1051
- errorMsg = '';
1052
- uploadedPath = '';
1053
- progress = null;
1054
-
1055
- const { data, error } = await db.storage('documents').upload(file, {
1056
- path: `uploads/${file.name}`,
1057
- onProgress: (p) => { progress = p; },
1058
- });
1059
-
1060
- if (error) {
1061
- errorMsg = error.message;
1062
- } else {
1063
- uploadedPath = data!.path;
1064
- progress = null;
1065
- }
1066
- }
1067
- </script>
1068
-
1069
- <input type="file" on:change={handleFile} />
1070
-
1071
- {#if progress}
1072
- <p>{progress.stage} — {progress.percent}%</p>
1073
- <progress value={progress.percent} max="100" />
1074
- {/if}
1075
-
1076
- {#if uploadedPath}
1077
- <p>✓ Saved at: {uploadedPath}</p>
1078
- {/if}
1079
-
1080
- {#if errorMsg}
1081
- <p style="color:red">{errorMsg}</p>
1082
- {/if}
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';
1083
901
  ```
1084
902
 
1085
903
  ---
1086
904
 
1087
- ## 10. Real-World Examples — Express / Node API
1088
-
1089
- ### Setup
905
+ ## Error Handling
1090
906
 
1091
- ```ts
1092
- // src/db.ts
1093
- import { createClient } from 'hydrousdb';
1094
- import 'dotenv/config';
1095
-
1096
- export const db = createClient({
1097
- url: process.env.HYDROUS_URL!,
1098
- authKey: process.env.HYDROUS_AUTH_KEY!,
1099
- bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
1100
- storageKeys: {
1101
- main: process.env.HYDROUS_STORAGE_MAIN!,
1102
- exports: process.env.HYDROUS_STORAGE_EXPORTS!,
1103
- },
1104
- });
1105
- ```
907
+ All errors thrown by the SDK extend `HydrousError`, which carries:
1106
908
 
1107
- ### Upload from a file buffer (Node)
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 |
1108
916
 
1109
- ```ts
1110
- // routes/upload.ts
1111
- import express from 'express';
1112
- import multer from 'multer';
1113
- import { db } from '../db';
917
+ ```typescript
918
+ import { HydrousError, NetworkError, AuthError } from 'hydrousdb';
1114
919
 
1115
- const router = express.Router();
1116
- const upload = multer({ storage: multer.memoryStorage() });
920
+ try {
921
+ const user = await auth.login({ email: 'a@b.com', password: 'wrong' });
922
+ } catch (err) {
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
934
+ }
935
+ }
936
+ ```
1117
937
 
1118
- router.post('/upload', upload.single('file'), async (req, res) => {
1119
- if (!req.file) return res.status(400).json({ error: 'No file' });
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 |
1120
954
 
1121
- const { data, error } = await db.storage('main').upload(
1122
- req.file.buffer,
1123
- {
1124
- path: `uploads/${Date.now()}-${req.file.originalname}`,
1125
- overwrite: false,
1126
- }
1127
- );
955
+ ---
1128
956
 
1129
- if (error) return res.status(500).json({ error: error.message });
1130
- res.json({ path: data!.path, size: data!.storedSize });
1131
- });
957
+ ## Security Best Practices
1132
958
 
1133
- export default router;
1134
- ```
959
+ 1. **Never hard-code your Security Key.** Use environment variables:
1135
960
 
1136
- ### Save JSON reports to storage
961
+ ```bash
962
+ # .env (add to .gitignore)
963
+ HYDROUS_SECURITY_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
964
+ ```
1137
965
 
1138
- ```ts
1139
- // jobs/generateReport.ts
1140
- import { db } from '../db';
966
+ ```typescript
967
+ const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
968
+ ```
1141
969
 
1142
- export async function generateMonthlyReport(month: string) {
1143
- // 1. Fetch data
1144
- const { data: orders } = await db.records.select('orders', {
1145
- where: { field: 'month', operator: 'eq', value: month },
1146
- });
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.
1147
972
 
1148
- // 2. Build report
1149
- const report = {
1150
- month,
1151
- generatedAt: new Date().toISOString(),
1152
- totalOrders: orders.length,
1153
- totalRevenue: orders.reduce((sum: number, o: any) => sum + o.total, 0),
1154
- orders,
1155
- };
1156
-
1157
- // 3. Save to storage
1158
- const { data, error } = await db.storage('exports').uploadText(
1159
- `reports/${month}.json`,
1160
- JSON.stringify(report, null, 2),
1161
- { mimeType: 'application/json', overwrite: true }
1162
- );
1163
-
1164
- if (error) throw new Error(`Failed to save report: ${error.message}`);
1165
-
1166
- // 4. Track the event
1167
- await db.analytics.track({
1168
- event: 'report_generated',
1169
- properties: { month, totalOrders: report.totalOrders },
1170
- });
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.
1171
976
 
1172
- return data!.path;
1173
- }
1174
- ```
977
+ 4. **Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
1175
978
 
1176
- ### Batch delete old records
979
+ 5. **Use scoped storage** (`db.storage.scope(...)`) to isolate access by user or
980
+ feature, reducing the blast radius of any misconfiguration.
1177
981
 
1178
- ```ts
1179
- // scripts/cleanup.ts
1180
- import { db } from '../db';
1181
- import { lt } from 'hydrousdb';
982
+ 6. **Use `isPublic: false` (the default) for sensitive files.** Use signed URLs
983
+ for time-limited sharing instead of making files permanently public.
1182
984
 
1183
- async function cleanupOldSessions() {
1184
- const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
985
+ ---
1185
986
 
1186
- // Fetch old sessions
1187
- const { data: old } = await db.records.select('sessions', {
1188
- where: lt('createdAt', cutoff),
1189
- limit: 500,
1190
- });
987
+ ## API Reference
1191
988
 
1192
- if (!old.length) { console.log('Nothing to clean up'); return; }
989
+ ### `createClient(config)`
1193
990
 
1194
- // Delete each one
1195
- await Promise.all(old.map((s: any) => db.records.delete('sessions', s.id)));
1196
- console.log(`Deleted ${old.length} expired sessions`);
1197
- }
991
+ Creates and returns a `HydrousClient` instance. Call this once and reuse the instance.
1198
992
 
1199
- cleanupOldSessions();
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
+ });
1200
998
  ```
1201
999
 
1202
- ---
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 |
1203
1089
 
1204
- ## 11. TypeScript Types Reference
1090
+ ---
1205
1091
 
1206
- ### `HydrousConfig`
1092
+ ## Contributing
1207
1093
 
1208
- ```ts
1209
- interface HydrousConfig {
1210
- url: string;
1211
- authKey: string; // one key for auth
1212
- bucketSecurityKey: string; // one key for records + analytics
1213
- storageKeys: Record<string, string>; // many named keys for storage
1214
- timeout?: number; // ms, default 30000
1215
- }
1216
- ```
1094
+ We love contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
1217
1095
 
1218
- ### `UploadProgress`
1219
-
1220
- ```ts
1221
- interface UploadProgress {
1222
- index: number; // 0-based file index
1223
- total: number; // total files in this operation
1224
- path: string; // destination path
1225
- stage: UploadStage; // 'pending'|'compressing'|'uploading'|'done'|'error'
1226
- bytesUploaded: number;
1227
- totalBytes: number;
1228
- percent: number; // 0–100
1229
- bytesPerSecond: number | null; // null until speed is calculable
1230
- eta: number | null; // seconds remaining, null until speed known
1231
- result?: UploadResult; // populated when stage === 'done'
1232
- error?: string; // populated when stage === 'error'
1233
- code?: string;
1234
- }
1235
- ```
1096
+ ```bash
1097
+ # Clone the repo
1098
+ git clone https://github.com/hydrousdb/hydrousdb-js.git
1099
+ cd hydrousdb-js
1236
1100
 
1237
- ### `UploadResult`
1101
+ # Install dependencies
1102
+ npm install
1238
1103
 
1239
- ```ts
1240
- interface UploadResult {
1241
- path: string;
1242
- compressed: boolean;
1243
- originalSize: number;
1244
- storedSize: number;
1245
- spaceSaved: number;
1246
- mimeType: string;
1247
- }
1248
- ```
1104
+ # Run tests
1105
+ npm test
1249
1106
 
1250
- ### `StorageItem`
1251
-
1252
- ```ts
1253
- interface StorageItem {
1254
- name: string;
1255
- path: string;
1256
- type: 'file' | 'folder';
1257
- size?: number | null;
1258
- mimeType?: string | null;
1259
- isCompressed?: boolean;
1260
- updatedAt?: string | null;
1261
- }
1262
- ```
1107
+ # Build
1108
+ npm run build
1263
1109
 
1264
- ### `StorageStats`
1265
-
1266
- ```ts
1267
- interface StorageStats {
1268
- totalFiles: number;
1269
- totalSizeBytes: number;
1270
- totalOriginalSizeBytes: number;
1271
- spaceSavedBytes: number;
1272
- uploadsCount: number;
1273
- downloadsCount: number;
1274
- creditsUsedUpload: number;
1275
- creditsUsedDownload: number;
1276
- creditsTotalUsed: number;
1277
- compressionRatio: string; // e.g. "34.2%"
1278
- }
1110
+ # Run tests in watch mode
1111
+ npm run test:watch
1279
1112
  ```
1280
1113
 
1281
1114
  ---
1282
1115
 
1283
1116
  ## License
1284
1117
 
1285
- MIT © HydrousDB
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>