hydrousdb 3.0.0 → 3.0.2

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 CHANGED
@@ -47,28 +47,36 @@ var NetworkError = class extends HydrousError {
47
47
  constructor(message, cause) {
48
48
  super(message, "NETWORK_ERROR");
49
49
  this.name = "NetworkError";
50
- this.cause = cause;
50
+ if (cause) this.cause = cause;
51
51
  }
52
52
  };
53
53
 
54
54
  // src/utils/http.ts
55
55
  var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
56
56
  var HttpClient = class {
57
- constructor(baseUrl, securityKey) {
57
+ constructor(baseUrl) {
58
58
  this.baseUrl = baseUrl.replace(/\/$/, "");
59
- this.securityKey = securityKey;
60
59
  }
61
- async request(path, opts = {}) {
60
+ async request(path, apiKeyOrOpts, opts = {}) {
61
+ let apiKey;
62
+ let resolvedOpts;
63
+ if (typeof apiKeyOrOpts === "string") {
64
+ apiKey = apiKeyOrOpts;
65
+ resolvedOpts = opts;
66
+ } else {
67
+ apiKey = void 0;
68
+ resolvedOpts = apiKeyOrOpts ?? opts;
69
+ }
62
70
  const {
63
71
  method = "GET",
64
72
  body,
65
73
  headers = {},
66
74
  rawBody,
67
75
  contentType = "application/json"
68
- } = opts;
76
+ } = resolvedOpts;
69
77
  const url = `${this.baseUrl}${path}`;
70
78
  const reqHeaders = {
71
- "X-Api-Key": this.securityKey,
79
+ ...apiKey ? { "X-Api-Key": apiKey } : {},
72
80
  ...headers
73
81
  };
74
82
  let reqBody = null;
@@ -126,25 +134,26 @@ var HttpClient = class {
126
134
  }
127
135
  return responseData;
128
136
  }
129
- get(path, headers) {
130
- return this.request(path, { method: "GET", headers });
137
+ get(path, apiKey, headers) {
138
+ return this.request(path, apiKey, { method: "GET", headers });
131
139
  }
132
- post(path, body, headers) {
133
- return this.request(path, { method: "POST", body, headers });
140
+ post(path, apiKey, body, headers) {
141
+ return this.request(path, apiKey, { method: "POST", body, headers });
134
142
  }
135
- put(path, body, headers) {
136
- return this.request(path, { method: "PUT", body, headers });
143
+ put(path, apiKey, body, headers) {
144
+ return this.request(path, apiKey, { method: "PUT", body, headers });
137
145
  }
138
- patch(path, body, headers) {
139
- return this.request(path, { method: "PATCH", body, headers });
146
+ patch(path, apiKey, body, headers) {
147
+ return this.request(path, apiKey, { method: "PATCH", body, headers });
140
148
  }
141
- delete(path, body, headers) {
142
- return this.request(path, { method: "DELETE", body, headers });
149
+ delete(path, apiKey, body, headers) {
150
+ return this.request(path, apiKey, { method: "DELETE", body, headers });
143
151
  }
144
152
  async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
145
- if (typeof XMLHttpRequest !== "undefined" && onProgress) {
153
+ const XHR = typeof globalThis["XMLHttpRequest"] !== "undefined" ? globalThis["XMLHttpRequest"] : void 0;
154
+ if (XHR && onProgress) {
146
155
  return new Promise((resolve, reject) => {
147
- const xhr = new XMLHttpRequest();
156
+ const xhr = new XHR();
148
157
  xhr.upload.onprogress = (e) => {
149
158
  if (e.lengthComputable) {
150
159
  onProgress(Math.round(e.loaded / e.total * 100));
@@ -178,280 +187,97 @@ var HttpClient = class {
178
187
 
179
188
  // src/auth/client.ts
180
189
  var AuthClient = class {
181
- constructor(http, bucketKey) {
190
+ constructor(http, authKey, bucketKey) {
182
191
  this.http = http;
183
- this.bucketKey = bucketKey;
192
+ this.authKey = authKey;
184
193
  this.basePath = `/auth/${bucketKey}`;
185
194
  }
195
+ post(path, body) {
196
+ return this.http.post(path, this.authKey, body);
197
+ }
198
+ get(path) {
199
+ return this.http.get(path, this.authKey);
200
+ }
201
+ patch(path, body) {
202
+ return this.http.patch(path, this.authKey, body);
203
+ }
204
+ delete(path, body) {
205
+ return this.http.delete(path, this.authKey, body);
206
+ }
186
207
  // ─── Registration & Login ─────────────────────────────────────────────────
187
- /**
188
- * Register a new user in this bucket.
189
- *
190
- * @example
191
- * ```ts
192
- * const { user, session } = await auth.signup({
193
- * email: 'alice@example.com',
194
- * password: 'hunter2',
195
- * fullName: 'Alice Wonderland',
196
- * // Any extra fields are stored on the user record:
197
- * plan: 'pro',
198
- * });
199
- * ```
200
- */
201
208
  async signup(options) {
202
- const result = await this.http.post(`${this.basePath}/signup`, options);
209
+ const result = await this.post(`${this.basePath}/signup`, options);
203
210
  return { user: result.user, session: result.session };
204
211
  }
205
- /**
206
- * Authenticate an existing user and create a session.
207
- * Sessions are valid for 24 hours; use `refreshSession` to extend.
208
- *
209
- * @example
210
- * ```ts
211
- * const { user, session } = await auth.login({
212
- * email: 'alice@example.com',
213
- * password: 'hunter2',
214
- * });
215
- * // Store session.sessionId safely — you'll need it for other calls.
216
- * ```
217
- */
218
212
  async login(options) {
219
- const result = await this.http.post(`${this.basePath}/login`, options);
213
+ const result = await this.post(`${this.basePath}/login`, options);
220
214
  return { user: result.user, session: result.session };
221
215
  }
222
- /**
223
- * Invalidate a session (sign out).
224
- *
225
- * @example
226
- * ```ts
227
- * await auth.logout({ sessionId: session.sessionId });
228
- * ```
229
- */
230
216
  async logout({ sessionId }) {
231
- await this.http.post(`${this.basePath}/logout`, { sessionId });
217
+ await this.post(`${this.basePath}/logout`, { sessionId });
232
218
  }
233
- /**
234
- * Extend a session using its refresh token.
235
- * Returns a new session object.
236
- *
237
- * @example
238
- * ```ts
239
- * const newSession = await auth.refreshSession({ refreshToken: session.refreshToken });
240
- * ```
241
- */
242
219
  async refreshSession({ refreshToken }) {
243
- const result = await this.http.post(
244
- `${this.basePath}/session/refresh`,
245
- { refreshToken }
246
- );
220
+ const result = await this.post(`${this.basePath}/session/refresh`, { refreshToken });
247
221
  return result.session;
248
222
  }
249
223
  // ─── User Profile ─────────────────────────────────────────────────────────
250
- /**
251
- * Fetch a user by their ID.
252
- *
253
- * @example
254
- * ```ts
255
- * const user = await auth.getUser({ userId: 'usr_abc123' });
256
- * ```
257
- */
258
224
  async getUser({ userId }) {
259
- const result = await this.http.get(`${this.basePath}/user/${userId}`);
225
+ const result = await this.get(`${this.basePath}/user/${userId}`);
260
226
  return result.user;
261
227
  }
262
- /**
263
- * Update fields on a user record. Requires a valid session.
264
- *
265
- * @example
266
- * ```ts
267
- * const updated = await auth.updateUser({
268
- * sessionId: session.sessionId,
269
- * userId: user.id,
270
- * data: { fullName: 'Alice Smith', plan: 'enterprise' },
271
- * });
272
- * ```
273
- */
274
228
  async updateUser(options) {
275
229
  const { sessionId, userId, data } = options;
276
- const result = await this.http.patch(
277
- `${this.basePath}/user`,
278
- { sessionId, userId, ...data }
279
- );
230
+ const result = await this.patch(`${this.basePath}/user`, { sessionId, userId, ...data });
280
231
  return result.user;
281
232
  }
282
- /**
283
- * Soft-delete a user. The record is marked deleted but not removed from storage.
284
- * Requires a valid session (the user can delete themselves, or an admin can delete any user).
285
- *
286
- * @example
287
- * ```ts
288
- * await auth.deleteUser({ sessionId: session.sessionId, userId: user.id });
289
- * ```
290
- */
291
233
  async deleteUser({ sessionId, userId }) {
292
- await this.http.delete(`${this.basePath}/user`, { sessionId, userId });
234
+ await this.delete(`${this.basePath}/user`, { sessionId, userId });
293
235
  }
294
236
  // ─── Admin Operations ─────────────────────────────────────────────────────
295
- /**
296
- * List all users in the bucket. **Admin session required.**
297
- *
298
- * @example
299
- * ```ts
300
- * const { users, total } = await auth.listUsers({
301
- * sessionId: adminSession.sessionId,
302
- * limit: 50,
303
- * offset: 0,
304
- * });
305
- * ```
306
- */
307
237
  async listUsers(options) {
308
238
  const { sessionId, limit = 50, offset = 0 } = options;
309
- const result = await this.http.post(
239
+ const result = await this.post(
310
240
  `${this.basePath}/users/list`,
311
241
  { sessionId, limit, offset }
312
242
  );
313
243
  return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
314
244
  }
315
- /**
316
- * Permanently hard-delete a user and all their data. **Admin session required.**
317
- * This action is irreversible.
318
- *
319
- * @example
320
- * ```ts
321
- * await auth.hardDeleteUser({ sessionId: adminSession.sessionId, userId: user.id });
322
- * ```
323
- */
324
245
  async hardDeleteUser({ sessionId, userId }) {
325
- await this.http.delete(`${this.basePath}/user/hard`, { sessionId, userId });
246
+ await this.delete(`${this.basePath}/user/hard`, { sessionId, userId });
326
247
  }
327
- /**
328
- * Bulk delete multiple users. **Admin session required.**
329
- *
330
- * @example
331
- * ```ts
332
- * const result = await auth.bulkDeleteUsers({
333
- * sessionId: adminSession.sessionId,
334
- * userIds: ['usr_a', 'usr_b'],
335
- * });
336
- * ```
337
- */
338
- async bulkDeleteUsers({
339
- sessionId,
340
- userIds
341
- }) {
342
- const result = await this.http.post(
248
+ async bulkDeleteUsers({ sessionId, userIds }) {
249
+ const result = await this.post(
343
250
  `${this.basePath}/users/bulk-delete`,
344
251
  { sessionId, userIds }
345
252
  );
346
253
  return { deleted: result.deleted, failed: result.failed };
347
254
  }
348
- /**
349
- * Lock a user account, preventing login. **Admin session required.**
350
- *
351
- * @param options.duration Lock duration in milliseconds. Defaults to 15 minutes.
352
- *
353
- * @example
354
- * ```ts
355
- * await auth.lockAccount({
356
- * sessionId: adminSession.sessionId,
357
- * userId: user.id,
358
- * duration: 60 * 60 * 1000, // 1 hour
359
- * });
360
- * ```
361
- */
362
- async lockAccount({
363
- sessionId,
364
- userId,
365
- duration
366
- }) {
367
- const result = await this.http.post(`${this.basePath}/account/lock`, { sessionId, userId, duration });
255
+ async lockAccount({ sessionId, userId, duration }) {
256
+ const result = await this.post(
257
+ `${this.basePath}/account/lock`,
258
+ { sessionId, userId, duration }
259
+ );
368
260
  return result.data;
369
261
  }
370
- /**
371
- * Unlock a previously locked user account. **Admin session required.**
372
- *
373
- * @example
374
- * ```ts
375
- * await auth.unlockAccount({ sessionId: adminSession.sessionId, userId: user.id });
376
- * ```
377
- */
378
- async unlockAccount({
379
- sessionId,
380
- userId
381
- }) {
382
- await this.http.post(`${this.basePath}/account/unlock`, { sessionId, userId });
262
+ async unlockAccount({ sessionId, userId }) {
263
+ await this.post(`${this.basePath}/account/unlock`, { sessionId, userId });
383
264
  }
384
265
  // ─── Password Management ──────────────────────────────────────────────────
385
- /**
386
- * Change a user's password. The user must supply their current password.
387
- *
388
- * @example
389
- * ```ts
390
- * await auth.changePassword({
391
- * sessionId: session.sessionId,
392
- * userId: user.id,
393
- * currentPassword: 'hunter2',
394
- * newPassword: 'correcthorsebatterystaple',
395
- * });
396
- * ```
397
- */
398
266
  async changePassword(options) {
399
- await this.http.post(`${this.basePath}/password/change`, options);
267
+ await this.post(`${this.basePath}/password/change`, options);
400
268
  }
401
- /**
402
- * Request a password reset email for a user.
403
- * Always returns success to prevent user enumeration.
404
- *
405
- * @example
406
- * ```ts
407
- * await auth.requestPasswordReset({ email: 'alice@example.com' });
408
- * ```
409
- */
410
269
  async requestPasswordReset({ email }) {
411
- await this.http.post(`${this.basePath}/password/reset/request`, { email });
270
+ await this.post(`${this.basePath}/password/reset/request`, { email });
412
271
  }
413
- /**
414
- * Complete a password reset using the token from the reset email.
415
- *
416
- * @example
417
- * ```ts
418
- * await auth.confirmPasswordReset({
419
- * resetToken: 'tok_from_email',
420
- * newPassword: 'correcthorsebatterystaple',
421
- * });
422
- * ```
423
- */
424
- async confirmPasswordReset({
425
- resetToken,
426
- newPassword
427
- }) {
428
- await this.http.post(`${this.basePath}/password/reset/confirm`, {
429
- resetToken,
430
- newPassword
431
- });
272
+ async confirmPasswordReset({ resetToken, newPassword }) {
273
+ await this.post(`${this.basePath}/password/reset/confirm`, { resetToken, newPassword });
432
274
  }
433
275
  // ─── Email Verification ───────────────────────────────────────────────────
434
- /**
435
- * Send (or resend) an email verification message to a user.
436
- *
437
- * @example
438
- * ```ts
439
- * await auth.requestEmailVerification({ userId: user.id });
440
- * ```
441
- */
442
276
  async requestEmailVerification({ userId }) {
443
- await this.http.post(`${this.basePath}/email/verify/request`, { userId });
277
+ await this.post(`${this.basePath}/email/verify/request`, { userId });
444
278
  }
445
- /**
446
- * Confirm an email address using the token from the verification email.
447
- *
448
- * @example
449
- * ```ts
450
- * await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
451
- * ```
452
- */
453
279
  async confirmEmailVerification({ verifyToken }) {
454
- await this.http.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
280
+ await this.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
455
281
  }
456
282
  };
457
283
 
@@ -511,149 +337,62 @@ function assertSafeName(name, label = "name") {
511
337
 
512
338
  // src/records/client.ts
513
339
  var RecordsClient = class {
514
- constructor(http, bucketKey) {
340
+ constructor(http, bucketSecurityKey, bucketKey) {
515
341
  assertSafeName(bucketKey, "bucketKey");
516
342
  this.http = http;
517
343
  this.bucketKey = bucketKey;
344
+ this.bucketKey_ = bucketSecurityKey;
518
345
  this.basePath = `/records/${bucketKey}`;
519
346
  }
347
+ get key() {
348
+ return this.bucketKey_;
349
+ }
520
350
  // ─── Single Record Operations ─────────────────────────────────────────────
521
- /**
522
- * Create a new record.
523
- *
524
- * @example
525
- * ```ts
526
- * const user = await records.create({
527
- * name: 'Alice',
528
- * email: 'alice@example.com',
529
- * score: 100,
530
- * });
531
- * console.log(user.id); // "rec_xxxxxxxx"
532
- * ```
533
- */
534
351
  async create(data) {
535
- const result = await this.http.post(this.basePath, data);
352
+ const result = await this.http.post(this.basePath, this.key, data);
536
353
  return result.record ?? result.data;
537
354
  }
538
- /**
539
- * Fetch a single record by ID.
540
- *
541
- * @example
542
- * ```ts
543
- * const post = await records.get('rec_abc123');
544
- * ```
545
- */
546
355
  async get(id) {
547
- const result = await this.http.get(`${this.basePath}/${id}`);
356
+ const result = await this.http.get(`${this.basePath}/${id}`, this.key);
548
357
  return result.record ?? result.data;
549
358
  }
550
- /**
551
- * Overwrite a record entirely (full replace).
552
- *
553
- * @example
554
- * ```ts
555
- * const updated = await records.set('rec_abc123', {
556
- * name: 'Alice Updated',
557
- * email: 'alice2@example.com',
558
- * });
559
- * ```
560
- */
561
359
  async set(id, data) {
562
- const result = await this.http.put(`${this.basePath}/${id}`, data);
360
+ const result = await this.http.put(`${this.basePath}/${id}`, this.key, data);
563
361
  return result.record ?? result.data;
564
362
  }
565
- /**
566
- * Partially update a record (merge by default).
567
- *
568
- * @example
569
- * ```ts
570
- * // Merge: only the provided fields are updated
571
- * const updated = await records.patch('rec_abc123', { score: 200 });
572
- *
573
- * // Replace: equivalent to set()
574
- * const replaced = await records.patch('rec_abc123', { score: 200 }, { merge: false });
575
- * ```
576
- */
577
363
  async patch(id, data, options = {}) {
578
364
  const { merge = true } = options;
579
365
  const result = await this.http.patch(
580
366
  `${this.basePath}/${id}`,
367
+ this.key,
581
368
  { ...data, _merge: merge }
582
369
  );
583
370
  return result.record ?? result.data;
584
371
  }
585
- /**
586
- * Delete a record permanently.
587
- *
588
- * @example
589
- * ```ts
590
- * await records.delete('rec_abc123');
591
- * ```
592
- */
593
372
  async delete(id) {
594
- await this.http.delete(`${this.basePath}/${id}`);
373
+ await this.http.delete(`${this.basePath}/${id}`, this.key);
595
374
  }
596
375
  // ─── Batch Operations ─────────────────────────────────────────────────────
597
- /**
598
- * Create multiple records in one request.
599
- *
600
- * @example
601
- * ```ts
602
- * const created = await records.batchCreate([
603
- * { name: 'Alice', score: 100 },
604
- * { name: 'Bob', score: 200 },
605
- * ]);
606
- * ```
607
- */
608
376
  async batchCreate(items) {
609
377
  const result = await this.http.post(
610
378
  `${this.basePath}/batch`,
379
+ this.key,
611
380
  { records: items }
612
381
  );
613
382
  return result.records;
614
383
  }
615
- /**
616
- * Delete multiple records by ID in one request.
617
- *
618
- * @example
619
- * ```ts
620
- * await records.batchDelete(['rec_a', 'rec_b', 'rec_c']);
621
- * ```
622
- */
623
384
  async batchDelete(ids) {
624
- const result = await this.http.post(`${this.basePath}/batch/delete`, { ids });
385
+ const result = await this.http.post(
386
+ `${this.basePath}/batch/delete`,
387
+ this.key,
388
+ { ids }
389
+ );
625
390
  return { deleted: result.deleted, failed: result.failed };
626
391
  }
627
392
  // ─── Querying ─────────────────────────────────────────────────────────────
628
- /**
629
- * Query records with optional filters, sorting, and pagination.
630
- *
631
- * @example
632
- * ```ts
633
- * // Simple query
634
- * const { records } = await posts.query({ limit: 10 });
635
- *
636
- * // Filtered query with cursor pagination
637
- * const page1 = await posts.query({
638
- * filters: [
639
- * { field: 'status', op: '==', value: 'published' },
640
- * { field: 'views', op: '>', value: 1000 },
641
- * ],
642
- * orderBy: 'createdAt',
643
- * order: 'desc',
644
- * limit: 20,
645
- * });
646
- *
647
- * const page2 = await posts.query({
648
- * filters: [{ field: 'status', op: '==', value: 'published' }],
649
- * limit: 20,
650
- * startAfter: page1.nextCursor,
651
- * });
652
- * ```
653
- */
654
393
  async query(options = {}) {
655
394
  const qs = buildQueryParams(options);
656
- const result = await this.http.get(`${this.basePath}${qs}`);
395
+ const result = await this.http.get(`${this.basePath}${qs}`, this.key);
657
396
  return {
658
397
  records: result.records,
659
398
  total: result.total,
@@ -661,60 +400,27 @@ var RecordsClient = class {
661
400
  nextCursor: result.nextCursor
662
401
  };
663
402
  }
664
- /**
665
- * Convenience alias: get all records up to `limit` (default 100).
666
- *
667
- * @example
668
- * ```ts
669
- * const allPosts = await posts.getAll({ limit: 500 });
670
- * ```
671
- */
672
403
  async getAll(options = {}) {
673
404
  const { records } = await this.query(options);
674
405
  return records;
675
406
  }
676
- /**
677
- * Count records matching optional filters.
678
- *
679
- * @example
680
- * ```ts
681
- * const total = await posts.count([{ field: 'status', op: '==', value: 'published' }]);
682
- * ```
683
- */
684
407
  async count(filters = []) {
685
408
  const result = await this.http.post(
686
409
  `${this.basePath}/count`,
410
+ this.key,
687
411
  { filters }
688
412
  );
689
413
  return result.count;
690
414
  }
691
415
  // ─── Version History ──────────────────────────────────────────────────────
692
- /**
693
- * Retrieve the full version history of a record.
694
- * Each write creates a new version stored in GCS.
695
- *
696
- * @example
697
- * ```ts
698
- * const history = await records.getHistory('rec_abc123');
699
- * console.log(history[0].data); // latest version
700
- * ```
701
- */
702
416
  async getHistory(id) {
703
- const result = await this.http.get(`${this.basePath}/${id}/history`);
417
+ const result = await this.http.get(`${this.basePath}/${id}/history`, this.key);
704
418
  return result.history;
705
419
  }
706
- /**
707
- * Restore a record to a specific historical version.
708
- *
709
- * @example
710
- * ```ts
711
- * const history = await records.getHistory('rec_abc123');
712
- * const restored = await records.restoreVersion('rec_abc123', history[2].version);
713
- * ```
714
- */
715
420
  async restoreVersion(id, version) {
716
421
  const result = await this.http.post(
717
422
  `${this.basePath}/${id}/restore`,
423
+ this.key,
718
424
  { version }
719
425
  );
720
426
  return result.record ?? result.data;
@@ -723,187 +429,53 @@ var RecordsClient = class {
723
429
 
724
430
  // src/analytics/client.ts
725
431
  var AnalyticsClient = class {
726
- constructor(http, bucketKey) {
432
+ constructor(http, bucketSecurityKey, bucketKey) {
727
433
  assertSafeName(bucketKey, "bucketKey");
728
434
  this.http = http;
729
- this.bucketKey = bucketKey;
435
+ this.bucketSecurityKey = bucketSecurityKey;
730
436
  this.basePath = `/analytics/${bucketKey}`;
731
437
  }
732
- /** Internal dispatcher — all queries POST to the same endpoint. */
733
438
  async run(query) {
734
- const result = await this.http.post(this.basePath, query);
439
+ const result = await this.http.post(
440
+ this.basePath,
441
+ this.bucketSecurityKey,
442
+ query
443
+ );
735
444
  return result.data;
736
445
  }
737
- // ─── Count ────────────────────────────────────────────────────────────────
738
- /**
739
- * Count the total number of records in the bucket, with optional date filter.
740
- *
741
- * @example
742
- * ```ts
743
- * const { count } = await analytics.count();
744
- * // → { count: 4821 }
745
- *
746
- * // Count only this month's records
747
- * const { count } = await analytics.count({
748
- * dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
749
- * });
750
- * ```
751
- */
752
446
  async count(opts = {}) {
753
447
  return this.run({ queryType: "count", ...opts });
754
448
  }
755
- // ─── Distribution ─────────────────────────────────────────────────────────
756
- /**
757
- * Count how many records have each unique value for a given field.
758
- * Great for pie charts and bar charts.
759
- *
760
- * @example
761
- * ```ts
762
- * const rows = await analytics.distribution({
763
- * field: 'status',
764
- * limit: 10,
765
- * order: 'desc',
766
- * });
767
- * // → [{ value: 'completed', count: 312 }, { value: 'pending', count: 88 }, ...]
768
- * ```
769
- */
770
449
  async distribution(opts) {
771
450
  assertSafeName(opts.field, "field");
772
451
  return this.run({ queryType: "distribution", ...opts });
773
452
  }
774
- // ─── Sum ──────────────────────────────────────────────────────────────────
775
- /**
776
- * Sum a numeric field, optionally grouped by another field.
777
- *
778
- * @example
779
- * ```ts
780
- * // Total revenue
781
- * const rows = await analytics.sum({ field: 'amount' });
782
- * // → [{ sum: 198432.50 }]
783
- *
784
- * // Revenue by country
785
- * const rows = await analytics.sum({ field: 'amount', groupBy: 'country', limit: 10 });
786
- * // → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
787
- * ```
788
- */
789
453
  async sum(opts) {
790
454
  assertSafeName(opts.field, "field");
791
455
  if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
792
456
  return this.run({ queryType: "sum", ...opts });
793
457
  }
794
- // ─── Time Series ──────────────────────────────────────────────────────────
795
- /**
796
- * Count records created over time, grouped by a time granularity.
797
- * Perfect for line charts and activity graphs.
798
- *
799
- * @example
800
- * ```ts
801
- * const rows = await analytics.timeSeries({
802
- * granularity: 'day',
803
- * dateRange: { start: Date.now() - 7 * 86400000, end: Date.now() },
804
- * });
805
- * // → [{ date: '2025-06-01', count: 42 }, { date: '2025-06-02', count: 67 }, ...]
806
- * ```
807
- */
808
458
  async timeSeries(opts = {}) {
809
459
  return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
810
460
  }
811
- // ─── Field Time Series ────────────────────────────────────────────────────
812
- /**
813
- * Aggregate a numeric field over time (e.g. daily revenue, hourly signups).
814
- *
815
- * @example
816
- * ```ts
817
- * const rows = await analytics.fieldTimeSeries({
818
- * field: 'amount',
819
- * aggregation: 'sum',
820
- * granularity: 'week',
821
- * });
822
- * // → [{ date: '2025-W22', value: 14230.50 }, ...]
823
- * ```
824
- */
825
461
  async fieldTimeSeries(opts) {
826
462
  assertSafeName(opts.field, "field");
827
- return this.run({
828
- queryType: "fieldTimeSeries",
829
- aggregation: "sum",
830
- granularity: "day",
831
- ...opts
832
- });
463
+ return this.run({ queryType: "fieldTimeSeries", aggregation: "sum", granularity: "day", ...opts });
833
464
  }
834
- // ─── Top N ────────────────────────────────────────────────────────────────
835
- /**
836
- * Get the top N values by frequency for a field.
837
- * Optionally pair with a `labelField` for human-readable labels.
838
- *
839
- * @example
840
- * ```ts
841
- * // Top 5 most purchased products
842
- * const rows = await analytics.topN({
843
- * field: 'productId',
844
- * labelField: 'productName',
845
- * n: 5,
846
- * });
847
- * // → [{ value: 'prod_123', label: 'Widget Pro', count: 892 }, ...]
848
- * ```
849
- */
850
465
  async topN(opts) {
851
466
  assertSafeName(opts.field, "field");
852
467
  if (opts.labelField) assertSafeName(opts.labelField, "labelField");
853
468
  return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
854
469
  }
855
- // ─── Field Stats ──────────────────────────────────────────────────────────
856
- /**
857
- * Get statistical summary (min, max, avg, sum, count, stddev) for a numeric field.
858
- *
859
- * @example
860
- * ```ts
861
- * const stats = await analytics.stats({ field: 'orderValue' });
862
- * // → { min: 4.99, max: 9999.99, avg: 87.23, sum: 420948.27, count: 4823, stddev: 143.2 }
863
- * ```
864
- */
865
470
  async stats(opts) {
866
471
  assertSafeName(opts.field, "field");
867
472
  return this.run({ queryType: "stats", ...opts });
868
473
  }
869
- // ─── Filtered Records ─────────────────────────────────────────────────────
870
- /**
871
- * Query raw records with filters, field selection, and pagination.
872
- * This is the analytics version of `records.query()` but powered by BigQuery.
873
- *
874
- * @example
875
- * ```ts
876
- * const { records } = await analytics.records({
877
- * filters: [{ field: 'status', op: '==', value: 'refunded' }],
878
- * selectFields: ['orderId', 'amount', 'createdAt'],
879
- * limit: 50,
880
- * orderBy: 'amount',
881
- * order: 'desc',
882
- * });
883
- * ```
884
- */
885
474
  async records(opts = {}) {
886
475
  if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
887
476
  if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
888
477
  return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
889
478
  }
890
- // ─── Multi Metric ─────────────────────────────────────────────────────────
891
- /**
892
- * Calculate multiple aggregations in a single query.
893
- * Ideal for dashboards that need several numbers at once.
894
- *
895
- * @example
896
- * ```ts
897
- * const result = await analytics.multiMetric({
898
- * metrics: [
899
- * { field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
900
- * { field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
901
- * { field: 'userId', name: 'uniqueCustomers', aggregation: 'count' },
902
- * ],
903
- * });
904
- * // → { totalRevenue: 198432.50, avgOrderValue: 87.23, uniqueCustomers: 2275 }
905
- * ```
906
- */
907
479
  async multiMetric(opts) {
908
480
  opts.metrics.forEach((m) => {
909
481
  assertSafeName(m.field, "metric.field");
@@ -911,58 +483,14 @@ var AnalyticsClient = class {
911
483
  });
912
484
  return this.run({ queryType: "multiMetric", ...opts });
913
485
  }
914
- // ─── Storage Stats ────────────────────────────────────────────────────────
915
- /**
916
- * Get storage statistics for this bucket: record counts, byte sizes.
917
- *
918
- * @example
919
- * ```ts
920
- * const stats = await analytics.storageStats();
921
- * // → { totalRecords: 4821, totalBytes: 48293820, avgBytes: 10015, ... }
922
- * ```
923
- */
924
486
  async storageStats(opts = {}) {
925
487
  return this.run({ queryType: "storageStats", ...opts });
926
488
  }
927
- // ─── Cross-Bucket Comparison ──────────────────────────────────────────────
928
- /**
929
- * Compare the same field aggregation across multiple buckets in one query.
930
- * Your security key must have read access to ALL listed buckets.
931
- *
932
- * @example
933
- * ```ts
934
- * const rows = await analytics.crossBucket({
935
- * bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
936
- * field: 'amount',
937
- * aggregation: 'sum',
938
- * });
939
- * // → [
940
- * // { bucket: 'orders-us', value: 120000 },
941
- * // { bucket: 'orders-eu', value: 45000 },
942
- * // { bucket: 'orders-apac', value: 33000 },
943
- * // ]
944
- * ```
945
- */
946
489
  async crossBucket(opts) {
947
490
  assertSafeName(opts.field, "field");
948
491
  opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
949
492
  return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
950
493
  }
951
- // ─── Raw Query ────────────────────────────────────────────────────────────
952
- /**
953
- * Send a raw analytics query object. Use this when you need full control
954
- * over the query shape or want to use a queryType not covered by the helpers.
955
- *
956
- * @example
957
- * ```ts
958
- * const result = await analytics.query({
959
- * queryType: 'topN',
960
- * field: 'category',
961
- * n: 3,
962
- * order: 'asc',
963
- * });
964
- * ```
965
- */
966
494
  async query(query) {
967
495
  const data = await this.run(query);
968
496
  return { queryType: query.queryType, data };
@@ -1457,10 +985,6 @@ var ScopedStorage = class _ScopedStorage {
1457
985
  getUploadUrl(opts) {
1458
986
  return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
1459
987
  }
1460
- /** Upload data directly to a signed GCS URL with optional progress tracking. */
1461
- uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1462
- return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
1463
- }
1464
988
  /** Confirm a direct upload within the scoped folder. */
1465
989
  confirmUpload(opts) {
1466
990
  return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
@@ -1469,9 +993,16 @@ var ScopedStorage = class _ScopedStorage {
1469
993
  download(path) {
1470
994
  return this.manager.download(this.scopedPath(path));
1471
995
  }
1472
- /** List files within the scoped folder. */
996
+ /**
997
+ * List files within the scoped folder.
998
+ * `prefix` in options is relative to the scope.
999
+ */
1473
1000
  list(opts = {}) {
1474
- return this.manager.list({ ...opts, prefix: this.scopedPath(opts.prefix ?? "") });
1001
+ const scopedOpts = {
1002
+ ...opts,
1003
+ prefix: this.scopedPath(opts.prefix ?? "")
1004
+ };
1005
+ return this.manager.list(scopedOpts);
1475
1006
  }
1476
1007
  /** Get metadata for a file within the scoped folder. */
1477
1008
  getMetadata(path) {
@@ -1511,7 +1042,7 @@ var ScopedStorage = class _ScopedStorage {
1511
1042
  * @example
1512
1043
  * ```ts
1513
1044
  * const uploads = db.storage.scope('user-uploads');
1514
- * const images = uploads.scope('images'); // → "user-uploads/images/"
1045
+ * const images = uploads.scope('images'); // → "user-uploads/images/"
1515
1046
  * ```
1516
1047
  */
1517
1048
  scope(subPrefix) {
@@ -1522,66 +1053,67 @@ var ScopedStorage = class _ScopedStorage {
1522
1053
  // src/client.ts
1523
1054
  var HydrousClient = class {
1524
1055
  constructor(config) {
1525
- this._storage = null;
1526
1056
  this._recordsCache = /* @__PURE__ */ new Map();
1527
1057
  this._authCache = /* @__PURE__ */ new Map();
1528
1058
  this._analyticsCache = /* @__PURE__ */ new Map();
1529
- if (!config.securityKey) {
1530
- throw new Error(
1531
- "[HydrousDB] securityKey is required. Get yours from https://hydrousdb.com/dashboard."
1532
- );
1059
+ this._storageCache = /* @__PURE__ */ new Map();
1060
+ if (!config.authKey) {
1061
+ throw new Error("[HydrousDB] authKey is required. Get yours from https://hydrousdb.com/dashboard.");
1062
+ }
1063
+ if (!config.bucketSecurityKey) {
1064
+ throw new Error("[HydrousDB] bucketSecurityKey is required. Get yours from https://hydrousdb.com/dashboard.");
1065
+ }
1066
+ if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
1067
+ throw new Error("[HydrousDB] storageKeys is required. Define at least one storage key from https://hydrousdb.com/dashboard.");
1533
1068
  }
1534
1069
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1535
- this.http = new HttpClient(baseUrl, config.securityKey);
1536
- this._storageKey = config.securityKey;
1070
+ this.http = new HttpClient(baseUrl);
1071
+ this.authKey_ = config.authKey;
1072
+ this.bucketSecurityKey_ = config.bucketSecurityKey;
1073
+ this.storageKeys_ = config.storageKeys;
1537
1074
  }
1538
1075
  // ─── Records ─────────────────────────────────────────────────────────────
1539
1076
  /**
1540
- * Get a typed records client for the given bucket.
1541
- *
1542
- * The generic type parameter `T` describes the shape of records in this
1543
- * bucket. Leave it unset for a generic `Record<string, unknown>` shape.
1544
- *
1545
- * @param bucketKey The name of your bucket (must match what you created in the dashboard).
1077
+ * Get a typed records client for the named bucket.
1078
+ * Uses your `bucketSecurityKey` automatically.
1546
1079
  *
1547
1080
  * @example
1548
1081
  * ```ts
1549
- * interface Post { title: string; body: string; published: boolean }
1082
+ * interface Post { title: string; published: boolean }
1550
1083
  * const posts = db.records<Post>('blog-posts');
1551
- *
1552
- * const post = await posts.create({ title: 'Hello', body: '...', published: false });
1553
- * // post.id, post.createdAt, post.updatedAt are added automatically
1084
+ * const post = await posts.create({ title: 'Hello', published: false });
1554
1085
  * ```
1555
1086
  */
1556
1087
  records(bucketKey) {
1557
1088
  if (!this._recordsCache.has(bucketKey)) {
1558
- this._recordsCache.set(bucketKey, new RecordsClient(this.http, bucketKey));
1089
+ this._recordsCache.set(
1090
+ bucketKey,
1091
+ new RecordsClient(this.http, this.bucketSecurityKey_, bucketKey)
1092
+ );
1559
1093
  }
1560
1094
  return this._recordsCache.get(bucketKey);
1561
1095
  }
1562
1096
  // ─── Auth ─────────────────────────────────────────────────────────────────
1563
1097
  /**
1564
- * Get an auth client for the given user bucket.
1565
- *
1566
- * @param bucketKey The name of your user bucket (e.g. `"app-users"`).
1098
+ * Get an auth client for the named user bucket.
1099
+ * Uses your `authKey` automatically.
1567
1100
  *
1568
1101
  * @example
1569
1102
  * ```ts
1570
1103
  * const auth = db.auth('app-users');
1571
- * const { user, session } = await auth.login({ email: '...', password: '...' });
1104
+ * const { user, session } = await auth.login({ email: '', password: '' });
1572
1105
  * ```
1573
1106
  */
1574
1107
  auth(bucketKey) {
1575
1108
  if (!this._authCache.has(bucketKey)) {
1576
- this._authCache.set(bucketKey, new AuthClient(this.http, bucketKey));
1109
+ this._authCache.set(bucketKey, new AuthClient(this.http, this.authKey_, bucketKey));
1577
1110
  }
1578
1111
  return this._authCache.get(bucketKey);
1579
1112
  }
1580
1113
  // ─── Analytics ────────────────────────────────────────────────────────────
1581
1114
  /**
1582
- * Get an analytics client for the given bucket.
1583
- *
1584
- * @param bucketKey The name of the bucket to analyse.
1115
+ * Get an analytics client for the named bucket.
1116
+ * Uses your `bucketSecurityKey` automatically.
1585
1117
  *
1586
1118
  * @example
1587
1119
  * ```ts
@@ -1591,31 +1123,46 @@ var HydrousClient = class {
1591
1123
  */
1592
1124
  analytics(bucketKey) {
1593
1125
  if (!this._analyticsCache.has(bucketKey)) {
1594
- this._analyticsCache.set(bucketKey, new AnalyticsClient(this.http, bucketKey));
1126
+ this._analyticsCache.set(
1127
+ bucketKey,
1128
+ new AnalyticsClient(this.http, this.bucketSecurityKey_, bucketKey)
1129
+ );
1595
1130
  }
1596
1131
  return this._analyticsCache.get(bucketKey);
1597
1132
  }
1598
1133
  // ─── Storage ──────────────────────────────────────────────────────────────
1599
1134
  /**
1600
- * The storage manager for uploading, downloading, listing, and managing files.
1135
+ * Get a storage manager for the named storage key.
1136
+ * The name must match a key you defined in `storageKeys` when calling `createClient`.
1137
+ * Uses the corresponding `ssk_…` key automatically via `X-Storage-Key` header.
1601
1138
  *
1602
- * Scoped to your project you can never access another project's files.
1139
+ * @param keyName The name of the storage key (e.g. `"avatars"`, `"documents"`, `"main"`).
1603
1140
  *
1604
1141
  * @example
1605
1142
  * ```ts
1606
- * // Upload a file
1607
- * const result = await db.storage.upload(file, 'images/photo.jpg', { isPublic: true });
1143
+ * const avatars = db.storage('avatars');
1144
+ * const documents = db.storage('documents');
1608
1145
  *
1609
- * // Scope to a folder
1610
- * const avatars = db.storage.scope('user-avatars');
1611
- * await avatars.upload(blob, `${userId}.jpg`, { isPublic: true });
1146
+ * // Upload to avatars bucket
1147
+ * await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
1148
+ *
1149
+ * // Scope to a sub-folder
1150
+ * const userDocs = db.storage('documents').scope(`users/${userId}`);
1151
+ * await userDocs.upload(pdfBuffer, 'contract.pdf');
1612
1152
  * ```
1613
1153
  */
1614
- get storage() {
1615
- if (!this._storage) {
1616
- this._storage = new StorageManager(this.http, this._storageKey);
1154
+ storage(keyName) {
1155
+ const ssk = this.storageKeys_[keyName];
1156
+ if (!ssk) {
1157
+ const available = Object.keys(this.storageKeys_).join(", ");
1158
+ throw new Error(
1159
+ `[HydrousDB] Unknown storage key name "${keyName}". Available keys: ${available}. Add it to storageKeys in your createClient() config.`
1160
+ );
1161
+ }
1162
+ if (!this._storageCache.has(keyName)) {
1163
+ this._storageCache.set(keyName, new StorageManager(this.http, ssk));
1617
1164
  }
1618
- const mgr = this._storage;
1165
+ const mgr = this._storageCache.get(keyName);
1619
1166
  const extended = mgr;
1620
1167
  if (!extended.scope) {
1621
1168
  extended.scope = (prefix) => new ScopedStorage(mgr, prefix);