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/LICENSE +1 -1
- package/README.md +976 -421
- package/dist/index.d.mts +226 -383
- package/dist/index.d.ts +226 -383
- package/dist/index.js +343 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +339 -592
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -27
package/README.md
CHANGED
|
@@ -1,682 +1,1237 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HydrousDB SDK
|
|
2
2
|
|
|
3
|
-
Official JavaScript / TypeScript SDK for the **
|
|
4
|
-
|
|
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
|
|
7
|
+
npm install hydrousdb
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
13
13
|
|
|
14
|
-
1. [
|
|
15
|
-
2. [
|
|
16
|
-
3. [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
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
|
-
- [
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
45
|
+
## 1. Setup & Client Configuration
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
import { createClient } from 'hydrous';
|
|
47
|
+
Create **one** client and reuse it across your app.
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
##
|
|
107
|
+
## 2. Auth
|
|
98
108
|
|
|
99
109
|
### Sign Up
|
|
100
110
|
|
|
101
111
|
```ts
|
|
102
|
-
const { data, error } = await
|
|
103
|
-
email: '
|
|
112
|
+
const { data, error } = await db.auth.signUp({
|
|
113
|
+
email: 'alice@example.com',
|
|
104
114
|
password: 'supersecret',
|
|
105
|
-
metadata: { plan: 'pro' },
|
|
115
|
+
metadata: { plan: 'pro', referral: 'google' },
|
|
106
116
|
});
|
|
107
117
|
|
|
108
|
-
if (
|
|
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
|
|
118
|
-
email: '
|
|
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
|
-
|
|
131
|
+
localStorage.setItem('token', data.accessToken);
|
|
124
132
|
}
|
|
125
133
|
```
|
|
126
134
|
|
|
127
135
|
### Sign Out
|
|
128
136
|
|
|
129
137
|
```ts
|
|
130
|
-
await
|
|
138
|
+
await db.auth.signOut();
|
|
139
|
+
localStorage.removeItem('token');
|
|
131
140
|
```
|
|
132
141
|
|
|
133
|
-
### Get
|
|
142
|
+
### Get current user
|
|
134
143
|
|
|
135
144
|
```ts
|
|
136
|
-
const { data: user } = await
|
|
145
|
+
const { data: user } = await db.auth.getUser();
|
|
137
146
|
console.log(user?.email);
|
|
138
147
|
```
|
|
139
148
|
|
|
140
|
-
### Refresh
|
|
149
|
+
### Refresh session
|
|
141
150
|
|
|
142
151
|
```ts
|
|
143
|
-
const { data: session } = await
|
|
152
|
+
const { data: session } = await db.auth.refreshSession();
|
|
144
153
|
```
|
|
145
154
|
|
|
146
155
|
---
|
|
147
156
|
|
|
148
|
-
##
|
|
157
|
+
## 3. Records
|
|
149
158
|
|
|
150
159
|
### Insert
|
|
151
160
|
|
|
152
|
-
Single record:
|
|
153
|
-
|
|
154
161
|
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
165
|
-
const { data } = await
|
|
166
|
-
{ name: 'Widget A', price: 9.99
|
|
167
|
-
{ name: 'Widget B', price:
|
|
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 ${
|
|
175
|
+
console.log(`Inserted ${count} products`);
|
|
170
176
|
```
|
|
171
177
|
|
|
172
|
-
### Select
|
|
178
|
+
### Select (query)
|
|
173
179
|
|
|
174
180
|
```ts
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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', '
|
|
188
|
+
select: ['id', 'name', 'price'], // return only these fields
|
|
181
189
|
});
|
|
182
190
|
```
|
|
183
191
|
|
|
184
|
-
|
|
192
|
+
### Get one by ID
|
|
185
193
|
|
|
186
194
|
```ts
|
|
187
|
-
|
|
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
|
|
208
|
-
|
|
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
|
|
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
|
-
##
|
|
215
|
+
## 4. Analytics
|
|
235
216
|
|
|
236
|
-
### Track an
|
|
217
|
+
### Track an event
|
|
237
218
|
|
|
238
219
|
```ts
|
|
239
|
-
await
|
|
240
|
-
event: '
|
|
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
|
-
###
|
|
248
|
-
|
|
249
|
-
More efficient than calling `track()` in a loop:
|
|
233
|
+
### Track many events at once
|
|
250
234
|
|
|
251
235
|
```ts
|
|
252
|
-
await
|
|
253
|
-
{ event: '
|
|
254
|
-
{ event: '
|
|
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
|
|
243
|
+
### Query events
|
|
259
244
|
|
|
260
245
|
```ts
|
|
261
|
-
const { data
|
|
262
|
-
event:
|
|
263
|
-
from:
|
|
264
|
-
to:
|
|
265
|
-
limit:
|
|
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
|
-
##
|
|
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
|
|
258
|
+
### How storage keys work
|
|
277
259
|
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
280
|
+
### Upload a file with progress
|
|
290
281
|
|
|
291
282
|
```ts
|
|
292
|
-
const { data, error } = await
|
|
293
|
-
'
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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('
|
|
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
|
-
|
|
307
|
+
```ts
|
|
308
|
+
const files = Array.from(fileInput.files);
|
|
313
309
|
|
|
314
|
-
|
|
310
|
+
const { data } = await db.storage('documents').batchUpload(files, {
|
|
311
|
+
prefix: 'uploads/2024/',
|
|
312
|
+
overwrite: false,
|
|
313
|
+
concurrency: 5,
|
|
315
314
|
|
|
316
|
-
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
328
|
+
```ts
|
|
329
|
+
const { data: buffer, error } = await db.storage('documents').download('reports/q1.pdf');
|
|
337
330
|
|
|
338
|
-
|
|
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
|
|
342
|
-
'
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
369
|
+
### Delete, move, copy
|
|
385
370
|
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
```
|
|
371
|
+
```ts
|
|
372
|
+
// Delete a single file
|
|
373
|
+
await db.storage('avatars').deleteFile('users/old-photo.jpg');
|
|
390
374
|
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
384
|
+
// Copy a file (original kept)
|
|
385
|
+
await db.storage('documents').copy('templates/invoice.pdf', 'invoices/inv-001.pdf');
|
|
386
|
+
```
|
|
398
387
|
|
|
399
|
-
|
|
400
|
-
individual progress bars.
|
|
388
|
+
### Signed URLs
|
|
401
389
|
|
|
402
390
|
```ts
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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(
|
|
420
|
-
console.log(
|
|
397
|
+
console.log(data!.signedUrl); // share this URL
|
|
398
|
+
console.log(data!.expiresAt); // ISO timestamp when it expires
|
|
421
399
|
```
|
|
422
400
|
|
|
423
|
-
|
|
401
|
+
### Upload raw text / JSON
|
|
424
402
|
|
|
425
403
|
```ts
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
424
|
+
## 6. Error Handling
|
|
440
425
|
|
|
441
|
-
|
|
426
|
+
Every method returns `{ data, error }`. `error` is `null` on success.
|
|
442
427
|
|
|
443
428
|
```ts
|
|
444
|
-
const { data
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
468
|
+
## 7. Real-World Examples — Next.js
|
|
469
|
+
|
|
470
|
+
### Shared client (lib/db.ts)
|
|
486
471
|
|
|
487
472
|
```ts
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
###
|
|
572
|
+
### Storage: Avatar upload with live progress bar
|
|
513
573
|
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
###
|
|
637
|
+
### Storage: Secure file download link (API route)
|
|
530
638
|
|
|
531
639
|
```ts
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
739
|
+
## 8. Real-World Examples — Vue 3
|
|
740
|
+
|
|
741
|
+
### Shared client (src/lib/db.ts)
|
|
551
742
|
|
|
552
743
|
```ts
|
|
553
|
-
|
|
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
|
-
###
|
|
760
|
+
### Auth: Login composable
|
|
559
761
|
|
|
560
762
|
```ts
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
###
|
|
804
|
+
### Storage: File upload with progress
|
|
571
805
|
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
###
|
|
866
|
+
### Records: Data table with pagination
|
|
583
867
|
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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.
|
|
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
|
-
###
|
|
918
|
+
### Vue: Batch file upload with per-file progress
|
|
599
919
|
|
|
600
|
-
```
|
|
601
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
##
|
|
969
|
+
## 9. Real-World Examples — SvelteKit
|
|
613
970
|
|
|
614
|
-
|
|
971
|
+
### Shared client
|
|
615
972
|
|
|
616
973
|
```ts
|
|
617
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
###
|
|
627
|
-
|
|
628
|
-
If you prefer `try/catch`, import `isHydrousError`:
|
|
1017
|
+
### Page load — server-side
|
|
629
1018
|
|
|
630
1019
|
```ts
|
|
631
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
##
|
|
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
|
|
666
|
-
total: number; // total files in
|
|
1222
|
+
index: number; // 0-based file index
|
|
1223
|
+
total: number; // total files in this operation
|
|
667
1224
|
path: string; // destination path
|
|
668
|
-
stage: UploadStage; //
|
|
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
|
|
1229
|
+
bytesPerSecond: number | null; // null until speed is calculable
|
|
673
1230
|
eta: number | null; // seconds remaining, null until speed known
|
|
674
|
-
result?: UploadResult; //
|
|
675
|
-
error?: string; //
|
|
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`
|
|
1250
|
+
### `StorageItem`
|
|
696
1251
|
|
|
697
1252
|
```ts
|
|
698
1253
|
interface StorageItem {
|
|
699
|
-
name:
|
|
700
|
-
path:
|
|
701
|
-
type:
|
|
702
|
-
size?:
|
|
703
|
-
mimeType?:
|
|
1254
|
+
name: string;
|
|
1255
|
+
path: string;
|
|
1256
|
+
type: 'file' | 'folder';
|
|
1257
|
+
size?: number | null;
|
|
1258
|
+
mimeType?: string | null;
|
|
704
1259
|
isCompressed?: boolean;
|
|
705
|
-
updatedAt?:
|
|
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;
|
|
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 ©
|
|
1285
|
+
MIT © HydrousDB
|