hydrousdb 2.0.0 → 2.0.3

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,774 +1,1282 @@
1
- # hydrousdb
1
+ # HydrousDB SDK
2
2
 
3
- > **Official TypeScript SDK for [HydrousDB](https://db-api-82687684612.us-central1.run.app)**
4
- > a cloud-native record store with built-in authentication, batch operations, analytics, and cursor-based pagination.
3
+ Official JavaScript / TypeScript SDK for the **HydrousDB** platform.
4
+ Auth · Records · Analytics · Storage one package, one client.
5
5
 
6
- [![npm version](https://img.shields.io/npm/v/hydrousdb)](https://npmjs.com/package/hydrousdb)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+ ```bash
7
+ npm install hydrousdb
8
+ ```
9
9
 
10
10
  ---
11
11
 
12
12
  ## Table of Contents
13
13
 
14
- - [Features](#features)
15
- - [Installation](#installation)
16
- - [Key Concepts](#key-concepts)
17
- - [Quick Start](#quick-start)
18
- - [Framework Guides](#framework-guides)
19
- - [Next.js (App Router)](#nextjs-app-router)
20
- - [Next.js (Pages Router)](#nextjs-pages-router)
21
- - [Vue 3 / Nuxt 3](#vue-3--nuxt-3)
22
- - [React (Vite / CRA)](#react-vite--cra)
23
- - [Node.js / Express](#nodejs--express)
24
- - [API Reference](#api-reference)
25
- - [createClient](#createclient)
26
- - [Records](#records)
27
- - [Auth](#auth)
28
- - [Analytics](#analytics)
29
- - [Error Handling](#error-handling)
30
- - [Pagination](#pagination)
31
- - [TypeScript](#typescript)
32
- - [Migration from v1](#migration-from-v1)
33
- - [Contributing](#contributing)
34
- - [License](#license)
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)
35
42
 
36
43
  ---
37
44
 
38
- ## Features
45
+ ## 1. Setup & Client Configuration
39
46
 
40
- - **Full TypeScript** every method and response is fully typed
41
- - ✅ **Multi-bucket** — one client, any number of buckets; pass `bucketKey` per call
42
- - ✅ **Modular** — import only what you need (`hydrousdb/records`, `hydrousdb/auth`, `hydrousdb/analytics`)
43
- - ✅ **Tree-shakeable** — ships ESM + CJS with dual exports
44
- - ✅ **Auto-retry** — retries on transient 5xx / network errors with linear back-off
45
- - ✅ **Timeout control** — configurable per-client request timeout
46
- - ✅ **Cursor pagination helpers** — `queryAll()` / `listAllUsers()` handle cursor following for you
47
- - ✅ **Zero dependencies** — uses the native `fetch` API (Node 18+, all modern browsers)
47
+ Create **one** client and reuse it across your app.
48
48
 
49
- ---
49
+ ```ts
50
+ import { createClient } from 'hydrousdb';
51
+
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
+ ```
64
+
65
+ ### Key types explained
66
+
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 |
50
72
 
51
- ## Installation
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.
76
+
77
+ ### Environment variables (recommended)
52
78
 
53
79
  ```bash
54
- npm install hydrousdb
55
- # or
56
- pnpm add hydrousdb
57
- # or
58
- yarn add hydrousdb
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_…
59
87
  ```
60
88
 
61
- > **Node.js ≥ 18** is required (uses native `fetch`). For Node 16/17, polyfill `fetch` with `node-fetch` or `undici`.
89
+ ```ts
90
+ // lib/db.ts (server-side only — never expose secret keys to the browser)
91
+ import { createClient } from 'hydrousdb';
92
+
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
+ ```
62
104
 
63
105
  ---
64
106
 
65
- ## Key Concepts
107
+ ## 2. Auth
66
108
 
67
- ### Two keys, one client
109
+ ### Sign Up
68
110
 
69
- The SDK separates two distinct credentials:
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
+ });
117
+
118
+ if (error) console.error(error.message);
119
+ if (data) console.log('New user:', data.user.id);
120
+ ```
121
+
122
+ ### Sign In
123
+
124
+ ```ts
125
+ const { data, error } = await db.auth.signIn({
126
+ email: 'alice@example.com',
127
+ password: 'supersecret',
128
+ });
129
+
130
+ if (data) {
131
+ localStorage.setItem('token', data.accessToken);
132
+ }
133
+ ```
70
134
 
71
- | Key | Config field | Used for |
72
- |-----|-------------|----------|
73
- | Auth key | `authKey` | All `/auth` routes — sign up, sign in, session management |
74
- | Security key | `securityKey` | All records and analytics routes — data access credential |
135
+ ### Sign Out
75
136
 
76
- ### Bucket per call
137
+ ```ts
138
+ await db.auth.signOut();
139
+ localStorage.removeItem('token');
140
+ ```
77
141
 
78
- Unlike v1, `bucketKey` is **not** set on the client — it's passed as an option on every records and analytics call. This means a **single client instance can read and write to any number of buckets** without re-initialising.
142
+ ### Get current user
79
143
 
80
144
  ```ts
81
- const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
145
+ const { data: user } = await db.auth.getUser();
146
+ console.log(user?.email);
147
+ ```
148
+
149
+ ### Refresh session
82
150
 
83
- // Same client, different buckets
84
- await db.records.insert({ values: { name: 'Alice' } }, { bucketKey: 'users' });
85
- await db.records.insert({ values: { order: '#1001' } }, { bucketKey: 'orders' });
86
- await db.analytics.count({ bucketKey: 'users' });
87
- await db.analytics.count({ bucketKey: 'orders' });
151
+ ```ts
152
+ const { data: session } = await db.auth.refreshSession();
88
153
  ```
89
154
 
90
155
  ---
91
156
 
92
- ## Quick Start
157
+ ## 3. Records
158
+
159
+ ### Insert
93
160
 
94
161
  ```ts
95
- import { createClient } from 'hydrousdb';
162
+ // Single record
163
+ const { data } = await db.records.insert('products', {
164
+ name: 'Widget Pro',
165
+ price: 49.99,
166
+ category: 'hardware',
167
+ inStock: true,
168
+ });
96
169
 
97
- const db = createClient({
98
- authKey: 'your-auth-key',
99
- securityKey: 'your-security-key',
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`);
176
+ ```
177
+
178
+ ### Select (query)
179
+
180
+ ```ts
181
+ import { eq, gt, inArray } from 'hydrousdb';
182
+
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
100
189
  });
