hydrousdb 1.1.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/dist/index.js ADDED
@@ -0,0 +1,810 @@
1
+ 'use strict';
2
+
3
+ // src/utils/errors.ts
4
+ var HydrousError = class extends Error {
5
+ constructor(body, status) {
6
+ super(body.error);
7
+ this.name = "HydrousError";
8
+ this.status = status;
9
+ this.code = body.code;
10
+ this.details = body.details ?? [];
11
+ this.requestId = body.requestId;
12
+ }
13
+ };
14
+ var HydrousNetworkError = class extends Error {
15
+ constructor(message, cause) {
16
+ super(message);
17
+ this.name = "HydrousNetworkError";
18
+ this.cause = cause;
19
+ }
20
+ };
21
+ var HydrousTimeoutError = class extends Error {
22
+ constructor(timeoutMs) {
23
+ super(`Request timed out after ${timeoutMs}ms`);
24
+ this.name = "HydrousTimeoutError";
25
+ }
26
+ };
27
+
28
+ // src/utils/http.ts
29
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
30
+ var DEFAULT_TIMEOUT = 3e4;
31
+ var DEFAULT_RETRIES = 2;
32
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
33
+ var HttpClient = class {
34
+ constructor(config) {
35
+ this.apiKey = config.apiKey;
36
+ this.bucketKey = config.bucketKey;
37
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
38
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
39
+ this.retries = config.retries ?? DEFAULT_RETRIES;
40
+ }
41
+ // ── Core request ────────────────────────────────────────────────────────────
42
+ async request(method, path, options) {
43
+ const url = this.buildUrl(path, options?.params);
44
+ const headers = {
45
+ "Content-Type": "application/json",
46
+ ...options?.headers
47
+ };
48
+ const init = {
49
+ method,
50
+ headers,
51
+ signal: this.buildSignal(options?.signal)
52
+ };
53
+ if (options?.body !== void 0) {
54
+ init.body = JSON.stringify(options.body);
55
+ }
56
+ return this.executeWithRetry(url, init, options?.raw ?? false, this.retries);
57
+ }
58
+ // ── Convenience methods ──────────────────────────────────────────────────────
59
+ get(path, params, opts) {
60
+ return this.request("GET", path, { params, ...opts });
61
+ }
62
+ post(path, body, opts) {
63
+ return this.request("POST", path, { body, ...opts });
64
+ }
65
+ patch(path, body, opts) {
66
+ return this.request("PATCH", path, { body, ...opts });
67
+ }
68
+ delete(path, params, opts) {
69
+ return this.request("DELETE", path, { params, ...opts });
70
+ }
71
+ head(path, params, opts) {
72
+ return this.request("HEAD", path, { params, raw: true, ...opts });
73
+ }
74
+ // ── Internals ────────────────────────────────────────────────────────────────
75
+ buildUrl(path, params) {
76
+ const url = new URL(`${this.baseUrl}${path}`);
77
+ if (params) {
78
+ for (const [k, v] of Object.entries(params)) {
79
+ if (v !== void 0 && v !== null && v !== "") {
80
+ url.searchParams.set(k, String(v));
81
+ }
82
+ }
83
+ }
84
+ return url.toString();
85
+ }
86
+ buildSignal(userSignal) {
87
+ const controller = new AbortController();
88
+ const timeoutId = setTimeout(() => controller.abort("timeout"), this.timeout);
89
+ userSignal?.addEventListener("abort", () => {
90
+ clearTimeout(timeoutId);
91
+ controller.abort(userSignal.reason);
92
+ });
93
+ return controller.signal;
94
+ }
95
+ async executeWithRetry(url, init, raw, retriesLeft) {
96
+ try {
97
+ const res = await fetch(url, init);
98
+ if (raw) return res;
99
+ if (res.ok) {
100
+ if (res.status === 204) return void 0;
101
+ return await res.json();
102
+ }
103
+ let body;
104
+ try {
105
+ body = await res.json();
106
+ } catch {
107
+ body = { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
108
+ }
109
+ if (retriesLeft > 0 && RETRYABLE_STATUSES.has(res.status)) {
110
+ await sleep(500 * (this.retries - retriesLeft + 1));
111
+ return this.executeWithRetry(url, init, raw, retriesLeft - 1);
112
+ }
113
+ throw new HydrousError(body, res.status);
114
+ } catch (err) {
115
+ if (err instanceof HydrousError) throw err;
116
+ if (err instanceof Error && err.message === "timeout") {
117
+ throw new HydrousTimeoutError(this.timeout);
118
+ }
119
+ if (retriesLeft > 0) {
120
+ await sleep(500 * (this.retries - retriesLeft + 1));
121
+ return this.executeWithRetry(url, init, raw, retriesLeft - 1);
122
+ }
123
+ throw new HydrousNetworkError(
124
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
125
+ err
126
+ );
127
+ }
128
+ }
129
+ };
130
+ function sleep(ms) {
131
+ return new Promise((resolve) => setTimeout(resolve, ms));
132
+ }
133
+
134
+ // src/utils/query.ts
135
+ function buildQueryParams(opts) {
136
+ const params = {};
137
+ if (opts.timeScope) params["timeScope"] = opts.timeScope;
138
+ if (opts.year) params["year"] = opts.year;
139
+ if (opts.sortOrder) params["sortOrder"] = opts.sortOrder;
140
+ if (opts.limit) params["limit"] = opts.limit;
141
+ if (opts.cursor) params["cursor"] = opts.cursor;
142
+ if (opts.fields) params["fields"] = opts.fields;
143
+ for (const filter of opts.filters ?? []) {
144
+ const paramKey = filter.op === "==" ? filter.field : `${filter.field}[${filter.op}]`;
145
+ params[paramKey] = filter.value;
146
+ }
147
+ return params;
148
+ }
149
+ function validateFilters(filters) {
150
+ const VALID_OPS = /* @__PURE__ */ new Set(["==", "!=", ">", "<", ">=", "<=", "contains"]);
151
+ if (filters.length > 3) {
152
+ throw new Error("HydrousDB supports a maximum of 3 filters per query.");
153
+ }
154
+ for (const f of filters) {
155
+ if (!VALID_OPS.has(f.op)) {
156
+ throw new Error(
157
+ `Invalid filter operator "${f.op}". Valid operators: ${[...VALID_OPS].join(", ")}`
158
+ );
159
+ }
160
+ if (!f.field || typeof f.field !== "string") {
161
+ throw new Error('Each filter must have a non-empty "field" string.');
162
+ }
163
+ }
164
+ }
165
+
166
+ // src/records/client.ts
167
+ var RecordsClient = class {
168
+ constructor(http) {
169
+ this.http = http;
170
+ }
171
+ get path() {
172
+ return `/api/${this.http.bucketKey}`;
173
+ }
174
+ // ── GET — single record ────────────────────────────────────────────────────
175
+ /**
176
+ * Fetch a single record by ID.
177
+ *
178
+ * @example
179
+ * const { data } = await db.records.get('rec_abc123');
180
+ * const { data, history } = await db.records.get('rec_abc123', { showHistory: true });
181
+ */
182
+ async get(recordId, options) {
183
+ const params = { recordId };
184
+ if (options?.showHistory) params["showHistory"] = "true";
185
+ return this.http.get(this.path, params, options);
186
+ }
187
+ // ── GET — historical snapshot ──────────────────────────────────────────────
188
+ /**
189
+ * Fetch a specific historical version (generation) of a record.
190
+ *
191
+ * @example
192
+ * const { data } = await db.records.getSnapshot('rec_abc123', '1700000000000000');
193
+ */
194
+ async getSnapshot(recordId, generation, opts) {
195
+ return this.http.get(this.path, { recordId, generation }, opts);
196
+ }
197
+ // ── GET — collection query ─────────────────────────────────────────────────
198
+ /**
199
+ * Query a collection with optional filters, sorting, and pagination.
200
+ *
201
+ * @example
202
+ * // Simple query
203
+ * const { data, meta } = await db.records.query({ limit: 50, sortOrder: 'desc' });
204
+ *
205
+ * // Filtered query
206
+ * const { data } = await db.records.query({
207
+ * filters: [{ field: 'status', op: '==', value: 'active' }],
208
+ * timeScope: '7d',
209
+ * });
210
+ *
211
+ * // Paginated
212
+ * let cursor: string | null = null;
213
+ * do {
214
+ * const result = await db.records.query({ limit: 100, cursor: cursor ?? undefined });
215
+ * cursor = result.meta.nextCursor;
216
+ * } while (cursor);
217
+ */
218
+ async query(options) {
219
+ if (options?.filters) validateFilters(options.filters);
220
+ const params = buildQueryParams(options ?? {});
221
+ return this.http.get(this.path, params, options);
222
+ }
223
+ // ── POST — insert ──────────────────────────────────────────────────────────
224
+ /**
225
+ * Insert a new record.
226
+ *
227
+ * @example
228
+ * const { data, meta } = await db.records.insert({
229
+ * values: { name: 'Alice', score: 99 },
230
+ * queryableFields: ['name'],
231
+ * userEmail: 'alice@example.com',
232
+ * });
233
+ */
234
+ async insert(payload, opts) {
235
+ return this.http.post(this.path, payload, opts);
236
+ }
237
+ // ── PATCH — update ─────────────────────────────────────────────────────────
238
+ /**
239
+ * Update an existing record.
240
+ *
241
+ * @example
242
+ * await db.records.update({
243
+ * recordId: 'rec_abc123',
244
+ * values: { score: 100 },
245
+ * track_record_history: true,
246
+ * });
247
+ */
248
+ async update(payload, opts) {
249
+ return this.http.patch(this.path, payload, opts);
250
+ }
251
+ // ── DELETE ─────────────────────────────────────────────────────────────────
252
+ /**
253
+ * Delete a record permanently.
254
+ *
255
+ * @example
256
+ * await db.records.delete('rec_abc123');
257
+ */
258
+ async delete(recordId, opts) {
259
+ return this.http.delete(this.path, { recordId }, opts);
260
+ }
261
+ // ── HEAD — existence check ─────────────────────────────────────────────────
262
+ /**
263
+ * Check whether a record exists without fetching its full data.
264
+ * Returns `null` if the record is not found.
265
+ *
266
+ * @example
267
+ * const info = await db.records.exists('rec_abc123');
268
+ * if (info?.exists) console.log('found at', info.updatedAt);
269
+ */
270
+ async exists(recordId, opts) {
271
+ const res = await this.http.head(this.path, { recordId }, opts);
272
+ if (res.status === 404) return null;
273
+ if (!res.ok) return null;
274
+ return {
275
+ exists: true,
276
+ id: res.headers.get("X-Record-Id") ?? recordId,
277
+ createdAt: res.headers.get("X-Created-At") ?? "",
278
+ updatedAt: res.headers.get("X-Updated-At") ?? "",
279
+ sizeBytes: res.headers.get("X-Size-Bytes") ?? ""
280
+ };
281
+ }
282
+ // ── Batch — update ─────────────────────────────────────────────────────────
283
+ /**
284
+ * Update up to 500 records in a single request.
285
+ *
286
+ * @example
287
+ * await db.records.batchUpdate({
288
+ * updates: [
289
+ * { recordId: 'rec_1', values: { status: 'archived' } },
290
+ * { recordId: 'rec_2', values: { status: 'archived' } },
291
+ * ],
292
+ * });
293
+ */
294
+ async batchUpdate(payload, opts) {
295
+ return this.http.post(
296
+ `${this.path}/batch/update`,
297
+ payload,
298
+ opts
299
+ );
300
+ }
301
+ // ── Batch — delete ─────────────────────────────────────────────────────────
302
+ /**
303
+ * Delete up to 500 records in a single request.
304
+ *
305
+ * @example
306
+ * await db.records.batchDelete({ recordIds: ['rec_1', 'rec_2', 'rec_3'] });
307
+ */
308
+ async batchDelete(payload, opts) {
309
+ return this.http.post(
310
+ `${this.path}/batch/delete`,
311
+ payload,
312
+ opts
313
+ );
314
+ }
315
+ // ── Batch — insert ─────────────────────────────────────────────────────────
316
+ /**
317
+ * Insert up to 500 records in a single request.
318
+ * Returns HTTP 207 (multi-status) — check `meta.failed` for partial failures.
319
+ *
320
+ * @example
321
+ * const result = await db.records.batchInsert({
322
+ * records: [{ name: 'Alice' }, { name: 'Bob' }],
323
+ * queryableFields: ['name'],
324
+ * });
325
+ */
326
+ async batchInsert(payload, opts) {
327
+ return this.http.post(
328
+ `${this.path}/batch/insert`,
329
+ payload,
330
+ opts
331
+ );
332
+ }
333
+ // ── Helpers ────────────────────────────────────────────────────────────────
334
+ /**
335
+ * Fetch ALL records matching a query, automatically following cursors.
336
+ * Use with care on large collections — prefer `query()` with manual pagination.
337
+ *
338
+ * @example
339
+ * const allRecords = await db.records.queryAll({
340
+ * filters: [{ field: 'type', op: '==', value: 'invoice' }],
341
+ * });
342
+ */
343
+ async queryAll(options) {
344
+ const all = [];
345
+ let cursor = null;
346
+ do {
347
+ const result = await this.query(cursor ? { ...options, cursor } : { ...options });
348
+ all.push(...result.data);
349
+ cursor = result.meta.nextCursor ?? null;
350
+ } while (cursor);
351
+ return all;
352
+ }
353
+ };
354
+
355
+ // src/auth/client.ts
356
+ var AuthClient = class {
357
+ constructor(http) {
358
+ this.http = http;
359
+ }
360
+ get path() {
361
+ return `/auth/${this.http.bucketKey}`;
362
+ }
363
+ // ── Sign-up ────────────────────────────────────────────────────────────────
364
+ /**
365
+ * Create a new user account. Returns the user and a session immediately.
366
+ *
367
+ * @example
368
+ * const { data, session } = await db.auth.signUp({
369
+ * email: 'alice@example.com',
370
+ * password: 'Str0ngP@ss!',
371
+ * fullName: 'Alice Smith',
372
+ * });
373
+ * // Store session.sessionId and session.refreshToken in your app
374
+ */
375
+ async signUp(payload, opts) {
376
+ return this.http.post(`${this.path}/signup`, payload, opts);
377
+ }
378
+ // ── Sign-in ────────────────────────────────────────────────────────────────
379
+ /**
380
+ * Authenticate with email + password. Returns user data and a new session.
381
+ *
382
+ * @example
383
+ * const { data, session } = await db.auth.signIn({
384
+ * email: 'alice@example.com',
385
+ * password: 'Str0ngP@ss!',
386
+ * });
387
+ */
388
+ async signIn(payload, opts) {
389
+ return this.http.post(`${this.path}/signin`, payload, opts);
390
+ }
391
+ // ── Sign-out ───────────────────────────────────────────────────────────────
392
+ /**
393
+ * Revoke a session (or all sessions for a user).
394
+ *
395
+ * @example
396
+ * // Single device
397
+ * await db.auth.signOut({ sessionId: 'sess_...' });
398
+ *
399
+ * // All devices
400
+ * await db.auth.signOut({ allDevices: true, userId: 'user_...' });
401
+ */
402
+ async signOut(payload, opts) {
403
+ return this.http.post(`${this.path}/signout`, payload, opts);
404
+ }
405
+ // ── Session: validate ──────────────────────────────────────────────────────
406
+ /**
407
+ * Validate a session token — use this on every protected request in your backend.
408
+ *
409
+ * @example
410
+ * try {
411
+ * const { data } = await db.auth.validateSession(sessionId);
412
+ * // data is the authenticated user
413
+ * } catch (err) {
414
+ * // Session expired or invalid
415
+ * }
416
+ */
417
+ async validateSession(sessionId, opts) {
418
+ return this.http.post(`${this.path}/session/validate`, { sessionId }, opts);
419
+ }
420
+ // ── Session: refresh ───────────────────────────────────────────────────────
421
+ /**
422
+ * Exchange a refresh token for a new session (rotation).
423
+ * The old session is revoked.
424
+ *
425
+ * @example
426
+ * const { session } = await db.auth.refreshSession(refreshToken);
427
+ */
428
+ async refreshSession(refreshToken, opts) {
429
+ return this.http.post(`${this.path}/session/refresh`, { refreshToken }, opts);
430
+ }
431
+ // ── User: get ──────────────────────────────────────────────────────────────
432
+ /**
433
+ * Fetch a user by their ID.
434
+ *
435
+ * @example
436
+ * const { data } = await db.auth.getUser('user_abc123');
437
+ */
438
+ async getUser(userId, opts) {
439
+ return this.http.get(`${this.path}/user`, { userId }, opts);
440
+ }
441
+ // ── User: list ─────────────────────────────────────────────────────────────
442
+ /**
443
+ * List users with cursor-based pagination.
444
+ *
445
+ * @example
446
+ * const { data, meta } = await db.auth.listUsers({ limit: 50 });
447
+ */
448
+ async listUsers(options) {
449
+ const params = {
450
+ limit: options?.limit
451
+ };
452
+ if (options?.cursor) params["cursor"] = options.cursor;
453
+ return this.http.get(`${this.path}/users`, params, options);
454
+ }
455
+ // ── User: update ───────────────────────────────────────────────────────────
456
+ /**
457
+ * Update user profile fields.
458
+ *
459
+ * @example
460
+ * await db.auth.updateUser({ userId: 'user_abc', updates: { fullName: 'Bob Smith' } });
461
+ */
462
+ async updateUser(payload, opts) {
463
+ return this.http.patch(`${this.path}/user`, payload, opts);
464
+ }
465
+ // ── User: delete ───────────────────────────────────────────────────────────
466
+ /**
467
+ * Soft-delete a user. All their sessions are revoked automatically.
468
+ *
469
+ * @example
470
+ * await db.auth.deleteUser('user_abc123');
471
+ */
472
+ async deleteUser(userId, opts) {
473
+ return this.http.delete(`${this.path}/user`, { userId }, opts);
474
+ }
475
+ // ── Password: change ───────────────────────────────────────────────────────
476
+ /**
477
+ * Change password for a signed-in user (requires old password).
478
+ * All sessions are revoked after success.
479
+ *
480
+ * @example
481
+ * await db.auth.changePassword({
482
+ * userId: 'user_abc',
483
+ * oldPassword: 'Old@Pass1',
484
+ * newPassword: 'New@Pass2',
485
+ * });
486
+ */
487
+ async changePassword(payload, opts) {
488
+ return this.http.post(`${this.path}/password/change`, payload, opts);
489
+ }
490
+ // ── Password: reset request ────────────────────────────────────────────────
491
+ /**
492
+ * Request a password reset email.
493
+ * Always returns success to prevent email enumeration.
494
+ *
495
+ * @example
496
+ * await db.auth.requestPasswordReset({ email: 'alice@example.com' });
497
+ */
498
+ async requestPasswordReset(payload, opts) {
499
+ return this.http.post(`${this.path}/password/reset/request`, payload, opts);
500
+ }
501
+ // ── Password: reset confirm ────────────────────────────────────────────────
502
+ /**
503
+ * Confirm a password reset using the token from the reset email.
504
+ *
505
+ * @example
506
+ * await db.auth.confirmPasswordReset({ resetToken: 'tok_...', newPassword: 'New@Pass2' });
507
+ */
508
+ async confirmPasswordReset(payload, opts) {
509
+ return this.http.post(`${this.path}/password/reset/confirm`, payload, opts);
510
+ }
511
+ // ── Email: verify request ──────────────────────────────────────────────────
512
+ /**
513
+ * Send a verification email to the user.
514
+ *
515
+ * @example
516
+ * await db.auth.requestEmailVerification({ userId: 'user_abc' });
517
+ */
518
+ async requestEmailVerification(payload, opts) {
519
+ return this.http.post(`${this.path}/email/verify/request`, payload, opts);
520
+ }
521
+ // ── Email: verify confirm ──────────────────────────────────────────────────
522
+ /**
523
+ * Confirm email address using the token from the verification email.
524
+ *
525
+ * @example
526
+ * await db.auth.confirmEmailVerification({ verifyToken: 'tok_...' });
527
+ */
528
+ async confirmEmailVerification(payload, opts) {
529
+ return this.http.post(`${this.path}/email/verify/confirm`, payload, opts);
530
+ }
531
+ // ── Account: lock ──────────────────────────────────────────────────────────
532
+ /**
533
+ * Lock a user account for a given duration (default: 15 min).
534
+ *
535
+ * @example
536
+ * await db.auth.lockAccount({ userId: 'user_abc', duration: 30 * 60 * 1000 });
537
+ */
538
+ async lockAccount(payload, opts) {
539
+ return this.http.post(`${this.path}/account/lock`, payload, opts);
540
+ }
541
+ // ── Account: unlock ────────────────────────────────────────────────────────
542
+ /**
543
+ * Unlock a previously locked user account.
544
+ *
545
+ * @example
546
+ * await db.auth.unlockAccount('user_abc');
547
+ */
548
+ async unlockAccount(userId, opts) {
549
+ return this.http.post(`${this.path}/account/unlock`, { userId }, opts);
550
+ }
551
+ // ── Helpers ────────────────────────────────────────────────────────────────
552
+ /**
553
+ * Fetch all users, automatically following cursors.
554
+ *
555
+ * @example
556
+ * const users = await db.auth.listAllUsers();
557
+ */
558
+ async listAllUsers(opts) {
559
+ const all = [];
560
+ let cursor = null;
561
+ do {
562
+ const result = await this.listUsers(cursor ? { cursor, ...opts } : { ...opts });
563
+ all.push(...result.data);
564
+ cursor = result.meta.nextCursor ?? null;
565
+ } while (cursor);
566
+ return all;
567
+ }
568
+ };
569
+
570
+ // src/analytics/client.ts
571
+ var AnalyticsClient = class {
572
+ constructor(http) {
573
+ this.http = http;
574
+ }
575
+ get path() {
576
+ return `/api/analytics/${this.http.bucketKey}/${this.http.bucketKey}`;
577
+ }
578
+ // ── Raw query ─────────────────────────────────────────────────────────────
579
+ /**
580
+ * Run any analytics query with full control over the payload.
581
+ * Prefer the typed convenience methods below for everyday use.
582
+ */
583
+ async query(payload, opts) {
584
+ return this.http.post(this.path, payload, opts);
585
+ }
586
+ // ── count ──────────────────────────────────────────────────────────────────
587
+ /**
588
+ * Total record count, optionally scoped to a date range.
589
+ * Server `queryType`: **"count"**
590
+ *
591
+ * @example
592
+ * const { data } = await db.analytics.count();
593
+ * console.log(data.count); // 1234
594
+ *
595
+ * // With date range
596
+ * const { data } = await db.analytics.count({
597
+ * dateRange: { startDate: '2025-01-01', endDate: '2025-12-31' }
598
+ * });
599
+ */
600
+ async count(options) {
601
+ return this.query({ queryType: "count", dateRange: options?.dateRange }, options);
602
+ }
603
+ // ── distribution ───────────────────────────────────────────────────────────
604
+ /**
605
+ * Value distribution (histogram) for a field.
606
+ * Server `queryType`: **"distribution"**
607
+ *
608
+ * @example
609
+ * const { data } = await db.analytics.distribution('status');
610
+ * // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
611
+ */
612
+ async distribution(field, options) {
613
+ return this.query({
614
+ queryType: "distribution",
615
+ field,
616
+ limit: options?.limit,
617
+ order: options?.order,
618
+ dateRange: options?.dateRange
619
+ }, options);
620
+ }
621
+ // ── sum ────────────────────────────────────────────────────────────────────
622
+ /**
623
+ * Sum a numeric field, with optional group-by.
624
+ * Server `queryType`: **"sum"**
625
+ *
626
+ * @example
627
+ * const { data } = await db.analytics.sum('revenue', { groupBy: 'region' });
628
+ */
629
+ async sum(field, options) {
630
+ return this.query({
631
+ queryType: "sum",
632
+ field,
633
+ groupBy: options?.groupBy,
634
+ limit: options?.limit,
635
+ dateRange: options?.dateRange
636
+ }, options);
637
+ }
638
+ // ── timeSeries ─────────────────────────────────────────────────────────────
639
+ /**
640
+ * Record count grouped over time.
641
+ * Server `queryType`: **"timeSeries"**
642
+ *
643
+ * @example
644
+ * const { data } = await db.analytics.timeSeries({ granularity: 'day' });
645
+ * // [{ date: '2025-01-01', count: 42 }, ...]
646
+ */
647
+ async timeSeries(options) {
648
+ return this.query({
649
+ queryType: "timeSeries",
650
+ granularity: options?.granularity,
651
+ dateRange: options?.dateRange
652
+ }, options);
653
+ }
654
+ // ── fieldTimeSeries ────────────────────────────────────────────────────────
655
+ /**
656
+ * Aggregate a numeric field over time.
657
+ * Server `queryType`: **"fieldTimeSeries"**
658
+ *
659
+ * @example
660
+ * const { data } = await db.analytics.fieldTimeSeries('revenue', {
661
+ * granularity: 'month',
662
+ * aggregation: 'sum',
663
+ * });
664
+ */
665
+ async fieldTimeSeries(field, options) {
666
+ return this.query({
667
+ queryType: "fieldTimeSeries",
668
+ field,
669
+ aggregation: options?.aggregation,
670
+ granularity: options?.granularity,
671
+ dateRange: options?.dateRange
672
+ }, options);
673
+ }
674
+ // ── topN ───────────────────────────────────────────────────────────────────
675
+ /**
676
+ * Top N most frequent values for a field.
677
+ * Server `queryType`: **"topN"**
678
+ *
679
+ * @example
680
+ * const { data } = await db.analytics.topN('country', 5);
681
+ * // [{ label: 'US', value: 'US', count: 500 }, ...]
682
+ */
683
+ async topN(field, n = 10, options) {
684
+ return this.query({
685
+ queryType: "topN",
686
+ field,
687
+ n,
688
+ labelField: options?.labelField,
689
+ order: options?.order,
690
+ dateRange: options?.dateRange
691
+ }, options);
692
+ }
693
+ // ── stats ──────────────────────────────────────────────────────────────────
694
+ /**
695
+ * Statistical summary for a numeric field: min, max, avg, stddev, p50, p90, p99.
696
+ * Server `queryType`: **"stats"**
697
+ *
698
+ * @example
699
+ * const { data } = await db.analytics.stats('score');
700
+ * console.log(data.avg, data.p99);
701
+ */
702
+ async stats(field, options) {
703
+ return this.query({ queryType: "stats", field, dateRange: options?.dateRange }, options);
704
+ }
705
+ // ── records ────────────────────────────────────────────────────────────────
706
+ /**
707
+ * Filtered, paginated raw records with optional field projection.
708
+ * Supports filter ops: == != > < >= <= CONTAINS
709
+ * Server `queryType`: **"records"**
710
+ *
711
+ * @example
712
+ * const { data } = await db.analytics.records({
713
+ * filters: [{ field: 'role', op: '==', value: 'admin' }],
714
+ * selectFields: ['email', 'createdAt'],
715
+ * limit: 25,
716
+ * });
717
+ * console.log(data.data, data.hasMore);
718
+ */
719
+ async records(options) {
720
+ return this.query({
721
+ queryType: "records",
722
+ filters: options?.filters,
723
+ selectFields: options?.selectFields,
724
+ limit: options?.limit,
725
+ offset: options?.offset,
726
+ orderBy: options?.orderBy,
727
+ order: options?.order,
728
+ dateRange: options?.dateRange
729
+ }, options);
730
+ }
731
+ // ── multiMetric ────────────────────────────────────────────────────────────
732
+ /**
733
+ * Multiple aggregations in a single BigQuery call — ideal for dashboard stat cards.
734
+ * Server `queryType`: **"multiMetric"**
735
+ *
736
+ * @example
737
+ * const { data } = await db.analytics.multiMetric([
738
+ * { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
739
+ * { name: 'avgScore', field: 'score', aggregation: 'avg' },
740
+ * { name: 'userCount', field: 'userId', aggregation: 'count' },
741
+ * ]);
742
+ * console.log(data.totalRevenue, data.avgScore, data.userCount);
743
+ */
744
+ async multiMetric(metrics, options) {
745
+ return this.query({
746
+ queryType: "multiMetric",
747
+ metrics,
748
+ dateRange: options?.dateRange
749
+ }, options);
750
+ }
751
+ // ── storageStats ───────────────────────────────────────────────────────────
752
+ /**
753
+ * Storage statistics for the bucket — total records, bytes, avg/min/max size.
754
+ * Server `queryType`: **"storageStats"**
755
+ *
756
+ * @example
757
+ * const { data } = await db.analytics.storageStats();
758
+ * console.log(data.totalRecords, data.totalBytes);
759
+ */
760
+ async storageStats(options) {
761
+ return this.query({ queryType: "storageStats", dateRange: options?.dateRange }, options);
762
+ }
763
+ // ── crossBucket ────────────────────────────────────────────────────────────
764
+ /**
765
+ * Compare a metric across multiple buckets in one query.
766
+ * Server `queryType`: **"crossBucket"**
767
+ *
768
+ * @example
769
+ * const { data } = await db.analytics.crossBucket({
770
+ * bucketKeys: ['sales', 'refunds', 'trials'],
771
+ * field: 'amount',
772
+ * aggregation: 'sum',
773
+ * });
774
+ */
775
+ async crossBucket(options) {
776
+ return this.query({
777
+ queryType: "crossBucket",
778
+ bucketKeys: options.bucketKeys,
779
+ field: options.field,
780
+ aggregation: options.aggregation,
781
+ dateRange: options.dateRange
782
+ }, options);
783
+ }
784
+ };
785
+
786
+ // src/client.ts
787
+ var HydrousClient = class {
788
+ constructor(config) {
789
+ if (!config.apiKey) throw new Error("[hydrousdb] apiKey is required");
790
+ if (!config.bucketKey) throw new Error("[hydrousdb] bucketKey is required");
791
+ this.http = new HttpClient(config);
792
+ this.records = new RecordsClient(this.http);
793
+ this.auth = new AuthClient(this.http);
794
+ this.analytics = new AnalyticsClient(this.http);
795
+ }
796
+ };
797
+ function createClient(config) {
798
+ return new HydrousClient(config);
799
+ }
800
+
801
+ exports.AnalyticsClient = AnalyticsClient;
802
+ exports.AuthClient = AuthClient;
803
+ exports.HydrousClient = HydrousClient;
804
+ exports.HydrousError = HydrousError;
805
+ exports.HydrousNetworkError = HydrousNetworkError;
806
+ exports.HydrousTimeoutError = HydrousTimeoutError;
807
+ exports.RecordsClient = RecordsClient;
808
+ exports.createClient = createClient;
809
+ //# sourceMappingURL=index.js.map
810
+ //# sourceMappingURL=index.js.map