hydrousdb 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1647 @@
1
+ 'use strict';
2
+
3
+ // src/utils/errors.ts
4
+ var HydrousError = class extends Error {
5
+ constructor(message, code, status, requestId, details) {
6
+ super(message);
7
+ this.name = "HydrousError";
8
+ this.code = code;
9
+ this.status = status;
10
+ this.requestId = requestId;
11
+ this.details = details;
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ toString() {
15
+ return `HydrousError [${this.code}]: ${this.message}`;
16
+ }
17
+ };
18
+ var AuthError = class extends HydrousError {
19
+ constructor(message, code, status, requestId, details) {
20
+ super(message, code, status, requestId, details);
21
+ this.name = "AuthError";
22
+ }
23
+ };
24
+ var RecordError = class extends HydrousError {
25
+ constructor(message, code, status, requestId, details) {
26
+ super(message, code, status, requestId, details);
27
+ this.name = "RecordError";
28
+ }
29
+ };
30
+ var StorageError = class extends HydrousError {
31
+ constructor(message, code, status, requestId) {
32
+ super(message, code, status, requestId);
33
+ this.name = "StorageError";
34
+ }
35
+ };
36
+ var AnalyticsError = class extends HydrousError {
37
+ constructor(message, code, status, requestId) {
38
+ super(message, code, status, requestId);
39
+ this.name = "AnalyticsError";
40
+ }
41
+ };
42
+ var ValidationError = class extends HydrousError {
43
+ constructor(message, details) {
44
+ super(message, "VALIDATION_ERROR", 400, void 0, details);
45
+ this.name = "ValidationError";
46
+ }
47
+ };
48
+ var NetworkError = class extends HydrousError {
49
+ constructor(message, cause) {
50
+ super(message, "NETWORK_ERROR");
51
+ this.name = "NetworkError";
52
+ this.cause = cause;
53
+ }
54
+ };
55
+
56
+ // src/utils/http.ts
57
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
58
+ var HttpClient = class {
59
+ constructor(baseUrl, securityKey) {
60
+ this.baseUrl = baseUrl.replace(/\/$/, "");
61
+ this.securityKey = securityKey;
62
+ }
63
+ async request(path, opts = {}) {
64
+ const {
65
+ method = "GET",
66
+ body,
67
+ headers = {},
68
+ rawBody,
69
+ contentType = "application/json"
70
+ } = opts;
71
+ const url = `${this.baseUrl}${path}`;
72
+ const reqHeaders = {
73
+ "X-Api-Key": this.securityKey,
74
+ ...headers
75
+ };
76
+ let reqBody = null;
77
+ if (rawBody !== void 0) {
78
+ reqBody = rawBody;
79
+ if (contentType) reqHeaders["Content-Type"] = contentType;
80
+ } else if (body !== void 0) {
81
+ reqBody = JSON.stringify(body);
82
+ reqHeaders["Content-Type"] = "application/json";
83
+ }
84
+ let response;
85
+ try {
86
+ response = await fetch(url, {
87
+ method,
88
+ headers: reqHeaders,
89
+ body: reqBody
90
+ });
91
+ } catch (err) {
92
+ throw new NetworkError(
93
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
94
+ err
95
+ );
96
+ }
97
+ const ct = response.headers.get("content-type") ?? "";
98
+ if (!ct.includes("application/json")) {
99
+ if (!response.ok) {
100
+ throw new HydrousError(
101
+ `Request failed with status ${response.status}`,
102
+ `HTTP_${response.status}`,
103
+ response.status
104
+ );
105
+ }
106
+ const buffer = await response.arrayBuffer();
107
+ return buffer;
108
+ }
109
+ let responseData;
110
+ try {
111
+ responseData = await response.json();
112
+ } catch {
113
+ throw new HydrousError(
114
+ "Failed to parse server response as JSON",
115
+ "PARSE_ERROR",
116
+ response.status
117
+ );
118
+ }
119
+ if (!response.ok) {
120
+ const errData = responseData;
121
+ throw new HydrousError(
122
+ errData["error"] ?? `Request failed with status ${response.status}`,
123
+ errData["code"] ?? `HTTP_${response.status}`,
124
+ response.status,
125
+ errData["requestId"],
126
+ errData["details"]
127
+ );
128
+ }
129
+ return responseData;
130
+ }
131
+ get(path, headers) {
132
+ return this.request(path, { method: "GET", headers });
133
+ }
134
+ post(path, body, headers) {
135
+ return this.request(path, { method: "POST", body, headers });
136
+ }
137
+ put(path, body, headers) {
138
+ return this.request(path, { method: "PUT", body, headers });
139
+ }
140
+ patch(path, body, headers) {
141
+ return this.request(path, { method: "PATCH", body, headers });
142
+ }
143
+ delete(path, body, headers) {
144
+ return this.request(path, { method: "DELETE", body, headers });
145
+ }
146
+ async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
147
+ if (typeof XMLHttpRequest !== "undefined" && onProgress) {
148
+ return new Promise((resolve, reject) => {
149
+ const xhr = new XMLHttpRequest();
150
+ xhr.upload.onprogress = (e) => {
151
+ if (e.lengthComputable) {
152
+ onProgress(Math.round(e.loaded / e.total * 100));
153
+ }
154
+ };
155
+ xhr.onload = () => {
156
+ if (xhr.status >= 200 && xhr.status < 300) {
157
+ resolve();
158
+ } else {
159
+ reject(new Error(`Upload failed: ${xhr.status}`));
160
+ }
161
+ };
162
+ xhr.onerror = () => reject(new Error("Upload network error"));
163
+ xhr.open("PUT", signedUrl);
164
+ xhr.setRequestHeader("Content-Type", mimeType);
165
+ const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
166
+ xhr.send(payload);
167
+ });
168
+ }
169
+ const fetchBody = data instanceof Blob ? data : new Blob([data], { type: mimeType });
170
+ const response = await fetch(signedUrl, {
171
+ method: "PUT",
172
+ headers: { "Content-Type": mimeType },
173
+ body: fetchBody
174
+ });
175
+ if (!response.ok) {
176
+ throw new NetworkError(`Signed URL upload failed with status ${response.status}`);
177
+ }
178
+ }
179
+ };
180
+
181
+ // src/auth/client.ts
182
+ var AuthClient = class {
183
+ constructor(http, bucketKey) {
184
+ this.http = http;
185
+ this.bucketKey = bucketKey;
186
+ this.basePath = `/auth/${bucketKey}`;
187
+ }
188
+ // ─── 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
+ async signup(options) {
204
+ const result = await this.http.post(`${this.basePath}/signup`, options);
205
+ return { user: result.user, session: result.session };
206
+ }
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
+ async login(options) {
221
+ const result = await this.http.post(`${this.basePath}/login`, options);
222
+ return { user: result.user, session: result.session };
223
+ }
224
+ /**
225
+ * Invalidate a session (sign out).
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * await auth.logout({ sessionId: session.sessionId });
230
+ * ```
231
+ */
232
+ async logout({ sessionId }) {
233
+ await this.http.post(`${this.basePath}/logout`, { sessionId });
234
+ }
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
+ async refreshSession({ refreshToken }) {
245
+ const result = await this.http.post(
246
+ `${this.basePath}/session/refresh`,
247
+ { refreshToken }
248
+ );
249
+ return result.session;
250
+ }
251
+ // ─── 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
+ async getUser({ userId }) {
261
+ const result = await this.http.get(`${this.basePath}/user/${userId}`);
262
+ return result.user;
263
+ }
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
+ async updateUser(options) {
277
+ const { sessionId, userId, data } = options;
278
+ const result = await this.http.patch(
279
+ `${this.basePath}/user`,
280
+ { sessionId, userId, ...data }
281
+ );
282
+ return result.user;
283
+ }
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
+ async deleteUser({ sessionId, userId }) {
294
+ await this.http.delete(`${this.basePath}/user`, { sessionId, userId });
295
+ }
296
+ // ─── 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
+ async listUsers(options) {
310
+ const { sessionId, limit = 50, offset = 0 } = options;
311
+ const result = await this.http.post(
312
+ `${this.basePath}/users/list`,
313
+ { sessionId, limit, offset }
314
+ );
315
+ return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
316
+ }
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
+ async hardDeleteUser({ sessionId, userId }) {
327
+ await this.http.delete(`${this.basePath}/user/hard`, { sessionId, userId });
328
+ }
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(
345
+ `${this.basePath}/users/bulk-delete`,
346
+ { sessionId, userIds }
347
+ );
348
+ return { deleted: result.deleted, failed: result.failed };
349
+ }
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 });
370
+ return result.data;
371
+ }
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 });
385
+ }
386
+ // ─── 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
+ async changePassword(options) {
401
+ await this.http.post(`${this.basePath}/password/change`, options);
402
+ }
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
+ async requestPasswordReset({ email }) {
413
+ await this.http.post(`${this.basePath}/password/reset/request`, { email });
414
+ }
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
+ });
434
+ }
435
+ // ─── 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
+ async requestEmailVerification({ userId }) {
445
+ await this.http.post(`${this.basePath}/email/verify/request`, { userId });
446
+ }
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
+ async confirmEmailVerification({ verifyToken }) {
456
+ await this.http.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
457
+ }
458
+ };
459
+
460
+ // src/utils/query.ts
461
+ function buildQueryParams(options = {}) {
462
+ const params = new URLSearchParams();
463
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
464
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
465
+ if (options.orderBy !== void 0) params.set("orderBy", options.orderBy);
466
+ if (options.order !== void 0) params.set("order", options.order);
467
+ if (options.fields !== void 0) params.set("fields", options.fields);
468
+ if (options.startAfter !== void 0) params.set("startAfter", options.startAfter);
469
+ if (options.startAt !== void 0) params.set("startAt", options.startAt);
470
+ if (options.endAt !== void 0) params.set("endAt", options.endAt);
471
+ if (options.dateRange?.start !== void 0)
472
+ params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
473
+ if (options.dateRange?.end !== void 0)
474
+ params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
475
+ if (options.filters && options.filters.length > 0) {
476
+ params.set("filters", JSON.stringify(options.filters));
477
+ }
478
+ const str = params.toString();
479
+ return str ? `?${str}` : "";
480
+ }
481
+ function guessMimeType(filename) {
482
+ const ext = filename.split(".").pop()?.toLowerCase();
483
+ const map = {
484
+ jpg: "image/jpeg",
485
+ jpeg: "image/jpeg",
486
+ png: "image/png",
487
+ gif: "image/gif",
488
+ webp: "image/webp",
489
+ svg: "image/svg+xml",
490
+ pdf: "application/pdf",
491
+ mp4: "video/mp4",
492
+ webm: "video/webm",
493
+ mp3: "audio/mpeg",
494
+ wav: "audio/wav",
495
+ txt: "text/plain",
496
+ html: "text/html",
497
+ css: "text/css",
498
+ js: "application/javascript",
499
+ json: "application/json",
500
+ xml: "application/xml",
501
+ zip: "application/zip",
502
+ csv: "text/csv"
503
+ };
504
+ return map[ext ?? ""] ?? "application/octet-stream";
505
+ }
506
+ function assertSafeName(name, label = "name") {
507
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
508
+ throw new Error(
509
+ `Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
510
+ );
511
+ }
512
+ }
513
+
514
+ // src/records/client.ts
515
+ var RecordsClient = class {
516
+ constructor(http, bucketKey) {
517
+ assertSafeName(bucketKey, "bucketKey");
518
+ this.http = http;
519
+ this.bucketKey = bucketKey;
520
+ this.basePath = `/records/${bucketKey}`;
521
+ }
522
+ // ─── 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
+ async create(data) {
537
+ const result = await this.http.post(this.basePath, data);
538
+ return result.record ?? result.data;
539
+ }
540
+ /**
541
+ * Fetch a single record by ID.
542
+ *
543
+ * @example
544
+ * ```ts
545
+ * const post = await records.get('rec_abc123');
546
+ * ```
547
+ */
548
+ async get(id) {
549
+ const result = await this.http.get(`${this.basePath}/${id}`);
550
+ return result.record ?? result.data;
551
+ }
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
+ async set(id, data) {
564
+ const result = await this.http.put(`${this.basePath}/${id}`, data);
565
+ return result.record ?? result.data;
566
+ }
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
+ async patch(id, data, options = {}) {
580
+ const { merge = true } = options;
581
+ const result = await this.http.patch(
582
+ `${this.basePath}/${id}`,
583
+ { ...data, _merge: merge }
584
+ );
585
+ return result.record ?? result.data;
586
+ }
587
+ /**
588
+ * Delete a record permanently.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * await records.delete('rec_abc123');
593
+ * ```
594
+ */
595
+ async delete(id) {
596
+ await this.http.delete(`${this.basePath}/${id}`);
597
+ }
598
+ // ─── 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
+ async batchCreate(items) {
611
+ const result = await this.http.post(
612
+ `${this.basePath}/batch`,
613
+ { records: items }
614
+ );
615
+ return result.records;
616
+ }
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
+ async batchDelete(ids) {
626
+ const result = await this.http.post(`${this.basePath}/batch/delete`, { ids });
627
+ return { deleted: result.deleted, failed: result.failed };
628
+ }
629
+ // ─── 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
+ async query(options = {}) {
657
+ const qs = buildQueryParams(options);
658
+ const result = await this.http.get(`${this.basePath}${qs}`);
659
+ return {
660
+ records: result.records,
661
+ total: result.total,
662
+ hasMore: result.hasMore,
663
+ nextCursor: result.nextCursor
664
+ };
665
+ }
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
+ async getAll(options = {}) {
675
+ const { records } = await this.query(options);
676
+ return records;
677
+ }
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
+ async count(filters = []) {
687
+ const result = await this.http.post(
688
+ `${this.basePath}/count`,
689
+ { filters }
690
+ );
691
+ return result.count;
692
+ }
693
+ // ─── 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
+ async getHistory(id) {
705
+ const result = await this.http.get(`${this.basePath}/${id}/history`);
706
+ return result.history;
707
+ }
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
+ async restoreVersion(id, version) {
718
+ const result = await this.http.post(
719
+ `${this.basePath}/${id}/restore`,
720
+ { version }
721
+ );
722
+ return result.record ?? result.data;
723
+ }
724
+ };
725
+
726
+ // src/analytics/client.ts
727
+ var AnalyticsClient = class {
728
+ constructor(http, bucketKey) {
729
+ assertSafeName(bucketKey, "bucketKey");
730
+ this.http = http;
731
+ this.bucketKey = bucketKey;
732
+ this.basePath = `/analytics/${bucketKey}`;
733
+ }
734
+ /** Internal dispatcher — all queries POST to the same endpoint. */
735
+ async run(query) {
736
+ const result = await this.http.post(this.basePath, query);
737
+ return result.data;
738
+ }
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
+ async count(opts = {}) {
755
+ return this.run({ queryType: "count", ...opts });
756
+ }
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
+ async distribution(opts) {
773
+ assertSafeName(opts.field, "field");
774
+ return this.run({ queryType: "distribution", ...opts });
775
+ }
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
+ async sum(opts) {
792
+ assertSafeName(opts.field, "field");
793
+ if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
794
+ return this.run({ queryType: "sum", ...opts });
795
+ }
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
+ async timeSeries(opts = {}) {
811
+ return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
812
+ }
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
+ async fieldTimeSeries(opts) {
828
+ assertSafeName(opts.field, "field");
829
+ return this.run({
830
+ queryType: "fieldTimeSeries",
831
+ aggregation: "sum",
832
+ granularity: "day",
833
+ ...opts
834
+ });
835
+ }
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
+ async topN(opts) {
853
+ assertSafeName(opts.field, "field");
854
+ if (opts.labelField) assertSafeName(opts.labelField, "labelField");
855
+ return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
856
+ }
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
+ async stats(opts) {
868
+ assertSafeName(opts.field, "field");
869
+ return this.run({ queryType: "stats", ...opts });
870
+ }
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
+ async records(opts = {}) {
888
+ if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
889
+ if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
890
+ return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
891
+ }
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
+ async multiMetric(opts) {
910
+ opts.metrics.forEach((m) => {
911
+ assertSafeName(m.field, "metric.field");
912
+ assertSafeName(m.name, "metric.name");
913
+ });
914
+ return this.run({ queryType: "multiMetric", ...opts });
915
+ }
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
+ async storageStats(opts = {}) {
927
+ return this.run({ queryType: "storageStats", ...opts });
928
+ }
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
+ async crossBucket(opts) {
949
+ assertSafeName(opts.field, "field");
950
+ opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
951
+ return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
952
+ }
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
+ async query(query) {
969
+ const data = await this.run(query);
970
+ return { queryType: query.queryType, data };
971
+ }
972
+ };
973
+
974
+ // src/storage/manager.ts
975
+ var StorageManager = class {
976
+ constructor(http, storageKey) {
977
+ this.basePath = "/storage";
978
+ this.http = http;
979
+ this.storageKey = storageKey;
980
+ }
981
+ /** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
982
+ get authHeaders() {
983
+ return { "X-Storage-Key": this.storageKey };
984
+ }
985
+ // ─── Upload: Simple (server-buffered) ────────────────────────────────────
986
+ /**
987
+ * Upload a file to storage in one step (server-buffered, up to 500 MB).
988
+ * For files >10 MB or when you need upload progress, use `getUploadUrl()` instead.
989
+ *
990
+ * @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
991
+ * @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
992
+ * @param options Upload options: isPublic, overwrite, mimeType.
993
+ *
994
+ * @example
995
+ * ```ts
996
+ * // Upload a public avatar
997
+ * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
998
+ * console.log(result.publicUrl); // → https://...
999
+ *
1000
+ * // Upload a private document
1001
+ * const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
1002
+ * console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
1003
+ * ```
1004
+ */
1005
+ async upload(data, path, options = {}) {
1006
+ const { isPublic = false, overwrite = false, mimeType } = options;
1007
+ const mime = mimeType ?? guessMimeType(path);
1008
+ const formData = new FormData();
1009
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
1010
+ formData.append("file", blob, path.split("/").pop() ?? "file");
1011
+ formData.append("path", path);
1012
+ formData.append("mimeType", mime);
1013
+ formData.append("isPublic", String(isPublic));
1014
+ formData.append("overwrite", String(overwrite));
1015
+ const result = await this.http.request(`${this.basePath}/upload`, {
1016
+ method: "POST",
1017
+ rawBody: formData,
1018
+ headers: this.authHeaders
1019
+ });
1020
+ return {
1021
+ path: result.path,
1022
+ mimeType: result.mimeType,
1023
+ size: result.size,
1024
+ isPublic: result.isPublic,
1025
+ publicUrl: result.publicUrl,
1026
+ downloadUrl: result.downloadUrl
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Upload raw JSON or plain text data as a file.
1031
+ *
1032
+ * @example
1033
+ * ```ts
1034
+ * const result = await storage.uploadRaw(
1035
+ * { config: { theme: 'dark' } },
1036
+ * 'settings/user-config.json',
1037
+ * { isPublic: false },
1038
+ * );
1039
+ * ```
1040
+ */
1041
+ async uploadRaw(data, path, options = {}) {
1042
+ const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
1043
+ const body = typeof data === "string" ? data : JSON.stringify(data);
1044
+ const result = await this.http.request(`${this.basePath}/upload-raw`, {
1045
+ method: "POST",
1046
+ body: { path, data: body, mimeType, isPublic, overwrite },
1047
+ headers: this.authHeaders
1048
+ });
1049
+ return {
1050
+ path: result.path,
1051
+ mimeType: result.mimeType,
1052
+ size: result.size,
1053
+ isPublic: result.isPublic,
1054
+ publicUrl: result.publicUrl,
1055
+ downloadUrl: result.downloadUrl
1056
+ };
1057
+ }
1058
+ // ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
1059
+ /**
1060
+ * Step 1 of the recommended upload flow.
1061
+ * Get a signed URL to upload directly to GCS from the client (supports progress).
1062
+ *
1063
+ * @example
1064
+ * ```ts
1065
+ * const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
1066
+ * path: 'videos/intro.mp4',
1067
+ * mimeType: 'video/mp4',
1068
+ * size: file.size,
1069
+ * isPublic: true,
1070
+ * });
1071
+ *
1072
+ * // Upload using XHR for progress tracking
1073
+ * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
1074
+ * console.log(`${pct}% uploaded`);
1075
+ * });
1076
+ *
1077
+ * // Step 3: confirm
1078
+ * const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
1079
+ * ```
1080
+ */
1081
+ async getUploadUrl(opts) {
1082
+ const result = await this.http.request(`${this.basePath}/upload-url`, {
1083
+ method: "POST",
1084
+ body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
1085
+ headers: this.authHeaders
1086
+ });
1087
+ return result;
1088
+ }
1089
+ /**
1090
+ * Upload data directly to a signed GCS URL (no auth headers needed).
1091
+ * Optionally tracks progress via a callback.
1092
+ *
1093
+ * @param signedUrl The URL returned by `getUploadUrl()`.
1094
+ * @param data File data.
1095
+ * @param mimeType Must match what was used in `getUploadUrl()`.
1096
+ * @param onProgress Optional callback called with 0–100 progress percentage.
1097
+ */
1098
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1099
+ await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
1100
+ }
1101
+ /**
1102
+ * Step 3 of the recommended upload flow.
1103
+ * Confirm a direct upload and register metadata on the server.
1104
+ *
1105
+ * @example
1106
+ * ```ts
1107
+ * const result = await storage.confirmUpload({
1108
+ * path: 'videos/intro.mp4',
1109
+ * mimeType: 'video/mp4',
1110
+ * isPublic: true,
1111
+ * });
1112
+ * console.log(result.publicUrl);
1113
+ * ```
1114
+ */
1115
+ async confirmUpload(opts) {
1116
+ const result = await this.http.request(`${this.basePath}/confirm`, {
1117
+ method: "POST",
1118
+ body: { isPublic: false, ...opts },
1119
+ headers: this.authHeaders
1120
+ });
1121
+ return {
1122
+ path: result.path,
1123
+ mimeType: result.mimeType,
1124
+ size: result.size,
1125
+ isPublic: result.isPublic,
1126
+ publicUrl: result.publicUrl,
1127
+ downloadUrl: result.downloadUrl
1128
+ };
1129
+ }
1130
+ // ─── Batch Upload ─────────────────────────────────────────────────────────
1131
+ /**
1132
+ * Get signed upload URLs for multiple files at once.
1133
+ *
1134
+ * @example
1135
+ * ```ts
1136
+ * const { files } = await storage.getBatchUploadUrls([
1137
+ * { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
1138
+ * { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
1139
+ * ]);
1140
+ *
1141
+ * // Upload each file and confirm
1142
+ * for (const f of files) {
1143
+ * await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
1144
+ * await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
1145
+ * }
1146
+ * ```
1147
+ */
1148
+ async getBatchUploadUrls(files) {
1149
+ const result = await this.http.request(
1150
+ `${this.basePath}/batch-upload-urls`,
1151
+ { method: "POST", body: { files }, headers: this.authHeaders }
1152
+ );
1153
+ return { files: result.files };
1154
+ }
1155
+ /**
1156
+ * Confirm multiple direct uploads at once.
1157
+ */
1158
+ async batchConfirmUploads(items) {
1159
+ const result = await this.http.request(
1160
+ `${this.basePath}/batch-confirm`,
1161
+ { method: "POST", body: { files: items }, headers: this.authHeaders }
1162
+ );
1163
+ return result.files.map((r) => ({
1164
+ path: r.path,
1165
+ mimeType: r.mimeType,
1166
+ size: r.size,
1167
+ isPublic: r.isPublic,
1168
+ publicUrl: r.publicUrl,
1169
+ downloadUrl: r.downloadUrl
1170
+ }));
1171
+ }
1172
+ // ─── Download ─────────────────────────────────────────────────────────────
1173
+ /**
1174
+ * Download a private file as an ArrayBuffer.
1175
+ * For public files, use the `publicUrl` directly — no SDK needed.
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * const buffer = await storage.download('docs/contract.pdf');
1180
+ * const blob = new Blob([buffer], { type: 'application/pdf' });
1181
+ * // Open in browser:
1182
+ * window.open(URL.createObjectURL(blob));
1183
+ * ```
1184
+ */
1185
+ async download(path) {
1186
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1187
+ return this.http.request(`${this.basePath}/download/${encoded}`, {
1188
+ method: "GET",
1189
+ headers: this.authHeaders
1190
+ });
1191
+ }
1192
+ /**
1193
+ * Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
1194
+ *
1195
+ * @example
1196
+ * ```ts
1197
+ * const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
1198
+ * ```
1199
+ */
1200
+ async batchDownload(paths) {
1201
+ const result = await this.http.request(
1202
+ `${this.basePath}/batch-download`,
1203
+ { method: "POST", body: { paths }, headers: this.authHeaders }
1204
+ );
1205
+ return result.files;
1206
+ }
1207
+ // ─── List ─────────────────────────────────────────────────────────────────
1208
+ /**
1209
+ * List files and folders at a given path prefix.
1210
+ *
1211
+ * @example
1212
+ * ```ts
1213
+ * // List everything in the root
1214
+ * const { files, folders } = await storage.list();
1215
+ *
1216
+ * // List a specific folder
1217
+ * const { files, folders, hasMore, nextCursor } = await storage.list({
1218
+ * prefix: 'avatars/',
1219
+ * limit: 20,
1220
+ * });
1221
+ *
1222
+ * // Next page
1223
+ * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
1224
+ * ```
1225
+ */
1226
+ async list(opts = {}) {
1227
+ const params = new URLSearchParams();
1228
+ if (opts.prefix) params.set("prefix", opts.prefix);
1229
+ if (opts.limit) params.set("limit", String(opts.limit));
1230
+ if (opts.cursor) params.set("cursor", opts.cursor);
1231
+ if (opts.recursive) params.set("recursive", "true");
1232
+ const qs = params.toString() ? `?${params}` : "";
1233
+ const result = await this.http.request(`${this.basePath}/list${qs}`, {
1234
+ method: "GET",
1235
+ headers: this.authHeaders
1236
+ });
1237
+ return {
1238
+ files: result.files,
1239
+ folders: result.folders,
1240
+ hasMore: result.hasMore,
1241
+ nextCursor: result.nextCursor
1242
+ };
1243
+ }
1244
+ // ─── Metadata ─────────────────────────────────────────────────────────────
1245
+ /**
1246
+ * Get metadata for a file (size, MIME type, visibility, URLs).
1247
+ *
1248
+ * @example
1249
+ * ```ts
1250
+ * const meta = await storage.getMetadata('avatars/alice.jpg');
1251
+ * console.log(meta.size, meta.isPublic, meta.publicUrl);
1252
+ * ```
1253
+ */
1254
+ async getMetadata(path) {
1255
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
1256
+ const result = await this.http.request(
1257
+ `${this.basePath}/metadata/${encoded}`,
1258
+ { method: "GET", headers: this.authHeaders }
1259
+ );
1260
+ return {
1261
+ path: result.path,
1262
+ size: result.size,
1263
+ mimeType: result.mimeType,
1264
+ isPublic: result.isPublic,
1265
+ publicUrl: result.publicUrl,
1266
+ downloadUrl: result.downloadUrl,
1267
+ createdAt: result.createdAt,
1268
+ updatedAt: result.updatedAt
1269
+ };
1270
+ }
1271
+ // ─── Signed URL (time-limited share) ─────────────────────────────────────
1272
+ /**
1273
+ * Generate a time-limited download URL for a private file.
1274
+ * The URL can be shared externally without requiring an `X-Storage-Key`.
1275
+ *
1276
+ * > **Note:** Downloads via signed URLs bypass the server, so download stats
1277
+ * > are NOT tracked. Use `downloadUrl` for tracked downloads.
1278
+ *
1279
+ * @param path Path to the file.
1280
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
1281
+ *
1282
+ * @example
1283
+ * ```ts
1284
+ * const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
1285
+ * // Share signedUrl with the recipient — it expires in 30 minutes.
1286
+ * ```
1287
+ */
1288
+ async getSignedUrl(path, expiresIn = 3600) {
1289
+ const result = await this.http.request(`${this.basePath}/signed-url`, {
1290
+ method: "POST",
1291
+ body: { path, expiresIn },
1292
+ headers: this.authHeaders
1293
+ });
1294
+ return {
1295
+ signedUrl: result.signedUrl,
1296
+ expiresAt: result.expiresAt,
1297
+ expiresIn: result.expiresIn,
1298
+ path: result.path
1299
+ };
1300
+ }
1301
+ // ─── Visibility ───────────────────────────────────────────────────────────
1302
+ /**
1303
+ * Change a file's visibility between public and private after upload.
1304
+ *
1305
+ * @example
1306
+ * ```ts
1307
+ * // Make a file public
1308
+ * const result = await storage.setVisibility('avatars/alice.jpg', true);
1309
+ * console.log(result.publicUrl); // CDN URL
1310
+ *
1311
+ * // Make a file private
1312
+ * const result = await storage.setVisibility('avatars/alice.jpg', false);
1313
+ * console.log(result.downloadUrl); // Auth-required URL
1314
+ * ```
1315
+ */
1316
+ async setVisibility(path, isPublic) {
1317
+ const result = await this.http.request(`${this.basePath}/visibility`, {
1318
+ method: "PATCH",
1319
+ body: { path, isPublic },
1320
+ headers: this.authHeaders
1321
+ });
1322
+ return {
1323
+ path: result.path,
1324
+ isPublic: result.isPublic,
1325
+ publicUrl: result.publicUrl,
1326
+ downloadUrl: result.downloadUrl
1327
+ };
1328
+ }
1329
+ // ─── Folder Operations ────────────────────────────────────────────────────
1330
+ /**
1331
+ * Create a folder (a GCS prefix placeholder).
1332
+ *
1333
+ * @example
1334
+ * ```ts
1335
+ * await storage.createFolder('uploads/2025/');
1336
+ * ```
1337
+ */
1338
+ async createFolder(path) {
1339
+ const result = await this.http.request(
1340
+ `${this.basePath}/folder`,
1341
+ { method: "POST", body: { path }, headers: this.authHeaders }
1342
+ );
1343
+ return { path: result.path };
1344
+ }
1345
+ // ─── File Operations ──────────────────────────────────────────────────────
1346
+ /**
1347
+ * Delete a single file.
1348
+ *
1349
+ * @example
1350
+ * ```ts
1351
+ * await storage.deleteFile('avatars/old-avatar.jpg');
1352
+ * ```
1353
+ */
1354
+ async deleteFile(path) {
1355
+ await this.http.request(`${this.basePath}/file`, {
1356
+ method: "DELETE",
1357
+ body: { path },
1358
+ headers: this.authHeaders
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Delete a folder and all its contents recursively.
1363
+ *
1364
+ * @example
1365
+ * ```ts
1366
+ * await storage.deleteFolder('temp/');
1367
+ * ```
1368
+ */
1369
+ async deleteFolder(path) {
1370
+ await this.http.request(`${this.basePath}/folder`, {
1371
+ method: "DELETE",
1372
+ body: { path },
1373
+ headers: this.authHeaders
1374
+ });
1375
+ }
1376
+ /**
1377
+ * Move or rename a file.
1378
+ *
1379
+ * @example
1380
+ * ```ts
1381
+ * // Rename
1382
+ * await storage.move('docs/draft.pdf', 'docs/final.pdf');
1383
+ * // Move to a different folder
1384
+ * await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
1385
+ * ```
1386
+ */
1387
+ async move(from, to) {
1388
+ const result = await this.http.request(
1389
+ `${this.basePath}/move`,
1390
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
1391
+ );
1392
+ return { from: result.from, to: result.to };
1393
+ }
1394
+ /**
1395
+ * Copy a file to a new path.
1396
+ *
1397
+ * @example
1398
+ * ```ts
1399
+ * await storage.copy('templates/base.html', 'sites/my-site/index.html');
1400
+ * ```
1401
+ */
1402
+ async copy(from, to) {
1403
+ const result = await this.http.request(
1404
+ `${this.basePath}/copy`,
1405
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
1406
+ );
1407
+ return { from: result.from, to: result.to };
1408
+ }
1409
+ // ─── Stats ────────────────────────────────────────────────────────────────
1410
+ /**
1411
+ * Get storage statistics for your key: total files, bytes, operation counts.
1412
+ *
1413
+ * @example
1414
+ * ```ts
1415
+ * const stats = await storage.getStats();
1416
+ * console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
1417
+ * ```
1418
+ */
1419
+ async getStats() {
1420
+ const result = await this.http.request(`${this.basePath}/stats`, {
1421
+ method: "GET",
1422
+ headers: this.authHeaders
1423
+ });
1424
+ return result.stats;
1425
+ }
1426
+ // ─── Info (no auth) ───────────────────────────────────────────────────────
1427
+ /**
1428
+ * Ping the storage service. No authentication required.
1429
+ *
1430
+ * @example
1431
+ * ```ts
1432
+ * const info = await storage.info();
1433
+ * // → { ok: true, storageRoot: 'hydrous-storage' }
1434
+ * ```
1435
+ */
1436
+ async info() {
1437
+ return this.http.get(`${this.basePath}/info`);
1438
+ }
1439
+ };
1440
+
1441
+ // src/storage/scoped.ts
1442
+ var ScopedStorage = class _ScopedStorage {
1443
+ constructor(manager, prefix) {
1444
+ this.manager = manager;
1445
+ this.prefix = prefix.replace(/\/+$/, "") + "/";
1446
+ }
1447
+ scopedPath(userPath) {
1448
+ return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
1449
+ }
1450
+ /** Upload a file within the scoped folder. */
1451
+ upload(data, path, options) {
1452
+ return this.manager.upload(data, this.scopedPath(path), options);
1453
+ }
1454
+ /** Upload raw JSON or text within the scoped folder. */
1455
+ uploadRaw(data, path, options) {
1456
+ return this.manager.uploadRaw(data, this.scopedPath(path), options);
1457
+ }
1458
+ /** Get a signed upload URL for a file within the scoped folder. */
1459
+ getUploadUrl(opts) {
1460
+ return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
1461
+ }
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
+ /** Confirm a direct upload within the scoped folder. */
1467
+ confirmUpload(opts) {
1468
+ return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
1469
+ }
1470
+ /** Download a file within the scoped folder. */
1471
+ download(path) {
1472
+ return this.manager.download(this.scopedPath(path));
1473
+ }
1474
+ /** List files within the scoped folder. */
1475
+ list(opts = {}) {
1476
+ return this.manager.list({ ...opts, prefix: this.scopedPath(opts.prefix ?? "") });
1477
+ }
1478
+ /** Get metadata for a file within the scoped folder. */
1479
+ getMetadata(path) {
1480
+ return this.manager.getMetadata(this.scopedPath(path));
1481
+ }
1482
+ /** Get a time-limited signed URL for a file within the scoped folder. */
1483
+ getSignedUrl(path, expiresIn) {
1484
+ return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
1485
+ }
1486
+ /** Change visibility of a file within the scoped folder. */
1487
+ setVisibility(path, isPublic) {
1488
+ return this.manager.setVisibility(this.scopedPath(path), isPublic);
1489
+ }
1490
+ /** Delete a file within the scoped folder. */
1491
+ deleteFile(path) {
1492
+ return this.manager.deleteFile(this.scopedPath(path));
1493
+ }
1494
+ /** Delete a sub-folder within the scoped folder. */
1495
+ deleteFolder(path) {
1496
+ return this.manager.deleteFolder(this.scopedPath(path));
1497
+ }
1498
+ /** Move a file within the scoped folder. */
1499
+ move(from, to) {
1500
+ return this.manager.move(this.scopedPath(from), this.scopedPath(to));
1501
+ }
1502
+ /** Copy a file within the scoped folder. */
1503
+ copy(from, to) {
1504
+ return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
1505
+ }
1506
+ /** Create a sub-folder within the scoped folder. */
1507
+ createFolder(path) {
1508
+ return this.manager.createFolder(this.scopedPath(path));
1509
+ }
1510
+ /**
1511
+ * Create a further-scoped instance nested within this scope.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * const uploads = db.storage.scope('user-uploads');
1516
+ * const images = uploads.scope('images'); // → "user-uploads/images/"
1517
+ * ```
1518
+ */
1519
+ scope(subPrefix) {
1520
+ return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
1521
+ }
1522
+ };
1523
+
1524
+ // src/client.ts
1525
+ var HydrousClient = class {
1526
+ constructor(config) {
1527
+ this._storage = null;
1528
+ this._recordsCache = /* @__PURE__ */ new Map();
1529
+ this._authCache = /* @__PURE__ */ new Map();
1530
+ 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
+ );
1535
+ }
1536
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1537
+ this.http = new HttpClient(baseUrl, config.securityKey);
1538
+ this._storageKey = config.securityKey;
1539
+ }
1540
+ // ─── Records ─────────────────────────────────────────────────────────────
1541
+ /**
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).
1548
+ *
1549
+ * @example
1550
+ * ```ts
1551
+ * interface Post { title: string; body: string; published: boolean }
1552
+ * 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
1556
+ * ```
1557
+ */
1558
+ records(bucketKey) {
1559
+ if (!this._recordsCache.has(bucketKey)) {
1560
+ this._recordsCache.set(bucketKey, new RecordsClient(this.http, bucketKey));
1561
+ }
1562
+ return this._recordsCache.get(bucketKey);
1563
+ }
1564
+ // ─── Auth ─────────────────────────────────────────────────────────────────
1565
+ /**
1566
+ * Get an auth client for the given user bucket.
1567
+ *
1568
+ * @param bucketKey The name of your user bucket (e.g. `"app-users"`).
1569
+ *
1570
+ * @example
1571
+ * ```ts
1572
+ * const auth = db.auth('app-users');
1573
+ * const { user, session } = await auth.login({ email: '...', password: '...' });
1574
+ * ```
1575
+ */
1576
+ auth(bucketKey) {
1577
+ if (!this._authCache.has(bucketKey)) {
1578
+ this._authCache.set(bucketKey, new AuthClient(this.http, bucketKey));
1579
+ }
1580
+ return this._authCache.get(bucketKey);
1581
+ }
1582
+ // ─── Analytics ────────────────────────────────────────────────────────────
1583
+ /**
1584
+ * Get an analytics client for the given bucket.
1585
+ *
1586
+ * @param bucketKey The name of the bucket to analyse.
1587
+ *
1588
+ * @example
1589
+ * ```ts
1590
+ * const analytics = db.analytics('orders');
1591
+ * const { count } = await analytics.count();
1592
+ * ```
1593
+ */
1594
+ analytics(bucketKey) {
1595
+ if (!this._analyticsCache.has(bucketKey)) {
1596
+ this._analyticsCache.set(bucketKey, new AnalyticsClient(this.http, bucketKey));
1597
+ }
1598
+ return this._analyticsCache.get(bucketKey);
1599
+ }
1600
+ // ─── Storage ──────────────────────────────────────────────────────────────
1601
+ /**
1602
+ * The storage manager for uploading, downloading, listing, and managing files.
1603
+ *
1604
+ * Scoped to your project — you can never access another project's files.
1605
+ *
1606
+ * @example
1607
+ * ```ts
1608
+ * // Upload a file
1609
+ * const result = await db.storage.upload(file, 'images/photo.jpg', { isPublic: true });
1610
+ *
1611
+ * // Scope to a folder
1612
+ * const avatars = db.storage.scope('user-avatars');
1613
+ * await avatars.upload(blob, `${userId}.jpg`, { isPublic: true });
1614
+ * ```
1615
+ */
1616
+ get storage() {
1617
+ if (!this._storage) {
1618
+ this._storage = new StorageManager(this.http, this._storageKey);
1619
+ }
1620
+ const mgr = this._storage;
1621
+ const extended = mgr;
1622
+ if (!extended.scope) {
1623
+ extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
1624
+ }
1625
+ return extended;
1626
+ }
1627
+ };
1628
+ function createClient(config) {
1629
+ return new HydrousClient(config);
1630
+ }
1631
+
1632
+ exports.AnalyticsClient = AnalyticsClient;
1633
+ exports.AnalyticsError = AnalyticsError;
1634
+ exports.AuthClient = AuthClient;
1635
+ exports.AuthError = AuthError;
1636
+ exports.HydrousClient = HydrousClient;
1637
+ exports.HydrousError = HydrousError;
1638
+ exports.NetworkError = NetworkError;
1639
+ exports.RecordError = RecordError;
1640
+ exports.RecordsClient = RecordsClient;
1641
+ exports.ScopedStorage = ScopedStorage;
1642
+ exports.StorageError = StorageError;
1643
+ exports.StorageManager = StorageManager;
1644
+ exports.ValidationError = ValidationError;
1645
+ exports.createClient = createClient;
1646
+ //# sourceMappingURL=index.cjs.map
1647
+ //# sourceMappingURL=index.cjs.map