190
+ ```
101
191
 
102
- // Insert a record into the 'users' bucket
103
- const { data, meta } = await db.records.insert(
104
- { values: { name: 'Alice', score: 99 }, queryableFields: ['name'] },
105
- { bucketKey: 'users' }
106
- );
107
- console.log('Created record:', meta.id);
192
+ ### Get one by ID
108
193
 
109
- // Query records from the 'users' bucket
110
- const { data: records } = await db.records.query({
111
- bucketKey: 'users',
112
- filters: [{ field: 'score', op: '>=', value: 90 }],
113
- limit: 20,
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,
203
+ inStock: false,
114
204
  });
205
+ ```
206
+
207
+ ### Delete
208
+
209
+ ```ts
210
+ const { error } = await db.records.delete('products', 'prod_abc123');
211
+ ```
115
212
 
116
- // Auth — no bucketKey needed
117
- const { data: user, session } = await db.auth.signIn({
118
- email: 'alice@example.com',
119
- password: 'Str0ng@Pass!',
213
+ ---
214
+
215
+ ## 4. Analytics
216
+
217
+ ### Track an event
218
+
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
+ },
120
230
  });
231
+ ```
121
232
 
122
- // Analytics
123
- const { data: stats } = await db.analytics.count({ bucketKey: 'users' });
124
- console.log(stats.count);
233
+ ### Track many events at once
234
+
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
+ ```
242
+
243
+ ### Query events
244
+
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,
251
+ });
252
+ ```
253
+
254
+ ---
255
+
256
+ ## 5. Storage
257
+
258
+ ### How storage keys work
259
+
260
+ `db.storage` is a function — call it with the **name** of the key you configured:
261
+
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
266
+ ```
267
+
268
+ You can cache the result if you use the same bucket a lot:
269
+
270
+ ```ts
271
+ const avatarStore = db.storage('avatars');
272
+ const documentStore = db.storage('documents');
273
+
274
+ await avatarStore.upload(file, { path: 'alice.jpg' });
275
+ const list = await documentStore.list({ prefix: 'invoices/' });
125
276
  ```
126
277
 
127
278
  ---
128
279
 
129
- ## Framework Guides
280
+ ### Upload a file with progress
281
+
282
+ ```ts
283
+ const { data, error } = await db.storage('avatars').upload(file, {
284
+ path: 'users/alice.jpg',
285
+ overwrite: true,
286
+
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
294
+
295
+ console.log(`[${p.stage}] ${p.percent}% — ${p.bytesPerSecond} B/s — ETA ${p.eta}s`);
296
+ },
297
+ });
130
298
 
131
- ### Next.js (App Router)
299
+ if (data) {
300
+ console.log('Path:', data.path);
301
+ console.log('Space saved by compression:', data.spaceSaved, 'bytes');
302
+ }
303
+ ```
132
304
 
133
- **1. Create a shared client singleton**
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,
314
+
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
+ },
319
+ });
320
+
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));
324
+ ```
325
+
326
+ ### Download a file
327
+
328
+ ```ts
329
+ const { data: buffer, error } = await db.storage('documents').download('reports/q1.pdf');
330
+
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);
340
+
341
+ // In Node — write to disk
342
+ // import { writeFileSync } from 'fs';
343
+ // writeFileSync('q1.pdf', Buffer.from(buffer));
344
+ }
345
+ ```
346
+
347
+ ### List files
348
+
349
+ ```ts
350
+ const { data } = await db.storage('documents').list({
351
+ prefix: 'invoices/',
352
+ limit: 50,
353
+ });
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
+ ```
368
+
369
+ ### Delete, move, copy
370
+
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/');
380
+
381
+ // Move (rename) a file
382
+ await db.storage('documents').move('drafts/report.pdf', 'published/report.pdf');
383
+
384
+ // Copy a file (original kept)
385
+ await db.storage('documents').copy('templates/invoice.pdf', 'invoices/inv-001.pdf');
386
+ ```
387
+
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
+ );
396
+
397
+ console.log(data!.signedUrl); // share this URL
398
+ console.log(data!.expiresAt); // ISO timestamp when it expires
399
+ ```
400
+
401
+ ### Upload raw text / JSON
402
+
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
+ ```
411
+
412
+ ### Stats
413
+
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);
420
+ ```
421
+
422
+ ---
423
+
424
+ ## 6. Error Handling
425
+
426
+ Every method returns `{ data, error }`. `error` is `null` on success.
427
+
428
+ ```ts
429
+ const { data, error } = await db.storage('avatars').upload(file);
430
+
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
+ ```
437
+
438
+ ### Catching thrown errors
439
+
440
+ ```ts
441
+ import { isHydrousError } from 'hydrousdb';
442
+
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
+ }
450
+ ```
451
+
452
+ ### Common error codes
453
+
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 |
465
+
466
+ ---
467
+
468
+ ## 7. Real-World Examples — Next.js
469
+
470
+ ### Shared client (lib/db.ts)
134
471
 
135
472
  ```ts
