hydrousdb 2.0.1 → 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,682 +1,1237 @@
1
- # Hydrous SDK
1
+ # HydrousDB SDK
2
2
 
3
- Official JavaScript / TypeScript SDK for the **Hydrous** platform.
4
- One packageauth, records, analytics, and storage.
3
+ Official JavaScript / TypeScript SDK for the **HydrousDB** platform.
4
+ Auth · Records · Analytics · Storage one package, one client.
5
5
 
6
6
  ```bash
7
- npm install hydrous
7
+ npm install hydrousdb
8
8
  ```
9
9
 
10
10
  ---
11
11
 
12
12
  ## Table of Contents
13
13
 
14
- 1. [Quick Start](#1-quick-start)
15
- 2. [Configuration](#2-configuration)
16
- 3. [Auth](#3-auth)
17
- - [Sign Up](#sign-up)
18
- - [Sign In](#sign-in)
19
- - [Sign Out](#sign-out)
20
- - [Get Current User](#get-current-user)
21
- 4. [Records](#4-records)
22
- - [Insert](#insert)
23
- - [Select / Query](#select--query)
24
- - [Get by ID](#get-by-id)
25
- - [Update](#update)
26
- - [Delete](#delete)
27
- - [Query Helpers](#query-helpers)
28
- 5. [Analytics](#5-analytics)
29
- - [Track an Event](#track-an-event)
30
- - [Batch Track](#batch-track)
31
- - [Query Events](#query-events)
32
- 6. [Storage](#6-storage)
33
- - [How Bucket Keys Work](#how-bucket-keys-work)
34
- - [Upload a File](#upload-a-file)
35
- - [Upload Raw Text / JSON](#upload-raw-text--json)
36
- - [Track Upload Progress](#track-upload-progress)
37
- - [Batch Upload](#batch-upload)
38
- - [Download a File](#download-a-file)
39
- - [Batch Download](#batch-download)
40
- - [List Files & Folders](#list-files--folders)
41
- - [File Metadata](#file-metadata)
42
- - [Delete a File](#delete-a-file)
43
- - [Delete a Folder](#delete-a-folder)
44
- - [Create a Folder](#create-a-folder)
45
- - [Move a File](#move-a-file)
46
- - [Copy a File](#copy-a-file)
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)
47
25
  - [Signed URLs](#signed-urls)
48
- - [Bucket Stats](#bucket-stats)
49
- 7. [Error Handling](#7-error-handling)
50
- 8. [TypeScript Types Reference](#8-typescript-types-reference)
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)
51
42
 
52
43
  ---
53
44
 
54
- ## 1. Quick Start
45
+ ## 1. Setup & Client Configuration
55
46
 
56
- ```ts
57
- import { createClient } from 'hydrous';
47
+ Create **one** client and reuse it across your app.
58
48
 
59
- const hydrous = createClient({
60
- url: 'https://api.yourapp.hydrous.app',
61
- apiKey: 'hk_live_…',
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
62
  });
63
-
64
- // Upload a file
65
- const { data, error } = await hydrous.storage.upload(
66
- 'ssk_my_bucket_key', // ← bucket key always comes first
67
- file,
68
- {
69
- path: 'avatars/alice.jpg',
70
- onProgress: (p) => console.log(`${p.percent}%`),
71
- }
72
- );
73
63
  ```
74
64
 
75
- ---
65
+ ### Key types explained
76
66
 
77
- ## 2. Configuration
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 |
78
72
 
79
- ```ts
80
- import { HydrousClient } from 'hydrous';
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.
81
76
 
82
- const hydrous = new HydrousClient({
83
- url: 'https://api.yourapp.hydrous.app', // your project URL
84
- apiKey: 'hk_live_…', // your project API key
85
- timeout: 30_000, // optional — ms (default 30s)
86
- });
77
+ ### Environment variables (recommended)
78
+
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
  ```
88
88
 
89
- | Option | Type | Required | Description |
90
- |-----------|----------|----------|--------------------------------------|
91
- | `url` | `string` | ✅ | Your Hydrous project base URL |
92
- | `apiKey` | `string` | ✅ | Your project API key |
93
- | `timeout` | `number` | ✗ | Request timeout in ms (default 30000) |
89
+ ```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
+ ```
94
104
 
95
105
  ---
96
106
 
97
- ## 3. Auth
107
+ ## 2. Auth
98
108
 
99
109
  ### Sign Up
100
110
 
101
111
  ```ts
102
- const { data, error } = await hydrous.auth.signUp({
103
- email: 'user@example.com',
112
+ const { data, error } = await db.auth.signUp({
113
+ email: 'alice@example.com',
104
114
  password: 'supersecret',
105
- metadata: { plan: 'pro' }, // optional
115
+ metadata: { plan: 'pro', referral: 'google' },
106
116
  });
107
117
 
108
- if (data) {
109
- console.log('New user:', data.user.id);
110
- console.log('Access token:', data.accessToken);
111
- }
118
+ if (error) console.error(error.message);
119
+ if (data) console.log('New user:', data.user.id);
112
120
  ```
113
121
 
114
122
  ### Sign In
115
123
 
116
124
  ```ts
117
- const { data, error } = await hydrous.auth.signIn({
118
- email: 'user@example.com',
125
+ const { data, error } = await db.auth.signIn({
126
+ email: 'alice@example.com',
119
127
  password: 'supersecret',
120
128
  });
121
129
 
122
130
  if (data) {
123
- console.log('Welcome back,', data.user.email);
131
+ localStorage.setItem('token', data.accessToken);
124
132
  }
125
133
  ```
126
134
 
127
135
  ### Sign Out
128
136
 
129
137
  ```ts
130
- await hydrous.auth.signOut();
138
+ await db.auth.signOut();
139
+ localStorage.removeItem('token');
131
140
  ```
132
141
 
133
- ### Get Current User
142
+ ### Get current user
134
143
 
135
144
  ```ts
136
- const { data: user } = await hydrous.auth.getUser();
145
+ const { data: user } = await db.auth.getUser();
137
146
  console.log(user?.email);
138
147
  ```
139
148
 
140
- ### Refresh Session
149
+ ### Refresh session
141
150
 
142
151
  ```ts
143
- const { data: session } = await hydrous.auth.refreshSession();
152
+ const { data: session } = await db.auth.refreshSession();
144
153
  ```
145
154
 
146
155
  ---
147
156
 
148
- ## 4. Records
157
+ ## 3. Records
149
158
 
150
159
  ### Insert
151
160
 
152
- Single record:
153
-
154
161
  ```ts
155
- const { data, error } = await hydrous.records.insert('users', {
156
- name: 'Alice',
157
- email: 'alice@example.com',
158
- role: 'admin',
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,
159
168
  });
160
- ```
161
-
162
- Bulk insert (array):
163
169
 
164
- ```ts
165
- const { data } = await hydrous.records.insert('products', [
166
- { name: 'Widget A', price: 9.99 },
167
- { name: 'Widget B', price: 14.99 },
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 },
168
174
  ]);
169
- console.log(`Inserted ${data.length} products`);
175
+ console.log(`Inserted ${count} products`);
170
176
  ```
171
177
 
172
- ### Select / Query
178
+ ### Select (query)
173
179
 
174
180
  ```ts
175
- const { data, count } = await hydrous.records.select('users', {
176
- where: { field: 'role', operator: 'eq', value: 'admin' },
177
- orderBy: { field: 'createdAt', direction: 'desc' },
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' },
178
186
  limit: 20,
179
187
  offset: 0,
180
- select: ['id', 'name', 'email'], // optional column projection
188
+ select: ['id', 'name', 'price'], // return only these fields
181
189
  });
182
190
  ```
183
191
 
184
- Multiple filters:
192
+ ### Get one by ID
185
193
 
186
194
  ```ts
187
- import { eq, gt, inArray } from 'hydrous';
188
-
189
- const { data } = await hydrous.records.select('orders', {
190
- where: [
191
- eq('status', 'shipped'),
192
- gt('total', 100),
193
- inArray('tag', ['vip', 'priority']),
194
- ],
195
- });
196
- ```
197
-
198
- ### Get by ID
199
-
200
- ```ts
201
- const { data: user } = await hydrous.records.get('users', 'user_abc123');
195
+ const { data: product } = await db.records.get('products', 'prod_abc123');
202
196
  ```
203
197
 
204
198
  ### Update
205
199
 
206
200
  ```ts
207
- const { data } = await hydrous.records.update('users', 'user_abc123', {
208
- name: 'Alice Smith',
201
+ const { data } = await db.records.update('products', 'prod_abc123', {
202
+ price: 59.99,
203
+ inStock: false,
209
204
  });
210
205
  ```
211
206
 
212
207
  ### Delete
213
208
 
214
209
  ```ts
215
- const { error } = await hydrous.records.delete('users', 'user_abc123');
216
- ```
217
-
218
- ### Query Helpers
219
-
220
- | Helper | SQL equivalent |
221
- |---------------------|------------------------|
222
- | `eq(field, val)` | `field = val` |
223
- | `neq(field, val)` | `field != val` |
224
- | `gt(field, val)` | `field > val` |
225
- | `lt(field, val)` | `field < val` |
226
- | `inArray(field, [])` | `field IN (…)` |
227
-
228
- ```ts
229
- import { eq, gt, inArray } from 'hydrous';
210
+ const { error } = await db.records.delete('products', 'prod_abc123');
230
211
  ```
231
212
 
232
213
  ---
233
214
 
234
- ## 5. Analytics
215
+ ## 4. Analytics
235
216
 
236
- ### Track an Event
217
+ ### Track an event
237
218
 
238
219
  ```ts
239
- await hydrous.analytics.track({
240
- event: 'page_view',
241
- properties: { page: '/home', referrer: 'google.com' },
220
+ await db.analytics.track({
221
+ event: 'purchase_completed',
242
222
  userId: 'user_abc123',
243
223
  sessionId: 'sess_xyz',
224
+ properties: {
225
+ orderId: 'ord_001',
226
+ total: 99.99,
227
+ currency: 'USD',
228
+ itemCount: 3,
229
+ },
244
230
  });
245
231
  ```
246
232
 
247
- ### Batch Track
248
-
249
- More efficient than calling `track()` in a loop:
233
+ ### Track many events at once
250
234
 
251
235
  ```ts
252
- await hydrous.analytics.trackBatch([
253
- { event: 'signup', userId: 'u1' },
254
- { event: 'onboarded', userId: 'u1', properties: { step: 'profile' } },
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' } },
255
240
  ]);
256
241
  ```
257
242
 
258
- ### Query Events
243
+ ### Query events
259
244
 
260
245
  ```ts
261
- const { data, count } = await hydrous.analytics.query({
262
- event: 'page_view',
263
- from: '2024-01-01',
264
- to: '2024-01-31',
265
- limit: 500,
266
- groupBy: 'properties.page',
246
+ const { data } = await db.analytics.query({
247
+ event: 'purchase_completed',
248
+ from: '2024-01-01',
249
+ to: '2024-01-31',
250
+ limit: 500,
267
251
  });
268
252
  ```
269
253
 
270
254
  ---
271
255
 
272
- ## 6. Storage
273
-
274
- The storage module handles all file operations. Every method takes a **bucket key** as its **first argument** — a string that begins with `ssk_`.
256
+ ## 5. Storage
275
257
 
276
- ### How Bucket Keys Work
258
+ ### How storage keys work
277
259
 
278
- A bucket key (`ssk_…`) is a scoped credential that grants specific permissions (read / write / delete) to a bucket. You create them in the Hydrous dashboard.
260
+ `db.storage` is a function call it with the **name** of the key you configured:
279
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
280
266
  ```
281
- hydrous.storage.<method>(
282
- 'ssk_your_bucket_key', // first, always a string
283
- ...args
284
- )
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/' });
285
276
  ```
286
277
 
287
278
  ---
288
279
 
289
- ### Upload a File
280
+ ### Upload a file with progress
290
281
 
291
282
  ```ts
292
- const { data, error } = await hydrous.storage.upload(
293
- 'ssk_my_bucket_key',
294
- file, // File | Blob | Uint8Array | ArrayBuffer
295
- {
296
- path: 'avatars/alice.jpg', // destination path in your bucket
297
- overwrite: true, // replace if exists (default: false)
298
- onProgress: (progress) => {
299
- console.log(progress.stage, progress.percent + '%');
300
- },
301
- }
302
- );
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
+ });
303
298
 
304
299
  if (data) {
305
- console.log('Stored at:', data.path);
306
- console.log('Space saved:', data.spaceSaved, 'bytes');
300
+ console.log('Path:', data.path);
301
+ console.log('Space saved by compression:', data.spaceSaved, 'bytes');
307
302
  }
308
303
  ```
309
304
 
310
- ---
305
+ ### Batch upload
311
306
 
312
- ### Upload Raw Text / JSON
307
+ ```ts
308
+ const files = Array.from(fileInput.files);
313
309
 
314
- No `File` object needed pass any string directly.
310
+ const { data } = await db.storage('documents').batchUpload(files, {
311
+ prefix: 'uploads/2024/',
312
+ overwrite: false,
313
+ concurrency: 5,
315
314
 
316
- ```ts
317
- // Upload a plain text file
318
- await hydrous.storage.uploadText(
319
- 'ssk_my_bucket_key',
320
- 'reports/summary.txt',
321
- 'Monthly report content…',
322
- { mimeType: 'text/plain' }
323
- );
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
+ });
324
320
 
325
- // Upload JSON
326
- await hydrous.storage.uploadText(
327
- 'ssk_my_bucket_key',
328
- 'data/config.json',
329
- JSON.stringify({ theme: 'dark', lang: 'en' }),
330
- { mimeType: 'application/json' }
331
- );
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));
332
324
  ```
333
325
 
334
- ---
326
+ ### Download a file
335
327
 
336
- ### Track Upload Progress
328
+ ```ts
329
+ const { data: buffer, error } = await db.storage('documents').download('reports/q1.pdf');
337
330
 
338
- `onProgress` is called on every progress tick with a rich `UploadProgress` object.
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
339
348
 
340
349
  ```ts
341
- await hydrous.storage.upload(
342
- 'ssk_my_bucket_key',
343
- file,
344
- {
345
- onProgress: (p) => {
346
- // p.stage — 'pending' | 'compressing' | 'uploading' | 'done' | 'error'
347
- // p.percent — 0–100 integer
348
- // p.bytesUploaded — bytes sent so far
349
- // p.totalBytes — total bytes to send
350
- // p.bytesPerSecond — current speed (null before first tick)
351
- // p.eta — estimated seconds remaining (null until speed is known)
352
- // p.index — file index (always 0 for single uploads)
353
- // p.total — total files (always 1 for single uploads)
354
-
355
- switch (p.stage) {
356
- case 'pending':
357
- console.log('Queued');
358
- break;
359
- case 'compressing':
360
- console.log('Compressing…');
361
- break;
362
- case 'uploading':
363
- console.log(`Uploading ${p.percent}% — ${formatSpeed(p.bytesPerSecond)} — ETA ${p.eta}s`);
364
- break;
365
- case 'done':
366
- console.log('✅ Done!', p.result);
367
- break;
368
- case 'error':
369
- console.error('❌ Error:', p.error);
370
- break;
371
- }
372
- },
373
- }
374
- );
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
+ }
375
359
 
376
- function formatSpeed(bps: number | null): string {
377
- if (!bps) return '—';
378
- if (bps > 1_000_000) return `${(bps / 1_000_000).toFixed(1)} MB/s`;
379
- if (bps > 1_000) return `${(bps / 1_000).toFixed(0)} KB/s`;
380
- return `${bps} B/s`;
360
+ // Paginate
361
+ if (data!.pagination.hasNextPage) {
362
+ const page2 = await db.storage('documents').list({
363
+ prefix: 'invoices/',
364
+ cursor: data!.pagination.nextCursor!,
365
+ });
381
366
  }
382
367
  ```
383
368
 
384
- **Stage lifecycle:**
369
+ ### Delete, move, copy
385
370
 
386
- ```
387
- pending compressing uploading → done
388
- error
389
- ```
371
+ ```ts
372
+ // Delete a single file
373
+ await db.storage('avatars').deleteFile('users/old-photo.jpg');
390
374
 
391
- > **Note:** In browsers, upload progress (bytes leaving the NIC) is tracked via
392
- > `XMLHttpRequest`. The final `done` stage fires only after the server confirms
393
- > the write to cloud storage — so `100%` means the file is truly saved.
375
+ // Recursively delete a folder
376
+ await db.storage('temp').deleteFolder('uploads/draft/');
394
377
 
395
- ---
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');
396
383
 
397
- ### Batch Upload
384
+ // Copy a file (original kept)
385
+ await db.storage('documents').copy('templates/invoice.pdf', 'invoices/inv-001.pdf');
386
+ ```
398
387
 
399
- Upload many files in one request. Progress fires per-file so you can render
400
- individual progress bars.
388
+ ### Signed URLs
401
389
 
402
390
  ```ts
403
- const inputFiles = Array.from(fileInput.files); // FileList array
404
-
405
- const { data, error } = await hydrous.storage.batchUpload(
406
- 'ssk_my_bucket_key',
407
- inputFiles,
408
- {
409
- prefix: 'uploads/2024/', // prepended to each filename
410
- overwrite: false,
411
- concurrency: 5, // max parallel uploads on the server (1–10)
412
- onProgress: (p) => {
413
- // p.index identifies WHICH file this event is for (0-based)
414
- console.log(`File ${p.index} (${p.path}): ${p.stage} ${p.percent}%`);
415
- },
416
- }
391
+ // Generate a link that expires in 10 minutes
392
+ const { data } = await db.storage('documents').signedUrl(
393
+ 'contracts/nda.pdf',
394
+ { expiresIn: 600 }
417
395
  );
418
396
 
419
- console.log('Succeeded:', data.succeeded.length);
420
- console.log('Failed:', data.failed.length);
397
+ console.log(data!.signedUrl); // share this URL
398
+ console.log(data!.expiresAt); // ISO timestamp when it expires
421
399
  ```
422
400
 
423
- Per-file paths override:
401
+ ### Upload raw text / JSON
424
402
 
425
403
  ```ts
426
- await hydrous.storage.batchUpload('ssk_key', files, {
427
- paths: [
428
- 'documents/report-q1.pdf',
429
- 'documents/report-q2.pdf',
430
- ],
431
- });
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
+ );
432
410
  ```
433
411
 
434
- > All files are validated upfront before any uploads begin — if your quota
435
- > would be exceeded the entire batch is rejected cleanly with no partial writes.
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
+ ```
436
421
 
437
422
  ---
438
423
 
439
- ### Download a File
424
+ ## 6. Error Handling
440
425
 
441
- Returns the file as an `ArrayBuffer`.
426
+ Every method returns `{ data, error }`. `error` is `null` on success.
442
427
 
443
428
  ```ts
444
- const { data: buffer, error } = await hydrous.storage.download(
445
- 'ssk_my_bucket_key',
446
- 'avatars/alice.jpg'
447
- );
448
-
449
- if (buffer) {
450
- // Browser: display image
451
- const blob = new Blob([buffer], { type: 'image/jpeg' });
452
- img.src = URL.createObjectURL(blob);
429
+ const { data, error } = await db.storage('avatars').upload(file);
453
430
 
454
- // Node: write to disk
455
- import { writeFileSync } from 'fs';
456
- writeFileSync('alice.jpg', Buffer.from(buffer));
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)
457
435
  }
458
436
  ```
459
437
 
460
- ---
461
-
462
- ### Batch Download
438
+ ### Catching thrown errors
463
439
 
464
440
  ```ts
465
- const { data: files } = await hydrous.storage.batchDownload(
466
- 'ssk_my_bucket_key',
467
- ['reports/jan.pdf', 'reports/feb.pdf', 'reports/mar.pdf'],
468
- {
469
- concurrency: 3,
470
- autoSave: true, // browser: auto-triggers Save dialog per file
471
- onProgress: (p) => {
472
- console.log(`${p.path}: ${p.status}`); // 'pending' | 'success' | 'error'
473
- },
474
- }
475
- );
441
+ import { isHydrousError } from 'hydrousdb';
476
442
 
477
- // files[n].content — ArrayBuffer
478
- // files[n].mimeType — string
479
- // files[n].path — original path
480
- // files[n].size — bytes
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
+ }
481
450
  ```
482
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
+
483
466
  ---
484
467
 
485
- ### List Files & Folders
468
+ ## 7. Real-World Examples — Next.js
469
+
470
+ ### Shared client (lib/db.ts)
486
471
 
487
472
  ```ts
488
- const { data } = await hydrous.storage.list('ssk_my_bucket_key', {
489
- prefix: 'avatars/', // list inside a folder — omit for root
490
- limit: 50, // max items per page (1–100)
473
+ // lib/db.ts
474
+ import { createClient } from 'hydrousdb';
475
+
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
+ },
491
484
  });
485
+ ```
492
486
 
493
- for (const item of data.items) {
494
- if (item.type === 'folder') {
495
- console.log('📁', item.path);
496
- } else {
497
- console.log('📄', item.path, item.size, 'bytes');
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');
498
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
+ );
499
535
  }
536
+ ```
500
537
 
501
- // Paginate
502
- if (data.pagination.hasNextPage) {
503
- const page2 = await hydrous.storage.list('ssk_my_bucket_key', {
504
- prefix: 'avatars/',
505
- cursor: data.pagination.nextCursor,
538
+ ---
539
+
540
+ ### Records: Product listing with filters
541
+
542
+ ```tsx
543
+ // app/products/page.tsx
544
+ import { db } from '@/lib/db';
545
+ import { eq } from 'hydrousdb';
546
+
547
+ type Product = { id: string; name: string; price: number; inStock: boolean };
548
+
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,
506
554
  });
555
+
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
+ );
507
567
  }
508
568
  ```
509
569
 
510
570
  ---
511
571
 
512
- ### File Metadata
572
+ ### Storage: Avatar upload with live progress bar
513
573
 
514
- ```ts
515
- const { data: meta } = await hydrous.storage.metadata(
516
- 'ssk_my_bucket_key',
517
- 'avatars/alice.jpg'
518
- );
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';
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; }
519
601
 
520
- console.log(meta.size); // stored bytes
521
- console.log(meta.originalSize); // bytes before compression
522
- console.log(meta.mimeType);
523
- console.log(meta.isCompressed);
524
- console.log(meta.updatedAt);
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
+ }
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
+ );
632
+ }
525
633
  ```
526
634
 
527
635
  ---
528
636
 
529
- ### Delete a File
637
+ ### Storage: Secure file download link (API route)
530
638
 
531
639
  ```ts
532
- const { error } = await hydrous.storage.deleteFile(
533
- 'ssk_my_bucket_key',
534
- 'avatars/old-photo.jpg'
535
- );
640
+ // app/api/download/route.ts
641
+ import { NextRequest, NextResponse } from 'next/server';
642
+ import { db } from '@/lib/db';
643
+
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 });
647
+
648
+ // Generate a short-lived signed URL (2 minutes)
649
+ const { data, error } = await db.storage('documents').signedUrl(filePath, {
650
+ expiresIn: 120,
651
+ });
652
+
653
+ if (error) return NextResponse.json({ error: error.message }, { status: 500 });
654
+
655
+ return NextResponse.json({ url: data!.signedUrl, expiresAt: data!.expiresAt });
656
+ }
657
+ ```
658
+
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');
536
664
  ```
537
665
 
538
666
  ---
539
667
 
540
- ### Delete a Folder
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
+ ```
541
694
 
542
- Recursively deletes the folder and everything inside it.
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
+ ```
710
+
711
+ ---
712
+
713
+ ### Records: Server Action — create a product
543
714
 
544
715
  ```ts
545
- await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
716
+ // app/products/actions.ts
717
+ 'use server';
718
+ import { db } from '@/lib/db';
719
+ import { revalidatePath } from 'next/cache';
720
+
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;
725
+
726
+ const { data, error } = await db.records.insert('products', {
727
+ name, price, category, inStock: true,
728
+ });
729
+
730
+ if (error) throw new Error(error.message);
731
+
732
+ revalidatePath('/products');
733
+ return data[0];
734
+ }
546
735
  ```
547
736
 
548
737
  ---
549
738
 
550
- ### Create a Folder
739
+ ## 8. Real-World Examples — Vue 3
740
+
741
+ ### Shared client (src/lib/db.ts)
551
742
 
552
743
  ```ts
553
- await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
744
+ // src/lib/db.ts
745
+ import { createClient } from 'hydrousdb';
746
+
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
+ },
755
+ });
554
756
  ```
555
757
 
556
758
  ---
557
759
 
558
- ### Move a File
760
+ ### Auth: Login composable
559
761
 
560
762
  ```ts
561
- await hydrous.storage.move(
562
- 'ssk_my_bucket_key',
563
- 'drafts/report.pdf',
564
- 'published/report.pdf'
565
- );
763
+ // composables/useAuth.ts
764
+ import { ref } from 'vue';
765
+ import { useRouter } from 'vue-router';
766
+ import { db } from '@/lib/db';
767
+
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();
773
+
774
+ async function login(email: string, password: string) {
775
+ loading.value = true;
776
+ error.value = null;
777
+
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
+ }
787
+
788
+ loading.value = false;
789
+ }
790
+
791
+ async function logout() {
792
+ await db.auth.signOut();
793
+ user.value = null;
794
+ localStorage.removeItem('token');
795
+ router.push('/login');
796
+ }
797
+
798
+ return { user, loading, error, login, logout };
799
+ }
566
800
  ```
567
801
 
568
802
  ---
569
803
 
570
- ### Copy a File
804
+ ### Storage: File upload with progress
571
805
 
572
- ```ts
573
- await hydrous.storage.copy(
574
- 'ssk_my_bucket_key',
575
- 'templates/invoice.pdf',
576
- 'invoices/invoice-001.pdf'
577
- );
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>
578
862
  ```
579
863
 
580
864
  ---
581
865
 
582
- ### Signed URLs
866
+ ### Records: Data table with pagination
583
867
 
584
- Generate a time-limited public URL for a private file.
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';
585
874
 
586
- ```ts
587
- const { data } = await hydrous.storage.signedUrl(
588
- 'ssk_my_bucket_key',
589
- 'contracts/agreement.pdf',
590
- { expiresIn: 300 } // 5 minutes (default: 3600 = 1 hour)
591
- );
875
+ type Product = { id: string; name: string; price: number; inStock: boolean };
876
+
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
+ });
592
889
 
593
- console.log(data.signedUrl); // share this — expires at data.expiresAt
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>
594
914
  ```
595
915
 
596
916
  ---
597
917
 
598
- ### Bucket Stats
918
+ ### Vue: Batch file upload with per-file progress
599
919
 
600
- ```ts
601
- const { data: stats } = await hydrous.storage.stats('ssk_my_bucket_key');
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';
926
+
927
+ type FileState = { name: string; percent: number; stage: string; done: boolean; error?: string };
602
928
 
603
- console.log('Files:', stats.totalFiles);
604
- console.log('Stored:', stats.totalSizeBytes, 'bytes');
605
- console.log('Space saved:', stats.spaceSavedBytes, 'bytes');
606
- console.log('Credits used:', stats.creditsTotalUsed);
607
- console.log('Compression:', stats.compressionRatio);
929
+ const fileStates = ref<FileState[]>([]);
930
+
931
+ async function onFilesChange(event: Event) {
932
+ const files = Array.from((event.target as HTMLInputElement).files ?? []);
933
+ if (!files.length) return;
934
+
935
+ fileStates.value = files.map(f => ({ name: f.name, percent: 0, stage: 'pending', done: false }));
936
+
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>
608
965
  ```
609
966
 
610
967
  ---
611
968
 
612
- ## 7. Error Handling
969
+ ## 9. Real-World Examples — SvelteKit
613
970
 
614
- Every method returns `{ data, error }`. `error` is `null` on success.
971
+ ### Shared client
615
972
 
616
973
  ```ts
617
- const { data, error } = await hydrous.storage.upload('ssk_key', file);
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,
993
+ },
994
+ });
995
+ ```
618
996
 
619
- if (error) {
620
- console.error(error.message); // human-readable message
621
- console.error(error.code); // machine-readable code e.g. 'QUOTA_EXCEEDED'
622
- console.error(error.status); // HTTP status code (if applicable)
997
+ ### Server route — fetch records
998
+
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
+ });
1011
+
1012
+ if (error) return json({ error: error.message }, { status: 500 });
1013
+ return json(data);
623
1014
  }
624
1015
  ```
625
1016
 
626
- ### Catching thrown errors
627
-
628
- If you prefer `try/catch`, import `isHydrousError`:
1017
+ ### Page load — server-side
629
1018
 
630
1019
  ```ts
631
- import { isHydrousError } from 'hydrous';
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
+ });
632
1029
 
633
- try {
634
- // ...
635
- } catch (err) {
636
- if (isHydrousError(err)) {
637
- console.error(err.code, err.message);
1030
+ if (error) throw error;
1031
+ return { products };
1032
+ };
1033
+ ```
1034
+
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
+ }
638
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}
1083
+ ```
1084
+
1085
+ ---
1086
+
1087
+ ## 10. Real-World Examples — Express / Node API
1088
+
1089
+ ### Setup
1090
+
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
+ ```
1106
+
1107
+ ### Upload from a file buffer (Node)
1108
+
1109
+ ```ts
1110
+ // routes/upload.ts
1111
+ import express from 'express';
1112
+ import multer from 'multer';
1113
+ import { db } from '../db';
1114
+
1115
+ const router = express.Router();
1116
+ const upload = multer({ storage: multer.memoryStorage() });
1117
+
1118
+ router.post('/upload', upload.single('file'), async (req, res) => {
1119
+ if (!req.file) return res.status(400).json({ error: 'No file' });
1120
+
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
+ );
1128
+
1129
+ if (error) return res.status(500).json({ error: error.message });
1130
+ res.json({ path: data!.path, size: data!.storedSize });
1131
+ });
1132
+
1133
+ export default router;
1134
+ ```
1135
+
1136
+ ### Save JSON reports to storage
1137
+
1138
+ ```ts
1139
+ // jobs/generateReport.ts
1140
+ import { db } from '../db';
1141
+
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
+ });
1147
+
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
+ });
1171
+
1172
+ return data!.path;
639
1173
  }
640
1174
  ```
641
1175
 
642
- ### Common error codes
1176
+ ### Batch delete old records
1177
+
1178
+ ```ts
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();
1185
+
1186
+ // Fetch old sessions
1187
+ const { data: old } = await db.records.select('sessions', {
1188
+ where: lt('createdAt', cutoff),
1189
+ limit: 500,
1190
+ });
643
1191
 
644
- | Code | Meaning |
645
- |---------------------|----------------------------------------------|
646
- | `HTTP_ERROR` | Non-2xx HTTP response from the server |
647
- | `NETWORK_ERROR` | Failed to reach the server |
648
- | `QUOTA_EXCEEDED` | Storage quota has been reached |
649
- | `FILE_TOO_LARGE` | File exceeds the per-file size limit (50 MB) |
650
- | `INVALID_MIME` | MIME type not permitted |
651
- | `FILE_EXISTS` | File already exists and `overwrite` is false |
652
- | `UPLOAD_ABORTED` | Upload was cancelled by the client |
653
- | `UPLOAD_TIMEOUT` | Upload timed out |
654
- | `NO_SESSION` | Auth operation requires a session |
655
- | `UNKNOWN_ERROR` | An unexpected error occurred |
1192
+ if (!old.length) { console.log('Nothing to clean up'); return; }
1193
+
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();
1200
+ ```
656
1201
 
657
1202
  ---
658
1203
 
659
- ## 8. TypeScript Types Reference
1204
+ ## 11. TypeScript Types Reference
1205
+
1206
+ ### `HydrousConfig`
1207
+
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
+ ```
660
1217
 
661
1218
  ### `UploadProgress`
662
1219
 
663
1220
  ```ts
664
1221
  interface UploadProgress {
665
- index: number; // file index (0-based)
666
- total: number; // total files in the operation
1222
+ index: number; // 0-based file index
1223
+ total: number; // total files in this operation
667
1224
  path: string; // destination path
668
- stage: UploadStage; // see below
1225
+ stage: UploadStage; // 'pending'|'compressing'|'uploading'|'done'|'error'
669
1226
  bytesUploaded: number;
670
1227
  totalBytes: number;
671
1228
  percent: number; // 0–100
672
- bytesPerSecond: number | null; // null before first tick
1229
+ bytesPerSecond: number | null; // null until speed is calculable
673
1230
  eta: number | null; // seconds remaining, null until speed known
674
- result?: UploadResult; // set when stage === 'done'
675
- error?: string; // set when stage === 'error'
1231
+ result?: UploadResult; // populated when stage === 'done'
1232
+ error?: string; // populated when stage === 'error'
676
1233
  code?: string;
677
1234
  }
678
-
679
- type UploadStage = 'pending' | 'compressing' | 'uploading' | 'done' | 'error';
680
1235
  ```
681
1236
 
682
1237
  ### `UploadResult`
@@ -692,17 +1247,17 @@ interface UploadResult {
692
1247
  }
693
1248
  ```
694
1249
 
695
- ### `StorageItem` (from `list()`)
1250
+ ### `StorageItem`
696
1251
 
697
1252
  ```ts
698
1253
  interface StorageItem {
699
- name: string;
700
- path: string;
701
- type: 'file' | 'folder';
702
- size?: number | null;
703
- mimeType?: string | null;
1254
+ name: string;
1255
+ path: string;
1256
+ type: 'file' | 'folder';
1257
+ size?: number | null;
1258
+ mimeType?: string | null;
704
1259
  isCompressed?: boolean;
705
- updatedAt?: string | null;
1260
+ updatedAt?: string | null;
706
1261
  }
707
1262
  ```
708
1263
 
@@ -719,7 +1274,7 @@ interface StorageStats {
719
1274
  creditsUsedUpload: number;
720
1275
  creditsUsedDownload: number;
721
1276
  creditsTotalUsed: number;
722
- compressionRatio: string; // e.g. "34.2%"
1277
+ compressionRatio: string; // e.g. "34.2%"
723
1278
  }
724
1279
  ```
725
1280
 
@@ -727,4 +1282,4 @@ interface StorageStats {
727
1282
 
728
1283
  ## License
729
1284
 
730
- MIT © Hydrous
1285
+ MIT © HydrousDB