hydrousdb 2.0.0 → 2.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/README.md CHANGED
@@ -1,777 +1,730 @@
1
- # hydrousdb
1
+ # Hydrous SDK
2
2
 
3
- > **Official TypeScript SDK for [HydrousDB](https://db-api-82687684612.us-central1.run.app)**
4
- >a cloud-native record store with built-in authentication, batch operations, analytics, and cursor-based pagination.
3
+ Official JavaScript / TypeScript SDK for the **Hydrous** platform.
4
+ One package auth, records, analytics, and storage.
5
5
 
6
- [![npm version](https://img.shields.io/npm/v/hydrousdb)](https://npmjs.com/package/hydrousdb)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+ ```bash
7
+ npm install hydrous
8
+ ```
9
9
 
10
10
  ---
11
11
 
12
12
  ## Table of Contents
13
13
 
14
- - [Features](#features)
15
- - [Installation](#installation)
16
- - [Key Concepts](#key-concepts)
17
- - [Quick Start](#quick-start)
18
- - [Framework Guides](#framework-guides)
19
- - [Next.js (App Router)](#nextjs-app-router)
20
- - [Next.js (Pages Router)](#nextjs-pages-router)
21
- - [Vue 3 / Nuxt 3](#vue-3--nuxt-3)
22
- - [React (Vite / CRA)](#react-vite--cra)
23
- - [Node.js / Express](#nodejs--express)
24
- - [API Reference](#api-reference)
25
- - [createClient](#createclient)
26
- - [Records](#records)
27
- - [Auth](#auth)
28
- - [Analytics](#analytics)
29
- - [Error Handling](#error-handling)
30
- - [Pagination](#pagination)
31
- - [TypeScript](#typescript)
32
- - [Migration from v1](#migration-from-v1)
33
- - [Contributing](#contributing)
34
- - [License](#license)
14
+ 1. [Quick Start](#1-quick-start)
15
+ 2. [Configuration](#2-configuration)
16
+ 3. [Auth](#3-auth)
17
+ - [Sign Up](#sign-up)
18
+ - [Sign In](#sign-in)
19
+ - [Sign Out](#sign-out)
20
+ - [Get Current User](#get-current-user)
21
+ 4. [Records](#4-records)
22
+ - [Insert](#insert)
23
+ - [Select / Query](#select--query)
24
+ - [Get by ID](#get-by-id)
25
+ - [Update](#update)
26
+ - [Delete](#delete)
27
+ - [Query Helpers](#query-helpers)
28
+ 5. [Analytics](#5-analytics)
29
+ - [Track an Event](#track-an-event)
30
+ - [Batch Track](#batch-track)
31
+ - [Query Events](#query-events)
32
+ 6. [Storage](#6-storage)
33
+ - [How Bucket Keys Work](#how-bucket-keys-work)
34
+ - [Upload a File](#upload-a-file)
35
+ - [Upload Raw Text / JSON](#upload-raw-text--json)
36
+ - [Track Upload Progress](#track-upload-progress)
37
+ - [Batch Upload](#batch-upload)
38
+ - [Download a File](#download-a-file)
39
+ - [Batch Download](#batch-download)
40
+ - [List Files & Folders](#list-files--folders)
41
+ - [File Metadata](#file-metadata)
42
+ - [Delete a File](#delete-a-file)
43
+ - [Delete a Folder](#delete-a-folder)
44
+ - [Create a Folder](#create-a-folder)
45
+ - [Move a File](#move-a-file)
46
+ - [Copy a File](#copy-a-file)
47
+ - [Signed URLs](#signed-urls)
48
+ - [Bucket Stats](#bucket-stats)
49
+ 7. [Error Handling](#7-error-handling)
50
+ 8. [TypeScript Types Reference](#8-typescript-types-reference)
35
51
 
36
52
  ---
37
53
 
38
- ## Features
54
+ ## 1. Quick Start
55
+
56
+ ```ts
57
+ import { createClient } from 'hydrous';
58
+
59
+ const hydrous = createClient({
60
+ url: 'https://api.yourapp.hydrous.app',
61
+ apiKey: 'hk_live_…',
62
+ });
39
63
 
40
- - **Full TypeScript** — every method and response is fully typed
41
- - **Multi-bucket** — one client, any number of buckets; pass `bucketKey` per call
42
- - **Modular** import only what you need (`hydrousdb/records`, `hydrousdb/auth`, `hydrousdb/analytics`)
43
- - ✅ **Tree-shakeable** — ships ESM + CJS with dual exports
44
- - ✅ **Auto-retry** — retries on transient 5xx / network errors with linear back-off
45
- - ✅ **Timeout control** — configurable per-client request timeout
46
- - ✅ **Cursor pagination helpers** — `queryAll()` / `listAllUsers()` handle cursor following for you
47
- - ✅ **Zero dependencies** — uses the native `fetch` API (Node 18+, all modern browsers)
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
+ ```
48
74
 
49
75
  ---
50
76
 
51
- ## Installation
77
+ ## 2. Configuration
52
78
 
53
- ```bash
54
- npm install hydrousdb
55
- # or
56
- pnpm add hydrousdb
57
- # or
58
- yarn add hydrousdb
79
+ ```ts
80
+ import { HydrousClient } from 'hydrous';
81
+
82
+ const hydrous = new HydrousClient({
83
+ url: 'https://api.yourapp.hydrous.app', // your project URL
84
+ apiKey: 'hk_live_…', // your project API key
85
+ timeout: 30_000, // optional — ms (default 30s)
86
+ });
59
87
  ```
60
88
 
61
- > **Node.js 18** is required (uses native `fetch`). For Node 16/17, polyfill `fetch` with `node-fetch` or `undici`.
89
+ | Option | Type | Required | Description |
90
+ |-----------|----------|----------|--------------------------------------|
91
+ | `url` | `string` | ✅ | Your Hydrous project base URL |
92
+ | `apiKey` | `string` | ✅ | Your project API key |
93
+ | `timeout` | `number` | ✗ | Request timeout in ms (default 30000) |
62
94
 
63
95
  ---
64
96
 
65
- ## Key Concepts
66
-
67
- ### Two keys, one client
97
+ ## 3. Auth
68
98
 
69
- The SDK separates two distinct credentials:
99
+ ### Sign Up
70
100
 
71
- | Key | Config field | Used for |
72
- |-----|-------------|----------|
73
- | Auth key | `authKey` | All `/auth` routes — sign up, sign in, session management |
74
- | Security key | `securityKey` | All records and analytics routes — data access credential |
101
+ ```ts
102
+ const { data, error } = await hydrous.auth.signUp({
103
+ email: 'user@example.com',
104
+ password: 'supersecret',
105
+ metadata: { plan: 'pro' }, // optional
106
+ });
75
107
 
76
- ### Bucket per call
108
+ if (data) {
109
+ console.log('New user:', data.user.id);
110
+ console.log('Access token:', data.accessToken);
111
+ }
112
+ ```
77
113
 
78
- Unlike v1, `bucketKey` is **not** set on the client — it's passed as an option on every records and analytics call. This means a **single client instance can read and write to any number of buckets** without re-initialising.
114
+ ### Sign In
79
115
 
80
116
  ```ts
81
- const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
117
+ const { data, error } = await hydrous.auth.signIn({
118
+ email: 'user@example.com',
119
+ password: 'supersecret',
120
+ });
82
121
 
83
- // Same client, different buckets
84
- await db.records.insert({ values: { name: 'Alice' } }, { bucketKey: 'users' });
85
- await db.records.insert({ values: { order: '#1001' } }, { bucketKey: 'orders' });
86
- await db.analytics.count({ bucketKey: 'users' });
87
- await db.analytics.count({ bucketKey: 'orders' });
122
+ if (data) {
123
+ console.log('Welcome back,', data.user.email);
124
+ }
88
125
  ```
89
126
 
90
- ---
91
-
92
- ## Quick Start
127
+ ### Sign Out
93
128
 
94
129
  ```ts
95
- import { createClient } from 'hydrousdb';
96
-
97
- const db = createClient({
98
- authKey: 'your-auth-key',
99
- securityKey: 'your-security-key',
100
- });
130
+ await hydrous.auth.signOut();
131
+ ```
101
132
 
102
- // Insert a record into the 'users' bucket
103
- const { data, meta } = await db.records.insert(
104
- { values: { name: 'Alice', score: 99 }, queryableFields: ['name'] },
105
- { bucketKey: 'users' }
106
- );
107
- console.log('Created record:', meta.id);
133
+ ### Get Current User
108
134
 
109
- // Query records from the 'users' bucket
110
- const { data: records } = await db.records.query({
111
- bucketKey: 'users',
112
- filters: [{ field: 'score', op: '>=', value: 90 }],
113
- limit: 20,
114
- });
135
+ ```ts
136
+ const { data: user } = await hydrous.auth.getUser();
137
+ console.log(user?.email);
138
+ ```
115
139
 
116
- // Auth — no bucketKey needed
117
- const { data: user, session } = await db.auth.signIn({
118
- email: 'alice@example.com',
119
- password: 'Str0ng@Pass!',
120
- });
140
+ ### Refresh Session
121
141
 
122
- // Analytics
123
- const { data: stats } = await db.analytics.count({ bucketKey: 'users' });
124
- console.log(stats.count);
142
+ ```ts
143
+ const { data: session } = await hydrous.auth.refreshSession();
125
144
  ```
126
145
 
127
146
  ---
128
147
 
129
- ## Framework Guides
148
+ ## 4. Records
130
149
 
131
- ### Next.js (App Router)
150
+ ### Insert
132
151
 
133
- **1. Create a shared client singleton**
152
+ Single record:
134
153
 
135
154
  ```ts
136
- // lib/db.ts
137
- import { createClient } from 'hydrousdb';
138
-
139
- export const db = createClient({
140
- authKey: process.env.HYDROUS_AUTH_KEY!,
141
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
155
+ const { data, error } = await hydrous.records.insert('users', {
156
+ name: 'Alice',
157
+ email: 'alice@example.com',
158
+ role: 'admin',
142
159
  });
143
160
  ```
144
161
 
145
- **2. Use in a Server Component**
162
+ Bulk insert (array):
146
163
 
147
- ```tsx
148
- // app/products/page.tsx
149
- import { db } from '@/lib/db';
150
-
151
- export default async function ProductsPage() {
152
- const { data: products } = await db.records.query({
153
- bucketKey: 'products',
154
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
155
- limit: 24,
156
- sortOrder: 'desc',
157
- });
158
-
159
- return (
160
- <ul>
161
- {products.map(p => (
162
- <li key={p.id}>{String(p.name)}</li>
163
- ))}
164
- </ul>
165
- );
166
- }
164
+ ```ts
165
+ const { data } = await hydrous.records.insert('products', [
166
+ { name: 'Widget A', price: 9.99 },
167
+ { name: 'Widget B', price: 14.99 },
168
+ ]);
169
+ console.log(`Inserted ${data.length} products`);
167
170
  ```
168
171
 
169
- **3. Use in a Route Handler**
172
+ ### Select / Query
170
173
 
171
174
  ```ts
172
- // app/api/records/route.ts
173
- import { NextRequest, NextResponse } from 'next/server';
174
- import { db } from '@/lib/db';
175
- import { HydrousError } from 'hydrousdb';
176
-
177
- export async function POST(req: NextRequest) {
178
- try {
179
- const body = await req.json();
180
- const result = await db.records.insert(
181
- { values: body, queryableFields: ['email', 'name'] },
182
- { bucketKey: 'users' }
183
- );
184
- return NextResponse.json(result, { status: 201 });
185
- } catch (err) {
186
- if (err instanceof HydrousError) {
187
- return NextResponse.json({ error: err.message }, { status: err.status });
188
- }
189
- return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
190
- }
191
- }
175
+ const { data, count } = await hydrous.records.select('users', {
176
+ where: { field: 'role', operator: 'eq', value: 'admin' },
177
+ orderBy: { field: 'createdAt', direction: 'desc' },
178
+ limit: 20,
179
+ offset: 0,
180
+ select: ['id', 'name', 'email'], // optional column projection
181
+ });
192
182
  ```
193
183
 
194
- **4. Auth middleware example**
184
+ Multiple filters:
195
185
 
196
186
  ```ts
197
- // middleware.ts
198
- import { NextRequest, NextResponse } from 'next/server';
199
- import { db } from '@/lib/db';
187
+ import { eq, gt, inArray } from 'hydrous';
200
188
 
201
- export async function middleware(req: NextRequest) {
202
- const sessionId = req.cookies.get('sessionId')?.value;
203
- if (!sessionId) return NextResponse.redirect(new URL('/login', req.url));
204
-
205
- try {
206
- await db.auth.validateSession(sessionId);
207
- return NextResponse.next();
208
- } catch {
209
- return NextResponse.redirect(new URL('/login', req.url));
210
- }
211
- }
212
-
213
- export const config = { matcher: ['/dashboard/:path*'] };
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
+ });
214
196
  ```
215
197
 
216
- **5. Environment variables**
198
+ ### Get by ID
217
199
 
218
- ```env
219
- # .env.local
220
- HYDROUS_AUTH_KEY=your-auth-key
221
- HYDROUS_SECURITY_KEY=your-security-key
200
+ ```ts
201
+ const { data: user } = await hydrous.records.get('users', 'user_abc123');
222
202
  ```
223
203
 
224
- ---
225
-
226
- ### Next.js (Pages Router)
204
+ ### Update
227
205
 
228
206
  ```ts
229
- // pages/api/records.ts
230
- import type { NextApiRequest, NextApiResponse } from 'next';
231
- import { createClient, HydrousError } from 'hydrousdb';
232
-
233
- const db = createClient({
234
- authKey: process.env.HYDROUS_AUTH_KEY!,
235
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
207
+ const { data } = await hydrous.records.update('users', 'user_abc123', {
208
+ name: 'Alice Smith',
236
209
  });
237
-
238
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
239
- if (req.method === 'GET') {
240
- try {
241
- const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
242
- return res.status(200).json(result);
243
- } catch (err) {
244
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
245
- return res.status(500).json({ error: 'Internal Server Error' });
246
- }
247
- }
248
- res.setHeader('Allow', ['GET']);
249
- res.status(405).end();
250
- }
251
210
  ```
252
211
 
253
- ---
212
+ ### Delete
254
213
 
255
- ### Vue 3 / Nuxt 3
214
+ ```ts
215
+ const { error } = await hydrous.records.delete('users', 'user_abc123');
216
+ ```
217
+
218
+ ### Query Helpers
256
219
 
257
- **Nuxt 3 plugin**
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 (…)` |
258
227
 
259
228
  ```ts
260
- // plugins/hydrousdb.ts
261
- import { createClient } from 'hydrousdb';
229
+ import { eq, gt, inArray } from 'hydrous';
230
+ ```
262
231
 
263
- export default defineNuxtPlugin(() => {
264
- const config = useRuntimeConfig();
232
+ ---
265
233
 
266
- const db = createClient({
267
- authKey: config.hydrousAuthKey as string,
268
- securityKey: config.hydrousSecurityKey as string,
269
- });
234
+ ## 5. Analytics
270
235
 
271
- return { provide: { db } };
272
- });
273
- ```
236
+ ### Track an Event
274
237
 
275
238
  ```ts
276
- // nuxt.config.ts
277
- export default defineNuxtConfig({
278
- runtimeConfig: {
279
- hydrousAuthKey: '', // NUXT_HYDROUS_AUTH_KEY
280
- hydrousSecurityKey: '', // NUXT_HYDROUS_SECURITY_KEY
281
- },
239
+ await hydrous.analytics.track({
240
+ event: 'page_view',
241
+ properties: { page: '/home', referrer: 'google.com' },
242
+ userId: 'user_abc123',
243
+ sessionId: 'sess_xyz',
282
244
  });
283
245
  ```
284
246
 
285
- **Usage in a component**
247
+ ### Batch Track
286
248
 
287
- ```vue
288
- <script setup lang="ts">
289
- const { $db } = useNuxtApp();
249
+ More efficient than calling `track()` in a loop:
290
250
 
291
- const { data: records, pending } = await useAsyncData('records', () =>
292
- $db.records.query({ bucketKey: 'products', limit: 20 })
293
- );
294
- </script>
295
-
296
- <template>
297
- <div v-if="pending">Loading…</div>
298
- <ul v-else>
299
- <li v-for="r in records?.data" :key="r.id">{{ r.name }}</li>
300
- </ul>
301
- </template>
251
+ ```ts
252
+ await hydrous.analytics.trackBatch([
253
+ { event: 'signup', userId: 'u1' },
254
+ { event: 'onboarded', userId: 'u1', properties: { step: 'profile' } },
255
+ ]);
302
256
  ```
303
257
 
304
- **Vue 3 standalone — composable**
258
+ ### Query Events
305
259
 
306
260
  ```ts
307
- // composables/useHydrous.ts
308
- import { createClient } from 'hydrousdb';
309
-
310
- const db = createClient({
311
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
312
- securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
261
+ const { data, count } = await hydrous.analytics.query({
262
+ event: 'page_view',
263
+ from: '2024-01-01',
264
+ to: '2024-01-31',
265
+ limit: 500,
266
+ groupBy: 'properties.page',
313
267
  });
314
-
315
- export function useHydrous() {
316
- return db;
317
- }
318
268
  ```
319
269
 
320
270
  ---
321
271
 
322
- ### React (Vite / CRA)
272
+ ## 6. Storage
323
273
 
324
- ```tsx
325
- // src/hooks/useRecords.ts
326
- import { useEffect, useState } from 'react';
327
- import { createClient, HydrousRecord, HydrousError } from 'hydrousdb';
274
+ The storage module handles all file operations. Every method takes a **bucket key** as its **first argument** — a string that begins with `ssk_`.
328
275
 
329
- const db = createClient({
330
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
331
- securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
332
- });
276
+ ### How Bucket Keys Work
333
277
 
334
- export function useRecords(bucketKey: string, limit = 20) {
335
- const [records, setRecords] = useState<HydrousRecord[]>([]);
336
- const [loading, setLoading] = useState(true);
337
- const [error, setError] = useState<string | null>(null);
278
+ A bucket key (`ssk_…`) is a scoped credential that grants specific permissions (read / write / delete) to a bucket. You create them in the Hydrous dashboard.
338
279
 
339
- useEffect(() => {
340
- const controller = new AbortController();
280
+ ```
281
+ hydrous.storage.<method>(
282
+ 'ssk_your_bucket_key', // ← first, always a string
283
+ ...args
284
+ )
285
+ ```
341
286
 
342
- db.records.query({ bucketKey, limit }, { signal: controller.signal })
343
- .then(r => setRecords(r.data))
344
- .catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
345
- .finally(() => setLoading(false));
287
+ ---
288
+
289
+ ### Upload a File
346
290
 
347
- return () => controller.abort();
348
- }, [bucketKey, limit]);
291
+ ```ts
292
+ const { data, error } = await hydrous.storage.upload(
293
+ 'ssk_my_bucket_key',
294
+ file, // File | Blob | Uint8Array | ArrayBuffer
295
+ {
296
+ path: 'avatars/alice.jpg', // destination path in your bucket
297
+ overwrite: true, // replace if exists (default: false)
298
+ onProgress: (progress) => {
299
+ console.log(progress.stage, progress.percent + '%');
300
+ },
301
+ }
302
+ );
349
303
 
350
- return { records, loading, error };
304
+ if (data) {
305
+ console.log('Stored at:', data.path);
306
+ console.log('Space saved:', data.spaceSaved, 'bytes');
351
307
  }
352
308
  ```
353
309
 
354
310
  ---
355
311
 
356
- ### Node.js / Express
312
+ ### Upload Raw Text / JSON
357
313
 
358
- ```ts
359
- // server.ts
360
- import express from 'express';
361
- import { createClient, HydrousError } from 'hydrousdb';
314
+ No `File` object needed — pass any string directly.
362
315
 
363
- const db = createClient({
364
- authKey: process.env.HYDROUS_AUTH_KEY!,
365
- securityKey: process.env.HYDROUS_SECURITY_KEY!,
366
- });
367
- const app = express();
368
- app.use(express.json());
369
-
370
- app.get('/orders', async (req, res) => {
371
- try {
372
- const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
373
- res.json(result);
374
- } catch (err) {
375
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
376
- res.status(500).json({ error: 'Internal Server Error' });
377
- }
378
- });
316
+ ```ts
317
+ // Upload a plain text file
318
+ await hydrous.storage.uploadText(
319
+ 'ssk_my_bucket_key',
320
+ 'reports/summary.txt',
321
+ 'Monthly report content…',
322
+ { mimeType: 'text/plain' }
323
+ );
379
324
 
380
- app.listen(3000, () => console.log('Listening on :3000'));
325
+ // Upload JSON
326
+ await hydrous.storage.uploadText(
327
+ 'ssk_my_bucket_key',
328
+ 'data/config.json',
329
+ JSON.stringify({ theme: 'dark', lang: 'en' }),
330
+ { mimeType: 'application/json' }
331
+ );
381
332
  ```
382
333
 
383
334
  ---
384
335
 
385
- ## API Reference
336
+ ### Track Upload Progress
386
337
 
387
- ### `createClient`
338
+ `onProgress` is called on every progress tick with a rich `UploadProgress` object.
388
339
 
389
340
  ```ts
390
- import { createClient } from 'hydrousdb';
341
+ await hydrous.storage.upload(
342
+ 'ssk_my_bucket_key',
343
+ file,
344
+ {
345
+ onProgress: (p) => {
346
+ // p.stage — 'pending' | 'compressing' | 'uploading' | 'done' | 'error'
347
+ // p.percent — 0–100 integer
348
+ // p.bytesUploaded — bytes sent so far
349
+ // p.totalBytes — total bytes to send
350
+ // p.bytesPerSecond — current speed (null before first tick)
351
+ // p.eta — estimated seconds remaining (null until speed is known)
352
+ // p.index — file index (always 0 for single uploads)
353
+ // p.total — total files (always 1 for single uploads)
354
+
355
+ switch (p.stage) {
356
+ case 'pending':
357
+ console.log('Queued');
358
+ break;
359
+ case 'compressing':
360
+ console.log('Compressing…');
361
+ break;
362
+ case 'uploading':
363
+ console.log(`Uploading ${p.percent}% — ${formatSpeed(p.bytesPerSecond)} — ETA ${p.eta}s`);
364
+ break;
365
+ case 'done':
366
+ console.log('✅ Done!', p.result);
367
+ break;
368
+ case 'error':
369
+ console.error('❌ Error:', p.error);
370
+ break;
371
+ }
372
+ },
373
+ }
374
+ );
391
375
 
392
- const db = createClient({
393
- authKey: string, // required — your HydrousDB auth key (for /auth routes)
394
- securityKey: string, // required your HydrousDB security key (for records & analytics)
395
- baseUrl?: string, // override API base URL
396
- timeout?: number, // request timeout in ms (default: 30_000)
397
- retries?: number, // retries on network/5xx errors (default: 2)
398
- });
376
+ function formatSpeed(bps: number | null): string {
377
+ if (!bps) return '—';
378
+ if (bps > 1_000_000) return `${(bps / 1_000_000).toFixed(1)} MB/s`;
379
+ if (bps > 1_000) return `${(bps / 1_000).toFixed(0)} KB/s`;
380
+ return `${bps} B/s`;
381
+ }
399
382
  ```
400
383
 
401
- > **Note:** `bucketKey` is no longer part of the client config. It is passed as an option on each individual records and analytics call.
384
+ **Stage lifecycle:**
402
385
 
403
- ---
386
+ ```
387
+ pending → compressing → uploading → done
388
+ ↘ error
389
+ ```
404
390
 
405
- ### Records
391
+ > **Note:** In browsers, upload progress (bytes leaving the NIC) is tracked via
392
+ > `XMLHttpRequest`. The final `done` stage fires only after the server confirms
393
+ > the write to cloud storage — so `100%` means the file is truly saved.
406
394
 
407
- All methods live on `db.records`. Every method requires a `bucketKey` option.
395
+ ---
408
396
 
409
- | Method | Description |
410
- |---|---|
411
- | `get(recordId, { bucketKey, ...opts })` | Fetch a single record |
412
- | `getSnapshot(recordId, generation, { bucketKey, ...opts })` | Fetch a historical version |
413
- | `query({ bucketKey, ...opts })` | Query a collection with filters / pagination |
414
- | `queryAll({ bucketKey, ...opts })` | Fetch all pages automatically |
415
- | `insert(payload, { bucketKey, ...opts })` | Create a new record |
416
- | `update(payload, { bucketKey, ...opts })` | Update an existing record |
417
- | `delete(recordId, { bucketKey, ...opts })` | Delete a record |
418
- | `exists(recordId, { bucketKey, ...opts })` | HEAD check — returns metadata or `null` |
419
- | `batchInsert(payload, { bucketKey, ...opts })` | Insert up to 500 records |
420
- | `batchUpdate(payload, { bucketKey, ...opts })` | Update up to 500 records |
421
- | `batchDelete(payload, { bucketKey, ...opts })` | Delete up to 500 records |
397
+ ### Batch Upload
422
398
 
423
- #### `db.records.get(recordId, options)`
399
+ Upload many files in one request. Progress fires per-file so you can render
400
+ individual progress bars.
424
401
 
425
402
  ```ts
426
- const { data, history } = await db.records.get('rec_abc', {
427
- bucketKey: 'users',
428
- showHistory: true,
429
- });
403
+ const inputFiles = Array.from(fileInput.files); // FileList → array
404
+
405
+ const { data, error } = await hydrous.storage.batchUpload(
406
+ 'ssk_my_bucket_key',
407
+ inputFiles,
408
+ {
409
+ prefix: 'uploads/2024/', // prepended to each filename
410
+ overwrite: false,
411
+ concurrency: 5, // max parallel uploads on the server (1–10)
412
+ onProgress: (p) => {
413
+ // p.index identifies WHICH file this event is for (0-based)
414
+ console.log(`File ${p.index} (${p.path}): ${p.stage} ${p.percent}%`);
415
+ },
416
+ }
417
+ );
418
+
419
+ console.log('Succeeded:', data.succeeded.length);
420
+ console.log('Failed:', data.failed.length);
430
421
  ```
431
422
 
432
- #### `db.records.query(options)`
423
+ Per-file paths override:
433
424
 
434
425
  ```ts
435
- const { data, meta } = await db.records.query({
436
- bucketKey: 'orders',
437
- filters: [{ field: 'status', op: '==', value: 'active' }],
438
- timeScope: '30d',
439
- sortOrder: 'desc',
440
- limit: 50,
441
- cursor: meta.nextCursor ?? undefined,
442
- fields: 'id,name,status',
426
+ await hydrous.storage.batchUpload('ssk_key', files, {
427
+ paths: [
428
+ 'documents/report-q1.pdf',
429
+ 'documents/report-q2.pdf',
430
+ ],
443
431
  });
444
432
  ```
445
433
 
446
- **Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
447
- **Max 3 filters per query.**
434
+ > All files are validated upfront before any uploads begin — if your quota
435
+ > would be exceeded the entire batch is rejected cleanly with no partial writes.
448
436
 
449
- #### `db.records.insert(payload, options)`
437
+ ---
450
438
 
451
- ```ts
452
- const { data, meta } = await db.records.insert(
453
- {
454
- values: { name: 'Alice', role: 'admin', score: 100 },
455
- queryableFields: ['name', 'role'],
456
- userEmail: 'system@example.com',
457
- },
458
- { bucketKey: 'users' }
459
- );
460
- // meta.id — the new record's ID
461
- ```
439
+ ### Download a File
462
440
 
463
- #### `db.records.update(payload, options)`
441
+ Returns the file as an `ArrayBuffer`.
464
442
 
465
443
  ```ts
466
- await db.records.update(
467
- {
468
- recordId: 'rec_abc',
469
- values: { score: 150 },
470
- track_record_history: true,
471
- reason: 'Manual score adjustment',
472
- },
473
- { bucketKey: 'users' }
444
+ const { data: buffer, error } = await hydrous.storage.download(
445
+ 'ssk_my_bucket_key',
446
+ 'avatars/alice.jpg'
474
447
  );
448
+
449
+ if (buffer) {
450
+ // Browser: display image
451
+ const blob = new Blob([buffer], { type: 'image/jpeg' });
452
+ img.src = URL.createObjectURL(blob);
453
+
454
+ // Node: write to disk
455
+ import { writeFileSync } from 'fs';
456
+ writeFileSync('alice.jpg', Buffer.from(buffer));
457
+ }
475
458
  ```
476
459
 
477
- #### `db.records.batchInsert(payload, options)`
460
+ ---
461
+
462
+ ### Batch Download
478
463
 
479
464
  ```ts
480
- const result = await db.records.batchInsert(
465
+ const { data: files } = await hydrous.storage.batchDownload(
466
+ 'ssk_my_bucket_key',
467
+ ['reports/jan.pdf', 'reports/feb.pdf', 'reports/mar.pdf'],
481
468
  {
482
- records: [{ name: 'Alice', score: 99 }, { name: 'Bob', score: 82 }],
483
- queryableFields: ['name'],
484
- userEmail: 'import@example.com',
485
- },
486
- { bucketKey: 'users' }
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
+ }
487
475
  );
488
- // result.meta.failed — number of records that failed
476
+
477
+ // files[n].content — ArrayBuffer
478
+ // files[n].mimeType — string
479
+ // files[n].path — original path
480
+ // files[n].size — bytes
489
481
  ```
490
482
 
491
483
  ---
492
484
 
493
- ### Auth
494
-
495
- All methods live on `db.auth`. Auth routes use `authKey` and do **not** require a `bucketKey`.
496
-
497
- | Method | Description |
498
- |---|---|
499
- | `signUp(payload, opts?)` | Register a new user |
500
- | `signIn(payload, opts?)` | Authenticate with email + password |
501
- | `signOut(payload, opts?)` | Revoke session(s) |
502
- | `validateSession(sessionId, opts?)` | Verify a session is valid |
503
- | `refreshSession(refreshToken, opts?)` | Rotate session using refresh token |
504
- | `getUser(userId, opts?)` | Fetch user by ID |
505
- | `listUsers(opts?)` | Paginated user list |
506
- | `listAllUsers(opts?)` | All users (auto-paginates) |
507
- | `updateUser(payload, opts?)` | Update user profile |
508
- | `deleteUser(userId, opts?)` | Soft-delete a user |
509
- | `changePassword(payload, opts?)` | Change password (requires old password) |
510
- | `requestPasswordReset(payload, opts?)` | Trigger reset email |
511
- | `confirmPasswordReset(payload, opts?)` | Confirm reset with token |
512
- | `requestEmailVerification(payload, opts?)` | Trigger verification email |
513
- | `confirmEmailVerification(payload, opts?)` | Confirm email with token |
514
- | `lockAccount(payload, opts?)` | Lock account for a duration |
515
- | `unlockAccount(userId, opts?)` | Unlock account |
516
-
517
- #### Auth flow example
518
-
519
- ```ts
520
- // 1. Sign up
521
- const { data: user, session } = await db.auth.signUp({
522
- email: 'alice@example.com',
523
- password: 'Str0ng@Pass!',
524
- fullName: 'Alice Smith',
525
- });
526
-
527
- // 2. Store session tokens
528
- const { sessionId, refreshToken, expiresAt } = session;
485
+ ### List Files & Folders
529
486
 
530
- // 3. On each request, validate the session
531
- const { data: currentUser } = await db.auth.validateSession(sessionId);
487
+ ```ts
488
+ const { data } = await hydrous.storage.list('ssk_my_bucket_key', {
489
+ prefix: 'avatars/', // list inside a folder — omit for root
490
+ limit: 50, // max items per page (1–100)
491
+ });
532
492
 
533
- // 4. Before session expires, refresh it
534
- const { session: newSession } = await db.auth.refreshSession(refreshToken);
493
+ for (const item of data.items) {
494
+ if (item.type === 'folder') {
495
+ console.log('📁', item.path);
496
+ } else {
497
+ console.log('📄', item.path, item.size, 'bytes');
498
+ }
499
+ }
535
500
 
536
- // 5. Sign out
537
- await db.auth.signOut({ sessionId });
501
+ // Paginate
502
+ if (data.pagination.hasNextPage) {
503
+ const page2 = await hydrous.storage.list('ssk_my_bucket_key', {
504
+ prefix: 'avatars/',
505
+ cursor: data.pagination.nextCursor,
506
+ });
507
+ }
538
508
  ```
539
509
 
540
510
  ---
541
511
 
542
- ### Analytics
512
+ ### File Metadata
543
513
 
544
- All methods live on `db.analytics`. Every method requires a `bucketKey` option.
514
+ ```ts
515
+ const { data: meta } = await hydrous.storage.metadata(
516
+ 'ssk_my_bucket_key',
517
+ 'avatars/alice.jpg'
518
+ );
545
519
 
546
- | Method | Server `queryType` | Description |
547
- |---|---|---|
548
- | `count({ bucketKey, ...opts })` | `"count"` | Total record count |
549
- | `distribution(field, { bucketKey, ...opts })` | `"distribution"` | Value histogram for a field |
550
- | `sum(field, { bucketKey, ...opts })` | `"sum"` | Sum a numeric field, with optional group-by |
551
- | `timeSeries({ bucketKey, ...opts })` | `"timeSeries"` | Record count over time |
552
- | `fieldTimeSeries(field, { bucketKey, ...opts })` | `"fieldTimeSeries"` | Numeric field aggregated over time |
553
- | `topN(field, n, { bucketKey, ...opts })` | `"topN"` | Top N field values by frequency |
554
- | `stats(field, { bucketKey, ...opts })` | `"stats"` | min/max/avg/stddev/p50/p90/p99 |
555
- | `records({ bucketKey, ...opts })` | `"records"` | Filtered + paginated raw records |
556
- | `multiMetric(metrics, { bucketKey, ...opts })` | `"multiMetric"` | Multiple aggregations in one call |
557
- | `storageStats({ bucketKey, ...opts })` | `"storageStats"` | Bucket storage usage |
558
- | `crossBucket({ bucketKey, bucketKeys, ...opts })` | `"crossBucket"` | Compare metric across buckets |
559
- | `query(payload, { bucketKey, ...opts })` | any | Raw query — full control |
520
+ console.log(meta.size); // stored bytes
521
+ console.log(meta.originalSize); // bytes before compression
522
+ console.log(meta.mimeType);
523
+ console.log(meta.isCompressed);
524
+ console.log(meta.updatedAt);
525
+ ```
560
526
 
561
- #### Date ranges
527
+ ---
562
528
 
563
- All analytics methods accept a `dateRange` option:
529
+ ### Delete a File
564
530
 
565
531
  ```ts
566
- const dateRange = { startDate: '2025-01-01', endDate: '2025-12-31' };
567
- // Or scope to a year / month
568
- const dateRange = { year: '2025', month: '03' };
532
+ const { error } = await hydrous.storage.deleteFile(
533
+ 'ssk_my_bucket_key',
534
+ 'avatars/old-photo.jpg'
535
+ );
569
536
  ```
570
537
 
571
- #### Examples
538
+ ---
539
+
540
+ ### Delete a Folder
541
+
542
+ Recursively deletes the folder and everything inside it.
572
543
 
573
544
  ```ts
574
- // Total records
575
- const { data } = await db.analytics.count({ bucketKey: 'orders' });
576
- console.log(data.count);
545
+ await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
546
+ ```
577
547
 
578
- // Status breakdown
579
- const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
580
- // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
548
+ ---
581
549
 
582
- // Monthly revenue for Q1
583
- const { data } = await db.analytics.sum('amount', {
584
- bucketKey: 'orders',
585
- groupBy: 'region',
586
- dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
587
- });
550
+ ### Create a Folder
588
551
 
589
- // Daily signups this week
590
- const { data } = await db.analytics.timeSeries({
591
- bucketKey: 'users',
592
- granularity: 'day',
593
- dateRange: { startDate: '2025-03-01' },
594
- });
552
+ ```ts
553
+ await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
554
+ ```
595
555
 
596
- // Score percentiles
597
- const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
598
- console.log(data.avg, data.p90, data.p99);
556
+ ---
599
557
 
600
- // Top 5 countries
601
- const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
558
+ ### Move a File
602
559
 
603
- // Dashboard stats — one round-trip for multiple metrics
604
- const { data } = await db.analytics.multiMetric(
605
- [
606
- { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
607
- { name: 'avgScore', field: 'score', aggregation: 'avg' },
608
- { name: 'userCount', field: 'userId', aggregation: 'count' },
609
- ],
610
- { bucketKey: 'orders' }
560
+ ```ts
561
+ await hydrous.storage.move(
562
+ 'ssk_my_bucket_key',
563
+ 'drafts/report.pdf',
564
+ 'published/report.pdf'
611
565
  );
612
- console.log(data.totalRevenue, data.avgScore);
613
-
614
- // Storage usage
615
- const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
616
- console.log(data.totalRecords, data.totalBytes);
617
-
618
- // Cross-bucket revenue comparison
619
- const { data } = await db.analytics.crossBucket({
620
- bucketKey: 'sales-2025',
621
- bucketKeys: ['sales-2024', 'sales-2025'],
622
- field: 'amount',
623
- aggregation: 'sum',
624
- });
625
-
626
- // Raw records with filters
627
- const { data } = await db.analytics.records({
628
- bucketKey: 'users',
629
- filters: [{ field: 'role', op: '==', value: 'admin' }],
630
- selectFields: ['email', 'createdAt'],
631
- limit: 50,
632
- });
633
- console.log(data.data, data.hasMore);
634
566
  ```
635
567
 
636
568
  ---
637
569
 
638
- ## Error Handling
570
+ ### Copy a File
639
571
 
640
572
  ```ts
641
- import { HydrousError, HydrousNetworkError, HydrousTimeoutError } from 'hydrousdb';
642
-
643
- try {
644
- await db.records.get('rec_does_not_exist', { bucketKey: 'users' });
645
- } catch (err) {
646
- if (err instanceof HydrousError) {
647
- console.error(err.message); // human-readable message
648
- console.error(err.status); // HTTP status code (e.g. 404)
649
- console.error(err.code); // machine-readable code (e.g. 'NOT_FOUND')
650
- console.error(err.details); // validation error details array
651
- console.error(err.requestId); // server request ID for support
652
- } else if (err instanceof HydrousTimeoutError) {
653
- console.error('Request timed out');
654
- } else if (err instanceof HydrousNetworkError) {
655
- console.error('Network failure:', err.cause);
656
- }
657
- }
573
+ await hydrous.storage.copy(
574
+ 'ssk_my_bucket_key',
575
+ 'templates/invoice.pdf',
576
+ 'invoices/invoice-001.pdf'
577
+ );
658
578
  ```
659
579
 
660
580
  ---
661
581
 
662
- ## Pagination
582
+ ### Signed URLs
663
583
 
664
- HydrousDB uses opaque cursor strings for pagination.
584
+ Generate a time-limited public URL for a private file.
665
585
 
666
586
  ```ts
667
- // Manual pagination
668
- let cursor: string | null = null;
587
+ const { data } = await hydrous.storage.signedUrl(
588
+ 'ssk_my_bucket_key',
589
+ 'contracts/agreement.pdf',
590
+ { expiresIn: 300 } // 5 minutes (default: 3600 = 1 hour)
591
+ );
669
592
 
670
- do {
671
- const { data, meta } = await db.records.query({
672
- bucketKey: 'orders',
673
- limit: 100,
674
- cursor: cursor ?? undefined,
675
- });
593
+ console.log(data.signedUrl); // share this — expires at data.expiresAt
594
+ ```
676
595
 
677
- processBatch(data);
678
- cursor = meta.nextCursor;
679
- } while (cursor);
596
+ ---
680
597
 
681
- // OR — let the SDK handle it
682
- const allRecords = await db.records.queryAll({
683
- bucketKey: 'orders',
684
- filters: [{ field: 'status', op: '==', value: 'active' }],
685
- });
598
+ ### Bucket Stats
599
+
600
+ ```ts
601
+ const { data: stats } = await hydrous.storage.stats('ssk_my_bucket_key');
602
+
603
+ console.log('Files:', stats.totalFiles);
604
+ console.log('Stored:', stats.totalSizeBytes, 'bytes');
605
+ console.log('Space saved:', stats.spaceSavedBytes, 'bytes');
606
+ console.log('Credits used:', stats.creditsTotalUsed);
607
+ console.log('Compression:', stats.compressionRatio);
686
608
  ```
687
609
 
688
610
  ---
689
611
 
690
- ## TypeScript
612
+ ## 7. Error Handling
691
613
 
692
- All types are exported from the root package:
614
+ Every method returns `{ data, error }`. `error` is `null` on success.
693
615
 
694
616
  ```ts
695
- import type {
696
- HydrousRecord,
697
- QueryOptions,
698
- InsertRecordPayload,
699
- AuthUser,
700
- SessionInfo,
701
- HydrousConfig,
702
- BucketOptions,
703
- Filter,
704
- } from 'hydrousdb';
617
+ const { data, error } = await hydrous.storage.upload('ssk_key', file);
618
+
619
+ if (error) {
620
+ console.error(error.message); // human-readable message
621
+ console.error(error.code); // machine-readable code e.g. 'QUOTA_EXCEEDED'
622
+ console.error(error.status); // HTTP status code (if applicable)
623
+ }
705
624
  ```
706
625
 
707
- Use the generic `query<T>` overload to type your records:
626
+ ### Catching thrown errors
708
627
 
709
- ```ts
710
- interface Product {
711
- name: string;
712
- price: number;
713
- category: string;
714
- }
628
+ If you prefer `try/catch`, import `isHydrousError`:
715
629
 
716
- const { data } = await db.records.query<Product>({
717
- bucketKey: 'products',
718
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
719
- });
630
+ ```ts
631
+ import { isHydrousError } from 'hydrous';
720
632
 
721
- // data is (Product & HydrousRecord)[]
722
- console.log(data[0].name, data[0].price);
633
+ try {
634
+ // ...
635
+ } catch (err) {
636
+ if (isHydrousError(err)) {
637
+ console.error(err.code, err.message);
638
+ }
639
+ }
723
640
  ```
724
641
 
725
- ---
642
+ ### Common error codes
643
+
644
+ | Code | Meaning |
645
+ |---------------------|----------------------------------------------|
646
+ | `HTTP_ERROR` | Non-2xx HTTP response from the server |
647
+ | `NETWORK_ERROR` | Failed to reach the server |
648
+ | `QUOTA_EXCEEDED` | Storage quota has been reached |
649
+ | `FILE_TOO_LARGE` | File exceeds the per-file size limit (50 MB) |
650
+ | `INVALID_MIME` | MIME type not permitted |
651
+ | `FILE_EXISTS` | File already exists and `overwrite` is false |
652
+ | `UPLOAD_ABORTED` | Upload was cancelled by the client |
653
+ | `UPLOAD_TIMEOUT` | Upload timed out |
654
+ | `NO_SESSION` | Auth operation requires a session |
655
+ | `UNKNOWN_ERROR` | An unexpected error occurred |
726
656
 
727
- ## Migration from v1
657
+ ---
728
658
 
729
- v2 introduces two breaking changes:
659
+ ## 8. TypeScript Types Reference
730
660
 
731
- ### 1. `bucketKey` moves from `createClient` to per-call options
661
+ ### `UploadProgress`
732
662
 
733
663
  ```ts
734
- // v1
735
- const db = createClient({ authKey: 'ak_...', bucketKey: 'users' });
736
- await db.records.query({ limit: 50 });
664
+ interface UploadProgress {
665
+ index: number; // file index (0-based)
666
+ total: number; // total files in the operation
667
+ path: string; // destination path
668
+ stage: UploadStage; // see below
669
+ bytesUploaded: number;
670
+ totalBytes: number;
671
+ percent: number; // 0–100
672
+ bytesPerSecond: number | null; // null before first tick
673
+ eta: number | null; // seconds remaining, null until speed known
674
+ result?: UploadResult; // set when stage === 'done'
675
+ error?: string; // set when stage === 'error'
676
+ code?: string;
677
+ }
737
678
 
738
- // v2
739
- const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
740
- await db.records.query({ bucketKey: 'users', limit: 50 });
679
+ type UploadStage = 'pending' | 'compressing' | 'uploading' | 'done' | 'error';
741
680
  ```
742
681
 
743
- ### 2. `securityKey` replaces the implicit credential
744
-
745
- In v1 the SDK incorrectly used `bucketKey` as both the bucket identifier and the security credential in the URL. v2 adds an explicit `securityKey` that maps to the server's `:key` parameter.
682
+ ### `UploadResult`
746
683
 
747
684
  ```ts
748
- // v1 (broken — used bucketKey as both segments)
749
- createClient({ authKey: 'ak_...', bucketKey: 'users' });
750
- // → /api/users/users
751
-
752
- // v2 (correct)
753
- createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
754
- // → /api/users/sk_... ✅ (bucketKey passed per-call)
685
+ interface UploadResult {
686
+ path: string;
687
+ compressed: boolean;
688
+ originalSize: number;
689
+ storedSize: number;
690
+ spaceSaved: number;
691
+ mimeType: string;
692
+ }
755
693
  ```
756
694
 
757
- **Quick migration checklist:**
758
- 1. Add `securityKey` to your `createClient` call
759
- 2. Remove `bucketKey` from `createClient`
760
- 3. Add `{ bucketKey: '...' }` to every `db.records.*` and `db.analytics.*` call
761
- 4. Auth calls (`db.auth.*`) are unchanged
695
+ ### `StorageItem` (from `list()`)
762
696
 
763
- ---
697
+ ```ts
698
+ interface StorageItem {
699
+ name: string;
700
+ path: string;
701
+ type: 'file' | 'folder';
702
+ size?: number | null;
703
+ mimeType?: string | null;
704
+ isCompressed?: boolean;
705
+ updatedAt?: string | null;
706
+ }
707
+ ```
764
708
 
765
- ## Contributing
709
+ ### `StorageStats`
766
710
 
767
- 1. Fork the repo and clone it
768
- 2. `npm install`
769
- 3. `npm run dev` — TypeScript watch mode
770
- 4. `npm test` — run the test suite
771
- 5. Open a PR 🚀
711
+ ```ts
712
+ interface StorageStats {
713
+ totalFiles: number;
714
+ totalSizeBytes: number;
715
+ totalOriginalSizeBytes: number;
716
+ spaceSavedBytes: number;
717
+ uploadsCount: number;
718
+ downloadsCount: number;
719
+ creditsUsedUpload: number;
720
+ creditsUsedDownload: number;
721
+ creditsTotalUsed: number;
722
+ compressionRatio: string; // e.g. "34.2%"
723
+ }
724
+ ```
772
725
 
773
726
  ---
774
727
 
775
728
  ## License
776
729
 
777
- MIT © HydrousDB
730
+ MIT © Hydrous