136
473
  // lib/db.ts
137
474
  import { createClient } from 'hydrousdb';
138
475
 
139
476
  export const db = createClient({
140
- authKey: process.env.HYDROUS_AUTH_KEY!,
141
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
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
+ },
142
484
  });
143
485
  ```
144
486
 
145
- **2. Use in a Server Component**
487
+ ---
488
+
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
+ }
522
+
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
+ }
536
+ ```
537
+
538
+ ---
539
+
540
+ ### Records: Product listing with filters
146
541
 
147
542
  ```tsx
148
543
  // app/products/page.tsx
149
544
  import { db } from '@/lib/db';
545
+ import { eq } from 'hydrousdb';
546
+
547
+ type Product = { id: string; name: string; price: number; inStock: boolean };
150
548
 
151
549
  export default async function ProductsPage() {
152
- const { data: products } = await db.records.query({
153
- bucketKey: 'products',
154
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
155
- limit: 24,
156
- sortOrder: 'desc',
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,
157
554
  });
158
555
 
556
+ if (error) return <p>Error: {error.message}</p>;
557
+
159
558
  return (
160
559
  <ul>
161
560
  {products.map(p => (
162
- <li key={p.id}>{String(p.name)}</li>
561
+ <li key={p.id}>
562
+ {p.name} — ${p.price}
563
+ </li>
163
564
  ))}
164
565
  </ul>
165
566
  );
166
567
  }
167
568
  ```
168
569
 
169
- **3. Use in a Route Handler**
570
+ ---
571
+
572
+ ### Storage: Avatar upload with live progress bar
170
573
 
171
- ```ts
172
- // app/api/records/route.ts
173
- import { NextRequest, NextResponse } from 'next/server';
574
+ ```tsx
575
+ // components/AvatarUploader.tsx
576
+ 'use client';
577
+ import { useState } from 'react';
174
578
  import { db } from '@/lib/db';
175
- import { HydrousError } from 'hydrousdb';
176
-
177
- export async function POST(req: NextRequest) {
178
- try {
179
- const body = await req.json();
180
- const result = await db.records.insert(
181
- { values: body, queryableFields: ['email', 'name'] },
182
- { bucketKey: 'users' }
579
+ import type { UploadProgress } from 'hydrousdb';
580
+
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);
585
+
586
+ async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
587
+ const file = e.target.files?.[0];
588
+ if (!file) return;
589
+
590
+ setError(null);
591
+ setProgress(null);
592
+
593
+ const { data, error } = await db.storage('avatars').upload(file, {
594
+ path: `users/${userId}/avatar.jpg`,
595
+ overwrite: true,
596
+
597
+ onProgress: (p) => setProgress(p),
598
+ });
599
+
600
+ if (error) { setError(error.message); return; }
601
+
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 }
183
606
  );
184
- return NextResponse.json(result, { status: 201 });
185
- } catch (err) {
186
- if (err instanceof HydrousError) {
187
- return NextResponse.json({ error: err.message }, { status: err.status });
188
- }
189
- return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
607
+ setAvatarUrl(urlData?.signedUrl ?? null);
608
+ setProgress(null);
190
609
  }
610
+
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
+ );
191
632
  }
192
633
  ```
193
634
 
194
- **4. Auth middleware example**
635
+ ---
636
+
637
+ ### Storage: Secure file download link (API route)
195
638
 
196
639
  ```ts
197
- // middleware.ts
640
+ // app/api/download/route.ts
198
641
  import { NextRequest, NextResponse } from 'next/server';
199
642
  import { db } from '@/lib/db';
200
643
 
201
- export async function middleware(req: NextRequest) {
202
- const sessionId = req.cookies.get('sessionId')?.value;
203
- if (!sessionId) return NextResponse.redirect(new URL('/login', req.url));
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 });
204
647
 
205
- try {
206
- await db.auth.validateSession(sessionId);
207
- return NextResponse.next();
208
- } catch {
209
- return NextResponse.redirect(new URL('/login', req.url));
210
- }
211
- }
648
+ // Generate a short-lived signed URL (2 minutes)
649
+ const { data, error } = await db.storage('documents').signedUrl(filePath, {
650
+ expiresIn: 120,
651
+ });
212
652
 
213
- export const config = { matcher: ['/dashboard/:path*'] };
214
- ```
653
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
215
654
 
216
- **5. Environment variables**
655
+ return NextResponse.json({ url: data!.signedUrl, expiresAt: data!.expiresAt });
656
+ }
657
+ ```
217
658
 
218
- ```env
219
- # .env.local
220
- HYDROUS_AUTH_KEY=your-auth-key
221
- HYDROUS_SECURITY_KEY=your-security-key
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');
222
664
  ```
223
665
 
224
666
  ---
225
667
 
