hydrousdb 1.1.0 → 2.0.0
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 +224 -133
- package/dist/analytics/index.d.mts +64 -57
- package/dist/analytics/index.d.ts +64 -57
- package/dist/analytics/index.js +57 -94
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +57 -94
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +1 -1
- package/dist/auth/index.mjs.map +1 -1
- package/dist/{http-CIXLF5GV.d.mts → http-DTukpdAU.d.mts} +15 -11
- package/dist/{http-CIXLF5GV.d.ts → http-DTukpdAU.d.ts} +15 -11
- package/dist/index.d.mts +16 -10
- package/dist/index.d.ts +16 -10
- package/dist/index.js +129 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -164
- package/dist/index.mjs.map +1 -1
- package/dist/records/index.d.mts +57 -44
- package/dist/records/index.d.ts +57 -44
- package/dist/records/index.js +67 -65
- package/dist/records/index.js.map +1 -1
- package/dist/records/index.mjs +67 -65
- package/dist/records/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
- [Features](#features)
|
|
15
15
|
- [Installation](#installation)
|
|
16
|
+
- [Key Concepts](#key-concepts)
|
|
16
17
|
- [Quick Start](#quick-start)
|
|
17
18
|
- [Framework Guides](#framework-guides)
|
|
18
19
|
- [Next.js (App Router)](#nextjs-app-router)
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
- [Error Handling](#error-handling)
|
|
29
30
|
- [Pagination](#pagination)
|
|
30
31
|
- [TypeScript](#typescript)
|
|
32
|
+
- [Migration from v1](#migration-from-v1)
|
|
31
33
|
- [Contributing](#contributing)
|
|
32
34
|
- [License](#license)
|
|
33
35
|
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
## Features
|
|
37
39
|
|
|
38
40
|
- ✅ **Full TypeScript** — every method and response is fully typed
|
|
41
|
+
- ✅ **Multi-bucket** — one client, any number of buckets; pass `bucketKey` per call
|
|
39
42
|
- ✅ **Modular** — import only what you need (`hydrousdb/records`, `hydrousdb/auth`, `hydrousdb/analytics`)
|
|
40
43
|
- ✅ **Tree-shakeable** — ships ESM + CJS with dual exports
|
|
41
44
|
- ✅ **Auto-retry** — retries on transient 5xx / network errors with linear back-off
|
|
@@ -59,34 +62,66 @@ yarn add hydrousdb
|
|
|
59
62
|
|
|
60
63
|
---
|
|
61
64
|
|
|
65
|
+
## Key Concepts
|
|
66
|
+
|
|
67
|
+
### Two keys, one client
|
|
68
|
+
|
|
69
|
+
The SDK separates two distinct credentials:
|
|
70
|
+
|
|
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 |
|
|
75
|
+
|
|
76
|
+
### Bucket per call
|
|
77
|
+
|
|
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.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
|
|
82
|
+
|
|
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' });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
62
92
|
## Quick Start
|
|
63
93
|
|
|
64
94
|
```ts
|
|
65
95
|
import { createClient } from 'hydrousdb';
|
|
66
96
|
|
|
67
97
|
const db = createClient({
|
|
68
|
-
|
|
69
|
-
|
|
98
|
+
authKey: 'your-auth-key',
|
|
99
|
+
securityKey: 'your-security-key',
|
|
70
100
|
});
|
|
71
101
|
|
|
72
|
-
// Insert a record
|
|
73
|
-
const { data, meta } = await db.records.insert(
|
|
74
|
-
values: { name: 'Alice', score: 99 },
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
);
|
|
77
107
|
console.log('Created record:', meta.id);
|
|
78
108
|
|
|
79
|
-
// Query records
|
|
109
|
+
// Query records from the 'users' bucket
|
|
80
110
|
const { data: records } = await db.records.query({
|
|
111
|
+
bucketKey: 'users',
|
|
81
112
|
filters: [{ field: 'score', op: '>=', value: 90 }],
|
|
82
113
|
limit: 20,
|
|
83
114
|
});
|
|
84
115
|
|
|
85
|
-
// Auth —
|
|
116
|
+
// Auth — no bucketKey needed
|
|
86
117
|
const { data: user, session } = await db.auth.signIn({
|
|
87
118
|
email: 'alice@example.com',
|
|
88
119
|
password: 'Str0ng@Pass!',
|
|
89
120
|
});
|
|
121
|
+
|
|
122
|
+
// Analytics
|
|
123
|
+
const { data: stats } = await db.analytics.count({ bucketKey: 'users' });
|
|
124
|
+
console.log(stats.count);
|
|
90
125
|
```
|
|
91
126
|
|
|
92
127
|
---
|
|
@@ -101,10 +136,9 @@ const { data: user, session } = await db.auth.signIn({
|
|
|
101
136
|
// lib/db.ts
|
|
102
137
|
import { createClient } from 'hydrousdb';
|
|
103
138
|
|
|
104
|
-
// The client is re-used across all server components in the same process
|
|
105
139
|
export const db = createClient({
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
141
|
+
securityKey: process.env.HYDROUS_SECURITY_KEY!,
|
|
108
142
|
});
|
|
109
143
|
```
|
|
110
144
|
|
|
@@ -116,6 +150,7 @@ import { db } from '@/lib/db';
|
|
|
116
150
|
|
|
117
151
|
export default async function ProductsPage() {
|
|
118
152
|
const { data: products } = await db.records.query({
|
|
153
|
+
bucketKey: 'products',
|
|
119
154
|
filters: [{ field: 'category', op: '==', value: 'shoes' }],
|
|
120
155
|
limit: 24,
|
|
121
156
|
sortOrder: 'desc',
|
|
@@ -142,10 +177,10 @@ import { HydrousError } from 'hydrousdb';
|
|
|
142
177
|
export async function POST(req: NextRequest) {
|
|
143
178
|
try {
|
|
144
179
|
const body = await req.json();
|
|
145
|
-
const result = await db.records.insert(
|
|
146
|
-
values: body,
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
const result = await db.records.insert(
|
|
181
|
+
{ values: body, queryableFields: ['email', 'name'] },
|
|
182
|
+
{ bucketKey: 'users' }
|
|
183
|
+
);
|
|
149
184
|
return NextResponse.json(result, { status: 201 });
|
|
150
185
|
} catch (err) {
|
|
151
186
|
if (err instanceof HydrousError) {
|
|
@@ -182,8 +217,8 @@ export const config = { matcher: ['/dashboard/:path*'] };
|
|
|
182
217
|
|
|
183
218
|
```env
|
|
184
219
|
# .env.local
|
|
185
|
-
|
|
186
|
-
|
|
220
|
+
HYDROUS_AUTH_KEY=your-auth-key
|
|
221
|
+
HYDROUS_SECURITY_KEY=your-security-key
|
|
187
222
|
```
|
|
188
223
|
|
|
189
224
|
---
|
|
@@ -196,14 +231,14 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|
|
196
231
|
import { createClient, HydrousError } from 'hydrousdb';
|
|
197
232
|
|
|
198
233
|
const db = createClient({
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
235
|
+
securityKey: process.env.HYDROUS_SECURITY_KEY!,
|
|
201
236
|
});
|
|
202
237
|
|
|
203
238
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
204
239
|
if (req.method === 'GET') {
|
|
205
240
|
try {
|
|
206
|
-
const result = await db.records.query({ limit: 50 });
|
|
241
|
+
const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
|
|
207
242
|
return res.status(200).json(result);
|
|
208
243
|
} catch (err) {
|
|
209
244
|
if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
|
|
@@ -229,8 +264,8 @@ export default defineNuxtPlugin(() => {
|
|
|
229
264
|
const config = useRuntimeConfig();
|
|
230
265
|
|
|
231
266
|
const db = createClient({
|
|
232
|
-
|
|
233
|
-
|
|
267
|
+
authKey: config.hydrousAuthKey as string,
|
|
268
|
+
securityKey: config.hydrousSecurityKey as string,
|
|
234
269
|
});
|
|
235
270
|
|
|
236
271
|
return { provide: { db } };
|
|
@@ -241,8 +276,8 @@ export default defineNuxtPlugin(() => {
|
|
|
241
276
|
// nuxt.config.ts
|
|
242
277
|
export default defineNuxtConfig({
|
|
243
278
|
runtimeConfig: {
|
|
244
|
-
|
|
245
|
-
|
|
279
|
+
hydrousAuthKey: '', // NUXT_HYDROUS_AUTH_KEY
|
|
280
|
+
hydrousSecurityKey: '', // NUXT_HYDROUS_SECURITY_KEY
|
|
246
281
|
},
|
|
247
282
|
});
|
|
248
283
|
```
|
|
@@ -254,7 +289,7 @@ export default defineNuxtConfig({
|
|
|
254
289
|
const { $db } = useNuxtApp();
|
|
255
290
|
|
|
256
291
|
const { data: records, pending } = await useAsyncData('records', () =>
|
|
257
|
-
$db.records.query({ limit: 20 })
|
|
292
|
+
$db.records.query({ bucketKey: 'products', limit: 20 })
|
|
258
293
|
);
|
|
259
294
|
</script>
|
|
260
295
|
|
|
@@ -273,8 +308,8 @@ const { data: records, pending } = await useAsyncData('records', () =>
|
|
|
273
308
|
import { createClient } from 'hydrousdb';
|
|
274
309
|
|
|
275
310
|
const db = createClient({
|
|
276
|
-
|
|
277
|
-
|
|
311
|
+
authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
|
|
312
|
+
securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
|
|
278
313
|
});
|
|
279
314
|
|
|
280
315
|
export function useHydrous() {
|
|
@@ -292,11 +327,11 @@ import { useEffect, useState } from 'react';
|
|
|
292
327
|
import { createClient, HydrousRecord, HydrousError } from 'hydrousdb';
|
|
293
328
|
|
|
294
329
|
const db = createClient({
|
|
295
|
-
|
|
296
|
-
|
|
330
|
+
authKey: import.meta.env.VITE_HYDROUS_AUTH_KEY,
|
|
331
|
+
securityKey: import.meta.env.VITE_HYDROUS_SECURITY_KEY,
|
|
297
332
|
});
|
|
298
333
|
|
|
299
|
-
export function useRecords(limit = 20) {
|
|
334
|
+
export function useRecords(bucketKey: string, limit = 20) {
|
|
300
335
|
const [records, setRecords] = useState<HydrousRecord[]>([]);
|
|
301
336
|
const [loading, setLoading] = useState(true);
|
|
302
337
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -304,13 +339,13 @@ export function useRecords(limit = 20) {
|
|
|
304
339
|
useEffect(() => {
|
|
305
340
|
const controller = new AbortController();
|
|
306
341
|
|
|
307
|
-
db.records.query({ limit }, { signal: controller.signal })
|
|
342
|
+
db.records.query({ bucketKey, limit }, { signal: controller.signal })
|
|
308
343
|
.then(r => setRecords(r.data))
|
|
309
344
|
.catch(e => { if (!(e instanceof DOMException)) setError(String(e)); })
|
|
310
345
|
.finally(() => setLoading(false));
|
|
311
346
|
|
|
312
347
|
return () => controller.abort();
|
|
313
|
-
}, [limit]);
|
|
348
|
+
}, [bucketKey, limit]);
|
|
314
349
|
|
|
315
350
|
return { records, loading, error };
|
|
316
351
|
}
|
|
@@ -326,15 +361,15 @@ import express from 'express';
|
|
|
326
361
|
import { createClient, HydrousError } from 'hydrousdb';
|
|
327
362
|
|
|
328
363
|
const db = createClient({
|
|
329
|
-
|
|
330
|
-
|
|
364
|
+
authKey: process.env.HYDROUS_AUTH_KEY!,
|
|
365
|
+
securityKey: process.env.HYDROUS_SECURITY_KEY!,
|
|
331
366
|
});
|
|
332
367
|
const app = express();
|
|
333
368
|
app.use(express.json());
|
|
334
369
|
|
|
335
|
-
app.get('/
|
|
370
|
+
app.get('/orders', async (req, res) => {
|
|
336
371
|
try {
|
|
337
|
-
const result = await db.records.query({ limit: 50 });
|
|
372
|
+
const result = await db.records.query({ bucketKey: 'orders', limit: 50 });
|
|
338
373
|
res.json(result);
|
|
339
374
|
} catch (err) {
|
|
340
375
|
if (err instanceof HydrousError) return res.status(err.status).json({ error: err.message });
|
|
@@ -355,89 +390,101 @@ app.listen(3000, () => console.log('Listening on :3000'));
|
|
|
355
390
|
import { createClient } from 'hydrousdb';
|
|
356
391
|
|
|
357
392
|
const db = createClient({
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
baseUrl?:
|
|
361
|
-
timeout?:
|
|
362
|
-
retries?:
|
|
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)
|
|
363
398
|
});
|
|
364
399
|
```
|
|
365
400
|
|
|
401
|
+
> **Note:** `bucketKey` is no longer part of the client config. It is passed as an option on each individual records and analytics call.
|
|
402
|
+
|
|
366
403
|
---
|
|
367
404
|
|
|
368
405
|
### Records
|
|
369
406
|
|
|
370
|
-
All methods live on `db.records`.
|
|
407
|
+
All methods live on `db.records`. Every method requires a `bucketKey` option.
|
|
371
408
|
|
|
372
409
|
| Method | Description |
|
|
373
410
|
|---|---|
|
|
374
|
-
| `get(recordId, opts
|
|
375
|
-
| `getSnapshot(recordId, generation, opts
|
|
376
|
-
| `query(opts
|
|
377
|
-
| `queryAll(opts
|
|
378
|
-
| `insert(payload, opts
|
|
379
|
-
| `update(payload, opts
|
|
380
|
-
| `delete(recordId, opts
|
|
381
|
-
| `exists(recordId, opts
|
|
382
|
-
| `batchInsert(payload, opts
|
|
383
|
-
| `batchUpdate(payload, opts
|
|
384
|
-
| `batchDelete(payload, opts
|
|
385
|
-
|
|
386
|
-
#### `db.records.get(recordId, options
|
|
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 |
|
|
422
|
+
|
|
423
|
+
#### `db.records.get(recordId, options)`
|
|
387
424
|
|
|
388
425
|
```ts
|
|
389
|
-
const { data, history } = await db.records.get('rec_abc', {
|
|
426
|
+
const { data, history } = await db.records.get('rec_abc', {
|
|
427
|
+
bucketKey: 'users',
|
|
428
|
+
showHistory: true,
|
|
429
|
+
});
|
|
390
430
|
```
|
|
391
431
|
|
|
392
|
-
#### `db.records.query(options
|
|
432
|
+
#### `db.records.query(options)`
|
|
393
433
|
|
|
394
434
|
```ts
|
|
395
435
|
const { data, meta } = await db.records.query({
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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',
|
|
402
443
|
});
|
|
403
444
|
```
|
|
404
445
|
|
|
405
|
-
**Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
|
|
446
|
+
**Filter operators:** `==` `!=` `>` `<` `>=` `<=` `contains`
|
|
406
447
|
**Max 3 filters per query.**
|
|
407
448
|
|
|
408
|
-
#### `db.records.insert(payload)`
|
|
449
|
+
#### `db.records.insert(payload, options)`
|
|
409
450
|
|
|
410
451
|
```ts
|
|
411
|
-
const { data, meta } = await db.records.insert(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
);
|
|
416
460
|
// meta.id — the new record's ID
|
|
417
461
|
```
|
|
418
462
|
|
|
419
|
-
#### `db.records.update(payload)`
|
|
463
|
+
#### `db.records.update(payload, options)`
|
|
420
464
|
|
|
421
465
|
```ts
|
|
422
|
-
await db.records.update(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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' }
|
|
474
|
+
);
|
|
428
475
|
```
|
|
429
476
|
|
|
430
|
-
#### `db.records.batchInsert(payload)`
|
|
477
|
+
#### `db.records.batchInsert(payload, options)`
|
|
431
478
|
|
|
432
479
|
```ts
|
|
433
|
-
const result = await db.records.batchInsert(
|
|
434
|
-
|
|
435
|
-
{ name: 'Alice', score: 99 },
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
480
|
+
const result = await db.records.batchInsert(
|
|
481
|
+
{
|
|
482
|
+
records: [{ name: 'Alice', score: 99 }, { name: 'Bob', score: 82 }],
|
|
483
|
+
queryableFields: ['name'],
|
|
484
|
+
userEmail: 'import@example.com',
|
|
485
|
+
},
|
|
486
|
+
{ bucketKey: 'users' }
|
|
487
|
+
);
|
|
441
488
|
// result.meta.failed — number of records that failed
|
|
442
489
|
```
|
|
443
490
|
|
|
@@ -445,7 +492,7 @@ const result = await db.records.batchInsert({
|
|
|
445
492
|
|
|
446
493
|
### Auth
|
|
447
494
|
|
|
448
|
-
All methods live on `db.auth`.
|
|
495
|
+
All methods live on `db.auth`. Auth routes use `authKey` and do **not** require a `bucketKey`.
|
|
449
496
|
|
|
450
497
|
| Method | Description |
|
|
451
498
|
|---|---|
|
|
@@ -477,7 +524,7 @@ const { data: user, session } = await db.auth.signUp({
|
|
|
477
524
|
fullName: 'Alice Smith',
|
|
478
525
|
});
|
|
479
526
|
|
|
480
|
-
// 2. Store session tokens
|
|
527
|
+
// 2. Store session tokens
|
|
481
528
|
const { sessionId, refreshToken, expiresAt } = session;
|
|
482
529
|
|
|
483
530
|
// 3. On each request, validate the session
|
|
@@ -494,32 +541,30 @@ await db.auth.signOut({ sessionId });
|
|
|
494
541
|
|
|
495
542
|
### Analytics
|
|
496
543
|
|
|
497
|
-
All methods live on `db.analytics`. Every method
|
|
544
|
+
All methods live on `db.analytics`. Every method requires a `bucketKey` option.
|
|
498
545
|
|
|
499
546
|
| Method | Server `queryType` | Description |
|
|
500
547
|
|---|---|---|
|
|
501
|
-
| `count(opts
|
|
502
|
-
| `distribution(field, opts
|
|
503
|
-
| `sum(field, opts
|
|
504
|
-
| `timeSeries(opts
|
|
505
|
-
| `fieldTimeSeries(field, opts
|
|
506
|
-
| `topN(field, n, opts
|
|
507
|
-
| `stats(field, opts
|
|
508
|
-
| `records(opts
|
|
509
|
-
| `multiMetric(metrics, opts
|
|
510
|
-
| `storageStats(opts
|
|
511
|
-
| `crossBucket(opts)` | `"crossBucket"` | Compare metric across buckets |
|
|
512
|
-
| `query(payload, opts
|
|
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 |
|
|
513
560
|
|
|
514
561
|
#### Date ranges
|
|
515
562
|
|
|
516
563
|
All analytics methods accept a `dateRange` option:
|
|
517
564
|
|
|
518
565
|
```ts
|
|
519
|
-
// Scope to a specific date window
|
|
520
566
|
const dateRange = { startDate: '2025-01-01', endDate: '2025-12-31' };
|
|
521
|
-
|
|
522
|
-
// Or scope to a year / month / day
|
|
567
|
+
// Or scope to a year / month
|
|
523
568
|
const dateRange = { year: '2025', month: '03' };
|
|
524
569
|
```
|
|
525
570
|
|
|
@@ -527,57 +572,63 @@ const dateRange = { year: '2025', month: '03' };
|
|
|
527
572
|
|
|
528
573
|
```ts
|
|
529
574
|
// Total records
|
|
530
|
-
const { data } = await db.analytics.count();
|
|
575
|
+
const { data } = await db.analytics.count({ bucketKey: 'orders' });
|
|
531
576
|
console.log(data.count);
|
|
532
577
|
|
|
533
578
|
// Status breakdown
|
|
534
|
-
const { data } = await db.analytics.distribution('status');
|
|
579
|
+
const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
|
|
535
580
|
// [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
|
|
536
581
|
|
|
537
582
|
// Monthly revenue for Q1
|
|
538
583
|
const { data } = await db.analytics.sum('amount', {
|
|
539
|
-
|
|
540
|
-
|
|
584
|
+
bucketKey: 'orders',
|
|
585
|
+
groupBy: 'region',
|
|
586
|
+
dateRange: { startDate: '2025-01-01', endDate: '2025-03-31' },
|
|
541
587
|
});
|
|
542
588
|
|
|
543
589
|
// Daily signups this week
|
|
544
590
|
const { data } = await db.analytics.timeSeries({
|
|
591
|
+
bucketKey: 'users',
|
|
545
592
|
granularity: 'day',
|
|
546
|
-
dateRange:
|
|
593
|
+
dateRange: { startDate: '2025-03-01' },
|
|
547
594
|
});
|
|
548
595
|
|
|
549
596
|
// Score percentiles
|
|
550
|
-
const { data } = await db.analytics.stats('score');
|
|
597
|
+
const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
|
|
551
598
|
console.log(data.avg, data.p90, data.p99);
|
|
552
599
|
|
|
553
600
|
// Top 5 countries
|
|
554
|
-
const { data } = await db.analytics.topN('country', 5);
|
|
601
|
+
const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
|
|
555
602
|
|
|
556
603
|
// Dashboard stats — one round-trip for multiple metrics
|
|
557
|
-
const { data } = await db.analytics.multiMetric(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
604
|
+
const { data } = await db.analytics.multiMetric(
|
|
605
|
+
[
|
|
606
|
+
{ name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
|
|
607
|
+
{ name: 'avgScore', field: 'score', aggregation: 'avg' },
|
|
608
|
+
{ name: 'userCount', field: 'userId', aggregation: 'count' },
|
|
609
|
+
],
|
|
610
|
+
{ bucketKey: 'orders' }
|
|
611
|
+
);
|
|
562
612
|
console.log(data.totalRevenue, data.avgScore);
|
|
563
613
|
|
|
564
614
|
// Storage usage
|
|
565
|
-
const { data } = await db.analytics.storageStats();
|
|
615
|
+
const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
|
|
566
616
|
console.log(data.totalRecords, data.totalBytes);
|
|
567
617
|
|
|
568
618
|
// Cross-bucket revenue comparison
|
|
569
619
|
const { data } = await db.analytics.crossBucket({
|
|
620
|
+
bucketKey: 'sales-2025',
|
|
570
621
|
bucketKeys: ['sales-2024', 'sales-2025'],
|
|
571
622
|
field: 'amount',
|
|
572
623
|
aggregation: 'sum',
|
|
573
624
|
});
|
|
574
625
|
|
|
575
|
-
// Raw records with filters
|
|
626
|
+
// Raw records with filters
|
|
576
627
|
const { data } = await db.analytics.records({
|
|
577
|
-
|
|
628
|
+
bucketKey: 'users',
|
|
629
|
+
filters: [{ field: 'role', op: '==', value: 'admin' }],
|
|
578
630
|
selectFields: ['email', 'createdAt'],
|
|
579
|
-
limit:
|
|
580
|
-
offset: 0,
|
|
631
|
+
limit: 50,
|
|
581
632
|
});
|
|
582
633
|
console.log(data.data, data.hasMore);
|
|
583
634
|
```
|
|
@@ -586,13 +637,11 @@ console.log(data.data, data.hasMore);
|
|
|
586
637
|
|
|
587
638
|
## Error Handling
|
|
588
639
|
|
|
589
|
-
The SDK throws typed errors you can `instanceof` check:
|
|
590
|
-
|
|
591
640
|
```ts
|
|
592
641
|
import { HydrousError, HydrousNetworkError, HydrousTimeoutError } from 'hydrousdb';
|
|
593
642
|
|
|
594
643
|
try {
|
|
595
|
-
await db.records.get('rec_does_not_exist');
|
|
644
|
+
await db.records.get('rec_does_not_exist', { bucketKey: 'users' });
|
|
596
645
|
} catch (err) {
|
|
597
646
|
if (err instanceof HydrousError) {
|
|
598
647
|
console.error(err.message); // human-readable message
|
|
@@ -620,17 +669,19 @@ let cursor: string | null = null;
|
|
|
620
669
|
|
|
621
670
|
do {
|
|
622
671
|
const { data, meta } = await db.records.query({
|
|
623
|
-
|
|
624
|
-
|
|
672
|
+
bucketKey: 'orders',
|
|
673
|
+
limit: 100,
|
|
674
|
+
cursor: cursor ?? undefined,
|
|
625
675
|
});
|
|
626
676
|
|
|
627
677
|
processBatch(data);
|
|
628
678
|
cursor = meta.nextCursor;
|
|
629
679
|
} while (cursor);
|
|
630
680
|
|
|
631
|
-
// — OR — let the SDK handle it
|
|
681
|
+
// — OR — let the SDK handle it
|
|
632
682
|
const allRecords = await db.records.queryAll({
|
|
633
|
-
|
|
683
|
+
bucketKey: 'orders',
|
|
684
|
+
filters: [{ field: 'status', op: '==', value: 'active' }],
|
|
634
685
|
});
|
|
635
686
|
```
|
|
636
687
|
|
|
@@ -648,21 +699,23 @@ import type {
|
|
|
648
699
|
AuthUser,
|
|
649
700
|
SessionInfo,
|
|
650
701
|
HydrousConfig,
|
|
702
|
+
BucketOptions,
|
|
651
703
|
Filter,
|
|
652
704
|
} from 'hydrousdb';
|
|
653
705
|
```
|
|
654
706
|
|
|
655
|
-
|
|
707
|
+
Use the generic `query<T>` overload to type your records:
|
|
656
708
|
|
|
657
709
|
```ts
|
|
658
710
|
interface Product {
|
|
659
|
-
name:
|
|
660
|
-
price:
|
|
711
|
+
name: string;
|
|
712
|
+
price: number;
|
|
661
713
|
category: string;
|
|
662
714
|
}
|
|
663
715
|
|
|
664
716
|
const { data } = await db.records.query<Product>({
|
|
665
|
-
|
|
717
|
+
bucketKey: 'products',
|
|
718
|
+
filters: [{ field: 'category', op: '==', value: 'shoes' }],
|
|
666
719
|
});
|
|
667
720
|
|
|
668
721
|
// data is (Product & HydrousRecord)[]
|
|
@@ -671,6 +724,44 @@ console.log(data[0].name, data[0].price);
|
|
|
671
724
|
|
|
672
725
|
---
|
|
673
726
|
|
|
727
|
+
## Migration from v1
|
|
728
|
+
|
|
729
|
+
v2 introduces two breaking changes:
|
|
730
|
+
|
|
731
|
+
### 1. `bucketKey` moves from `createClient` to per-call options
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
// v1
|
|
735
|
+
const db = createClient({ authKey: 'ak_...', bucketKey: 'users' });
|
|
736
|
+
await db.records.query({ limit: 50 });
|
|
737
|
+
|
|
738
|
+
// v2
|
|
739
|
+
const db = createClient({ authKey: 'ak_...', securityKey: 'sk_...' });
|
|
740
|
+
await db.records.query({ bucketKey: 'users', limit: 50 });
|
|
741
|
+
```
|
|
742
|
+
|
|
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.
|
|
746
|
+
|
|
747
|
+
```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)
|
|
755
|
+
```
|
|
756
|
+
|
|
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
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
674
765
|
## Contributing
|
|
675
766
|
|
|
676
767
|
1. Fork the repo and clone it
|