hydrousdb 1.1.1 → 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,686 +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
- - [Quick Start](#quick-start)
17
- - [Framework Guides](#framework-guides)
18
- - [Next.js (App Router)](#nextjs-app-router)
19
- - [Next.js (Pages Router)](#nextjs-pages-router)
20
- - [Vue 3 / Nuxt 3](#vue-3--nuxt-3)
21
- - [React (Vite / CRA)](#react-vite--cra)
22
- - [Node.js / Express](#nodejs--express)
23
- - [API Reference](#api-reference)
24
- - [createClient](#createclient)
25
- - [Records](#records)
26
- - [Auth](#auth)
27
- - [Analytics](#analytics)
28
- - [Error Handling](#error-handling)
29
- - [Pagination](#pagination)
30
- - [TypeScript](#typescript)
31
- - [Contributing](#contributing)
32
- - [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)
33
51
 
34
52
  ---
35
53
 
36
- ## Features
37
-
38
- - ✅ **Full TypeScript** — every method and response is fully typed
39
- - ✅ **Modular** — import only what you need (`hydrousdb/records`, `hydrousdb/auth`, `hydrousdb/analytics`)
40
- - ✅ **Tree-shakeable** — ships ESM + CJS with dual exports
41
- - ✅ **Auto-retry** — retries on transient 5xx / network errors with linear back-off
42
- - ✅ **Timeout control** — configurable per-client request timeout
43
- - ✅ **Cursor pagination helpers** — `queryAll()` / `listAllUsers()` handle cursor following for you
44
- - ✅ **Zero dependencies** — uses the native `fetch` API (Node 18+, all modern browsers)
54
+ ## 1. Quick Start
45
55
 
46
- ---
56
+ ```ts
57
+ import { createClient } from 'hydrous';
47
58
 
48
- ## Installation
59
+ const hydrous = createClient({
60
+ url: 'https://api.yourapp.hydrous.app',
61
+ apiKey: 'hk_live_…',
62
+ });
49
63
 
50
- ```bash
51
- npm install hydrousdb
52
- # or
53
- pnpm add hydrousdb
54
- # or
55
- yarn add hydrousdb
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
+ );
56
73
  ```
57
74
 
58
- > **Node.js ≥ 18** is required (uses native `fetch`). For Node 16/17, polyfill `fetch` with `node-fetch` or `undici`.
59
-
60
75
  ---
61
76
 
62
- ## Quick Start
77
+ ## 2. Configuration
63
78
 
64
79
  ```ts
65
- import { createClient } from 'hydrousdb';
66
-
67
- const db = createClient({
68
- authKey: 'your-auth-key',
69
- bucketKey: 'your-bucket-key',
70
- });
80
+ import { HydrousClient } from 'hydrous';
71
81
 
72
- // Insert a record
73
- const { data, meta } = await db.records.insert({
74
- values: { name: 'Alice', score: 99 },
75
- queryableFields: ['name'],
76
- });
77
- console.log('Created record:', meta.id);
78
-
79
- // Query records
80
- const { data: records } = await db.records.query({
81
- filters: [{ field: 'score', op: '>=', value: 90 }],
82
- limit: 20,
83
- });
84
-
85
- // Auth — sign a user in
86
- const { data: user, session } = await db.auth.signIn({
87
- email: 'alice@example.com',
88
- password: 'Str0ng@Pass!',
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)
89
86
  });
90
87
  ```
91
88
 
92
- ---
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) |
93
94
 
94
- ## Framework Guides
95
+ ---
95
96
 
96
- ### Next.js (App Router)
97
+ ## 3. Auth
97
98
 
98
- **1. Create a shared client singleton**
99
+ ### Sign Up
99
100
 
100
101
  ```ts
101
- // lib/db.ts
102
- import { createClient } from 'hydrousdb';
103
-
104
- // The client is re-used across all server components in the same process
105
- export const db = createClient({
106
- authKey: process.env.HYDROUS_AUTH_KEY!,
107
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
102
+ const { data, error } = await hydrous.auth.signUp({
103
+ email: 'user@example.com',
104
+ password: 'supersecret',
105
+ metadata: { plan: 'pro' }, // optional
108
106
  });
109
- ```
110
107
 
111
- **2. Use in a Server Component**
108
+ if (data) {
109
+ console.log('New user:', data.user.id);
110
+ console.log('Access token:', data.accessToken);
111
+ }
112
+ ```
112
113
 
113
- ```tsx
114
- // app/products/page.tsx
115
- import { db } from '@/lib/db';
114
+ ### Sign In
116
115
 
117
- export default async function ProductsPage() {
118
- const { data: products } = await db.records.query({
119
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
120
- limit: 24,
121
- sortOrder: 'desc',
122
- });
116
+ ```ts
117
+ const { data, error } = await hydrous.auth.signIn({
118
+ email: 'user@example.com',
119
+ password: 'supersecret',
120
+ });
123
121
 
124
- return (
125
- <ul>
126
- {products.map(p => (
127
- <li key={p.id}>{String(p.name)}</li>
128
- ))}
129
- </ul>
130
- );
122
+ if (data) {
123
+ console.log('Welcome back,', data.user.email);
131
124
  }
132
125
  ```
133
126
 
134
- **3. Use in a Route Handler**
127
+ ### Sign Out
135
128
 
136
129
  ```ts
137
- // app/api/records/route.ts
138
- import { NextRequest, NextResponse } from 'next/server';
139
- import { db } from '@/lib/db';
140
- import { HydrousError } from 'hydrousdb';
130
+ await hydrous.auth.signOut();
131
+ ```
141
132
 
142
- export async function POST(req: NextRequest) {
143
- try {
144
- const body = await req.json();
145
- const result = await db.records.insert({
146
- values: body,
147
- queryableFields: ['email', 'name'],
148
- });
149
- return NextResponse.json(result, { status: 201 });
150
- } catch (err) {
151
- if (err instanceof HydrousError) {
152
- return NextResponse.json({ error: err.message }, { status: err.status });
153
- }
154
- return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
155
- }
156
- }
133
+ ### Get Current User
134
+
135
+ ```ts
136
+ const { data: user } = await hydrous.auth.getUser();
137
+ console.log(user?.email);
157
138
  ```
158
139
 
159
- **4. Auth middleware example**
140
+ ### Refresh Session
160
141
 
161
142
  ```ts
162
- // middleware.ts
163
- import { NextRequest, NextResponse } from 'next/server';
164
- import { db } from '@/lib/db';
143
+ const { data: session } = await hydrous.auth.refreshSession();
144
+ ```
165
145
 
166
- export async function middleware(req: NextRequest) {
167
- const sessionId = req.cookies.get('sessionId')?.value;
168
- if (!sessionId) return NextResponse.redirect(new URL('/login', req.url));
146
+ ---
169
147
 
170
- try {
171
- await db.auth.validateSession(sessionId);
172
- return NextResponse.next();
173
- } catch {
174
- return NextResponse.redirect(new URL('/login', req.url));
175
- }
176
- }
148
+ ## 4. Records
149
+
150
+ ### Insert
151
+
152
+ Single record:
177
153
 
178
- export const config = { matcher: ['/dashboard/:path*'] };
154
+ ```ts
155
+ const { data, error } = await hydrous.records.insert('users', {
156
+ name: 'Alice',
157
+ email: 'alice@example.com',
158
+ role: 'admin',
159
+ });
179
160
  ```
180
161
 
181
- **5. Environment variables**
162
+ Bulk insert (array):
182
163
 
183
- ```env
184
- # .env.local
185
- HYDROUS_API_KEY=your-api-key
186
- HYDROUS_BUCKET_KEY=your-bucket-key
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`);
187
170
  ```
188
171
 
189
- ---
172
+ ### Select / Query
173
+
174
+ ```ts
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
+ });
182
+ ```
190
183
 
191
- ### Next.js (Pages Router)
184
+ Multiple filters:
192
185
 
193
186
  ```ts
194
- // pages/api/records.ts
195
- import type { NextApiRequest, NextApiResponse } from 'next';
196
- import { createClient, HydrousError } from 'hydrousdb';
187
+ import { eq, gt, inArray } from 'hydrous';
197
188
 
198
- const db = createClient({
199
- authKey: process.env.HYDROUS_AUTH_KEY!,
200
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
189
+ const { data } = await hydrous.records.select('orders', {
190
+ where: [
191
+ eq('status', 'shipped'),
192
+ gt('total', 100),
193
+ inArray('tag', ['vip', 'priority']),
194
+ ],
201
195
  });
196
+ ```
202
197
 
203
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
204
- if (req.method === 'GET') {
205
- try {
206
- const result = await db.records.query({ limit: 50 });
207
- return res.status(200).json(result);
208
- } catch (err) {
209
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
210
- return res.status(500).json({ error: 'Internal Server Error' });
211
- }
212
- }
213
- res.setHeader('Allow', ['GET']);
214
- res.status(405).end();
215
- }
198
+ ### Get by ID
199
+
200
+ ```ts
201
+ const { data: user } = await hydrous.records.get('users', 'user_abc123');
216
202
  ```
217
203
 
218
- ---
204
+ ### Update
219
205
 
220
- ### Vue 3 / Nuxt 3
206
+ ```ts
207
+ const { data } = await hydrous.records.update('users', 'user_abc123', {
208
+ name: 'Alice Smith',
209
+ });
210
+ ```
221
211
 
222
- **Nuxt 3 — plugin**
212
+ ### Delete
223
213
 
224
214
  ```ts
225
- // plugins/hydrousdb.ts
226
- import { createClient } from 'hydrousdb';
215
+ const { error } = await hydrous.records.delete('users', 'user_abc123');
216
+ ```
227
217
 
228
- export default defineNuxtPlugin(() => {
229
- const config = useRuntimeConfig();
218
+ ### Query Helpers
230
219
 
231
- const db = createClient({
232
- authKey: config.hydrousAuthKey as string,
233
- bucketKey: config.hydrousBucketKey as string,
234
- });
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 (…)` |
235
227
 
236
- return { provide: { db } };
237
- });
228
+ ```ts
229
+ import { eq, gt, inArray } from 'hydrous';
238
230
  ```
239
231
 
232
+ ---
233
+
234
+ ## 5. Analytics
235
+
236
+ ### Track an Event
237
+
240
238
  ```ts
241
- // nuxt.config.ts
242
- export default defineNuxtConfig({
243
- runtimeConfig: {
244
- hydrousApiKey: '', // Set via NUXT_HYDROUS_API_KEY env var
245
- hydrousBucketKey: '', // Set via NUXT_HYDROUS_BUCKET_KEY env var
246
- },
239
+ await hydrous.analytics.track({
240
+ event: 'page_view',
241
+ properties: { page: '/home', referrer: 'google.com' },
242
+ userId: 'user_abc123',
243
+ sessionId: 'sess_xyz',
247
244
  });
248
245
  ```
249
246
 
250
- **Usage in a component**
247
+ ### Batch Track
251
248
 
252
- ```vue
253
- <script setup lang="ts">
254
- const { $db } = useNuxtApp();
249
+ More efficient than calling `track()` in a loop:
255
250
 
256
- const { data: records, pending } = await useAsyncData('records', () =>
257
- $db.records.query({ limit: 20 })
258
- );
259
- </script>
260
-
261
- <template>
262
- <div v-if="pending">Loading…</div>
263
- <ul v-else>
264
- <li v-for="r in records?.data" :key="r.id">{{ r.name }}</li>
265
- </ul>
266
- </template>
251
+ ```ts
252
+ await hydrous.analytics.trackBatch([
253
+ { event: 'signup', userId: 'u1' },
254
+ { event: 'onboarded', userId: 'u1', properties: { step: 'profile' } },
255
+ ]);
267
256
  ```
268
257
 
269
- **Vue 3 standalone — composable**
258
+ ### Query Events
270
259
 
271
260
  ```ts
272
- // composables/useHydrous.ts
273
- import { createClient } from 'hydrousdb';
274
-
275
- const db = createClient({
276
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
277
- bucketKey: import.meta.env.VITE_HYDROUS_BUCKET_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',
278
267
  });
279
-
280
- export function useHydrous() {
281
- return db;
282
- }
283
268
  ```
284
269
 
285
270
  ---
286
271
 
287
- ### React (Vite / CRA)
272
+ ## 6. Storage
288
273
 
289
- ```tsx
290
- // src/hooks/useRecords.ts
291
- import { useEffect, useState } from 'react';
292
- 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_`.
293
275
 
294
- const db = createClient({
295
- authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
296
- bucketKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
297
- });
276
+ ### How Bucket Keys Work
298
277
 
299
- export function useRecords(limit = 20) {
300
- const [records, setRecords] = useState<HydrousRecord[]>([]);
301
- const [loading, setLoading] = useState(true);
302
- 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.
303
279
 
304
- useEffect(() => {
305
- const controller = new AbortController();
280
+ ```
281
+ hydrous.storage.<method>(
282
+ 'ssk_your_bucket_key', // ← first, always a string
283
+ ...args
284
+ )
285
+ ```
286
+
287
+ ---
306
288
 
307
- db.records.query({ limit }, { signal: controller.signal })
308
- .then(r => setRecords(r.data))
309
- .catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
310
- .finally(() => setLoading(false));
289
+ ### Upload a File
311
290
 
312
- return () => controller.abort();
313
- }, [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
+ );
314
303
 
315
- return { records, loading, error };
304
+ if (data) {
305
+ console.log('Stored at:', data.path);
306
+ console.log('Space saved:', data.spaceSaved, 'bytes');
316
307
  }
317
308
  ```
318
309
 
319
310
  ---
320
311
 
321
- ### Node.js / Express
312
+ ### Upload Raw Text / JSON
322
313
 
323
- ```ts
324
- // server.ts
325
- import express from 'express';
326
- import { createClient, HydrousError } from 'hydrousdb';
314
+ No `File` object needed — pass any string directly.
327
315
 
328
- const db = createClient({
329
- authKey: process.env.HYDROUS_AUTH_KEY!,
330
- bucketKey: process.env.HYDROUS_BUCKET_KEY!,
331
- });
332
- const app = express();
333
- app.use(express.json());
334
-
335
- app.get('/records', async (req, res) => {
336
- try {
337
- const result = await db.records.query({ limit: 50 });
338
- res.json(result);
339
- } catch (err) {
340
- if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
341
- res.status(500).json({ error: 'Internal Server Error' });
342
- }
343
- });
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
+ );
344
324
 
345
- 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
+ );
346
332
  ```
347
333
 
348
334
  ---
349
335
 
350
- ## API Reference
336
+ ### Track Upload Progress
351
337
 
352
- ### `createClient`
338
+ `onProgress` is called on every progress tick with a rich `UploadProgress` object.
353
339
 
354
340
  ```ts
355
- 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
+ );
356
375
 
357
- const db = createClient({
358
- authKey: string, // required your HydrousDB auth service key
359
- bucketKey: string, // required your bucket identifier
360
- baseUrl?: string, // override API base URL
361
- timeout?: number, // request timeout in ms (default: 30_000)
362
- retries?: number, // retries on network/5xx errors (default: 2)
363
- });
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
+ }
364
382
  ```
365
383
 
366
- ---
384
+ **Stage lifecycle:**
385
+
386
+ ```
387
+ pending → compressing → uploading → done
388
+ ↘ error
389
+ ```
367
390
 
368
- ### 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.
369
394
 
370
- All methods live on `db.records`.
395
+ ---
371
396
 
372
- | Method | Description |
373
- |---|---|
374
- | `get(recordId, opts?)` | Fetch a single record |
375
- | `getSnapshot(recordId, generation, opts?)` | Fetch a historical version |
376
- | `query(opts?)` | Query a collection with filters / pagination |
377
- | `queryAll(opts?)` | Fetch all pages automatically |
378
- | `insert(payload, opts?)` | Create a new record |
379
- | `update(payload, opts?)` | Update an existing record |
380
- | `delete(recordId, opts?)` | Delete a record |
381
- | `exists(recordId, opts?)` | HEAD check — returns metadata or `null` |
382
- | `batchInsert(payload, opts?)` | Insert up to 500 records |
383
- | `batchUpdate(payload, opts?)` | Update up to 500 records |
384
- | `batchDelete(payload, opts?)` | Delete up to 500 records |
397
+ ### Batch Upload
385
398
 
386
- #### `db.records.get(recordId, options?)`
399
+ Upload many files in one request. Progress fires per-file so you can render
400
+ individual progress bars.
387
401
 
388
402
  ```ts
389
- const { data, history } = await db.records.get('rec_abc', { showHistory: true });
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);
390
421
  ```
391
422
 
392
- #### `db.records.query(options?)`
423
+ Per-file paths override:
393
424
 
394
425
  ```ts
395
- const { data, meta } = await db.records.query({
396
- filters: [{ field: 'status', op: '==', value: 'active' }],
397
- timeScope: '30d',
398
- sortOrder: 'desc',
399
- limit: 50,
400
- cursor: meta.nextCursor ?? undefined, // for pagination
401
- fields: 'id,name,status', // select specific fields
426
+ await hydrous.storage.batchUpload('ssk_key', files, {
427
+ paths: [
428
+ 'documents/report-q1.pdf',
429
+ 'documents/report-q2.pdf',
430
+ ],
402
431
  });
403
432
  ```
404
433
 
405
- **Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
406
- **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.
436
+
437
+ ---
407
438
 
408
- #### `db.records.insert(payload)`
439
+ ### Download a File
440
+
441
+ Returns the file as an `ArrayBuffer`.
409
442
 
410
443
  ```ts
411
- const { data, meta } = await db.records.insert({
412
- values: { name: 'Alice', role: 'admin', score: 100 },
413
- queryableFields: ['name', 'role'], // fields you want to filter by later
414
- userEmail: 'system@example.com',
415
- });
416
- // meta.id — the new record's ID
444
+ const { data: buffer, error } = await hydrous.storage.download(
445
+ 'ssk_my_bucket_key',
446
+ 'avatars/alice.jpg'
447
+ );
448
+
449
+ if (buffer) {
450
+ // Browser: display image
451
+ const blob = new Blob([buffer], { type: 'image/jpeg' });
452
+ img.src = URL.createObjectURL(blob);
453
+
454
+ // Node: write to disk
455
+ import { writeFileSync } from 'fs';
456
+ writeFileSync('alice.jpg', Buffer.from(buffer));
457
+ }
417
458
  ```
418
459
 
419
- #### `db.records.update(payload)`
460
+ ---
461
+
462
+ ### Batch Download
420
463
 
421
464
  ```ts
422
- await db.records.update({
423
- recordId: 'rec_abc',
424
- values: { score: 150 },
425
- track_record_history: true, // snapshot the previous version
426
- reason: 'Manual score adjustment',
427
- });
465
+ const { data: files } = await hydrous.storage.batchDownload(
466
+ 'ssk_my_bucket_key',
467
+ ['reports/jan.pdf', 'reports/feb.pdf', 'reports/mar.pdf'],
468
+ {
469
+ concurrency: 3,
470
+ autoSave: true, // browser: auto-triggers Save dialog per file
471
+ onProgress: (p) => {
472
+ console.log(`${p.path}: ${p.status}`); // 'pending' | 'success' | 'error'
473
+ },
474
+ }
475
+ );
476
+
477
+ // files[n].content — ArrayBuffer
478
+ // files[n].mimeType — string
479
+ // files[n].path — original path
480
+ // files[n].size — bytes
428
481
  ```
429
482
 
430
- #### `db.records.batchInsert(payload)`
483
+ ---
484
+
485
+ ### List Files & Folders
431
486
 
432
487
  ```ts
433
- const result = await db.records.batchInsert({
434
- records: [
435
- { name: 'Alice', score: 99 },
436
- { name: 'Bob', score: 82 },
437
- ],
438
- queryableFields: ['name'],
439
- userEmail: 'import@example.com',
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)
440
491
  });
441
- // result.meta.failed — number of records that failed
492
+
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
+ }
500
+
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
+ }
442
508
  ```
443
509
 
444
510
  ---
445
511
 
446
- ### Auth
447
-
448
- All methods live on `db.auth`.
449
-
450
- | Method | Description |
451
- |---|---|
452
- | `signUp(payload, opts?)` | Register a new user |
453
- | `signIn(payload, opts?)` | Authenticate with email + password |
454
- | `signOut(payload, opts?)` | Revoke session(s) |
455
- | `validateSession(sessionId, opts?)` | Verify a session is valid |
456
- | `refreshSession(refreshToken, opts?)` | Rotate session using refresh token |
457
- | `getUser(userId, opts?)` | Fetch user by ID |
458
- | `listUsers(opts?)` | Paginated user list |
459
- | `listAllUsers(opts?)` | All users (auto-paginates) |
460
- | `updateUser(payload, opts?)` | Update user profile |
461
- | `deleteUser(userId, opts?)` | Soft-delete a user |
462
- | `changePassword(payload, opts?)` | Change password (requires old password) |
463
- | `requestPasswordReset(payload, opts?)` | Trigger reset email |
464
- | `confirmPasswordReset(payload, opts?)` | Confirm reset with token |
465
- | `requestEmailVerification(payload, opts?)` | Trigger verification email |
466
- | `confirmEmailVerification(payload, opts?)` | Confirm email with token |
467
- | `lockAccount(payload, opts?)` | Lock account for a duration |
468
- | `unlockAccount(userId, opts?)` | Unlock account |
469
-
470
- #### Auth flow example
471
-
472
- ```ts
473
- // 1. Sign up
474
- const { data: user, session } = await db.auth.signUp({
475
- email: 'alice@example.com',
476
- password: 'Str0ng@Pass!',
477
- fullName: 'Alice Smith',
478
- });
512
+ ### File Metadata
479
513
 
480
- // 2. Store session tokens (e.g. in a cookie or localStorage)
481
- const { sessionId, refreshToken, expiresAt } = session;
514
+ ```ts
515
+ const { data: meta } = await hydrous.storage.metadata(
516
+ 'ssk_my_bucket_key',
517
+ 'avatars/alice.jpg'
518
+ );
519
+
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
+ ```
482
526
 
483
- // 3. On each request, validate the session
484
- const { data: currentUser } = await db.auth.validateSession(sessionId);
527
+ ---
485
528
 
486
- // 4. Before session expires, refresh it
487
- const { session: newSession } = await db.auth.refreshSession(refreshToken);
529
+ ### Delete a File
488
530
 
489
- // 5. Sign out
490
- await db.auth.signOut({ sessionId });
531
+ ```ts
532
+ const { error } = await hydrous.storage.deleteFile(
533
+ 'ssk_my_bucket_key',
534
+ 'avatars/old-photo.jpg'
535
+ );
491
536
  ```
492
537
 
493
538
  ---
494
539
 
495
- ### Analytics
540
+ ### Delete a Folder
496
541
 
497
- All methods live on `db.analytics`. Every method maps to a BigQuery-backed analytics query. The server `queryType` string each method sends is noted for reference.
542
+ Recursively deletes the folder and everything inside it.
498
543
 
499
- | Method | Server `queryType` | Description |
500
- |---|---|---|
501
- | `count(opts?)` | `"count"` | Total record count |
502
- | `distribution(field, opts?)` | `"distribution"` | Value histogram for a field |
503
- | `sum(field, opts?)` | `"sum"` | Sum a numeric field, with optional group-by |
504
- | `timeSeries(opts?)` | `"timeSeries"` | Record count over time |
505
- | `fieldTimeSeries(field, opts?)` | `"fieldTimeSeries"` | Numeric field aggregated over time |
506
- | `topN(field, n, opts?)` | `"topN"` | Top N field values by frequency |
507
- | `stats(field, opts?)` | `"stats"` | min/max/avg/stddev/p50/p90/p99 |
508
- | `records(opts?)` | `"records"` | Filtered + paginated raw records |
509
- | `multiMetric(metrics, opts?)` | `"multiMetric"` | Multiple aggregations in one call |
510
- | `storageStats(opts?)` | `"storageStats"` | Bucket storage usage |
511
- | `crossBucket(opts)` | `"crossBucket"` | Compare metric across buckets |
512
- | `query(payload, opts?)` | any | Raw query — full control |
544
+ ```ts
545
+ await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
546
+ ```
513
547
 
514
- #### Date ranges
548
+ ---
515
549
 
516
- All analytics methods accept a `dateRange` option:
550
+ ### Create a Folder
517
551
 
518
552
  ```ts
519
- // Scope to a specific date window
520
- const dateRange = { startDate: '2025-01-01', endDate: '2025-12-31' };
521
-
522
- // Or scope to a year / month / day
523
- const dateRange = { year: '2025', month: '03' };
553
+ await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
524
554
  ```
525
555
 
526
- #### Examples
556
+ ---
557
+
558
+ ### Move a File
527
559
 
528
560
  ```ts
529
- // Total records
530
- const { data } = await db.analytics.count();
531
- console.log(data.count);
561
+ await hydrous.storage.move(
562
+ 'ssk_my_bucket_key',
563
+ 'drafts/report.pdf',
564
+ 'published/report.pdf'
565
+ );
566
+ ```
532
567
 
533
- // Status breakdown
534
- const { data } = await db.analytics.distribution('status');
535
- // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
568
+ ---
536
569
 
537
- // Monthly revenue for Q1
538
- const { data } = await db.analytics.sum('amount', {
539
- groupBy: 'region',
540
- dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
541
- });
570
+ ### Copy a File
542
571
 
543
- // Daily signups this week
544
- const { data } = await db.analytics.timeSeries({
545
- granularity: 'day',
546
- dateRange: { startDate: '2025-03-01' },
547
- });
572
+ ```ts
573
+ await hydrous.storage.copy(
574
+ 'ssk_my_bucket_key',
575
+ 'templates/invoice.pdf',
576
+ 'invoices/invoice-001.pdf'
577
+ );
578
+ ```
548
579
 
549
- // Score percentiles
550
- const { data } = await db.analytics.stats('score');
551
- console.log(data.avg, data.p90, data.p99);
580
+ ---
552
581
 
553
- // Top 5 countries
554
- const { data } = await db.analytics.topN('country', 5);
582
+ ### Signed URLs
555
583
 
556
- // Dashboard stats one round-trip for multiple metrics
557
- const { data } = await db.analytics.multiMetric([
558
- { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
559
- { name: 'avgScore', field: 'score', aggregation: 'avg' },
560
- { name: 'userCount', field: 'userId', aggregation: 'count' },
561
- ]);
562
- console.log(data.totalRevenue, data.avgScore);
584
+ Generate a time-limited public URL for a private file.
563
585
 
564
- // Storage usage
565
- const { data } = await db.analytics.storageStats();
566
- console.log(data.totalRecords, data.totalBytes);
567
-
568
- // Cross-bucket revenue comparison
569
- const { data } = await db.analytics.crossBucket({
570
- bucketKeys: ['sales-2024', 'sales-2025'],
571
- field: 'amount',
572
- aggregation: 'sum',
573
- });
586
+ ```ts
587
+ const { data } = await hydrous.storage.signedUrl(
588
+ 'ssk_my_bucket_key',
589
+ 'contracts/agreement.pdf',
590
+ { expiresIn: 300 } // 5 minutes (default: 3600 = 1 hour)
591
+ );
574
592
 
575
- // Raw records with filters (supports: == != > < >= <= CONTAINS)
576
- const { data } = await db.analytics.records({
577
- filters: [{ field: 'role', op: '==', value: 'admin' }],
578
- selectFields: ['email', 'createdAt'],
579
- limit: 50,
580
- offset: 0,
581
- });
582
- console.log(data.data, data.hasMore);
593
+ console.log(data.signedUrl); // share this expires at data.expiresAt
583
594
  ```
584
595
 
585
596
  ---
586
597
 
587
- ## Error Handling
588
-
589
- The SDK throws typed errors you can `instanceof` check:
598
+ ### Bucket Stats
590
599
 
591
600
  ```ts
592
- import { HydrousError, HydrousNetworkError, HydrousTimeoutError } from 'hydrousdb';
601
+ const { data: stats } = await hydrous.storage.stats('ssk_my_bucket_key');
593
602
 
594
- try {
595
- await db.records.get('rec_does_not_exist');
596
- } catch (err) {
597
- if (err instanceof HydrousError) {
598
- console.error(err.message); // human-readable message
599
- console.error(err.status); // HTTP status code (e.g. 404)
600
- console.error(err.code); // machine-readable code (e.g. 'NOT_FOUND')
601
- console.error(err.details); // validation error details array
602
- console.error(err.requestId); // server request ID for support
603
- } else if (err instanceof HydrousTimeoutError) {
604
- console.error('Request timed out');
605
- } else if (err instanceof HydrousNetworkError) {
606
- console.error('Network failure:', err.cause);
607
- }
608
- }
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);
609
608
  ```
610
609
 
611
610
  ---
612
611
 
613
- ## Pagination
612
+ ## 7. Error Handling
614
613
 
615
- HydrousDB uses opaque cursor strings for pagination.
614
+ Every method returns `{ data, error }`. `error` is `null` on success.
616
615
 
617
616
  ```ts
618
- // Manual pagination
619
- let cursor: string | null = null;
617
+ const { data, error } = await hydrous.storage.upload('ssk_key', file);
620
618
 
621
- do {
622
- const { data, meta } = await db.records.query({
623
- limit: 100,
624
- cursor: cursor ?? undefined,
625
- });
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
+ }
624
+ ```
626
625
 
627
- processBatch(data);
628
- cursor = meta.nextCursor;
629
- } while (cursor);
626
+ ### Catching thrown errors
630
627
 
631
- // OR let the SDK handle it for you
632
- const allRecords = await db.records.queryAll({
633
- filters: [{ field: 'status', op: '==', value: 'active' }],
634
- });
628
+ If you prefer `try/catch`, import `isHydrousError`:
629
+
630
+ ```ts
631
+ import { isHydrousError } from 'hydrous';
632
+
633
+ try {
634
+ // ...
635
+ } catch (err) {
636
+ if (isHydrousError(err)) {
637
+ console.error(err.code, err.message);
638
+ }
639
+ }
635
640
  ```
636
641
 
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 |
656
+
637
657
  ---
638
658
 
639
- ## TypeScript
659
+ ## 8. TypeScript Types Reference
640
660
 
641
- All types are exported from the root package:
661
+ ### `UploadProgress`
642
662
 
643
663
  ```ts
644
- import type {
645
- HydrousRecord,
646
- QueryOptions,
647
- InsertRecordPayload,
648
- AuthUser,
649
- SessionInfo,
650
- HydrousConfig,
651
- Filter,
652
- } from 'hydrousdb';
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
+ }
678
+
679
+ type UploadStage = 'pending' | 'compressing' | 'uploading' | 'done' | 'error';
653
680
  ```
654
681
 
655
- You can also use the generic `query<T>` overload to type your records:
682
+ ### `UploadResult`
656
683
 
657
684
  ```ts
658
- interface Product {
659
- name: string;
660
- price: number;
661
- category: string;
685
+ interface UploadResult {
686
+ path: string;
687
+ compressed: boolean;
688
+ originalSize: number;
689
+ storedSize: number;
690
+ spaceSaved: number;
691
+ mimeType: string;
662
692
  }
693
+ ```
663
694
 
664
- const { data } = await db.records.query<Product>({
665
- filters: [{ field: 'category', op: '==', value: 'shoes' }],
666
- });
695
+ ### `StorageItem` (from `list()`)
667
696
 
668
- // data is (Product & HydrousRecord)[]
669
- console.log(data[0].name, data[0].price);
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
+ }
670
707
  ```
671
708
 
672
- ---
673
-
674
- ## Contributing
709
+ ### `StorageStats`
675
710
 
676
- 1. Fork the repo and clone it
677
- 2. `npm install`
678
- 3. `npm run dev` — TypeScript watch mode
679
- 4. `npm test` — run the test suite
680
- 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
+ ```
681
725
 
682
726
  ---
683
727
 
684
728
  ## License
685
729
 
686
- MIT © HydrousDB
730
+ MIT © Hydrous