226
- ### Next.js (Pages Router)
668
+ ### Analytics: Page-view tracking
227
669
 
228
- ```ts
229
- // pages/api/records.ts
230
- import type { NextApiRequest, NextApiResponse } from 'next';
231
- import { createClient, HydrousError } from 'hydrousdb';
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';
232
676
 
233
- const db = createClient({
234
- authKey: process.env.HYDROUS_AUTH_KEY!,
235
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
236
- });
677
+ export default function Analytics() {
678
+ const pathname = usePathname();
237
679
 
238
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
239
- if (req.method === 'GET') {
240
- try {
241
- const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
242
- return res.status(200).json(result);
243
- } catch (err) {
244
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
245
- return res.status(500).json({ error: 'Internal Server Error' });
246
- }
247
- }
248
- res.setHeader('Allow', ['GET']);
249
- res.status(405).end();
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;
250
692
  }
251
693
  ```
252
694
 
253
- ---
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
+ }
709
+ ```
254
710
 
255
- ### Vue 3 / Nuxt 3
711
+ ---
256
712
 
257
- **Nuxt 3plugin**
713
+ ### Records: Server Action create a product
258
714
 
259
715
  ```ts
260
- // plugins/hydrousdb.ts
261
- import { createClient } from 'hydrousdb';
716
+ // app/products/actions.ts
717
+ 'use server';
718
+ import { db } from '@/lib/db';
719
+ import { revalidatePath } from 'next/cache';
262
720
 
263
- export default defineNuxtPlugin(() => {
264
- const config = useRuntimeConfig();
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;
265
725
 
266
- const db = createClient({
267
- authKey: config.hydrousAuthKey as string,
268
- securityKey: config.hydrousSecurityKey as string,
726
+ const { data, error } = await db.records.insert('products', {
727
+ name, price, category, inStock: true,
269
728
  });
270
729
 
271
- return { provide: { db } };
272
- });
273
- ```
730
+ if (error) throw new Error(error.message);
274
731
 
275
- ```ts
276
- // nuxt.config.ts
277
- export default defineNuxtConfig({
278
- runtimeConfig: {
279
- hydrousAuthKey: '', // NUXT_HYDROUS_AUTH_KEY
280
- hydrousSecurityKey: '', // NUXT_HYDROUS_SECURITY_KEY
281
- },
282
- });
732
+ revalidatePath('/products');
733
+ return data[0];
734
+ }
283
735
  ```
284
736
 
285
- **Usage in a component**
286
-
287
- ```vue
288
- <script setup lang="ts">
289
- const { $db } = useNuxtApp();
290
-
291
- const { data: records, pending } = await useAsyncData('records', () =>
292
- $db.records.query({ bucketKey: 'products', limit: 20 })
293
- );
294
- </script>
737
+ ---
295
738
 
296
- <template>
297
- <div v-if="pending">Loading…</div>
298
- <ul v-else>
299
- <li v-for="r in records?.data" :key="r.id">{{ r.name }}</li>
300
- </ul>
301
- </template>
302
- ```
739
+ ## 8. Real-World Examples — Vue 3
303
740
 
304
- **Vue 3 standalone — composable**
741
+ ### Shared client (src/lib/db.ts)
305
742
 
306
743
  ```ts
307
- // composables/useHydrous.ts
744
+ // src/lib/db.ts
308
745
  import { createClient } from 'hydrousdb';
309
746
 
310
- const db = createClient({
311
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
312
- securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
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,
754
+ },
313
755
  });
314
-
315
- export function useHydrous() {
316
- return db;
317
- }
318
756
  ```
319
757
 
320
758
  ---
321
759
 
322
- ### React (Vite / CRA)
760
+ ### Auth: Login composable
323
761
 
324
- ```tsx
325
- // src/hooks/useRecords.ts
326
- import { useEffect, useState } from 'react';
327
- import { createClient, HydrousRecord, HydrousError } from 'hydrousdb';
762
+ ```ts
763
+ // composables/useAuth.ts
764
+ import { ref } from 'vue';
765
+ import { useRouter } from 'vue-router';
766
+ import { db } from '@/lib/db';
328
767
 
329
- const db = createClient({
330
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
331
- securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
332
- });
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();
333
773
 
334
- export function useRecords(bucketKey: string, limit = 20) {
335
- const [records, setRecords] = useState<HydrousRecord[]>([]);
336
- const [loading, setLoading] = useState(true);
337
- const [error, setError] = useState<string | null>(null);
774
+ async function login(email: string, password: string) {
775
+ loading.value = true;
776
+ error.value = null;
338
777
 
339
- useEffect(() => {
340
- const controller = new AbortController();
778
+ const { data, error: err } = await db.auth.signIn({ email, password });
779
+
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
+ }
341
787
 
342
- db.records.query({ bucketKey, limit }, { signal: controller.signal })
343
- .then(r => setRecords(r.data))
344
- .catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
345
- .finally(() => setLoading(false));
788
+ loading.value = false;
789
+ }
346
790
 
347
- return () => controller.abort();
348
- }, [bucketKey, limit]);
791
+ async function logout() {
792
+ await db.auth.signOut();
793
+ user.value = null;
794
+ localStorage.removeItem('token');
795
+ router.push('/login');
796
+ }
349
797
 
350
- return { records, loading, error };
798
+ return { user, loading, error, login, logout };
351
799
  }
352
800
  ```
353
801
 
354
802
  ---
355
803
 
