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/LICENSE +1 -1
- package/README.md +1048 -540
- package/dist/index.d.mts +526 -51
- package/dist/index.d.ts +526 -51
- package/dist/index.js +924 -644
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +914 -642
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -51
- package/dist/analytics/index.d.mts +0 -185
- package/dist/analytics/index.d.ts +0 -185
- package/dist/analytics/index.js +0 -184
- package/dist/analytics/index.js.map +0 -1
- package/dist/analytics/index.mjs +0 -182
- package/dist/analytics/index.mjs.map +0 -1
- package/dist/auth/index.d.mts +0 -180
- package/dist/auth/index.d.ts +0 -180
- package/dist/auth/index.js +0 -220
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/index.mjs +0 -218
- package/dist/auth/index.mjs.map +0 -1
- package/dist/http-DTukpdAU.d.mts +0 -470
- package/dist/http-DTukpdAU.d.ts +0 -470
- package/dist/records/index.d.mts +0 -137
- package/dist/records/index.d.ts +0 -137
- package/dist/records/index.js +0 -228
- package/dist/records/index.js.map +0 -1
- package/dist/records/index.mjs +0 -226
- package/dist/records/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,774 +1,1282 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HydrousDB SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Official JavaScript / TypeScript SDK for the **HydrousDB** platform.
|
|
4
|
+
Auth · Records · Analytics · Storage — one package, one client.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
```bash
|
|
7
|
+
npm install hydrousdb
|
|
8
|
+
```
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
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
|
-
##
|
|
45
|
+
## 1. Setup & Client Configuration
|
|
39
46
|
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
107
|
+
## 2. Auth
|
|
66
108
|
|
|
67
|
-
###
|
|
109
|
+
### Sign Up
|
|
68
110
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
```ts
|
|
138
|
+
await db.auth.signOut();
|
|
139
|
+
localStorage.removeItem('token');
|
|
140
|
+
```
|
|
77
141
|
|
|
78
|
-
|
|
142
|
+
### Get current user
|
|
79
143
|
|
|
80
144
|
```ts
|
|
81
|
-
const
|
|
145
|
+
const { data: user } = await db.auth.getUser();
|
|
146
|
+
console.log(user?.email);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Refresh session
|
|
82
150
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
##
|
|
157
|
+
## 3. Records
|
|
158
|
+
|
|
159
|
+
### Insert
|
|
93
160
|
|
|
94
161
|
```ts
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 },
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
const { data:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
+
if (data) {
|
|
300
|
+
console.log('Path:', data.path);
|
|
301
|
+
console.log('Space saved by compression:', data.spaceSaved, 'bytes');
|
|
302
|
+
}
|
|
303
|
+
```
|
|
132
304
|
|
|
133
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
limit:
|
|
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}>
|
|
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
|
-
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
### Storage: Avatar upload with live progress bar
|
|
170
573
|
|
|
171
|
-
```
|
|
172
|
-
//
|
|
173
|
-
|
|
574
|
+
```tsx
|
|
575
|
+
// components/AvatarUploader.tsx
|
|
576
|
+
'use client';
|
|
577
|
+
import { useState } from 'react';
|
|
174
578
|
import { db } from '@/lib/db';
|
|
175
|
-
import {
|
|
176
|
-
|
|
177
|
-
export
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
### Storage: Secure file download link (API route)
|
|
195
638
|
|
|
196
639
|
```ts
|
|
197
|
-
//
|
|
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
|
|
202
|
-
const
|
|
203
|
-
if (!
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
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
|
-
|
|
214
|
-
```
|
|
653
|
+
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
|
215
654
|
|
|
216
|
-
|
|
655
|
+
return NextResponse.json({ url: data!.signedUrl, expiresAt: data!.expiresAt });
|
|
656
|
+
}
|
|
657
|
+
```
|
|
217
658
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
###
|
|
668
|
+
### Analytics: Page-view tracking
|
|
227
669
|
|
|
228
|
-
```
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
import {
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
securityKey: process.env.HYDROUS_SECURITY_KEY!,
|
|
236
|
-
});
|
|
677
|
+
export default function Analytics() {
|
|
678
|
+
const pathname = usePathname();
|
|
237
679
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
711
|
+
---
|
|
256
712
|
|
|
257
|
-
|
|
713
|
+
### Records: Server Action — create a product
|
|
258
714
|
|
|
259
715
|
```ts
|
|
260
|
-
//
|
|
261
|
-
|
|
716
|
+
// app/products/actions.ts
|
|
717
|
+
'use server';
|
|
718
|
+
import { db } from '@/lib/db';
|
|
719
|
+
import { revalidatePath } from 'next/cache';
|
|
262
720
|
|
|
263
|
-
export
|
|
264
|
-
const
|
|
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
|
|
267
|
-
|
|
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
|
-
|
|
272
|
-
});
|
|
273
|
-
```
|
|
730
|
+
if (error) throw new Error(error.message);
|
|
274
731
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
741
|
+
### Shared client (src/lib/db.ts)
|
|
305
742
|
|
|
306
743
|
```ts
|
|
307
|
-
//
|
|
744
|
+
// src/lib/db.ts
|
|
308
745
|
import { createClient } from 'hydrousdb';
|
|
309
746
|
|
|
310
|
-
const db = createClient({
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
###
|
|
760
|
+
### Auth: Login composable
|
|
323
761
|
|
|
324
|
-
```
|
|
325
|
-
//
|
|
326
|
-
import {
|
|
327
|
-
import {
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
.catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
|
|
345
|
-
.finally(() => setLoading(false));
|
|
788
|
+
loading.value = false;
|
|
789
|
+
}
|
|
346
790
|
|
|
347
|
-
|
|
348
|
-
|
|
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 {
|
|
798
|
+
return { user, loading, error, login, logout };
|
|
351
799
|
}
|
|
352
800
|
```
|
|
353
801
|
|
|
354
802
|
---
|
|
355
803
|
|
|
356
|
-
###
|
|
804
|
+
### Storage: File upload with progress
|
|
357
805
|
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
import {
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
866
|
+
### Records: Data table with pagination
|
|
386
867
|
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
import { createClient } from 'hydrousdb';
|
|
875
|
+
type Product = { id: string; name: string; price: number; inStock: boolean };
|
|
391
876
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
918
|
+
### Vue: Batch file upload with per-file progress
|
|
406
919
|
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
929
|
+
const fileStates = ref<FileState[]>([]);
|
|
424
930
|
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
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
|
-
|
|
935
|
+
fileStates.value = files.map(f => ({ name: f.name, percent: 0, stage: 'pending', done: false }));
|
|
433
936
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
447
|
-
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
## 9. Real-World Examples — SvelteKit
|
|
448
970
|
|
|
449
|
-
|
|
971
|
+
### Shared client
|
|
450
972
|
|
|
451
973
|
```ts
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
459
|
-
);
|
|
460
|
-
// meta.id — the new record's ID
|
|
994
|
+
});
|
|
461
995
|
```
|
|
462
996
|
|
|
463
|
-
|
|
997
|
+
### Server route — fetch records
|
|
464
998
|
|
|
465
999
|
```ts
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
},
|
|
473
|
-
|
|
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
|
-
|
|
1017
|
+
### Page load — server-side
|
|
478
1018
|
|
|
479
1019
|
```ts
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
},
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
528
|
-
|
|
1043
|
+
let progress: UploadProgress | null = null;
|
|
1044
|
+
let uploadedPath = '';
|
|
1045
|
+
let errorMsg = '';
|
|
529
1046
|
|
|
530
|
-
|
|
531
|
-
const
|
|
1047
|
+
async function handleFile(event: Event) {
|
|
1048
|
+
const file = (event.target as HTMLInputElement).files?.[0];
|
|
1049
|
+
if (!file) return;
|
|
532
1050
|
|
|
533
|
-
|
|
534
|
-
|
|
1051
|
+
errorMsg = '';
|
|
1052
|
+
uploadedPath = '';
|
|
1053
|
+
progress = null;
|
|
535
1054
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
1069
|
+
<input type="file" on:change={handleFile} />
|
|
543
1070
|
|
|
544
|
-
|
|
1071
|
+
{#if progress}
|
|
1072
|
+
<p>{progress.stage} — {progress.percent}%</p>
|
|
1073
|
+
<progress value={progress.percent} max="100" />
|
|
1074
|
+
{/if}
|
|
545
1075
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
1080
|
+
{#if errorMsg}
|
|
1081
|
+
<p style="color:red">{errorMsg}</p>
|
|
1082
|
+
{/if}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
---
|
|
562
1086
|
|
|
563
|
-
|
|
1087
|
+
## 10. Real-World Examples — Express / Node API
|
|
1088
|
+
|
|
1089
|
+
### Setup
|
|
564
1090
|
|
|
565
1091
|
```ts
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
1107
|
+
### Upload from a file buffer (Node)
|
|
572
1108
|
|
|
573
1109
|
```ts
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
1110
|
+
// routes/upload.ts
|
|
1111
|
+
import express from 'express';
|
|
1112
|
+
import multer from 'multer';
|
|
1113
|
+
import { db } from '../db';
|
|
577
1114
|
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
// [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
|
|
1115
|
+
const router = express.Router();
|
|
1116
|
+
const upload = multer({ storage: multer.memoryStorage() });
|
|
581
1117
|
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1139
|
+
// jobs/generateReport.ts
|
|
1140
|
+
import { db } from '../db';
|
|
642
1141
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1176
|
+
### Batch delete old records
|
|
665
1177
|
|
|
666
1178
|
```ts
|
|
667
|
-
//
|
|
668
|
-
|
|
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
|
-
|
|
671
|
-
const { data
|
|
672
|
-
|
|
673
|
-
limit:
|
|
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
|
-
|
|
678
|
-
cursor = meta.nextCursor;
|
|
679
|
-
} while (cursor);
|
|
1192
|
+
if (!old.length) { console.log('Nothing to clean up'); return; }
|
|
680
1193
|
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
1206
|
+
### `HydrousConfig`
|
|
693
1207
|
|
|
694
1208
|
```ts
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
1218
|
+
### `UploadProgress`
|
|
708
1219
|
|
|
709
1220
|
```ts
|
|
710
|
-
interface
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|