hydrousdb 2.0.3 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +857 -1016
- package/dist/index.cjs +1194 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1080 -0
- package/dist/index.d.ts +921 -395
- package/dist/index.js +1048 -924
- package/dist/index.js.map +1 -1
- package/package.json +47 -18
- package/dist/index.d.mts +0 -554
- package/dist/index.mjs +0 -1038
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,1285 +1,1126 @@
|
|
|
1
|
-
# HydrousDB SDK
|
|
1
|
+
# HydrousDB JavaScript / TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>The official SDK for <a href="https://hydrousdb.com">HydrousDB</a> — records, auth, file storage, and analytics in one package.</strong>
|
|
5
|
+
</p>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
npm
|
|
8
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/v/hydrousdb.svg" alt="npm version"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/hydrousdb"><img src="https://img.shields.io/npm/dm/hydrousdb.svg" alt="npm downloads"></a>
|
|
10
|
+
<a href="https://github.com/hydrousdb/hydrousdb-js/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hydrousdb.svg" alt="MIT License"></a>
|
|
11
|
+
<a href="https://hydrousdb.com"><img src="https://img.shields.io/badge/docs-hydrousdb.com-blue" alt="Documentation"></a>
|
|
12
|
+
</p>
|
|
9
13
|
|
|
10
14
|
---
|
|
11
15
|
|
|
12
16
|
## Table of Contents
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
18
|
+
- [What is HydrousDB?](#what-is-hydrousdb)
|
|
19
|
+
- [Quick Start (5 minutes)](#quick-start-5-minutes)
|
|
20
|
+
- [Step 1 — Create your account](#step-1--create-your-account)
|
|
21
|
+
- [Step 2 — Create your first bucket](#step-2--create-your-first-bucket)
|
|
22
|
+
- [Step 3 — Grab your Security Key](#step-3--grab-your-security-key)
|
|
23
|
+
- [Step 4 — Install the SDK](#step-4--install-the-sdk)
|
|
24
|
+
- [Step 5 — Your first record](#step-5--your-first-record)
|
|
25
|
+
- [Records](#records)
|
|
26
|
+
- [Create](#create-a-record)
|
|
27
|
+
- [Read](#read-a-record)
|
|
28
|
+
- [Update](#update-a-record)
|
|
29
|
+
- [Delete](#delete-a-record)
|
|
30
|
+
- [Query](#query-records)
|
|
31
|
+
- [Batch Operations](#batch-operations)
|
|
32
|
+
- [Version History](#version-history)
|
|
33
|
+
- [Authentication](#authentication)
|
|
34
|
+
- [Sign Up](#sign-up-users)
|
|
35
|
+
- [Log In / Log Out](#log-in--log-out)
|
|
36
|
+
- [Session Management](#session-management)
|
|
37
|
+
- [Password Reset](#password-reset-flow)
|
|
38
|
+
- [Email Verification](#email-verification)
|
|
39
|
+
- [Admin Operations](#admin-operations)
|
|
40
|
+
- [File Storage](#file-storage)
|
|
41
|
+
- [Simple Upload](#simple-upload)
|
|
42
|
+
- [Large File Upload (with progress)](#large-file-upload-with-progress)
|
|
43
|
+
- [Download](#download-files)
|
|
44
|
+
- [List Files](#list-files)
|
|
45
|
+
- [Scoped Storage](#scoped-storage)
|
|
46
|
+
- [Share & Visibility](#share--visibility)
|
|
47
|
+
- [File Operations](#file-operations)
|
|
48
|
+
- [Analytics](#analytics)
|
|
49
|
+
- [Count](#count)
|
|
50
|
+
- [Distribution](#distribution)
|
|
51
|
+
- [Time Series](#time-series)
|
|
52
|
+
- [Top N](#top-n)
|
|
53
|
+
- [Field Stats](#field-stats)
|
|
54
|
+
- [Multi-Metric Dashboard](#multi-metric-dashboard)
|
|
55
|
+
- [Cross-Bucket Comparison](#cross-bucket-comparison)
|
|
56
|
+
- [TypeScript Support](#typescript-support)
|
|
57
|
+
- [Error Handling](#error-handling)
|
|
58
|
+
- [Security Best Practices](#security-best-practices)
|
|
59
|
+
- [API Reference](#api-reference)
|
|
60
|
+
- [Contributing](#contributing)
|
|
61
|
+
- [License](#license)
|
|
42
62
|
|
|
43
63
|
---
|
|
44
64
|
|
|
45
|
-
##
|
|
65
|
+
## What is HydrousDB?
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
HydrousDB is a **backend-as-a-service** platform that gives your app a fully managed backend in minutes. Instead of spinning up servers, databases, and storage buckets yourself, you call an API.
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
| Feature | What it does |
|
|
70
|
+
|---|---|
|
|
71
|
+
| **Records** | Schemaless JSON document store. Create, read, update, delete and query records in named *buckets*. |
|
|
72
|
+
| **Auth** | Full user authentication — signup, login, sessions, password reset, email verification, and admin controls. |
|
|
73
|
+
| **Storage** | File uploads and downloads backed by Google Cloud Storage. Public and private files, signed share URLs. |
|
|
74
|
+
| **Analytics** | BigQuery-powered aggregations — counts, distributions, time series, top-N, multi-metric dashboards, and cross-bucket comparisons. |
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
url: 'https://api.myapp.hydrous.app', // your project URL
|
|
54
|
-
authKey: 'hk_auth_…', // auth service key
|
|
55
|
-
bucketSecurityKey: 'hk_bucket_…', // records + analytics key
|
|
56
|
-
storageKeys: {
|
|
57
|
-
main: 'ssk_main_…', // your default storage bucket
|
|
58
|
-
avatars: 'ssk_avatars_…', // a dedicated bucket for user avatars
|
|
59
|
-
documents: 'ssk_docs_…', // a bucket for user documents
|
|
60
|
-
// …add as many as you need
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
```
|
|
76
|
+
Everything is organised around two concepts:
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
- **Security Key (`sk_...`)** — your master credential. Authenticate all API calls. Keep it secret.
|
|
79
|
+
- **Bucket Key** — just the name of your bucket (e.g. `"blog-posts"`, `"app-users"`). Not a secret.
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|---------------------|-------------------------|----------------------------------------------------------|
|
|
69
|
-
| `authKey` | `db.auth.*` | One key — controls all auth operations |
|
|
70
|
-
| `bucketSecurityKey` | `db.records.*` `db.analytics.*` | One key — controls data and event access |
|
|
71
|
-
| `storageKeys` | `db.storage('name').*` | **Many keys** — one per storage bucket you use |
|
|
81
|
+
---
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
`storageKeys` is an object where **you choose the name**. When you call a
|
|
75
|
-
storage method you pick which bucket to use by that name.
|
|
83
|
+
## Quick Start (5 minutes)
|
|
76
84
|
|
|
77
|
-
###
|
|
85
|
+
### Step 1 — Create your account
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
# .env.local
|
|
81
|
-
NEXT_PUBLIC_HYDROUS_URL=https://api.myapp.hydrous.app
|
|
82
|
-
HYDROUS_AUTH_KEY=hk_auth_…
|
|
83
|
-
HYDROUS_BUCKET_KEY=hk_bucket_…
|
|
84
|
-
HYDROUS_STORAGE_MAIN=ssk_main_…
|
|
85
|
-
HYDROUS_STORAGE_AVATARS=ssk_avatars_…
|
|
86
|
-
HYDROUS_STORAGE_DOCS=ssk_docs_…
|
|
87
|
-
```
|
|
87
|
+
Go to **[https://hydrousdb.com](https://hydrousdb.com)** and sign up for a free account.
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
// lib/db.ts (server-side only — never expose secret keys to the browser)
|
|
91
|
-
import { createClient } from 'hydrousdb';
|
|
89
|
+
### Step 2 — Create your first bucket
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
storageKeys: {
|
|
98
|
-
main: process.env.HYDROUS_STORAGE_MAIN!,
|
|
99
|
-
avatars: process.env.HYDROUS_STORAGE_AVATARS!,
|
|
100
|
-
documents: process.env.HYDROUS_STORAGE_DOCS!,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
```
|
|
91
|
+
1. Log in to your dashboard at **[https://hydrousdb.com/dashboard](https://hydrousdb.com/dashboard)**.
|
|
92
|
+
2. Click **"New Bucket"**.
|
|
93
|
+
3. Give it a name — use lowercase letters, numbers, hyphens, or underscores (e.g. `my-first-bucket`).
|
|
94
|
+
4. Click **"Create"**.
|
|
104
95
|
|
|
105
|
-
|
|
96
|
+
> 💡 **What is a bucket?** A bucket is a named collection of JSON records — similar to a table in SQL or a collection in MongoDB.
|
|
106
97
|
|
|
107
|
-
|
|
98
|
+
### Step 3 — Grab your Security Key
|
|
108
99
|
|
|
109
|
-
|
|
100
|
+
1. In the dashboard, go to **Settings → API Keys**.
|
|
101
|
+
2. Click **"Generate Security Key"**.
|
|
102
|
+
3. Copy the key — it looks like `sk_live_xxxxxxxxxxxxxxxxxxxx`.
|
|
110
103
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
password: 'supersecret',
|
|
115
|
-
metadata: { plan: 'pro', referral: 'google' },
|
|
116
|
-
});
|
|
104
|
+
> ⚠️ **Your Security Key is your most important credential.** Treat it like a password. Never commit it to Git. Use environment variables.
|
|
105
|
+
|
|
106
|
+
### Step 4 — Install the SDK
|
|
117
107
|
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
```bash
|
|
109
|
+
npm install hydrousdb
|
|
110
|
+
# or
|
|
111
|
+
yarn add hydrousdb
|
|
112
|
+
# or
|
|
113
|
+
pnpm add hydrousdb
|
|
120
114
|
```
|
|
121
115
|
|
|
122
|
-
|
|
116
|
+
**Requirements:** Node.js 18+ (uses the native `fetch` API).
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
const { data, error } = await db.auth.signIn({
|
|
126
|
-
email: 'alice@example.com',
|
|
127
|
-
password: 'supersecret',
|
|
128
|
-
});
|
|
118
|
+
### Step 5 — Your first record
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
```
|
|
120
|
+
```typescript
|
|
121
|
+
import { createClient } from 'hydrousdb';
|
|
134
122
|
|
|
135
|
-
|
|
123
|
+
// Create the client once — reuse it everywhere
|
|
124
|
+
const db = createClient({
|
|
125
|
+
securityKey: process.env.HYDROUS_SECURITY_KEY!, // from Step 3
|
|
126
|
+
});
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
await db.
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
// Write a record to your bucket
|
|
129
|
+
const post = await db.records('my-first-bucket').create({
|
|
130
|
+
title: 'Hello, HydrousDB!',
|
|
131
|
+
body: 'My first record.',
|
|
132
|
+
published: false,
|
|
133
|
+
});
|
|
141
134
|
|
|
142
|
-
|
|
135
|
+
console.log(post.id); // "rec_a1b2c3d4"
|
|
136
|
+
console.log(post.createdAt); // 1717200000000
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
console.log(
|
|
147
|
-
```
|
|
138
|
+
// Read it back
|
|
139
|
+
const fetched = await db.records('my-first-bucket').get(post.id);
|
|
140
|
+
console.log(fetched.title); // "Hello, HydrousDB!"
|
|
148
141
|
|
|
149
|
-
|
|
142
|
+
// Update it
|
|
143
|
+
const updated = await db.records('my-first-bucket').patch(post.id, { published: true });
|
|
150
144
|
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
// Delete it
|
|
146
|
+
await db.records('my-first-bucket').delete(post.id);
|
|
153
147
|
```
|
|
154
148
|
|
|
149
|
+
🎉 **That's it!** You're live with zero configuration beyond your Security Key.
|
|
150
|
+
|
|
155
151
|
---
|
|
156
152
|
|
|
157
|
-
##
|
|
153
|
+
## Records
|
|
154
|
+
|
|
155
|
+
Records are JSON objects stored in named buckets. Every record automatically gets:
|
|
156
|
+
- `id` — unique record identifier (e.g. `"rec_a1b2c3d4"`)
|
|
157
|
+
- `createdAt` — Unix timestamp in milliseconds
|
|
158
|
+
- `updatedAt` — Unix timestamp in milliseconds (updated on every write)
|
|
158
159
|
|
|
159
|
-
###
|
|
160
|
+
### Create a Record
|
|
160
161
|
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
```typescript
|
|
163
|
+
const products = db.records('products');
|
|
164
|
+
|
|
165
|
+
const product = await products.create({
|
|
166
|
+
name: 'Wireless Headphones',
|
|
167
|
+
price: 79.99,
|
|
167
168
|
inStock: true,
|
|
169
|
+
tags: ['audio', 'wireless'],
|
|
168
170
|
});
|
|
169
171
|
|
|
170
|
-
//
|
|
171
|
-
const { data, count } = await db.records.insert('products', [
|
|
172
|
-
{ name: 'Widget A', price: 9.99 },
|
|
173
|
-
{ name: 'Widget B', price: 19.99 },
|
|
174
|
-
]);
|
|
175
|
-
console.log(`Inserted ${count} products`);
|
|
172
|
+
// product.id, product.createdAt, product.updatedAt are added automatically
|
|
176
173
|
```
|
|
177
174
|
|
|
178
|
-
###
|
|
175
|
+
### Read a Record
|
|
179
176
|
|
|
180
|
-
```
|
|
181
|
-
|
|
177
|
+
```typescript
|
|
178
|
+
// Get by ID
|
|
179
|
+
const product = await products.get('rec_abc123');
|
|
182
180
|
|
|
183
|
-
|
|
184
|
-
where: [eq('category', 'hardware'), gt('price', 10)],
|
|
185
|
-
orderBy: { field: 'price', direction: 'asc' },
|
|
186
|
-
limit: 20,
|
|
187
|
-
offset: 0,
|
|
188
|
-
select: ['id', 'name', 'price'], // return only these fields
|
|
189
|
-
});
|
|
181
|
+
// product is null-safe: throws HydrousError with code RECORD_NOT_FOUND if missing
|
|
190
182
|
```
|
|
191
183
|
|
|
192
|
-
###
|
|
184
|
+
### Update a Record
|
|
193
185
|
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
### Update
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
const { data } = await db.records.update('products', 'prod_abc123', {
|
|
202
|
-
price: 59.99,
|
|
186
|
+
```typescript
|
|
187
|
+
// Patch (merge) — only the listed fields are changed
|
|
188
|
+
const updated = await products.patch('rec_abc123', {
|
|
189
|
+
price: 69.99,
|
|
203
190
|
inStock: false,
|
|
204
191
|
});
|
|
192
|
+
|
|
193
|
+
// Set (full replace) — the entire record is replaced
|
|
194
|
+
const replaced = await products.set('rec_abc123', {
|
|
195
|
+
name: 'Wireless Headphones v2',
|
|
196
|
+
price: 89.99,
|
|
197
|
+
inStock: true,
|
|
198
|
+
tags: ['audio', 'wireless', 'premium'],
|
|
199
|
+
});
|
|
205
200
|
```
|
|
206
201
|
|
|
207
|
-
### Delete
|
|
202
|
+
### Delete a Record
|
|
208
203
|
|
|
209
|
-
```
|
|
210
|
-
|
|
204
|
+
```typescript
|
|
205
|
+
await products.delete('rec_abc123');
|
|
211
206
|
```
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
## 4. Analytics
|
|
208
|
+
### Query Records
|
|
216
209
|
|
|
217
|
-
|
|
210
|
+
```typescript
|
|
211
|
+
// Get all records (up to 100 by default)
|
|
212
|
+
const { records } = await products.query();
|
|
218
213
|
|
|
219
|
-
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
orderId: 'ord_001',
|
|
226
|
-
total: 99.99,
|
|
227
|
-
currency: 'USD',
|
|
228
|
-
itemCount: 3,
|
|
229
|
-
},
|
|
214
|
+
// With filters
|
|
215
|
+
const { records: affordableStock } = await products.query({
|
|
216
|
+
filters: [
|
|
217
|
+
{ field: 'inStock', op: '==', value: true },
|
|
218
|
+
{ field: 'price', op: '<', value: 100 },
|
|
219
|
+
],
|
|
230
220
|
});
|
|
231
|
-
```
|
|
232
221
|
|
|
233
|
-
|
|
222
|
+
// Sort and paginate
|
|
223
|
+
const { records, hasMore, nextCursor } = await products.query({
|
|
224
|
+
orderBy: 'price',
|
|
225
|
+
order: 'asc',
|
|
226
|
+
limit: 20,
|
|
227
|
+
});
|
|
234
228
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
229
|
+
// Next page
|
|
230
|
+
if (hasMore) {
|
|
231
|
+
const page2 = await products.query({
|
|
232
|
+
orderBy: 'price',
|
|
233
|
+
order: 'asc',
|
|
234
|
+
limit: 20,
|
|
235
|
+
startAfter: nextCursor,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
242
238
|
|
|
243
|
-
|
|
239
|
+
// Select only specific fields
|
|
240
|
+
const { records: lightRecords } = await products.query({
|
|
241
|
+
fields: 'name,price,inStock',
|
|
242
|
+
});
|
|
244
243
|
|
|
245
|
-
|
|
246
|
-
const {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
244
|
+
// Filter by date range
|
|
245
|
+
const { records: recent } = await products.query({
|
|
246
|
+
dateRange: {
|
|
247
|
+
start: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
|
|
248
|
+
end: Date.now(),
|
|
249
|
+
},
|
|
251
250
|
});
|
|
252
251
|
```
|
|
253
252
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
253
|
+
**Available filter operators:**
|
|
254
|
+
|
|
255
|
+
| Operator | Meaning |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `==` | Equal |
|
|
258
|
+
| `!=` | Not equal |
|
|
259
|
+
| `>` | Greater than |
|
|
260
|
+
| `<` | Less than |
|
|
261
|
+
| `>=` | Greater than or equal |
|
|
262
|
+
| `<=` | Less than or equal |
|
|
263
|
+
| `CONTAINS` | String contains (case-sensitive) |
|
|
264
|
+
|
|
265
|
+
### Batch Operations
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Create multiple records at once
|
|
269
|
+
const created = await products.batchCreate([
|
|
270
|
+
{ name: 'Item A', price: 10.00, inStock: true },
|
|
271
|
+
{ name: 'Item B', price: 20.00, inStock: false },
|
|
272
|
+
{ name: 'Item C', price: 30.00, inStock: true },
|
|
273
|
+
]);
|
|
274
|
+
// → [{ id: 'rec_1', ... }, { id: 'rec_2', ... }, { id: 'rec_3', ... }]
|
|
259
275
|
|
|
260
|
-
|
|
276
|
+
// Count records
|
|
277
|
+
const total = await products.count();
|
|
278
|
+
const inStock = await products.count([{ field: 'inStock', op: '==', value: true }]);
|
|
261
279
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
db.storage('documents') // returns one for your documents bucket
|
|
265
|
-
db.storage('main') // returns one for your main bucket
|
|
280
|
+
// Delete multiple records
|
|
281
|
+
const { deleted, failed } = await products.batchDelete(['rec_1', 'rec_2', 'rec_3']);
|
|
266
282
|
```
|
|
267
283
|
|
|
268
|
-
|
|
284
|
+
### Version History
|
|
269
285
|
|
|
270
|
-
|
|
271
|
-
const avatarStore = db.storage('avatars');
|
|
272
|
-
const documentStore = db.storage('documents');
|
|
286
|
+
Every write to a record creates a new version stored in GCS, so you can travel back in time.
|
|
273
287
|
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
```typescript
|
|
289
|
+
// Get the full version history of a record
|
|
290
|
+
const history = await products.getHistory('rec_abc123');
|
|
291
|
+
// history[0] is the latest version, history[1] is one write before, etc.
|
|
292
|
+
|
|
293
|
+
// Restore to a specific version
|
|
294
|
+
const restored = await products.restoreVersion('rec_abc123', history[2]!.version);
|
|
276
295
|
```
|
|
277
296
|
|
|
278
297
|
---
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
## Authentication
|
|
300
|
+
|
|
301
|
+
HydrousDB has a built-in user auth system. Your users live in a bucket you create
|
|
302
|
+
(e.g. `"app-users"`). You get sessions, refresh tokens, password reset, email
|
|
303
|
+
verification, and admin controls out of the box.
|
|
281
304
|
|
|
282
|
-
```
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
overwrite: true,
|
|
305
|
+
```typescript
|
|
306
|
+
const auth = db.auth('app-users');
|
|
307
|
+
```
|
|
286
308
|
|
|
287
|
-
|
|
288
|
-
// p.stage — 'pending' | 'compressing' | 'uploading' | 'done' | 'error'
|
|
289
|
-
// p.percent — 0–100 integer
|
|
290
|
-
// p.bytesUploaded — bytes sent so far
|
|
291
|
-
// p.totalBytes — total size
|
|
292
|
-
// p.bytesPerSecond — current speed (null until first tick)
|
|
293
|
-
// p.eta — estimated seconds remaining
|
|
309
|
+
### Sign Up Users
|
|
294
310
|
|
|
295
|
-
|
|
296
|
-
|
|
311
|
+
```typescript
|
|
312
|
+
const { user, session } = await auth.signup({
|
|
313
|
+
email: 'alice@example.com',
|
|
314
|
+
password: 'hunter2', // min 8 characters, validated server-side
|
|
315
|
+
fullName: 'Alice Wonderland',
|
|
316
|
+
// Any extra fields are stored on the user record:
|
|
317
|
+
plan: 'pro',
|
|
318
|
+
referral: 'friend123',
|
|
297
319
|
});
|
|
298
320
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
321
|
+
// user.id → "usr_xxxxxxxxxx"
|
|
322
|
+
// session.sessionId → persist this in your app
|
|
323
|
+
// session.refreshToken → persist this for long-lived sessions
|
|
303
324
|
```
|
|
304
325
|
|
|
305
|
-
###
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
const files = Array.from(fileInput.files);
|
|
309
|
-
|
|
310
|
-
const { data } = await db.storage('documents').batchUpload(files, {
|
|
311
|
-
prefix: 'uploads/2024/',
|
|
312
|
-
overwrite: false,
|
|
313
|
-
concurrency: 5,
|
|
326
|
+
### Log In / Log Out
|
|
314
327
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
```typescript
|
|
329
|
+
// Log in
|
|
330
|
+
const { user, session } = await auth.login({
|
|
331
|
+
email: 'alice@example.com',
|
|
332
|
+
password: 'hunter2',
|
|
319
333
|
});
|
|
320
334
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
data!.failed.forEach(f => console.error(f.path, f.error));
|
|
335
|
+
// Log out (invalidates the session server-side)
|
|
336
|
+
await auth.logout({ sessionId: session.sessionId });
|
|
324
337
|
```
|
|
325
338
|
|
|
326
|
-
###
|
|
339
|
+
### Session Management
|
|
327
340
|
|
|
328
|
-
|
|
329
|
-
const { data: buffer, error } = await db.storage('documents').download('reports/q1.pdf');
|
|
341
|
+
Sessions expire after **24 hours**. Use the refresh token to get a new session (refresh tokens last **30 days**).
|
|
330
342
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
a.download = 'q1.pdf';
|
|
338
|
-
a.click();
|
|
339
|
-
URL.revokeObjectURL(url);
|
|
343
|
+
```typescript
|
|
344
|
+
// Refresh the session before it expires
|
|
345
|
+
const newSession = await auth.refreshSession({
|
|
346
|
+
refreshToken: session.refreshToken,
|
|
347
|
+
});
|
|
348
|
+
// Store newSession.sessionId and newSession.refreshToken
|
|
340
349
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// writeFileSync('q1.pdf', Buffer.from(buffer));
|
|
344
|
-
}
|
|
350
|
+
// Get the current user
|
|
351
|
+
const user = await auth.getUser({ userId: session.userId });
|
|
345
352
|
```
|
|
346
353
|
|
|
347
|
-
###
|
|
354
|
+
### Update User Profile
|
|
348
355
|
|
|
349
|
-
```
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
356
|
+
```typescript
|
|
357
|
+
const updated = await auth.updateUser({
|
|
358
|
+
sessionId: session.sessionId,
|
|
359
|
+
userId: user.id,
|
|
360
|
+
data: {
|
|
361
|
+
fullName: 'Alice Smith',
|
|
362
|
+
plan: 'enterprise',
|
|
363
|
+
avatar: 'https://example.com/avatar.jpg',
|
|
364
|
+
},
|
|
353
365
|
});
|
|
354
|
-
|
|
355
|
-
for (const item of data!.items) {
|
|
356
|
-
if (item.type === 'folder') console.log('📁', item.path);
|
|
357
|
-
else console.log('📄', item.path, item.size, 'bytes');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Paginate
|
|
361
|
-
if (data!.pagination.hasNextPage) {
|
|
362
|
-
const page2 = await db.storage('documents').list({
|
|
363
|
-
prefix: 'invoices/',
|
|
364
|
-
cursor: data!.pagination.nextCursor!,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
366
|
```
|
|
368
367
|
|
|
369
|
-
###
|
|
368
|
+
### Password Reset Flow
|
|
370
369
|
|
|
371
|
-
```
|
|
372
|
-
//
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
// Recursively delete a folder
|
|
376
|
-
await db.storage('temp').deleteFolder('uploads/draft/');
|
|
377
|
-
|
|
378
|
-
// Create a folder
|
|
379
|
-
await db.storage('documents').createFolder('reports/2025/');
|
|
370
|
+
```typescript
|
|
371
|
+
// 1. User requests a reset (always returns success — prevents email enumeration)
|
|
372
|
+
await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
380
373
|
|
|
381
|
-
//
|
|
382
|
-
await db.storage('documents').move('drafts/report.pdf', 'published/report.pdf');
|
|
374
|
+
// 2. User receives an email with a reset token (your app handles the email sending)
|
|
383
375
|
|
|
384
|
-
//
|
|
385
|
-
await
|
|
376
|
+
// 3. User submits the new password
|
|
377
|
+
await auth.confirmPasswordReset({
|
|
378
|
+
resetToken: 'tok_from_email',
|
|
379
|
+
newPassword: 'correcthorsebatterystaple',
|
|
380
|
+
});
|
|
381
|
+
// All existing sessions for this user are automatically revoked
|
|
386
382
|
```
|
|
387
383
|
|
|
388
|
-
###
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
// Generate a link that expires in 10 minutes
|
|
392
|
-
const { data } = await db.storage('documents').signedUrl(
|
|
393
|
-
'contracts/nda.pdf',
|
|
394
|
-
{ expiresIn: 600 }
|
|
395
|
-
);
|
|
384
|
+
### Change Password (authenticated)
|
|
396
385
|
|
|
397
|
-
|
|
398
|
-
|
|
386
|
+
```typescript
|
|
387
|
+
await auth.changePassword({
|
|
388
|
+
sessionId: session.sessionId,
|
|
389
|
+
userId: user.id,
|
|
390
|
+
currentPassword: 'hunter2',
|
|
391
|
+
newPassword: 'correcthorsebatterystaple',
|
|
392
|
+
});
|
|
399
393
|
```
|
|
400
394
|
|
|
401
|
-
###
|
|
395
|
+
### Email Verification
|
|
402
396
|
|
|
403
|
-
```
|
|
404
|
-
//
|
|
405
|
-
await
|
|
406
|
-
'settings/app.json',
|
|
407
|
-
JSON.stringify({ theme: 'dark', language: 'en' }, null, 2),
|
|
408
|
-
{ mimeType: 'application/json' }
|
|
409
|
-
);
|
|
410
|
-
```
|
|
397
|
+
```typescript
|
|
398
|
+
// 1. Send verification email
|
|
399
|
+
await auth.requestEmailVerification({ userId: user.id });
|
|
411
400
|
|
|
412
|
-
|
|
401
|
+
// 2. User clicks link in email, your app extracts the token
|
|
413
402
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
console.log('Files:', stats!.totalFiles);
|
|
417
|
-
console.log('Stored:', stats!.totalSizeBytes, 'bytes');
|
|
418
|
-
console.log('Saved:', stats!.spaceSavedBytes, 'bytes via compression');
|
|
419
|
-
console.log('Credits:', stats!.creditsTotalUsed);
|
|
403
|
+
// 3. Confirm the token
|
|
404
|
+
await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
|
|
420
405
|
```
|
|
421
406
|
|
|
422
|
-
|
|
407
|
+
### Admin Operations
|
|
423
408
|
|
|
424
|
-
|
|
409
|
+
Admin operations require a valid session from a user with `role: 'admin'`.
|
|
425
410
|
|
|
426
|
-
|
|
411
|
+
```typescript
|
|
412
|
+
// List all users
|
|
413
|
+
const { users, total } = await auth.listUsers({
|
|
414
|
+
sessionId: adminSession.sessionId,
|
|
415
|
+
limit: 50,
|
|
416
|
+
offset: 0,
|
|
417
|
+
});
|
|
427
418
|
|
|
428
|
-
|
|
429
|
-
|
|
419
|
+
// Lock an account (prevents login)
|
|
420
|
+
await auth.lockAccount({
|
|
421
|
+
sessionId: adminSession.sessionId,
|
|
422
|
+
userId: 'usr_abc123',
|
|
423
|
+
duration: 60 * 60 * 1000, // lock for 1 hour (default: 15 minutes)
|
|
424
|
+
});
|
|
430
425
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
```
|
|
426
|
+
// Unlock an account
|
|
427
|
+
await auth.unlockAccount({
|
|
428
|
+
sessionId: adminSession.sessionId,
|
|
429
|
+
userId: 'usr_abc123',
|
|
430
|
+
});
|
|
437
431
|
|
|
438
|
-
|
|
432
|
+
// Soft-delete a user (marks as deleted, keeps data)
|
|
433
|
+
await auth.deleteUser({
|
|
434
|
+
sessionId: adminSession.sessionId,
|
|
435
|
+
userId: 'usr_abc123',
|
|
436
|
+
});
|
|
439
437
|
|
|
440
|
-
|
|
441
|
-
|
|
438
|
+
// Hard-delete a user (permanent — irreversible)
|
|
439
|
+
await auth.hardDeleteUser({
|
|
440
|
+
sessionId: adminSession.sessionId,
|
|
441
|
+
userId: 'usr_abc123',
|
|
442
|
+
});
|
|
442
443
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
}
|
|
444
|
+
// Bulk delete multiple users
|
|
445
|
+
const { deleted, failed } = await auth.bulkDeleteUsers({
|
|
446
|
+
sessionId: adminSession.sessionId,
|
|
447
|
+
userIds: ['usr_a', 'usr_b', 'usr_c'],
|
|
448
|
+
});
|
|
450
449
|
```
|
|
451
450
|
|
|
452
|
-
|
|
451
|
+
---
|
|
453
452
|
|
|
454
|
-
|
|
455
|
-
|-----------------------|------------------------------------------------|
|
|
456
|
-
| `HTTP_ERROR` | Non-2xx response from the server |
|
|
457
|
-
| `NETWORK_ERROR` | Could not reach the server |
|
|
458
|
-
| `QUOTA_EXCEEDED` | Storage quota reached |
|
|
459
|
-
| `FILE_TOO_LARGE` | File exceeds the 50 MB per-file limit |
|
|
460
|
-
| `FILE_EXISTS` | File exists and `overwrite` is false |
|
|
461
|
-
| `UPLOAD_ABORTED` | Upload cancelled |
|
|
462
|
-
| `UPLOAD_TIMEOUT` | Upload timed out |
|
|
463
|
-
| `UNKNOWN_STORAGE_KEY` | Key name not in your `storageKeys` config |
|
|
464
|
-
| `NO_SESSION` | Auth operation requires a signed-in session |
|
|
453
|
+
## File Storage
|
|
465
454
|
|
|
466
|
-
|
|
455
|
+
HydrousDB Storage is backed by Google Cloud Storage. Your files live at:
|
|
456
|
+
```
|
|
457
|
+
hydrous-storage/{your-owner-id}/{your-path}
|
|
458
|
+
```
|
|
459
|
+
You never see or specify the owner prefix — the SDK handles it transparently.
|
|
467
460
|
|
|
468
|
-
|
|
461
|
+
### Simple Upload
|
|
469
462
|
|
|
470
|
-
|
|
463
|
+
For files up to **500 MB** when you don't need upload progress:
|
|
471
464
|
|
|
472
|
-
```
|
|
473
|
-
//
|
|
474
|
-
|
|
465
|
+
```typescript
|
|
466
|
+
// Browser: upload from a file input
|
|
467
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
468
|
+
const file = fileInput.files[0];
|
|
475
469
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
480
|
-
storageKeys: {
|
|
481
|
-
avatars: process.env.HYDROUS_STORAGE_AVATARS!,
|
|
482
|
-
documents: process.env.HYDROUS_STORAGE_DOCS!,
|
|
483
|
-
},
|
|
470
|
+
const result = await db.storage.upload(file, `uploads/${file.name}`, {
|
|
471
|
+
isPublic: true, // publicly accessible without auth
|
|
472
|
+
overwrite: false, // throw if the file already exists
|
|
484
473
|
});
|
|
485
|
-
```
|
|
486
474
|
|
|
487
|
-
|
|
475
|
+
console.log(result.publicUrl); // CDN URL — usable anywhere
|
|
476
|
+
console.log(result.downloadUrl); // null (it's public)
|
|
477
|
+
console.log(result.size); // bytes
|
|
478
|
+
console.log(result.mimeType); // auto-detected from extension
|
|
488
479
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
import { useRouter } from 'next/navigation';
|
|
496
|
-
import { db } from '@/lib/db';
|
|
497
|
-
|
|
498
|
-
export default function LoginPage() {
|
|
499
|
-
const router = useRouter();
|
|
500
|
-
const [email, setEmail] = useState('');
|
|
501
|
-
const [password, setPassword] = useState('');
|
|
502
|
-
const [error, setError] = useState<string | null>(null);
|
|
503
|
-
const [loading, setLoading] = useState(false);
|
|
504
|
-
|
|
505
|
-
async function handleLogin(e: React.FormEvent) {
|
|
506
|
-
e.preventDefault();
|
|
507
|
-
setLoading(true);
|
|
508
|
-
setError(null);
|
|
509
|
-
|
|
510
|
-
const { data, error } = await db.auth.signIn({ email, password });
|
|
511
|
-
|
|
512
|
-
if (error) {
|
|
513
|
-
setError(error.message);
|
|
514
|
-
setLoading(false);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Persist session
|
|
519
|
-
localStorage.setItem('hydrous_token', data!.accessToken);
|
|
520
|
-
router.push('/dashboard');
|
|
521
|
-
}
|
|
480
|
+
// Node.js: upload from a Buffer
|
|
481
|
+
import { readFileSync } from 'fs';
|
|
482
|
+
const buffer = readFileSync('./report.pdf');
|
|
483
|
+
const result = await db.storage.upload(buffer, 'reports/q3.pdf');
|
|
484
|
+
console.log(result.downloadUrl); // requires X-Storage-Key to access
|
|
485
|
+
```
|
|
522
486
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
{loading ? 'Signing in…' : 'Sign In'}
|
|
532
|
-
</button>
|
|
533
|
-
</form>
|
|
534
|
-
);
|
|
535
|
-
}
|
|
487
|
+
### Upload Raw JSON or Text
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
const result = await db.storage.uploadRaw(
|
|
491
|
+
{ theme: 'dark', language: 'en' },
|
|
492
|
+
'user-config/alice.json',
|
|
493
|
+
{ isPublic: false },
|
|
494
|
+
);
|
|
536
495
|
```
|
|
537
496
|
|
|
538
|
-
|
|
497
|
+
### Large File Upload (with progress)
|
|
539
498
|
|
|
540
|
-
|
|
499
|
+
For files over 10 MB or when you need a progress bar. The file goes directly
|
|
500
|
+
to GCS — your server never buffers it.
|
|
541
501
|
|
|
542
|
-
```
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
502
|
+
```typescript
|
|
503
|
+
// Step 1: Get a signed upload URL
|
|
504
|
+
const { uploadUrl, path } = await db.storage.getUploadUrl({
|
|
505
|
+
path: 'videos/product-demo.mp4',
|
|
506
|
+
mimeType: 'video/mp4',
|
|
507
|
+
size: file.size,
|
|
508
|
+
isPublic: true,
|
|
509
|
+
});
|
|
546
510
|
|
|
547
|
-
|
|
511
|
+
// Step 2: Upload directly to GCS with progress
|
|
512
|
+
await db.storage.uploadToSignedUrl(
|
|
513
|
+
uploadUrl,
|
|
514
|
+
file,
|
|
515
|
+
'video/mp4',
|
|
516
|
+
(percent) => {
|
|
517
|
+
progressBar.style.width = `${percent}%`;
|
|
518
|
+
console.log(`${percent}% uploaded`);
|
|
519
|
+
},
|
|
520
|
+
);
|
|
548
521
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
522
|
+
// Step 3: Confirm the upload (registers metadata)
|
|
523
|
+
const result = await db.storage.confirmUpload({
|
|
524
|
+
path: path,
|
|
525
|
+
mimeType: 'video/mp4',
|
|
526
|
+
isPublic: true,
|
|
527
|
+
});
|
|
555
528
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return (
|
|
559
|
-
<ul>
|
|
560
|
-
{products.map(p => (
|
|
561
|
-
<li key={p.id}>
|
|
562
|
-
{p.name} — ${p.price}
|
|
563
|
-
</li>
|
|
564
|
-
))}
|
|
565
|
-
</ul>
|
|
566
|
-
);
|
|
567
|
-
}
|
|
529
|
+
console.log(result.publicUrl); // ready to use
|
|
568
530
|
```
|
|
569
531
|
|
|
570
|
-
|
|
532
|
+
### Batch Upload
|
|
571
533
|
|
|
572
|
-
|
|
534
|
+
```typescript
|
|
535
|
+
// Get signed URLs for multiple files
|
|
536
|
+
const { files } = await db.storage.getBatchUploadUrls([
|
|
537
|
+
{ path: 'gallery/photo1.jpg', mimeType: 'image/jpeg', size: 204800, isPublic: true },
|
|
538
|
+
{ path: 'gallery/photo2.jpg', mimeType: 'image/jpeg', size: 153600, isPublic: true },
|
|
539
|
+
]);
|
|
573
540
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
import { db } from '@/lib/db';
|
|
579
|
-
import type { UploadProgress } from 'hydrousdb';
|
|
541
|
+
// Upload each one
|
|
542
|
+
for (const f of files) {
|
|
543
|
+
await db.storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
544
|
+
}
|
|
580
545
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
546
|
+
// Confirm all at once
|
|
547
|
+
const results = await db.storage.batchConfirmUploads(
|
|
548
|
+
files.map(f => ({ path: f.path, mimeType: f.mimeType, isPublic: true }))
|
|
549
|
+
);
|
|
550
|
+
```
|
|
585
551
|
|
|
586
|
-
|
|
587
|
-
const file = e.target.files?.[0];
|
|
588
|
-
if (!file) return;
|
|
552
|
+
### Download Files
|
|
589
553
|
|
|
590
|
-
|
|
591
|
-
|
|
554
|
+
```typescript
|
|
555
|
+
// Private files require authentication — download as ArrayBuffer
|
|
556
|
+
const buffer = await db.storage.download('reports/q3.pdf');
|
|
557
|
+
const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
592
558
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
559
|
+
// In a browser: trigger a file download
|
|
560
|
+
const url = URL.createObjectURL(blob);
|
|
561
|
+
const a = document.createElement('a');
|
|
562
|
+
a.href = url;
|
|
563
|
+
a.download = 'q3.pdf';
|
|
564
|
+
a.click();
|
|
596
565
|
|
|
597
|
-
|
|
598
|
-
|
|
566
|
+
// Public files: just use the publicUrl directly (no SDK needed)
|
|
567
|
+
// <img src={result.publicUrl} />
|
|
568
|
+
```
|
|
599
569
|
|
|
600
|
-
|
|
570
|
+
### List Files
|
|
601
571
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
572
|
+
```typescript
|
|
573
|
+
// List everything at the root
|
|
574
|
+
const { files, folders } = await db.storage.list();
|
|
575
|
+
|
|
576
|
+
// List a specific folder
|
|
577
|
+
const { files, folders, hasMore, nextCursor } = await db.storage.list({
|
|
578
|
+
prefix: 'gallery/',
|
|
579
|
+
limit: 50,
|
|
580
|
+
recursive: false,
|
|
581
|
+
});
|
|
610
582
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
{progress && (
|
|
616
|
-
<div>
|
|
617
|
-
<p>{progress.stage} — {progress.percent}%</p>
|
|
618
|
-
<progress value={progress.percent} max={100} />
|
|
619
|
-
{progress.bytesPerSecond && (
|
|
620
|
-
<p>
|
|
621
|
-
{(progress.bytesPerSecond / 1024).toFixed(1)} KB/s
|
|
622
|
-
{progress.eta != null && ` · ${progress.eta}s remaining`}
|
|
623
|
-
</p>
|
|
624
|
-
)}
|
|
625
|
-
</div>
|
|
626
|
-
)}
|
|
627
|
-
|
|
628
|
-
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
629
|
-
{avatarUrl && <img src={avatarUrl} alt="Avatar" width={100} />}
|
|
630
|
-
</div>
|
|
631
|
-
);
|
|
583
|
+
// Paginate
|
|
584
|
+
if (hasMore) {
|
|
585
|
+
const page2 = await db.storage.list({ prefix: 'gallery/', cursor: nextCursor });
|
|
632
586
|
}
|
|
633
587
|
```
|
|
634
588
|
|
|
635
|
-
|
|
589
|
+
Each file entry includes:
|
|
590
|
+
```typescript
|
|
591
|
+
{
|
|
592
|
+
name: 'photo1.jpg',
|
|
593
|
+
path: 'gallery/photo1.jpg',
|
|
594
|
+
size: 204800,
|
|
595
|
+
mimeType: 'image/jpeg',
|
|
596
|
+
isPublic: true,
|
|
597
|
+
publicUrl: 'https://storage.googleapis.com/...',
|
|
598
|
+
downloadUrl: null,
|
|
599
|
+
updatedAt: '2025-06-01T12:00:00.000Z',
|
|
600
|
+
}
|
|
601
|
+
```
|
|
636
602
|
|
|
637
|
-
### Storage
|
|
603
|
+
### Scoped Storage
|
|
638
604
|
|
|
639
|
-
|
|
640
|
-
// app/api/download/route.ts
|
|
641
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
642
|
-
import { db } from '@/lib/db';
|
|
605
|
+
Working within a specific folder? Use `.scope()` to avoid typing the prefix on every call.
|
|
643
606
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
607
|
+
```typescript
|
|
608
|
+
// All operations in the "user-avatars/" folder
|
|
609
|
+
const avatars = db.storage.scope('user-avatars');
|
|
647
610
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
expiresIn: 120,
|
|
651
|
-
});
|
|
611
|
+
await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
|
|
612
|
+
// → uploads to "user-avatars/{userId}.jpg"
|
|
652
613
|
|
|
653
|
-
|
|
614
|
+
const { files } = await avatars.list();
|
|
615
|
+
// → lists files under "user-avatars/"
|
|
654
616
|
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
```
|
|
617
|
+
await avatars.deleteFile(`${userId}.jpg`);
|
|
618
|
+
// → deletes "user-avatars/{userId}.jpg"
|
|
658
619
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
//
|
|
662
|
-
// const { url } = await res.json();
|
|
663
|
-
// window.open(url, '_blank');
|
|
620
|
+
// Nest scopes
|
|
621
|
+
const thumbnails = avatars.scope('thumbnails');
|
|
622
|
+
// → all operations under "user-avatars/thumbnails/"
|
|
664
623
|
```
|
|
665
624
|
|
|
666
|
-
|
|
625
|
+
### Share & Visibility
|
|
667
626
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// components/Analytics.tsx
|
|
672
|
-
'use client';
|
|
673
|
-
import { useEffect } from 'react';
|
|
674
|
-
import { usePathname } from 'next/navigation';
|
|
675
|
-
import { db } from '@/lib/db';
|
|
676
|
-
|
|
677
|
-
export default function Analytics() {
|
|
678
|
-
const pathname = usePathname();
|
|
679
|
-
|
|
680
|
-
useEffect(() => {
|
|
681
|
-
db.analytics.track({
|
|
682
|
-
event: 'page_view',
|
|
683
|
-
properties: {
|
|
684
|
-
path: pathname,
|
|
685
|
-
referrer: document.referrer,
|
|
686
|
-
userAgent: navigator.userAgent,
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
|
-
}, [pathname]);
|
|
690
|
-
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
```
|
|
627
|
+
```typescript
|
|
628
|
+
// Get file metadata (sizes, URLs, visibility)
|
|
629
|
+
const meta = await db.storage.getMetadata('reports/q3.pdf');
|
|
694
630
|
|
|
695
|
-
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
</html>
|
|
707
|
-
);
|
|
708
|
-
}
|
|
631
|
+
// Generate a time-limited share link for a private file
|
|
632
|
+
// (no auth key needed to use the link)
|
|
633
|
+
const { signedUrl, expiresAt } = await db.storage.getSignedUrl(
|
|
634
|
+
'reports/q3.pdf',
|
|
635
|
+
3600, // expires in 1 hour (default)
|
|
636
|
+
);
|
|
637
|
+
// Share signedUrl with whoever needs it
|
|
638
|
+
|
|
639
|
+
// Toggle visibility after upload
|
|
640
|
+
const result = await db.storage.setVisibility('reports/q3.pdf', true); // make public
|
|
641
|
+
const result2 = await db.storage.setVisibility('reports/q3.pdf', false); // make private
|
|
709
642
|
```
|
|
710
643
|
|
|
711
|
-
|
|
644
|
+
### File Operations
|
|
712
645
|
|
|
713
|
-
|
|
646
|
+
```typescript
|
|
647
|
+
// Rename / move a file
|
|
648
|
+
await db.storage.move('drafts/report.pdf', 'published/report-2025.pdf');
|
|
714
649
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
'use server';
|
|
718
|
-
import { db } from '@/lib/db';
|
|
719
|
-
import { revalidatePath } from 'next/cache';
|
|
650
|
+
// Copy a file
|
|
651
|
+
await db.storage.copy('templates/invoice.html', 'invoices/inv-001.html');
|
|
720
652
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const price = Number(formData.get('price'));
|
|
724
|
-
const category = formData.get('category') as string;
|
|
653
|
+
// Create a folder
|
|
654
|
+
await db.storage.createFolder('archive/2025/');
|
|
725
655
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
656
|
+
// Delete a file
|
|
657
|
+
await db.storage.deleteFile('temp/scratch.txt');
|
|
729
658
|
|
|
730
|
-
|
|
659
|
+
// Delete a folder and all its contents
|
|
660
|
+
await db.storage.deleteFolder('temp/');
|
|
731
661
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
662
|
+
// Get key-level stats
|
|
663
|
+
const stats = await db.storage.getStats();
|
|
664
|
+
// → { totalFiles: 842, totalBytes: 1073741824, uploadCount: 1200, ... }
|
|
735
665
|
```
|
|
736
666
|
|
|
737
667
|
---
|
|
738
668
|
|
|
739
|
-
##
|
|
669
|
+
## Analytics
|
|
740
670
|
|
|
741
|
-
|
|
671
|
+
HydrousDB Analytics runs your queries against BigQuery, so they're fast even
|
|
672
|
+
on millions of records. All queries accept an optional `dateRange` filter.
|
|
742
673
|
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
|
|
674
|
+
```typescript
|
|
675
|
+
const analytics = db.analytics('orders');
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Count
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
// Total records
|
|
682
|
+
const { count } = await analytics.count();
|
|
746
683
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
avatars: import.meta.env.VITE_HYDROUS_STORAGE_AVATARS,
|
|
753
|
-
documents: import.meta.env.VITE_HYDROUS_STORAGE_DOCS,
|
|
684
|
+
// Records in a date range
|
|
685
|
+
const { count: lastWeek } = await analytics.count({
|
|
686
|
+
dateRange: {
|
|
687
|
+
start: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
|
688
|
+
end: Date.now(),
|
|
754
689
|
},
|
|
755
690
|
});
|
|
756
691
|
```
|
|
757
692
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
### Auth: Login composable
|
|
693
|
+
### Distribution
|
|
761
694
|
|
|
762
|
-
|
|
763
|
-
// composables/useAuth.ts
|
|
764
|
-
import { ref } from 'vue';
|
|
765
|
-
import { useRouter } from 'vue-router';
|
|
766
|
-
import { db } from '@/lib/db';
|
|
695
|
+
How many records have each unique value for a field?
|
|
767
696
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
697
|
+
```typescript
|
|
698
|
+
const rows = await analytics.distribution({ field: 'status', limit: 10, order: 'desc' });
|
|
699
|
+
// → [
|
|
700
|
+
// { value: 'completed', count: 8234 },
|
|
701
|
+
// { value: 'pending', count: 1203 },
|
|
702
|
+
// { value: 'refunded', count: 412 },
|
|
703
|
+
// ]
|
|
704
|
+
```
|
|
773
705
|
|
|
774
|
-
|
|
775
|
-
loading.value = true;
|
|
776
|
-
error.value = null;
|
|
706
|
+
### Sum
|
|
777
707
|
|
|
778
|
-
|
|
708
|
+
```typescript
|
|
709
|
+
// Total revenue
|
|
710
|
+
const rows = await analytics.sum({ field: 'amount' });
|
|
711
|
+
// → [{ sum: 198432.50 }]
|
|
779
712
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
713
|
+
// Revenue by country
|
|
714
|
+
const byCountry = await analytics.sum({
|
|
715
|
+
field: 'amount',
|
|
716
|
+
groupBy: 'country',
|
|
717
|
+
limit: 10,
|
|
718
|
+
});
|
|
719
|
+
// → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
|
|
720
|
+
```
|
|
787
721
|
|
|
788
|
-
|
|
789
|
-
}
|
|
722
|
+
### Time Series
|
|
790
723
|
|
|
791
|
-
|
|
792
|
-
await db.auth.signOut();
|
|
793
|
-
user.value = null;
|
|
794
|
-
localStorage.removeItem('token');
|
|
795
|
-
router.push('/login');
|
|
796
|
-
}
|
|
724
|
+
Record counts over time — ideal for activity charts.
|
|
797
725
|
|
|
798
|
-
|
|
799
|
-
|
|
726
|
+
```typescript
|
|
727
|
+
const rows = await analytics.timeSeries({
|
|
728
|
+
granularity: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
|
|
729
|
+
dateRange: {
|
|
730
|
+
start: new Date('2025-01-01').getTime(),
|
|
731
|
+
end: new Date('2025-06-01').getTime(),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
// → [{ date: '2025-01-01', count: 42 }, { date: '2025-01-02', count: 67 }, ...]
|
|
800
735
|
```
|
|
801
736
|
|
|
802
|
-
|
|
737
|
+
Aggregate a field over time:
|
|
803
738
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
import type { UploadProgress } from 'hydrousdb';
|
|
812
|
-
|
|
813
|
-
const progress = ref<UploadProgress | null>(null);
|
|
814
|
-
const done = ref(false);
|
|
815
|
-
const error = ref<string | null>(null);
|
|
816
|
-
|
|
817
|
-
async function onFileChange(event: Event) {
|
|
818
|
-
const file = (event.target as HTMLInputElement).files?.[0];
|
|
819
|
-
if (!file) return;
|
|
820
|
-
|
|
821
|
-
done.value = false;
|
|
822
|
-
error.value = null;
|
|
823
|
-
progress.value = null;
|
|
824
|
-
|
|
825
|
-
const { data, error: err } = await db.storage('documents').upload(file, {
|
|
826
|
-
path: `uploads/${Date.now()}-${file.name}`,
|
|
827
|
-
overwrite: false,
|
|
828
|
-
onProgress: (p) => {
|
|
829
|
-
progress.value = p;
|
|
830
|
-
},
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
if (err) {
|
|
834
|
-
error.value = err.message;
|
|
835
|
-
} else {
|
|
836
|
-
done.value = true;
|
|
837
|
-
progress.value = null;
|
|
838
|
-
console.log('Uploaded to', data!.path);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
</script>
|
|
842
|
-
|
|
843
|
-
<template>
|
|
844
|
-
<div>
|
|
845
|
-
<input type="file" @change="onFileChange" />
|
|
846
|
-
|
|
847
|
-
<div v-if="progress">
|
|
848
|
-
<p>{{ progress.stage }} — {{ progress.percent }}%</p>
|
|
849
|
-
<div class="progress-bar">
|
|
850
|
-
<div class="fill" :style="{ width: progress.percent + '%' }" />
|
|
851
|
-
</div>
|
|
852
|
-
<p v-if="progress.bytesPerSecond">
|
|
853
|
-
{{ (progress.bytesPerSecond / 1024).toFixed(1) }} KB/s
|
|
854
|
-
<span v-if="progress.eta != null"> · {{ progress.eta }}s left</span>
|
|
855
|
-
</p>
|
|
856
|
-
</div>
|
|
857
|
-
|
|
858
|
-
<p v-if="done" style="color: green">✓ Upload complete!</p>
|
|
859
|
-
<p v-if="error" style="color: red">{{ error }}</p>
|
|
860
|
-
</div>
|
|
861
|
-
</template>
|
|
739
|
+
```typescript
|
|
740
|
+
const revenue = await analytics.fieldTimeSeries({
|
|
741
|
+
field: 'amount',
|
|
742
|
+
aggregation: 'sum', // 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
743
|
+
granularity: 'week',
|
|
744
|
+
});
|
|
745
|
+
// → [{ date: '2025-W01', value: 12340.50 }, ...]
|
|
862
746
|
```
|
|
863
747
|
|
|
864
|
-
|
|
748
|
+
### Top N
|
|
749
|
+
|
|
750
|
+
Most common values for a field:
|
|
865
751
|
|
|
866
|
-
|
|
752
|
+
```typescript
|
|
753
|
+
const topProducts = await analytics.topN({
|
|
754
|
+
field: 'productId',
|
|
755
|
+
labelField: 'productName', // optional: include a human-readable label
|
|
756
|
+
n: 5,
|
|
757
|
+
order: 'desc',
|
|
758
|
+
});
|
|
759
|
+
// → [
|
|
760
|
+
// { value: 'prod_123', label: 'Widget Pro', count: 892 },
|
|
761
|
+
// { value: 'prod_456', label: 'Gizmo Plus', count: 743 },
|
|
762
|
+
// ]
|
|
763
|
+
```
|
|
867
764
|
|
|
868
|
-
|
|
869
|
-
<!-- components/ProductTable.vue -->
|
|
870
|
-
<script setup lang="ts">
|
|
871
|
-
import { ref, onMounted } from 'vue';
|
|
872
|
-
import { db } from '@/lib/db';
|
|
873
|
-
import { eq } from 'hydrousdb';
|
|
765
|
+
### Field Stats
|
|
874
766
|
|
|
875
|
-
|
|
767
|
+
Statistical summary for any numeric field:
|
|
876
768
|
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
769
|
+
```typescript
|
|
770
|
+
const stats = await analytics.stats({ field: 'orderValue' });
|
|
771
|
+
// → {
|
|
772
|
+
// min: 4.99, max: 9999.99, avg: 87.23,
|
|
773
|
+
// sum: 420948.27, count: 4823, stddev: 143.2
|
|
774
|
+
// }
|
|
775
|
+
```
|
|
881
776
|
|
|
882
|
-
|
|
883
|
-
const { data, count, error } = await db.records.select<Product>('products', {
|
|
884
|
-
where: eq('inStock', true),
|
|
885
|
-
orderBy: { field: 'name', direction: 'asc' },
|
|
886
|
-
limit,
|
|
887
|
-
offset: (p - 1) * limit,
|
|
888
|
-
});
|
|
777
|
+
### Multi-Metric Dashboard
|
|
889
778
|
|
|
890
|
-
|
|
891
|
-
products.value = data;
|
|
892
|
-
total.value = count;
|
|
893
|
-
page.value = p;
|
|
894
|
-
}
|
|
779
|
+
Calculate several aggregations in a single BigQuery query:
|
|
895
780
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
</div>
|
|
913
|
-
</template>
|
|
781
|
+
```typescript
|
|
782
|
+
const dashboard = await analytics.multiMetric({
|
|
783
|
+
metrics: [
|
|
784
|
+
{ field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
785
|
+
{ field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
|
|
786
|
+
{ field: 'amount', name: 'maxOrder', aggregation: 'max' },
|
|
787
|
+
{ field: 'userId', name: 'totalOrders', aggregation: 'count' },
|
|
788
|
+
],
|
|
789
|
+
dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
|
|
790
|
+
});
|
|
791
|
+
// → {
|
|
792
|
+
// totalRevenue: 198432.50,
|
|
793
|
+
// avgOrderValue: 87.23,
|
|
794
|
+
// maxOrder: 9999.99,
|
|
795
|
+
// totalOrders: 2275,
|
|
796
|
+
// }
|
|
914
797
|
```
|
|
915
798
|
|
|
916
|
-
|
|
799
|
+
### Filtered Records (BigQuery)
|
|
917
800
|
|
|
918
|
-
|
|
801
|
+
Query raw records with full BigQuery speed:
|
|
919
802
|
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
803
|
+
```typescript
|
|
804
|
+
const records = await analytics.records({
|
|
805
|
+
filters: [
|
|
806
|
+
{ field: 'status', op: '==', value: 'refunded' },
|
|
807
|
+
{ field: 'amount', op: '>', value: 100 },
|
|
808
|
+
],
|
|
809
|
+
selectFields: ['orderId', 'amount', 'userId', 'createdAt'],
|
|
810
|
+
orderBy: 'amount',
|
|
811
|
+
order: 'desc',
|
|
812
|
+
limit: 50,
|
|
813
|
+
});
|
|
814
|
+
```
|
|
926
815
|
|
|
927
|
-
|
|
816
|
+
### Cross-Bucket Comparison
|
|
928
817
|
|
|
929
|
-
|
|
818
|
+
Compare the same metric across multiple buckets in one query:
|
|
930
819
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
820
|
+
```typescript
|
|
821
|
+
const comparison = await analytics.crossBucket({
|
|
822
|
+
bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
|
|
823
|
+
field: 'amount',
|
|
824
|
+
aggregation: 'sum',
|
|
825
|
+
});
|
|
826
|
+
// → [
|
|
827
|
+
// { bucket: 'orders-us', value: 120000 },
|
|
828
|
+
// { bucket: 'orders-eu', value: 45000 },
|
|
829
|
+
// { bucket: 'orders-apac', value: 33000 },
|
|
830
|
+
// ]
|
|
831
|
+
```
|
|
934
832
|
|
|
935
|
-
|
|
833
|
+
> ⚠️ Your Security Key must have read access to **all** buckets in the list.
|
|
936
834
|
|
|
937
|
-
|
|
938
|
-
prefix: 'batch-uploads/',
|
|
835
|
+
### Storage Stats
|
|
939
836
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
s.percent = p.percent;
|
|
944
|
-
s.stage = p.stage;
|
|
945
|
-
s.done = p.stage === 'done';
|
|
946
|
-
s.error = p.error;
|
|
947
|
-
},
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
</script>
|
|
951
|
-
|
|
952
|
-
<template>
|
|
953
|
-
<input type="file" multiple @change="onFilesChange" />
|
|
954
|
-
|
|
955
|
-
<ul>
|
|
956
|
-
<li v-for="(f, i) in fileStates" :key="i">
|
|
957
|
-
<span>{{ f.name }}</span>
|
|
958
|
-
<progress :value="f.percent" max="100" />
|
|
959
|
-
<span :style="{ color: f.done ? 'green' : f.error ? 'red' : 'inherit' }">
|
|
960
|
-
{{ f.done ? '✓' : f.error ?? f.stage + ' ' + f.percent + '%' }}
|
|
961
|
-
</span>
|
|
962
|
-
</li>
|
|
963
|
-
</ul>
|
|
964
|
-
</template>
|
|
837
|
+
```typescript
|
|
838
|
+
const stats = await analytics.storageStats();
|
|
839
|
+
// → { totalRecords: 48210, totalBytes: 921600000, avgBytes: 19112, minBytes: 128, maxBytes: 5242880 }
|
|
965
840
|
```
|
|
966
841
|
|
|
967
842
|
---
|
|
968
843
|
|
|
969
|
-
##
|
|
844
|
+
## TypeScript Support
|
|
970
845
|
|
|
971
|
-
|
|
846
|
+
The SDK is written in TypeScript and ships with full type definitions. Use generic
|
|
847
|
+
type parameters to describe the shape of your records and get autocomplete throughout.
|
|
972
848
|
|
|
973
|
-
```
|
|
974
|
-
// src/lib/db.ts
|
|
849
|
+
```typescript
|
|
975
850
|
import { createClient } from 'hydrousdb';
|
|
976
|
-
import {
|
|
977
|
-
PUBLIC_HYDROUS_URL,
|
|
978
|
-
} from '$env/static/public';
|
|
979
|
-
import {
|
|
980
|
-
HYDROUS_AUTH_KEY,
|
|
981
|
-
HYDROUS_BUCKET_KEY,
|
|
982
|
-
HYDROUS_STORAGE_AVATARS,
|
|
983
|
-
HYDROUS_STORAGE_DOCS,
|
|
984
|
-
} from '$env/static/private';
|
|
985
|
-
|
|
986
|
-
export const db = createClient({
|
|
987
|
-
url: PUBLIC_HYDROUS_URL,
|
|
988
|
-
authKey: HYDROUS_AUTH_KEY,
|
|
989
|
-
bucketSecurityKey: HYDROUS_BUCKET_KEY,
|
|
990
|
-
storageKeys: {
|
|
991
|
-
avatars: HYDROUS_STORAGE_AVATARS,
|
|
992
|
-
documents: HYDROUS_STORAGE_DOCS,
|
|
993
|
-
},
|
|
994
|
-
});
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
### Server route — fetch records
|
|
998
851
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
where: eq('inStock', true),
|
|
1008
|
-
orderBy: { field: 'price', direction: 'asc' },
|
|
1009
|
-
limit: 50,
|
|
1010
|
-
});
|
|
852
|
+
// Define your data models
|
|
853
|
+
interface Order {
|
|
854
|
+
customerId: string;
|
|
855
|
+
items: Array<{ productId: string; qty: number; price: number }>;
|
|
856
|
+
total: number;
|
|
857
|
+
status: 'pending' | 'processing' | 'completed' | 'refunded';
|
|
858
|
+
country: string;
|
|
859
|
+
}
|
|
1011
860
|
|
|
1012
|
-
|
|
1013
|
-
|
|
861
|
+
interface Customer {
|
|
862
|
+
name: string;
|
|
863
|
+
email: string;
|
|
864
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
865
|
+
credits: number;
|
|
1014
866
|
}
|
|
1015
|
-
```
|
|
1016
867
|
|
|
1017
|
-
|
|
868
|
+
const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
|
|
1018
869
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
import type { PageServerLoad } from './$types';
|
|
870
|
+
// Fully typed clients
|
|
871
|
+
const orders = db.records<Order>('orders');
|
|
872
|
+
const customers = db.records<Customer>('customers');
|
|
1023
873
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
874
|
+
// order.total, order.status, etc. are all type-safe
|
|
875
|
+
const order = await orders.create({
|
|
876
|
+
customerId: 'cust_abc',
|
|
877
|
+
items: [{ productId: 'prod_1', qty: 2, price: 29.99 }],
|
|
878
|
+
total: 59.98,
|
|
879
|
+
status: 'pending',
|
|
880
|
+
country: 'US',
|
|
881
|
+
});
|
|
1029
882
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
883
|
+
// TypeScript catches mistakes at compile time:
|
|
884
|
+
// order.nonExistentField // ← TS error ✓
|
|
885
|
+
// order.status = 'invalid' // ← TS error ✓
|
|
1033
886
|
```
|
|
1034
887
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const file = (event.target as HTMLInputElement).files?.[0];
|
|
1049
|
-
if (!file) return;
|
|
1050
|
-
|
|
1051
|
-
errorMsg = '';
|
|
1052
|
-
uploadedPath = '';
|
|
1053
|
-
progress = null;
|
|
1054
|
-
|
|
1055
|
-
const { data, error } = await db.storage('documents').upload(file, {
|
|
1056
|
-
path: `uploads/${file.name}`,
|
|
1057
|
-
onProgress: (p) => { progress = p; },
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
if (error) {
|
|
1061
|
-
errorMsg = error.message;
|
|
1062
|
-
} else {
|
|
1063
|
-
uploadedPath = data!.path;
|
|
1064
|
-
progress = null;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
</script>
|
|
1068
|
-
|
|
1069
|
-
<input type="file" on:change={handleFile} />
|
|
1070
|
-
|
|
1071
|
-
{#if progress}
|
|
1072
|
-
<p>{progress.stage} — {progress.percent}%</p>
|
|
1073
|
-
<progress value={progress.percent} max="100" />
|
|
1074
|
-
{/if}
|
|
1075
|
-
|
|
1076
|
-
{#if uploadedPath}
|
|
1077
|
-
<p>✓ Saved at: {uploadedPath}</p>
|
|
1078
|
-
{/if}
|
|
1079
|
-
|
|
1080
|
-
{#if errorMsg}
|
|
1081
|
-
<p style="color:red">{errorMsg}</p>
|
|
1082
|
-
{/if}
|
|
888
|
+
All exported types are available for import:
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
import type {
|
|
892
|
+
HydrousConfig,
|
|
893
|
+
RecordResult,
|
|
894
|
+
QueryFilter,
|
|
895
|
+
QueryOptions,
|
|
896
|
+
UploadResult,
|
|
897
|
+
AnalyticsQuery,
|
|
898
|
+
DateRange,
|
|
899
|
+
// ... and many more
|
|
900
|
+
} from 'hydrousdb';
|
|
1083
901
|
```
|
|
1084
902
|
|
|
1085
903
|
---
|
|
1086
904
|
|
|
1087
|
-
##
|
|
1088
|
-
|
|
1089
|
-
### Setup
|
|
905
|
+
## Error Handling
|
|
1090
906
|
|
|
1091
|
-
|
|
1092
|
-
// src/db.ts
|
|
1093
|
-
import { createClient } from 'hydrousdb';
|
|
1094
|
-
import 'dotenv/config';
|
|
1095
|
-
|
|
1096
|
-
export const db = createClient({
|
|
1097
|
-
url: process.env.HYDROUS_URL!,
|
|
1098
|
-
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
1099
|
-
bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
|
|
1100
|
-
storageKeys: {
|
|
1101
|
-
main: process.env.HYDROUS_STORAGE_MAIN!,
|
|
1102
|
-
exports: process.env.HYDROUS_STORAGE_EXPORTS!,
|
|
1103
|
-
},
|
|
1104
|
-
});
|
|
1105
|
-
```
|
|
907
|
+
All errors thrown by the SDK extend `HydrousError`, which carries:
|
|
1106
908
|
|
|
1107
|
-
|
|
909
|
+
| Property | Type | Description |
|
|
910
|
+
|---|---|---|
|
|
911
|
+
| `message` | `string` | Human-readable description |
|
|
912
|
+
| `code` | `string` | Machine-readable error code (e.g. `"RECORD_NOT_FOUND"`) |
|
|
913
|
+
| `status` | `number` | HTTP status code |
|
|
914
|
+
| `requestId` | `string` | Server request ID (for support tracing) |
|
|
915
|
+
| `details` | `string[]` | Validation error details |
|
|
1108
916
|
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
import express from 'express';
|
|
1112
|
-
import multer from 'multer';
|
|
1113
|
-
import { db } from '../db';
|
|
917
|
+
```typescript
|
|
918
|
+
import { HydrousError, NetworkError, AuthError } from 'hydrousdb';
|
|
1114
919
|
|
|
1115
|
-
|
|
1116
|
-
const
|
|
920
|
+
try {
|
|
921
|
+
const user = await auth.login({ email: 'a@b.com', password: 'wrong' });
|
|
922
|
+
} catch (err) {
|
|
923
|
+
if (err instanceof AuthError) {
|
|
924
|
+
// Authentication-specific error
|
|
925
|
+
console.error(`Auth failed: ${err.code}`);
|
|
926
|
+
// err.code might be: INVALID_CREDENTIALS, ACCOUNT_LOCKED, EMAIL_NOT_VERIFIED, etc.
|
|
927
|
+
} else if (err instanceof NetworkError) {
|
|
928
|
+
// No internet / server unreachable
|
|
929
|
+
console.error('Cannot reach HydrousDB — check your internet connection');
|
|
930
|
+
} else if (err instanceof HydrousError) {
|
|
931
|
+
// Any other API error
|
|
932
|
+
console.error(`API error [${err.code}]: ${err.message}`);
|
|
933
|
+
console.error(`Request ID: ${err.requestId}`); // include this in support tickets
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
1117
937
|
|
|
1118
|
-
|
|
1119
|
-
|
|
938
|
+
**Common error codes:**
|
|
939
|
+
|
|
940
|
+
| Code | Meaning |
|
|
941
|
+
|---|---|
|
|
942
|
+
| `RECORD_NOT_FOUND` | The requested record ID does not exist |
|
|
943
|
+
| `INVALID_CREDENTIALS` | Wrong email or password |
|
|
944
|
+
| `ACCOUNT_LOCKED` | The account is temporarily locked |
|
|
945
|
+
| `INVALID_SESSION` | Session expired or revoked — re-authenticate |
|
|
946
|
+
| `MISSING_API_KEY` | Security key not provided |
|
|
947
|
+
| `INVALID_SECURITY_KEY` | Security key is wrong or revoked |
|
|
948
|
+
| `FORBIDDEN` | Insufficient permissions |
|
|
949
|
+
| `FILE_EXISTS` | File already exists at path (use `overwrite: true`) |
|
|
950
|
+
| `LIMIT_EXCEEDED` | Storage quota or file size limit reached |
|
|
951
|
+
| `SYSTEM_BUCKET_FORBIDDEN` | Cannot query system buckets via analytics |
|
|
952
|
+
| `VALIDATION_ERROR` | Invalid input — check `err.details` |
|
|
953
|
+
| `NETWORK_ERROR` | Failed to reach the API |
|
|
1120
954
|
|
|
1121
|
-
|
|
1122
|
-
req.file.buffer,
|
|
1123
|
-
{
|
|
1124
|
-
path: `uploads/${Date.now()}-${req.file.originalname}`,
|
|
1125
|
-
overwrite: false,
|
|
1126
|
-
}
|
|
1127
|
-
);
|
|
955
|
+
---
|
|
1128
956
|
|
|
1129
|
-
|
|
1130
|
-
res.json({ path: data!.path, size: data!.storedSize });
|
|
1131
|
-
});
|
|
957
|
+
## Security Best Practices
|
|
1132
958
|
|
|
1133
|
-
|
|
1134
|
-
```
|
|
959
|
+
1. **Never hard-code your Security Key.** Use environment variables:
|
|
1135
960
|
|
|
1136
|
-
|
|
961
|
+
```bash
|
|
962
|
+
# .env (add to .gitignore)
|
|
963
|
+
HYDROUS_SECURITY_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
|
|
964
|
+
```
|
|
1137
965
|
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
|
|
966
|
+
```typescript
|
|
967
|
+
const db = createClient({ securityKey: process.env.HYDROUS_SECURITY_KEY! });
|
|
968
|
+
```
|
|
1141
969
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
const { data: orders } = await db.records.select('orders', {
|
|
1145
|
-
where: { field: 'month', operator: 'eq', value: month },
|
|
1146
|
-
});
|
|
970
|
+
2. **Never expose your Security Key to browsers.** For browser-side apps,
|
|
971
|
+
route requests through your own backend, or use per-user session tokens.
|
|
1147
972
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
generatedAt: new Date().toISOString(),
|
|
1152
|
-
totalOrders: orders.length,
|
|
1153
|
-
totalRevenue: orders.reduce((sum: number, o: any) => sum + o.total, 0),
|
|
1154
|
-
orders,
|
|
1155
|
-
};
|
|
1156
|
-
|
|
1157
|
-
// 3. Save to storage
|
|
1158
|
-
const { data, error } = await db.storage('exports').uploadText(
|
|
1159
|
-
`reports/${month}.json`,
|
|
1160
|
-
JSON.stringify(report, null, 2),
|
|
1161
|
-
{ mimeType: 'application/json', overwrite: true }
|
|
1162
|
-
);
|
|
1163
|
-
|
|
1164
|
-
if (error) throw new Error(`Failed to save report: ${error.message}`);
|
|
1165
|
-
|
|
1166
|
-
// 4. Track the event
|
|
1167
|
-
await db.analytics.track({
|
|
1168
|
-
event: 'report_generated',
|
|
1169
|
-
properties: { month, totalOrders: report.totalOrders },
|
|
1170
|
-
});
|
|
973
|
+
3. **Your Security Key is sent via the `X-Api-Key` header — never in URLs.**
|
|
974
|
+
The SDK enforces this automatically, so keys never appear in server logs or
|
|
975
|
+
browser history.
|
|
1171
976
|
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1174
|
-
```
|
|
977
|
+
4. **Rotate keys periodically.** Revoke old keys from the dashboard after rotation.
|
|
1175
978
|
|
|
1176
|
-
|
|
979
|
+
5. **Use scoped storage** (`db.storage.scope(...)`) to isolate access by user or
|
|
980
|
+
feature, reducing the blast radius of any misconfiguration.
|
|
1177
981
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
import { db } from '../db';
|
|
1181
|
-
import { lt } from 'hydrousdb';
|
|
982
|
+
6. **Use `isPublic: false` (the default) for sensitive files.** Use signed URLs
|
|
983
|
+
for time-limited sharing instead of making files permanently public.
|
|
1182
984
|
|
|
1183
|
-
|
|
1184
|
-
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
985
|
+
---
|
|
1185
986
|
|
|
1186
|
-
|
|
1187
|
-
const { data: old } = await db.records.select('sessions', {
|
|
1188
|
-
where: lt('createdAt', cutoff),
|
|
1189
|
-
limit: 500,
|
|
1190
|
-
});
|
|
987
|
+
## API Reference
|
|
1191
988
|
|
|
1192
|
-
|
|
989
|
+
### `createClient(config)`
|
|
1193
990
|
|
|
1194
|
-
|
|
1195
|
-
await Promise.all(old.map((s: any) => db.records.delete('sessions', s.id)));
|
|
1196
|
-
console.log(`Deleted ${old.length} expired sessions`);
|
|
1197
|
-
}
|
|
991
|
+
Creates and returns a `HydrousClient` instance. Call this once and reuse the instance.
|
|
1198
992
|
|
|
1199
|
-
|
|
993
|
+
```typescript
|
|
994
|
+
const db = createClient({
|
|
995
|
+
securityKey: 'sk_live_...', // Required — from https://hydrousdb.com/dashboard
|
|
996
|
+
baseUrl: 'https://...', // Optional — defaults to official HydrousDB endpoint
|
|
997
|
+
});
|
|
1200
998
|
```
|
|
1201
999
|
|
|
1202
|
-
|
|
1000
|
+
### `db.records<T>(bucketKey)`
|
|
1001
|
+
|
|
1002
|
+
Returns a `RecordsClient<T>` for the named bucket.
|
|
1003
|
+
|
|
1004
|
+
| Method | Description |
|
|
1005
|
+
|---|---|
|
|
1006
|
+
| `create(data)` | Create a new record |
|
|
1007
|
+
| `get(id)` | Get a record by ID |
|
|
1008
|
+
| `set(id, data)` | Full replace |
|
|
1009
|
+
| `patch(id, data, opts?)` | Partial update (merge by default) |
|
|
1010
|
+
| `delete(id)` | Delete a record |
|
|
1011
|
+
| `query(opts?)` | Query with filters, sort, pagination |
|
|
1012
|
+
| `getAll(opts?)` | Shortcut for query without filters |
|
|
1013
|
+
| `count(filters?)` | Count matching records |
|
|
1014
|
+
| `batchCreate(items)` | Create multiple records |
|
|
1015
|
+
| `batchDelete(ids)` | Delete multiple records |
|
|
1016
|
+
| `getHistory(id)` | Get version history |
|
|
1017
|
+
| `restoreVersion(id, version)` | Restore to a version |
|
|
1018
|
+
|
|
1019
|
+
### `db.auth(bucketKey)`
|
|
1020
|
+
|
|
1021
|
+
Returns an `AuthClient` for the named user bucket.
|
|
1022
|
+
|
|
1023
|
+
| Method | Description |
|
|
1024
|
+
|---|---|
|
|
1025
|
+
| `signup(opts)` | Register a new user |
|
|
1026
|
+
| `login(opts)` | Authenticate and create session |
|
|
1027
|
+
| `logout({ sessionId })` | Invalidate session |
|
|
1028
|
+
| `refreshSession({ refreshToken })` | Extend a session |
|
|
1029
|
+
| `getUser({ userId })` | Get user by ID |
|
|
1030
|
+
| `updateUser(opts)` | Update user fields |
|
|
1031
|
+
| `deleteUser(opts)` | Soft-delete a user |
|
|
1032
|
+
| `hardDeleteUser(opts)` | Permanently delete a user (admin) |
|
|
1033
|
+
| `listUsers(opts)` | List all users (admin) |
|
|
1034
|
+
| `bulkDeleteUsers(opts)` | Bulk delete (admin) |
|
|
1035
|
+
| `lockAccount(opts)` | Lock a user (admin) |
|
|
1036
|
+
| `unlockAccount(opts)` | Unlock a user (admin) |
|
|
1037
|
+
| `changePassword(opts)` | Change password (authenticated) |
|
|
1038
|
+
| `requestPasswordReset(opts)` | Send reset email |
|
|
1039
|
+
| `confirmPasswordReset(opts)` | Apply new password |
|
|
1040
|
+
| `requestEmailVerification(opts)` | Send verification email |
|
|
1041
|
+
| `confirmEmailVerification(opts)` | Verify email with token |
|
|
1042
|
+
|
|
1043
|
+
### `db.analytics(bucketKey)`
|
|
1044
|
+
|
|
1045
|
+
Returns an `AnalyticsClient` for the named bucket.
|
|
1046
|
+
|
|
1047
|
+
| Method | Description |
|
|
1048
|
+
|---|---|
|
|
1049
|
+
| `count(opts?)` | Count records |
|
|
1050
|
+
| `distribution(opts)` | Value distribution for a field |
|
|
1051
|
+
| `sum(opts)` | Sum (with optional groupBy) |
|
|
1052
|
+
| `timeSeries(opts?)` | Records over time |
|
|
1053
|
+
| `fieldTimeSeries(opts)` | Field aggregation over time |
|
|
1054
|
+
| `topN(opts)` | Top N values for a field |
|
|
1055
|
+
| `stats(opts)` | Statistical summary for a field |
|
|
1056
|
+
| `records(opts?)` | Filtered raw records (BigQuery) |
|
|
1057
|
+
| `multiMetric(opts)` | Multiple aggregations in one query |
|
|
1058
|
+
| `storageStats(opts?)` | Bucket storage statistics |
|
|
1059
|
+
| `crossBucket(opts)` | Compare across multiple buckets |
|
|
1060
|
+
| `query(query)` | Raw analytics query |
|
|
1061
|
+
|
|
1062
|
+
### `db.storage`
|
|
1063
|
+
|
|
1064
|
+
The `StorageManager` instance. Also has `.scope(prefix)` for folder-scoped access.
|
|
1065
|
+
|
|
1066
|
+
| Method | Description |
|
|
1067
|
+
|---|---|
|
|
1068
|
+
| `upload(data, path, opts?)` | Simple server-buffered upload |
|
|
1069
|
+
| `uploadRaw(data, path, opts?)` | Upload JSON/text data |
|
|
1070
|
+
| `getUploadUrl(opts)` | Step 1: Get signed GCS upload URL |
|
|
1071
|
+
| `uploadToSignedUrl(url, data, mime, onProgress?)` | Step 2: Upload to GCS directly |
|
|
1072
|
+
| `confirmUpload(opts)` | Step 3: Register upload metadata |
|
|
1073
|
+
| `getBatchUploadUrls(files)` | Batch signed upload URLs |
|
|
1074
|
+
| `batchConfirmUploads(items)` | Confirm batch uploads |
|
|
1075
|
+
| `download(path)` | Download private file |
|
|
1076
|
+
| `batchDownload(paths)` | Batch download |
|
|
1077
|
+
| `list(opts?)` | List files and folders |
|
|
1078
|
+
| `getMetadata(path)` | File metadata |
|
|
1079
|
+
| `getSignedUrl(path, expiresIn?)` | Time-limited share URL |
|
|
1080
|
+
| `setVisibility(path, isPublic)` | Toggle public/private |
|
|
1081
|
+
| `createFolder(path)` | Create a folder |
|
|
1082
|
+
| `deleteFile(path)` | Delete a file |
|
|
1083
|
+
| `deleteFolder(path)` | Delete a folder recursively |
|
|
1084
|
+
| `move(from, to)` | Move/rename |
|
|
1085
|
+
| `copy(from, to)` | Copy |
|
|
1086
|
+
| `getStats()` | Key-level stats |
|
|
1087
|
+
| `info()` | Server ping (no auth) |
|
|
1088
|
+
| `scope(prefix)` | Get a ScopedStorage instance |
|
|
1203
1089
|
|
|
1204
|
-
|
|
1090
|
+
---
|
|
1205
1091
|
|
|
1206
|
-
|
|
1092
|
+
## Contributing
|
|
1207
1093
|
|
|
1208
|
-
|
|
1209
|
-
interface HydrousConfig {
|
|
1210
|
-
url: string;
|
|
1211
|
-
authKey: string; // one key for auth
|
|
1212
|
-
bucketSecurityKey: string; // one key for records + analytics
|
|
1213
|
-
storageKeys: Record<string, string>; // many named keys for storage
|
|
1214
|
-
timeout?: number; // ms, default 30000
|
|
1215
|
-
}
|
|
1216
|
-
```
|
|
1094
|
+
We love contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
1217
1095
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
index: number; // 0-based file index
|
|
1223
|
-
total: number; // total files in this operation
|
|
1224
|
-
path: string; // destination path
|
|
1225
|
-
stage: UploadStage; // 'pending'|'compressing'|'uploading'|'done'|'error'
|
|
1226
|
-
bytesUploaded: number;
|
|
1227
|
-
totalBytes: number;
|
|
1228
|
-
percent: number; // 0–100
|
|
1229
|
-
bytesPerSecond: number | null; // null until speed is calculable
|
|
1230
|
-
eta: number | null; // seconds remaining, null until speed known
|
|
1231
|
-
result?: UploadResult; // populated when stage === 'done'
|
|
1232
|
-
error?: string; // populated when stage === 'error'
|
|
1233
|
-
code?: string;
|
|
1234
|
-
}
|
|
1235
|
-
```
|
|
1096
|
+
```bash
|
|
1097
|
+
# Clone the repo
|
|
1098
|
+
git clone https://github.com/hydrousdb/hydrousdb-js.git
|
|
1099
|
+
cd hydrousdb-js
|
|
1236
1100
|
|
|
1237
|
-
|
|
1101
|
+
# Install dependencies
|
|
1102
|
+
npm install
|
|
1238
1103
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
path: string;
|
|
1242
|
-
compressed: boolean;
|
|
1243
|
-
originalSize: number;
|
|
1244
|
-
storedSize: number;
|
|
1245
|
-
spaceSaved: number;
|
|
1246
|
-
mimeType: string;
|
|
1247
|
-
}
|
|
1248
|
-
```
|
|
1104
|
+
# Run tests
|
|
1105
|
+
npm test
|
|
1249
1106
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
```ts
|
|
1253
|
-
interface StorageItem {
|
|
1254
|
-
name: string;
|
|
1255
|
-
path: string;
|
|
1256
|
-
type: 'file' | 'folder';
|
|
1257
|
-
size?: number | null;
|
|
1258
|
-
mimeType?: string | null;
|
|
1259
|
-
isCompressed?: boolean;
|
|
1260
|
-
updatedAt?: string | null;
|
|
1261
|
-
}
|
|
1262
|
-
```
|
|
1107
|
+
# Build
|
|
1108
|
+
npm run build
|
|
1263
1109
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
```ts
|
|
1267
|
-
interface StorageStats {
|
|
1268
|
-
totalFiles: number;
|
|
1269
|
-
totalSizeBytes: number;
|
|
1270
|
-
totalOriginalSizeBytes: number;
|
|
1271
|
-
spaceSavedBytes: number;
|
|
1272
|
-
uploadsCount: number;
|
|
1273
|
-
downloadsCount: number;
|
|
1274
|
-
creditsUsedUpload: number;
|
|
1275
|
-
creditsUsedDownload: number;
|
|
1276
|
-
creditsTotalUsed: number;
|
|
1277
|
-
compressionRatio: string; // e.g. "34.2%"
|
|
1278
|
-
}
|
|
1110
|
+
# Run tests in watch mode
|
|
1111
|
+
npm run test:watch
|
|
1279
1112
|
```
|
|
1280
1113
|
|
|
1281
1114
|
---
|
|
1282
1115
|
|
|
1283
1116
|
## License
|
|
1284
1117
|
|
|
1285
|
-
MIT
|
|
1118
|
+
MIT — see [LICENSE](./LICENSE) for details.
|
|
1119
|
+
|
|
1120
|
+
---
|
|
1121
|
+
|
|
1122
|
+
<p align="center">
|
|
1123
|
+
Built with ❤️ by the <a href="https://hydrousdb.com">HydrousDB</a> team.<br>
|
|
1124
|
+
Questions? <a href="mailto:support@hydrousdb.com">support@hydrousdb.com</a> ·
|
|
1125
|
+
<a href="https://github.com/hydrousdb/hydrousdb-js/issues">Open an issue</a>
|
|
1126
|
+
</p>
|