356
- ### Node.js / Express
804
+ ### Storage: File upload with progress
357
805
 
358
- ```ts
359
- // server.ts
360
- import express from 'express';
361
- import { createClient, HydrousError } from 'hydrousdb';
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
+ });
362
832
 
363
- const db = createClient({
364
- authKey: process.env.HYDROUS_AUTH_KEY!,
365
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
366
- });
367
- const app = express();
368
- app.use(express.json());
369
-
370
- app.get('/orders', async (req, res) => {
371
- try {
372
- const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
373
- res.json(result);
374
- } catch (err) {
375
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
376
- res.status(500).json({ error: 'Internal Server Error' });
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);
377
839
  }
378
- });
840
+ }
841
+ </script>
379
842
 
380
- app.listen(3000, () => console.log('Listening on :3000'));
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>
381
862
  ```
382
863
 
383
864
  ---
384
865
 
385
- ## API Reference
866
+ ### Records: Data table with pagination
386
867
 
387
- ### `createClient`
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';
388
874
 
389
- ```ts
390
- import { createClient } from 'hydrousdb';
875
+ type Product = { id: string; name: string; price: number; inStock: boolean };
391
876
 
392
- const db = createClient({
393
- authKey: string, // required — your HydrousDB auth key (for /auth routes)
394
- securityKey: string, // required — your HydrousDB security key (for records & analytics)
395
- baseUrl?: string, // override API base URL
396
- timeout?: number, // request timeout in ms (default: 30_000)
397
- retries?: number, // retries on network/5xx errors (default: 2)
398
- });
399
- ```
877
+ const products = ref<Product[]>([]);
878
+ const total = ref(0);
879
+ const page = ref(1);
880
+ const limit = 10;
881
+
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
+ });
400
889
 
401
- > **Note:** `bucketKey` is no longer part of the client config. It is passed as an option on each individual records and analytics call.
890
+ if (error) { console.error(error.message); return; }
891
+ products.value = data;
892
+ total.value = count;
893
+ page.value = p;
894
+ }
895
+
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>
914
+ ```
402
915
 
403
916
  ---
404
917
 
405
- ### Records
918
+ ### Vue: Batch file upload with per-file progress
406
919
 
407
- All methods live on `db.records`. Every method requires a `bucketKey` option.
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';
408
926
 
409
- | Method | Description |
410
- |---|---|
411
- | `get(recordId, { bucketKey, ...opts })` | Fetch a single record |
412
- | `getSnapshot(recordId, generation, { bucketKey, ...opts })` | Fetch a historical version |
413
- | `query({ bucketKey, ...opts })` | Query a collection with filters / pagination |
414
- | `queryAll({ bucketKey, ...opts })` | Fetch all pages automatically |
415
- | `insert(payload, { bucketKey, ...opts })` | Create a new record |
416
- | `update(payload, { bucketKey, ...opts })` | Update an existing record |
417
- | `delete(recordId, { bucketKey, ...opts })` | Delete a record |
418
- | `exists(recordId, { bucketKey, ...opts })` | HEAD check — returns metadata or `null` |
419
- | `batchInsert(payload, { bucketKey, ...opts })` | Insert up to 500 records |
420
- | `batchUpdate(payload, { bucketKey, ...opts })` | Update up to 500 records |
421
- | `batchDelete(payload, { bucketKey, ...opts })` | Delete up to 500 records |
927
+ type FileState = { name: string; percent: number; stage: string; done: boolean; error?: string };
422
928
 
423
- #### `db.records.get(recordId, options)`
929
+ const fileStates = ref<FileState[]>([]);
424
930
 
425
- ```ts
426
- const { data, history } = await db.records.get('rec_abc', {
427
- bucketKey: 'users',
428
- showHistory: true,
429
- });
430
- ```
931
+ async function onFilesChange(event: Event) {
932
+ const files = Array.from((event.target as HTMLInputElement).files ?? []);
933
+ if (!files.length) return;
431
934
 
432
- #### `db.records.query(options)`
935
+ fileStates.value = files.map(f => ({ name: f.name, percent: 0, stage: 'pending', done: false }));
433
936
 
434
- ```ts
435
- const { data, meta } = await db.records.query({
436
- bucketKey: 'orders',
437
- filters: [{ field: 'status', op: '==', value: 'active' }],
438
- timeScope: '30d',
439
- sortOrder: 'desc',
440
- limit: 50,
441
- cursor: meta.nextCursor ?? undefined,
442
- fields: 'id,name,status',
443
- });
937
+ await db.storage('documents').batchUpload(files, {
938
+ prefix: 'batch-uploads/',
939
+
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>
444
965
  ```
445
966
 
446
- **Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
447
- **Max 3 filters per query.**
967
+ ---
968
+
969
+ ## 9. Real-World Examples — SvelteKit
448
970
 
449
- #### `db.records.insert(payload, options)`
971
+ ### Shared client
450
972
 
451
973
  ```ts
452
- const { data, meta } = await db.records.insert(
453
- {
454
- values: { name: 'Alice', role: 'admin', score: 100 },
455
- queryableFields: ['name', 'role'],
456
- userEmail: 'system@example.com',
974
+ // src/lib/db.ts
975
+ 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,
457
993
  },
458
- { bucketKey: 'users' }
459
- );
460
- // meta.id — the new record's ID
994
+ });
461
995
  ```
462
996
 
463
- #### `db.records.update(payload, options)`
997
+ ### Server route — fetch records
464
998
 
465
999
  ```ts
