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