466
- await db.records.update(
467
- {
468
- recordId: 'rec_abc',
469
- values: { score: 150 },
470
- track_record_history: true,
471
- reason: 'Manual score adjustment',
472
- },
473
- { bucketKey: 'users' }
474
- );
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
+ });
1011
+
1012
+ if (error) return json({ error: error.message }, { status: 500 });
1013
+ return json(data);
1014
+ }
475
1015
  ```
476
1016
 
477
- #### `db.records.batchInsert(payload, options)`
1017
+ ### Page load — server-side
478
1018
 
479
1019
  ```ts
480
- const result = await db.records.batchInsert(
481
- {
482
- records: [{ name: 'Alice', score: 99 }, { name: 'Bob', score: 82 }],
483
- queryableFields: ['name'],
484
- userEmail: 'import@example.com',
485
- },
486
- { bucketKey: 'users' }
487
- );
488
- // result.meta.failed — number of records that failed
1020
+ // src/routes/products/+page.server.ts
1021
+ import { db } from '$lib/db';
1022
+ import type { PageServerLoad } from './$types';
1023
+
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
+ });
1029
+
1030
+ if (error) throw error;
1031
+ return { products };
1032
+ };
489
1033
  ```
490
1034
 
491
- ---
1035
+ ### File upload component
492
1036
 
493
- ### Auth
494
-
495
- All methods live on `db.auth`. Auth routes use `authKey` and do **not** require a `bucketKey`.
496
-
497
- | Method | Description |
498
- |---|---|
499
- | `signUp(payload, opts?)` | Register a new user |
500
- | `signIn(payload, opts?)` | Authenticate with email + password |
501
- | `signOut(payload, opts?)` | Revoke session(s) |
502
- | `validateSession(sessionId, opts?)` | Verify a session is valid |
503
- | `refreshSession(refreshToken, opts?)` | Rotate session using refresh token |
504
- | `getUser(userId, opts?)` | Fetch user by ID |
505
- | `listUsers(opts?)` | Paginated user list |
506
- | `listAllUsers(opts?)` | All users (auto-paginates) |
507
- | `updateUser(payload, opts?)` | Update user profile |
508
- | `deleteUser(userId, opts?)` | Soft-delete a user |
509
- | `changePassword(payload, opts?)` | Change password (requires old password) |
510
- | `requestPasswordReset(payload, opts?)` | Trigger reset email |
511
- | `confirmPasswordReset(payload, opts?)` | Confirm reset with token |
512
- | `requestEmailVerification(payload, opts?)` | Trigger verification email |
513
- | `confirmEmailVerification(payload, opts?)` | Confirm email with token |
514
- | `lockAccount(payload, opts?)` | Lock account for a duration |
515
- | `unlockAccount(userId, opts?)` | Unlock account |
516
-
517
- #### Auth flow example
518
-
519
- ```ts
520
- // 1. Sign up
521
- const { data: user, session } = await db.auth.signUp({
522
- email: 'alice@example.com',
523
- password: 'Str0ng@Pass!',
524
- fullName: 'Alice Smith',
525
- });
1037
+ ```svelte
1038
+ <!-- src/lib/components/Uploader.svelte -->
1039
+ <script lang="ts">
1040
+ import { db } from '$lib/db';
1041
+ import type { UploadProgress } from 'hydrousdb';
526
1042
 
527
- // 2. Store session tokens
528
- const { sessionId, refreshToken, expiresAt } = session;
1043
+ let progress: UploadProgress | null = null;
1044
+ let uploadedPath = '';
1045
+ let errorMsg = '';
529
1046
 
530
- // 3. On each request, validate the session
531
- const { data: currentUser } = await db.auth.validateSession(sessionId);
1047
+ async function handleFile(event: Event) {
1048
+ const file = (event.target as HTMLInputElement).files?.[0];
1049
+ if (!file) return;
532
1050
 
533
- // 4. Before session expires, refresh it
534
- const { session: newSession } = await db.auth.refreshSession(refreshToken);
1051
+ errorMsg = '';
1052
+ uploadedPath = '';
1053
+ progress = null;
535
1054
 
536
- // 5. Sign out
537
- await db.auth.signOut({ sessionId });
538
- ```
1055
+ const { data, error } = await db.storage('documents').upload(file, {
1056
+ path: `uploads/${file.name}`,
1057
+ onProgress: (p) => { progress = p; },
1058
+ });
539
1059
 
540
- ---
1060
+ if (error) {
1061
+ errorMsg = error.message;
1062
+ } else {
1063
+ uploadedPath = data!.path;
1064
+ progress = null;
1065
+ }
1066
+ }
1067
+ </script>
541
1068
 
542
- ### Analytics
1069
+ <input type="file" on:change={handleFile} />
543
1070
 
544
- All methods live on `db.analytics`. Every method requires a `bucketKey` option.
1071
+ {#if progress}
1072
+ <p>{progress.stage} — {progress.percent}%</p>
1073
+ <progress value={progress.percent} max="100" />
1074
+ {/if}
545
1075
 
546
- | Method | Server `queryType` | Description |
547
- |---|---|---|
548
- | `count({ bucketKey, ...opts })` | `"count"` | Total record count |
549
- | `distribution(field, { bucketKey, ...opts })` | `"distribution"` | Value histogram for a field |
550
- | `sum(field, { bucketKey, ...opts })` | `"sum"` | Sum a numeric field, with optional group-by |
551
- | `timeSeries({ bucketKey, ...opts })` | `"timeSeries"` | Record count over time |
552
- | `fieldTimeSeries(field, { bucketKey, ...opts })` | `"fieldTimeSeries"` | Numeric field aggregated over time |
553
- | `topN(field, n, { bucketKey, ...opts })` | `"topN"` | Top N field values by frequency |
554
- | `stats(field, { bucketKey, ...opts })` | `"stats"` | min/max/avg/stddev/p50/p90/p99 |
555
- | `records({ bucketKey, ...opts })` | `"records"` | Filtered + paginated raw records |
556
- | `multiMetric(metrics, { bucketKey, ...opts })` | `"multiMetric"` | Multiple aggregations in one call |
557
- | `storageStats({ bucketKey, ...opts })` | `"storageStats"` | Bucket storage usage |
558
- | `crossBucket({ bucketKey, bucketKeys, ...opts })` | `"crossBucket"` | Compare metric across buckets |
559
- | `query(payload, { bucketKey, ...opts })` | any | Raw query — full control |
1076
+ {#if uploadedPath}
1077
+ <p>✓ Saved at: {uploadedPath}</p>
1078
+ {/if}
560
1079
 
561
- #### Date ranges
1080
+ {#if errorMsg}
1081
+ <p style="color:red">{errorMsg}</p>
1082
+ {/if}
1083
+ ```
1084
+
1085
+ ---
562
1086
 
563
- All analytics methods accept a `dateRange` option:
1087
+ ## 10. Real-World Examples Express / Node API
1088
+
1089
+ ### Setup
564
1090
 
565
1091
  ```ts
566
- const dateRange = { startDate: '2025-01-01', endDate: '2025-12-31' };
567
- // Or scope to a year / month
568
- const dateRange = { year: '2025', month: '03' };
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
+ });
569
1105
  ```
570
1106
 
571
- #### Examples
1107
+ ### Upload from a file buffer (Node)
572
1108
 
573
1109
  ```ts
574
- // Total records
575
- const { data } = await db.analytics.count({ bucketKey: 'orders' });
576
- console.log(data.count);
1110
+ // routes/upload.ts
1111
+ import express from 'express';
1112
+ import multer from 'multer';
1113
+ import { db } from '../db';
577
1114
 
578
- // Status breakdown
579
- const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
580
- // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
1115
+ const router = express.Router();
1116
+ const upload = multer({ storage: multer.memoryStorage() });
581
1117
 
582
- // Monthly revenue for Q1
583
- const { data } = await db.analytics.sum('amount', {
584
- bucketKey: 'orders',
585
- groupBy: 'region',
586
- dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
587
- });
1118
+ router.post('/upload', upload.single('file'), async (req, res) => {
1119
+ if (!req.file) return res.status(400).json({ error: 'No file' });
588
1120
 
589
- // Daily signups this week
590
- const { data } = await db.analytics.timeSeries({
591
- bucketKey: 'users',
592
- granularity: 'day',
593
- dateRange: { startDate: '2025-03-01' },
594
- });
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
+ );
595
1128
 
596
- // Score percentiles
597
- const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
598
- console.log(data.avg, data.p90, data.p99);
599
-
600
- // Top 5 countries
601
- const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
602
-
603
- // Dashboard stats — one round-trip for multiple metrics
604
- const { data } = await db.analytics.multiMetric(
605
- [
606
- { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
607
- { name: 'avgScore', field: 'score', aggregation: 'avg' },
608
- { name: 'userCount', field: 'userId', aggregation: 'count' },
609
- ],
610
- { bucketKey: 'orders' }
611
- );
612
- console.log(data.totalRevenue, data.avgScore);
613
-
614
- // Storage usage
615
- const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
616
- console.log(data.totalRecords, data.totalBytes);
617
-
618
- // Cross-bucket revenue comparison
619
- const { data } = await db.analytics.crossBucket({
620
- bucketKey: 'sales-2025',
621
- bucketKeys: ['sales-2024', 'sales-2025'],
622
- field: 'amount',
623
- aggregation: 'sum',
1129
+ if (error) return res.status(500).json({ error: error.message });
1130
+ res.json({ path: data!.path, size: data!.storedSize });
624
1131
  });
625
1132
 
626
- // Raw records with filters
627
- const { data } = await db.analytics.records({
628
- bucketKey: 'users',
629
- filters: [{ field: 'role', op: '==', value: 'admin' }],
630
- selectFields: ['email', 'createdAt'],
631
- limit: 50,
632
- });
633
- console.log(data.data, data.hasMore);
1133
+ export default router;
634
1134
  ```
635
1135
 
636
- ---
637
-
638
- ## Error Handling
1136
+ ### Save JSON reports to storage
639
1137
 
640
1138
  ```ts
641
- import { HydrousError, HydrousNetworkError, HydrousTimeoutError } from 'hydrousdb';
1139
+ // jobs/generateReport.ts
1140
+ import { db } from '../db';
642
1141
 
643
- try {
644
- await db.records.get('rec_does_not_exist', { bucketKey: 'users' });
645
- } catch (err) {
646
- if (err instanceof HydrousError) {
647
- console.error(err.message); // human-readable message
648
- console.error(err.status); // HTTP status code (e.g. 404)
649
- console.error(err.code); // machine-readable code (e.g. 'NOT_FOUND')
650
- console.error(err.details); // validation error details array
651
- console.error(err.requestId); // server request ID for support
652
- } else if (err instanceof HydrousTimeoutError) {
653
- console.error('Request timed out');
654
- } else if (err instanceof HydrousNetworkError) {
655
- console.error('Network failure:', err.cause);
656
- }
657
- }
658
- ```
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
+ });
659
1147
 
660
- ---
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}`);
661
1165
 
662
- ## Pagination
1166
+ // 4. Track the event
1167
+ await db.analytics.track({
1168
+ event: 'report_generated',
1169
+ properties: { month, totalOrders: report.totalOrders },
1170
+ });
1171
+
1172
+ return data!.path;
1173
+ }
1174
+ ```
663
1175
 
664
- HydrousDB uses opaque cursor strings for pagination.
1176
+ ### Batch delete old records
665
1177
 
666
1178
  ```ts
667
- // Manual pagination
668
- let cursor: string | null = null;
1179
+ // scripts/cleanup.ts
1180
+ import { db } from '../db';
1181
+ import { lt } from 'hydrousdb';
1182
+
1183
+ async function cleanupOldSessions() {
1184
+ const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
669
1185
 
670
- do {
671
- const { data, meta } = await db.records.query({
672
- bucketKey: 'orders',
673
- limit: 100,
674
- cursor: cursor ?? undefined,
1186
+ // Fetch old sessions
1187
+ const { data: old } = await db.records.select('sessions', {
1188
+ where: lt('createdAt', cutoff),
1189
+ limit: 500,
675
1190
  });
676
1191
 
677
- processBatch(data);
678
- cursor = meta.nextCursor;
679
- } while (cursor);
1192
+ if (!old.length) { console.log('Nothing to clean up'); return; }
680
1193
 
681
- // OR — let the SDK handle it
682
- const allRecords = await db.records.queryAll({
683
- bucketKey: 'orders',
684
- filters: [{ field: 'status', op: '==', value: 'active' }],
685
- });
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
+ }
1198
+
1199
+ cleanupOldSessions();
686
1200
  ```
687
1201
 
688
1202
  ---
689
1203
 
690
- ## TypeScript
1204
+ ## 11. TypeScript Types Reference
691
1205
 
692
- All types are exported from the root package:
1206
+ ### `HydrousConfig`
693
1207
 
694
1208
  ```ts
695
- import type {
696
- HydrousRecord,
697
- QueryOptions,
698
- InsertRecordPayload,
699
- AuthUser,
700
- SessionInfo,
701
- HydrousConfig,
702
- BucketOptions,
703
- Filter,
704
- } from 'hydrousdb';
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
+ }
705
1216
  ```
706
1217
 
707
- Use the generic `query<T>` overload to type your records:
1218
+ ### `UploadProgress`
708
1219
 
709
1220
  ```ts
710
- interface Product {
711
- name: string;
712
- price: number;
713
- category: string;
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;
714
1234
  }
715
-
716
- const { data } = await db.records.query<Product>({
717
- bucketKey: 'products',
718
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
719
- });
720
-
721
- // data is (Product & HydrousRecord)[]
722
- console.log(data[0].name, data[0].price);
723
1235
  ```
724
1236
 
725
- ---
726
-
727
- ## Migration from v1
728
-
729
- v2 introduces two breaking changes:
730
-
731
- ### 1. `bucketKey` moves from `createClient` to per-call options
1237
+ ### `UploadResult`
732
1238
 
733
1239
  ```ts
734
- // v1
735
- const db = createClient({ authKey: 'ak_...', bucketKey: 'users' });
736
- await db.records.query({ limit: 50 });
737
-
738
- // v2
739
- const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
740
- await db.records.query({ bucketKey: 'users', limit: 50 });
1240
+ interface UploadResult {
1241
+ path: string;
1242
+ compressed: boolean;
1243
+ originalSize: number;
1244
+ storedSize: number;
1245
+ spaceSaved: number;
1246
+ mimeType: string;
1247
+ }
741
1248
  ```
742
1249
 
743
- ### 2. `securityKey` replaces the implicit credential
744
-
745
- In v1 the SDK incorrectly used `bucketKey` as both the bucket identifier and the security credential in the URL. v2 adds an explicit `securityKey` that maps to the server's `:key` parameter.
1250
+ ### `StorageItem`
746
1251
 
747
1252
  ```ts
748
- // v1 (broken — used bucketKey as both segments)
749
- createClient({ authKey: 'ak_...', bucketKey: 'users' });
750
- // → /api/users/users
751
-
752
- // v2 (correct)
753
- createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
754
- // → /api/users/sk_... ✅ (bucketKey passed per-call)
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
+ }
755
1262
  ```
756
1263
 
757
- **Quick migration checklist:**
758
- 1. Add `securityKey` to your `createClient` call
759
- 2. Remove `bucketKey` from `createClient`
760
- 3. Add `{ bucketKey: '...' }` to every `db.records.*` and `db.analytics.*` call
761
- 4. Auth calls (`db.auth.*`) are unchanged
1264
+ ### `StorageStats`
762
1265
 
763
- ---
764
-
765
- ## Contributing
766
-
767
- 1. Fork the repo and clone it
768
- 2. `npm install`
769
- 3. `npm run dev` — TypeScript watch mode
770
- 4. `npm test` — run the test suite
771
- 5. Open a PR 🚀
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
+ }
1279
+ ```
772
1280
 
773
1281
  ---
774
1282