hydrousdb 3.0.1 → 3.2.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 CHANGED
@@ -49,7 +49,7 @@ var NetworkError = class extends HydrousError {
49
49
  constructor(message, cause) {
50
50
  super(message, "NETWORK_ERROR");
51
51
  this.name = "NetworkError";
52
- if (cause) this.cause = cause;
52
+ if (cause !== void 0) this.cause = cause;
53
53
  }
54
54
  };
55
55
 
@@ -59,42 +59,23 @@ var HttpClient = class {
59
59
  constructor(baseUrl) {
60
60
  this.baseUrl = baseUrl.replace(/\/$/, "");
61
61
  }
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
- }
72
- const {
73
- method = "GET",
74
- body,
75
- headers = {},
76
- rawBody,
77
- contentType = "application/json"
78
- } = resolvedOpts;
62
+ // ─── Core request method ──────────────────────────────────────────────────
63
+ async request(path, options) {
79
64
  const url = `${this.baseUrl}${path}`;
80
- const reqHeaders = {
81
- ...apiKey ? { "X-Api-Key": apiKey } : {},
82
- ...headers
83
- };
84
- let reqBody = null;
85
- if (rawBody !== void 0) {
86
- reqBody = rawBody;
87
- if (contentType) reqHeaders["Content-Type"] = contentType;
88
- } else if (body !== void 0) {
89
- reqBody = JSON.stringify(body);
90
- reqHeaders["Content-Type"] = "application/json";
65
+ const headers = { ...options.headers };
66
+ let body;
67
+ if (options.rawBody !== void 0) {
68
+ body = options.rawBody;
69
+ } else if (options.body !== void 0) {
70
+ headers["Content-Type"] = "application/json";
71
+ body = JSON.stringify(options.body);
91
72
  }
92
73
  let response;
93
74
  try {
94
75
  response = await fetch(url, {
95
- method,
96
- headers: reqHeaders,
97
- body: reqBody
76
+ method: options.method,
77
+ headers,
78
+ body
98
79
  });
99
80
  } catch (err) {
100
81
  throw new NetworkError(
@@ -102,87 +83,100 @@ var HttpClient = class {
102
83
  err
103
84
  );
104
85
  }
105
- const ct = response.headers.get("content-type") ?? "";
106
- if (!ct.includes("application/json")) {
86
+ const contentType = response.headers.get("content-type") ?? "";
87
+ if (!contentType.includes("application/json")) {
107
88
  if (!response.ok) {
108
89
  throw new HydrousError(
109
90
  `Request failed with status ${response.status}`,
110
- `HTTP_${response.status}`,
91
+ "REQUEST_FAILED",
111
92
  response.status
112
93
  );
113
94
  }
114
- const buffer = await response.arrayBuffer();
115
- return buffer;
95
+ return response.arrayBuffer();
116
96
  }
117
- let responseData;
97
+ let data;
118
98
  try {
119
- responseData = await response.json();
99
+ data = await response.json();
120
100
  } catch {
121
101
  throw new HydrousError(
122
- "Failed to parse server response as JSON",
102
+ `Failed to parse response JSON`,
123
103
  "PARSE_ERROR",
124
104
  response.status
125
105
  );
126
106
  }
127
107
  if (!response.ok) {
128
- const errData = responseData;
129
108
  throw new HydrousError(
130
- errData["error"] ?? `Request failed with status ${response.status}`,
131
- errData["code"] ?? `HTTP_${response.status}`,
109
+ data["error"] || data["message"] || `Request failed with status ${response.status}`,
110
+ data["code"] || "REQUEST_FAILED",
132
111
  response.status,
133
- errData["requestId"],
134
- errData["details"]
112
+ data["requestId"],
113
+ data["details"]
135
114
  );
136
115
  }
137
- return responseData;
116
+ return data;
138
117
  }
139
- get(path, apiKey, headers) {
140
- return this.request(path, apiKey, { method: "GET", headers });
118
+ // ─── Convenience wrappers ─────────────────────────────────────────────────
119
+ get(path, apiKey, extraHeaders) {
120
+ return this.request(path, {
121
+ method: "GET",
122
+ headers: apiKey ? { "X-Api-Key": apiKey, ...extraHeaders } : { ...extraHeaders }
123
+ });
141
124
  }
142
- post(path, apiKey, body, headers) {
143
- return this.request(path, apiKey, { method: "POST", body, headers });
125
+ post(path, apiKey, body) {
126
+ return this.request(path, {
127
+ method: "POST",
128
+ body,
129
+ headers: { "X-Api-Key": apiKey }
130
+ });
144
131
  }
145
- put(path, apiKey, body, headers) {
146
- return this.request(path, apiKey, { method: "PUT", body, headers });
132
+ put(path, apiKey, body) {
133
+ return this.request(path, {
134
+ method: "PUT",
135
+ body,
136
+ headers: { "X-Api-Key": apiKey }
137
+ });
147
138
  }
148
- patch(path, apiKey, body, headers) {
149
- return this.request(path, apiKey, { method: "PATCH", body, headers });
139
+ patch(path, apiKey, body) {
140
+ return this.request(path, {
141
+ method: "PATCH",
142
+ body,
143
+ headers: { "X-Api-Key": apiKey }
144
+ });
150
145
  }
151
- delete(path, apiKey, body, headers) {
152
- return this.request(path, apiKey, { method: "DELETE", body, headers });
146
+ delete(path, apiKey, body) {
147
+ return this.request(path, {
148
+ method: "DELETE",
149
+ body,
150
+ headers: { "X-Api-Key": apiKey }
151
+ });
153
152
  }
153
+ /**
154
+ * Upload directly to a signed GCS URL (no auth headers — signed URL is self-authenticating).
155
+ * Supports optional progress tracking via XHR in browser environments.
156
+ */
154
157
  async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
155
- const XHR = typeof globalThis["XMLHttpRequest"] !== "undefined" ? globalThis["XMLHttpRequest"] : void 0;
156
- if (XHR && onProgress) {
157
- return new Promise((resolve, reject) => {
158
- const xhr = new XHR();
158
+ const body = data instanceof Blob ? data : data instanceof Uint8Array ? data.buffer : data;
159
+ if (typeof XMLHttpRequest !== "undefined" && typeof onProgress === "function") {
160
+ await new Promise((resolve, reject) => {
161
+ const xhr = new XMLHttpRequest();
159
162
  xhr.upload.onprogress = (e) => {
160
- if (e.lengthComputable) {
161
- onProgress(Math.round(e.loaded / e.total * 100));
162
- }
163
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
163
164
  };
164
- xhr.onload = () => {
165
- if (xhr.status >= 200 && xhr.status < 300) {
166
- resolve();
167
- } else {
168
- reject(new Error(`Upload failed: ${xhr.status}`));
169
- }
170
- };
171
- xhr.onerror = () => reject(new Error("Upload network error"));
165
+ xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`GCS upload failed: ${xhr.status}`));
166
+ xhr.onerror = () => reject(new Error("GCS upload network error"));
172
167
  xhr.open("PUT", signedUrl);
173
168
  xhr.setRequestHeader("Content-Type", mimeType);
174
- const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
175
- xhr.send(payload);
169
+ xhr.send(body);
176
170
  });
177
- }
178
- const fetchBody = data instanceof Blob ? data : new Blob([data], { type: mimeType });
179
- const response = await fetch(signedUrl, {
180
- method: "PUT",
181
- headers: { "Content-Type": mimeType },
182
- body: fetchBody
183
- });
184
- if (!response.ok) {
185
- throw new NetworkError(`Signed URL upload failed with status ${response.status}`);
171
+ } else {
172
+ const res = await fetch(signedUrl, {
173
+ method: "PUT",
174
+ headers: { "Content-Type": mimeType },
175
+ body
176
+ });
177
+ if (!res.ok) {
178
+ throw new HydrousError(`GCS upload failed: ${res.status}`, "GCS_UPLOAD_FAILED", res.status);
179
+ }
186
180
  }
187
181
  }
188
182
  };
@@ -197,8 +191,9 @@ var AuthClient = class {
197
191
  post(path, body) {
198
192
  return this.http.post(path, this.authKey, body);
199
193
  }
200
- get(path) {
201
- return this.http.get(path, this.authKey);
194
+ get(path, query) {
195
+ const qs = query && Object.keys(query).length ? "?" + new URLSearchParams(query).toString() : "";
196
+ return this.http.get(`${path}${qs}`, this.authKey);
202
197
  }
203
198
  patch(path, body) {
204
199
  return this.http.patch(path, this.authKey, body);
@@ -207,57 +202,136 @@ var AuthClient = class {
207
202
  return this.http.delete(path, this.authKey, body);
208
203
  }
209
204
  // ─── Registration & Login ─────────────────────────────────────────────────
205
+ // Server: POST /auth/:bucket/signup → { user, session }
206
+ // Server: POST /auth/:bucket/signin → { user, session }
207
+ // Server: POST /auth/:bucket/signout → { success, message }
210
208
  async signup(options) {
211
209
  const result = await this.post(`${this.basePath}/signup`, options);
212
- return { user: result.user, session: result.session };
210
+ const user = result.user ?? result.data;
211
+ return {
212
+ user,
213
+ session: {
214
+ sessionId: result.session.sessionId,
215
+ userId: user.id,
216
+ bucketId: this.basePath.split("/")[2],
217
+ createdAt: Date.now(),
218
+ expiresAt: result.session.expiresAt,
219
+ refreshToken: result.session.refreshToken,
220
+ refreshExpiresAt: result.session.expiresAt
221
+ }
222
+ };
213
223
  }
214
224
  async login(options) {
215
- const result = await this.post(`${this.basePath}/login`, options);
216
- return { user: result.user, session: result.session };
225
+ const result = await this.post(`${this.basePath}/signin`, options);
226
+ const user = result.user ?? result.data;
227
+ return {
228
+ user,
229
+ session: {
230
+ sessionId: result.session.sessionId,
231
+ userId: user.id,
232
+ bucketId: this.basePath.split("/")[2],
233
+ createdAt: Date.now(),
234
+ expiresAt: result.session.expiresAt,
235
+ refreshToken: result.session.refreshToken,
236
+ refreshExpiresAt: result.session.expiresAt
237
+ }
238
+ };
217
239
  }
218
- async logout({ sessionId }) {
219
- await this.post(`${this.basePath}/logout`, { sessionId });
240
+ async logout(options) {
241
+ await this.post(`${this.basePath}/signout`, options);
220
242
  }
221
243
  async refreshSession({ refreshToken }) {
222
- const result = await this.post(`${this.basePath}/session/refresh`, { refreshToken });
223
- return result.session;
244
+ const result = await this.post(
245
+ `${this.basePath}/session/refresh`,
246
+ { refreshToken }
247
+ );
248
+ const s = result.session;
249
+ return {
250
+ sessionId: s.sessionId,
251
+ userId: result.data?.id ?? "",
252
+ bucketId: this.basePath.split("/")[2],
253
+ createdAt: Date.now(),
254
+ expiresAt: s.expiresAt,
255
+ refreshToken: s.refreshToken,
256
+ refreshExpiresAt: s.expiresAt
257
+ };
258
+ }
259
+ async validateSession({ sessionId }) {
260
+ const result = await this.post(
261
+ `${this.basePath}/session/validate`,
262
+ { sessionId }
263
+ );
264
+ return { user: result.data, session: result.session };
224
265
  }
225
266
  // ─── User Profile ─────────────────────────────────────────────────────────
267
+ // Server: GET /auth/:bucket/user?userId=... → { success, data: UserRecord }
268
+ // Server: PATCH /auth/:bucket/user body: { sessionId, userId, updates: {...} }
269
+ // Server: DELETE /auth/:bucket/user?userId=... header/body: sessionId
226
270
  async getUser({ userId }) {
227
- const result = await this.get(`${this.basePath}/user/${userId}`);
228
- return result.user;
271
+ const result = await this.get(`${this.basePath}/user`, { userId });
272
+ return result.data;
229
273
  }
230
274
  async updateUser(options) {
231
- const { sessionId, userId, data } = options;
232
- const result = await this.patch(`${this.basePath}/user`, { sessionId, userId, ...data });
233
- return result.user;
275
+ const { sessionId, userId, updates } = options;
276
+ const result = await this.patch(
277
+ `${this.basePath}/user`,
278
+ { sessionId, userId, updates }
279
+ );
280
+ return result.data;
234
281
  }
235
282
  async deleteUser({ sessionId, userId }) {
236
- await this.delete(`${this.basePath}/user`, { sessionId, userId });
283
+ await this.http.request(`${this.basePath}/user?userId=${encodeURIComponent(userId)}`, {
284
+ method: "DELETE",
285
+ body: { sessionId },
286
+ headers: { "X-Api-Key": this.authKey }
287
+ });
237
288
  }
238
289
  // ─── Admin Operations ─────────────────────────────────────────────────────
290
+ // Server: GET /auth/:bucket/users?limit=&cursor= → { data: UserRecord[], meta: { hasMore, nextCursor } }
291
+ // Server: DELETE /auth/:bucket/users/bulk body: { userIds, hard?, sessionId }
292
+ // Server: DELETE /auth/:bucket/user/hard?userId=... body: { sessionId }
239
293
  async listUsers(options) {
240
- const { sessionId, limit = 50, offset = 0 } = options;
241
- const result = await this.post(
242
- `${this.basePath}/users/list`,
243
- { sessionId, limit, offset }
294
+ const { sessionId, limit = 50, cursor } = options;
295
+ const params = { limit: String(limit) };
296
+ if (cursor) params["cursor"] = cursor;
297
+ const result = await this.http.request(
298
+ `${this.basePath}/users?${new URLSearchParams(params)}`,
299
+ {
300
+ method: "GET",
301
+ headers: { "X-Api-Key": this.authKey, "X-Session-Id": sessionId }
302
+ }
244
303
  );
245
- return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
304
+ return {
305
+ users: result.data ?? result.users ?? [],
306
+ hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
307
+ nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? null
308
+ };
246
309
  }
247
310
  async hardDeleteUser({ sessionId, userId }) {
248
- await this.delete(`${this.basePath}/user/hard`, { sessionId, userId });
311
+ await this.http.request(
312
+ `${this.basePath}/user/hard?userId=${encodeURIComponent(userId)}`,
313
+ {
314
+ method: "DELETE",
315
+ body: { sessionId },
316
+ headers: { "X-Api-Key": this.authKey }
317
+ }
318
+ );
249
319
  }
250
- async bulkDeleteUsers({ sessionId, userIds }) {
251
- const result = await this.post(
252
- `${this.basePath}/users/bulk-delete`,
253
- { sessionId, userIds }
320
+ async bulkDeleteUsers(options) {
321
+ const result = await this.http.request(
322
+ `${this.basePath}/users/bulk`,
323
+ {
324
+ method: "DELETE",
325
+ body: { userIds: options.userIds, hard: options.hard ?? false, sessionId: options.sessionId },
326
+ headers: { "X-Api-Key": this.authKey }
327
+ }
254
328
  );
255
- return { deleted: result.deleted, failed: result.failed };
329
+ return { succeeded: result.meta.succeeded, failed: result.meta.failed };
256
330
  }
257
- async lockAccount({ sessionId, userId, duration }) {
331
+ async lockAccount(options) {
258
332
  const result = await this.post(
259
333
  `${this.basePath}/account/lock`,
260
- { sessionId, userId, duration }
334
+ options
261
335
  );
262
336
  return result.data;
263
337
  }
@@ -265,8 +339,17 @@ var AuthClient = class {
265
339
  await this.post(`${this.basePath}/account/unlock`, { sessionId, userId });
266
340
  }
267
341
  // ─── Password Management ──────────────────────────────────────────────────
342
+ // Server: POST /auth/:bucket/password/change body: { sessionId, userId, oldPassword, newPassword }
343
+ // Server: POST /auth/:bucket/password/reset/request body: { email }
344
+ // Server: POST /auth/:bucket/password/reset/confirm body: { resetToken, newPassword }
268
345
  async changePassword(options) {
269
- await this.post(`${this.basePath}/password/change`, options);
346
+ const { sessionId, userId, currentPassword, newPassword } = options;
347
+ await this.post(`${this.basePath}/password/change`, {
348
+ sessionId,
349
+ userId,
350
+ oldPassword: currentPassword,
351
+ newPassword
352
+ });
270
353
  }
271
354
  async requestPasswordReset({ email }) {
272
355
  await this.post(`${this.basePath}/password/reset/request`, { email });
@@ -275,6 +358,8 @@ var AuthClient = class {
275
358
  await this.post(`${this.basePath}/password/reset/confirm`, { resetToken, newPassword });
276
359
  }
277
360
  // ─── Email Verification ───────────────────────────────────────────────────
361
+ // Server: POST /auth/:bucket/email/verify/request body: { userId }
362
+ // Server: POST /auth/:bucket/email/verify/confirm body: { verifyToken }
278
363
  async requestEmailVerification({ userId }) {
279
364
  await this.post(`${this.basePath}/email/verify/request`, { userId });
280
365
  }
@@ -298,9 +383,8 @@ function buildQueryParams(options = {}) {
298
383
  params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
299
384
  if (options.dateRange?.end !== void 0)
300
385
  params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
301
- if (options.filters && options.filters.length > 0) {
386
+ if (options.filters && options.filters.length > 0)
302
387
  params.set("filters", JSON.stringify(options.filters));
303
- }
304
388
  const str = params.toString();
305
389
  return str ? `?${str}` : "";
306
390
  }
@@ -343,89 +427,179 @@ var RecordsClient = class {
343
427
  assertSafeName(bucketKey, "bucketKey");
344
428
  this.http = http;
345
429
  this.bucketKey = bucketKey;
346
- this.bucketKey_ = bucketSecurityKey;
430
+ this.apiKey = bucketSecurityKey;
347
431
  this.basePath = `/records/${bucketKey}`;
348
432
  }
349
- get key() {
350
- return this.bucketKey_;
351
- }
352
433
  // ─── Single Record Operations ─────────────────────────────────────────────
353
- async create(data) {
354
- const result = await this.http.post(this.basePath, this.key, data);
355
- return result.record ?? result.data;
434
+ /**
435
+ * Create a new record.
436
+ *
437
+ * @param data The record fields to store.
438
+ * @param options Optional: queryableFields (for server-side filtering),
439
+ * userEmail (audit trail), customRecordId (upsert by ID).
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const post = await posts.create(
444
+ * { title: 'Hello', status: 'draft', authorId: 'u1' },
445
+ * { queryableFields: ['status', 'authorId'], userEmail: 'alice@example.com' },
446
+ * );
447
+ * ```
448
+ */
449
+ async create(data, options = {}) {
450
+ const { queryableFields, userEmail, customRecordId } = options;
451
+ const body = { values: data };
452
+ if (queryableFields?.length) body["queryableFields"] = queryableFields;
453
+ if (userEmail) body["userEmail"] = userEmail;
454
+ if (customRecordId) body["customRecordId"] = customRecordId;
455
+ const result = await this.http.post(this.basePath, this.apiKey, body);
456
+ return result.data ?? result.record;
356
457
  }
458
+ /**
459
+ * Get a single record by ID.
460
+ */
357
461
  async get(id) {
358
- const result = await this.http.get(`${this.basePath}/${id}`, this.key);
359
- return result.record ?? result.data;
462
+ const result = await this.http.get(
463
+ `${this.basePath}/${encodeURIComponent(id)}`,
464
+ this.apiKey
465
+ );
466
+ return result.data ?? result.record;
360
467
  }
468
+ /**
469
+ * Fully replace a record (PUT semantics).
470
+ */
361
471
  async set(id, data) {
362
- const result = await this.http.put(`${this.basePath}/${id}`, this.key, data);
363
- return result.record ?? result.data;
472
+ const result = await this.http.put(
473
+ `${this.basePath}/${encodeURIComponent(id)}`,
474
+ this.apiKey,
475
+ data
476
+ );
477
+ return result.data ?? result.record;
364
478
  }
479
+ /**
480
+ * Partially update a record (PATCH semantics).
481
+ * By default merges with existing fields; set `merge: false` to replace.
482
+ * Supports write-filter sentinels: `{ __op: 'increment', delta: 1 }` etc.
483
+ */
365
484
  async patch(id, data, options = {}) {
366
485
  const { merge = true } = options;
367
486
  const result = await this.http.patch(
368
- `${this.basePath}/${id}`,
369
- this.key,
487
+ `${this.basePath}/${encodeURIComponent(id)}`,
488
+ this.apiKey,
370
489
  { ...data, _merge: merge }
371
490
  );
372
- return result.record ?? result.data;
491
+ return result.data ?? result.record;
373
492
  }
493
+ /**
494
+ * Delete a record permanently.
495
+ */
374
496
  async delete(id) {
375
- await this.http.delete(`${this.basePath}/${id}`, this.key);
497
+ await this.http.delete(
498
+ `${this.basePath}/${encodeURIComponent(id)}`,
499
+ this.apiKey
500
+ );
376
501
  }
377
502
  // ─── Batch Operations ─────────────────────────────────────────────────────
378
- async batchCreate(items) {
503
+ /**
504
+ * Create multiple records in one request (max 500).
505
+ * Each record can include a `_customRecordId` field for upsert behaviour.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * const created = await posts.batchCreate(
510
+ * [{ title: 'A' }, { title: 'B' }],
511
+ * { queryableFields: ['title'], userEmail: 'alice@example.com' },
512
+ * );
513
+ * ```
514
+ */
515
+ async batchCreate(items, options = {}) {
516
+ const { queryableFields, userEmail } = options;
517
+ const body = { records: items };
518
+ if (queryableFields?.length) body["queryableFields"] = queryableFields;
519
+ if (userEmail) body["userEmail"] = userEmail;
379
520
  const result = await this.http.post(
380
- `${this.basePath}/batch`,
381
- this.key,
382
- { records: items }
521
+ `${this.basePath}/batch/insert`,
522
+ this.apiKey,
523
+ body
383
524
  );
384
- return result.records;
525
+ return result.data ?? result.records ?? [];
385
526
  }
527
+ /**
528
+ * Delete multiple records in one request (max 500).
529
+ */
386
530
  async batchDelete(ids) {
387
531
  const result = await this.http.post(
388
532
  `${this.basePath}/batch/delete`,
389
- this.key,
390
- { ids }
533
+ this.apiKey,
534
+ { recordIds: ids }
391
535
  );
392
- return { deleted: result.deleted, failed: result.failed };
536
+ return {
537
+ deleted: result.data?.successful ?? result.deleted ?? 0,
538
+ failed: result.data?.failed ?? result.failed ?? []
539
+ };
393
540
  }
394
541
  // ─── Querying ─────────────────────────────────────────────────────────────
542
+ /**
543
+ * Query records with optional filters, sorting, and pagination.
544
+ *
545
+ * @example
546
+ * ```ts
547
+ * const { records, hasMore, nextCursor } = await posts.query({
548
+ * filters: [{ field: 'status', op: '==', value: 'published' }],
549
+ * orderBy: 'createdAt',
550
+ * order: 'desc',
551
+ * limit: 20,
552
+ * });
553
+ * ```
554
+ */
395
555
  async query(options = {}) {
396
556
  const qs = buildQueryParams(options);
397
- const result = await this.http.get(`${this.basePath}${qs}`, this.key);
398
- return {
399
- records: result.records,
400
- total: result.total,
401
- hasMore: result.hasMore,
402
- nextCursor: result.nextCursor
403
- };
557
+ const result = await this.http.get(`${this.basePath}${qs}`, this.apiKey);
558
+ const records = result.data ?? result.records ?? [];
559
+ const hasMore = result.meta?.hasMore ?? result.hasMore ?? false;
560
+ const total = result.meta?.total ?? result.total;
561
+ const nextCursor = result.meta?.nextCursor ?? result.nextCursor ?? void 0;
562
+ return { records, total, hasMore, nextCursor: nextCursor ?? void 0 };
404
563
  }
564
+ /**
565
+ * Get all records matching the given options (no filter support — use `query()` for filters).
566
+ */
405
567
  async getAll(options = {}) {
406
568
  const { records } = await this.query(options);
407
569
  return records;
408
570
  }
571
+ /**
572
+ * Count records matching the given filters.
573
+ */
409
574
  async count(filters = []) {
410
575
  const result = await this.http.post(
411
576
  `${this.basePath}/count`,
412
- this.key,
577
+ this.apiKey,
413
578
  { filters }
414
579
  );
415
- return result.count;
580
+ return result.data?.count ?? result.count ?? 0;
416
581
  }
417
582
  // ─── Version History ──────────────────────────────────────────────────────
583
+ /**
584
+ * Get the version history of a record.
585
+ */
418
586
  async getHistory(id) {
419
- const result = await this.http.get(`${this.basePath}/${id}/history`, this.key);
587
+ const result = await this.http.get(
588
+ `${this.basePath}/${encodeURIComponent(id)}?showHistory=true`,
589
+ this.apiKey
590
+ );
420
591
  return result.history;
421
592
  }
593
+ /**
594
+ * Restore a record to a previous version.
595
+ */
422
596
  async restoreVersion(id, version) {
423
597
  const result = await this.http.post(
424
- `${this.basePath}/${id}/restore`,
425
- this.key,
598
+ `${this.basePath}/${encodeURIComponent(id)}/restore`,
599
+ this.apiKey,
426
600
  { version }
427
601
  );
428
- return result.record ?? result.data;
602
+ return result.data ?? result.record;
429
603
  }
430
604
  };
431
605
 
@@ -445,39 +619,54 @@ var AnalyticsClient = class {
445
619
  );
446
620
  return result.data;
447
621
  }
622
+ // ─── Query methods ────────────────────────────────────────────────────────
623
+ /** Count all records, optionally within a date range. */
448
624
  async count(opts = {}) {
449
625
  return this.run({ queryType: "count", ...opts });
450
626
  }
627
+ /** Get value distribution for a field (e.g. how many records per status). */
451
628
  async distribution(opts) {
452
629
  assertSafeName(opts.field, "field");
453
630
  return this.run({ queryType: "distribution", ...opts });
454
631
  }
632
+ /** Sum a numeric field, optionally grouped by another field. */
455
633
  async sum(opts) {
456
634
  assertSafeName(opts.field, "field");
457
635
  if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
458
636
  return this.run({ queryType: "sum", ...opts });
459
637
  }
638
+ /** Count of records over time, bucketed by granularity. */
460
639
  async timeSeries(opts = {}) {
461
640
  return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
462
641
  }
642
+ /** Aggregate a numeric field over time. */
463
643
  async fieldTimeSeries(opts) {
464
644
  assertSafeName(opts.field, "field");
465
- return this.run({ queryType: "fieldTimeSeries", aggregation: "sum", granularity: "day", ...opts });
645
+ return this.run({
646
+ queryType: "fieldTimeSeries",
647
+ aggregation: "sum",
648
+ granularity: "day",
649
+ ...opts
650
+ });
466
651
  }
652
+ /** Top N values for a field by count. */
467
653
  async topN(opts) {
468
654
  assertSafeName(opts.field, "field");
469
655
  if (opts.labelField) assertSafeName(opts.labelField, "labelField");
470
656
  return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
471
657
  }
658
+ /** Statistical summary (min, max, avg, sum, count, stddev) for a numeric field. */
472
659
  async stats(opts) {
473
660
  assertSafeName(opts.field, "field");
474
661
  return this.run({ queryType: "stats", ...opts });
475
662
  }
663
+ /** Fetch filtered records via the analytics engine (bypasses Firestore pagination). */
476
664
  async records(opts = {}) {
477
665
  if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
478
666
  if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
479
667
  return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
480
668
  }
669
+ /** Compute multiple aggregations in a single request. */
481
670
  async multiMetric(opts) {
482
671
  opts.metrics.forEach((m) => {
483
672
  assertSafeName(m.field, "metric.field");
@@ -485,14 +674,22 @@ var AnalyticsClient = class {
485
674
  });
486
675
  return this.run({ queryType: "multiMetric", ...opts });
487
676
  }
677
+ /** Storage usage stats for the bucket (record count, bytes, avg/min/max size). */
488
678
  async storageStats(opts = {}) {
489
679
  return this.run({ queryType: "storageStats", ...opts });
490
680
  }
681
+ /**
682
+ * Compare a metric across multiple buckets in one query.
683
+ * The caller's key must have read access to every bucket in `bucketKeys`.
684
+ */
491
685
  async crossBucket(opts) {
492
686
  assertSafeName(opts.field, "field");
493
687
  opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
494
688
  return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
495
689
  }
690
+ /**
691
+ * Raw query — use this when none of the typed helpers cover your use case.
692
+ */
496
693
  async query(query) {
497
694
  const data = await this.run(query);
498
695
  return { queryType: query.queryType, data };
@@ -500,34 +697,34 @@ var AnalyticsClient = class {
500
697
  };
501
698
 
502
699
  // src/storage/manager.ts
700
+ function toUploadResult(r) {
701
+ return {
702
+ path: r.path,
703
+ mimeType: r.mimeType,
704
+ size: r.size,
705
+ isPublic: r.isPublic,
706
+ publicUrl: r.publicUrl,
707
+ downloadUrl: r.downloadUrl
708
+ };
709
+ }
503
710
  var StorageManager = class {
504
711
  constructor(http, storageKey) {
505
712
  this.basePath = "/storage";
506
713
  this.http = http;
507
714
  this.storageKey = storageKey;
508
715
  }
509
- /** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
510
716
  get authHeaders() {
511
717
  return { "X-Storage-Key": this.storageKey };
512
718
  }
513
719
  // ─── Upload: Simple (server-buffered) ────────────────────────────────────
514
720
  /**
515
721
  * Upload a file to storage in one step (server-buffered, up to 500 MB).
516
- * For files >10 MB or when you need upload progress, use `getUploadUrl()` instead.
517
- *
518
- * @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
519
- * @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
520
- * @param options Upload options: isPublic, overwrite, mimeType.
722
+ * For files >10 MB or when upload progress is needed, use `getUploadUrl()`.
521
723
  *
522
724
  * @example
523
725
  * ```ts
524
- * // Upload a public avatar
525
726
  * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
526
- * console.log(result.publicUrl); // → https://...
527
- *
528
- * // Upload a private document
529
- * const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
530
- * console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
727
+ * console.log(result.publicUrl);
531
728
  * ```
532
729
  */
533
730
  async upload(data, path, options = {}) {
@@ -545,100 +742,67 @@ var StorageManager = class {
545
742
  rawBody: formData,
546
743
  headers: this.authHeaders
547
744
  });
548
- return {
549
- path: result.path,
550
- mimeType: result.mimeType,
551
- size: result.size,
552
- isPublic: result.isPublic,
553
- publicUrl: result.publicUrl,
554
- downloadUrl: result.downloadUrl
555
- };
745
+ return toUploadResult(result);
556
746
  }
557
747
  /**
558
- * Upload raw JSON or plain text data as a file.
748
+ * Upload raw JSON or plain-text data as a file.
559
749
  *
560
750
  * @example
561
751
  * ```ts
562
- * const result = await storage.uploadRaw(
563
- * { config: { theme: 'dark' } },
564
- * 'settings/user-config.json',
565
- * { isPublic: false },
566
- * );
752
+ * await storage.uploadRaw({ theme: 'dark' }, 'settings/config.json');
567
753
  * ```
568
754
  */
569
755
  async uploadRaw(data, path, options = {}) {
570
756
  const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
571
- const body = typeof data === "string" ? data : JSON.stringify(data);
757
+ const content = typeof data === "string" ? data : JSON.stringify(data);
572
758
  const result = await this.http.request(`${this.basePath}/upload-raw`, {
573
759
  method: "POST",
574
- body: { path, data: body, mimeType, isPublic, overwrite },
760
+ body: { path, content, mimeType, isPublic, overwrite },
575
761
  headers: this.authHeaders
576
762
  });
577
- return {
578
- path: result.path,
579
- mimeType: result.mimeType,
580
- size: result.size,
581
- isPublic: result.isPublic,
582
- publicUrl: result.publicUrl,
583
- downloadUrl: result.downloadUrl
584
- };
763
+ return toUploadResult(result);
585
764
  }
586
765
  // ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
587
766
  /**
588
- * Step 1 of the recommended upload flow.
589
- * Get a signed URL to upload directly to GCS from the client (supports progress).
767
+ * Step 1 get a signed GCS URL to upload directly from the client.
768
+ * Supports upload progress via `uploadToSignedUrl()`.
590
769
  *
591
770
  * @example
592
771
  * ```ts
593
772
  * const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
594
- * path: 'videos/intro.mp4',
773
+ * path: 'videos/intro.mp4',
595
774
  * mimeType: 'video/mp4',
596
- * size: file.size,
775
+ * size: file.size,
597
776
  * isPublic: true,
598
777
  * });
599
- *
600
- * // Upload using XHR for progress tracking
601
- * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
602
- * console.log(`${pct}% uploaded`);
603
- * });
604
- *
605
- * // Step 3: confirm
778
+ * await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => console.log(pct + '%'));
606
779
  * const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
607
780
  * ```
608
781
  */
609
782
  async getUploadUrl(opts) {
783
+ const { expiresInSeconds, ...rest } = opts;
610
784
  const result = await this.http.request(`${this.basePath}/upload-url`, {
611
785
  method: "POST",
612
- body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
786
+ body: { isPublic: false, overwrite: false, expiresIn: expiresInSeconds ?? 900, ...rest },
613
787
  headers: this.authHeaders
614
788
  });
615
- return result;
789
+ return {
790
+ uploadUrl: result.uploadUrl,
791
+ path: result.path,
792
+ mimeType: result.mimeType,
793
+ expiresAt: result.expiresAt,
794
+ expiresIn: result.expiresIn
795
+ };
616
796
  }
617
797
  /**
618
- * Upload data directly to a signed GCS URL (no auth headers needed).
619
- * Optionally tracks progress via a callback.
620
- *
621
- * @param signedUrl The URL returned by `getUploadUrl()`.
622
- * @param data File data.
623
- * @param mimeType Must match what was used in `getUploadUrl()`.
624
- * @param onProgress Optional callback called with 0–100 progress percentage.
798
+ * Step 2 — upload data directly to a signed GCS URL.
799
+ * Supports progress tracking in browser environments.
625
800
  */
626
801
  async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
627
802
  await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
628
803
  }
629
804
  /**
630
- * Step 3 of the recommended upload flow.
631
- * Confirm a direct upload and register metadata on the server.
632
- *
633
- * @example
634
- * ```ts
635
- * const result = await storage.confirmUpload({
636
- * path: 'videos/intro.mp4',
637
- * mimeType: 'video/mp4',
638
- * isPublic: true,
639
- * });
640
- * console.log(result.publicUrl);
641
- * ```
805
+ * Step 3 confirm a direct upload and register metadata on the server.
642
806
  */
643
807
  async confirmUpload(opts) {
644
808
  const result = await this.http.request(`${this.basePath}/confirm`, {
@@ -646,27 +810,19 @@ var StorageManager = class {
646
810
  body: { isPublic: false, ...opts },
647
811
  headers: this.authHeaders
648
812
  });
649
- return {
650
- path: result.path,
651
- mimeType: result.mimeType,
652
- size: result.size,
653
- isPublic: result.isPublic,
654
- publicUrl: result.publicUrl,
655
- downloadUrl: result.downloadUrl
656
- };
813
+ return toUploadResult(result);
657
814
  }
658
815
  // ─── Batch Upload ─────────────────────────────────────────────────────────
659
816
  /**
660
- * Get signed upload URLs for multiple files at once.
817
+ * Get signed upload URLs for multiple files at once (max 50).
818
+ * Server returns `{ succeeded, failed }` — only succeeded items have URLs.
661
819
  *
662
820
  * @example
663
821
  * ```ts
664
822
  * const { files } = await storage.getBatchUploadUrls([
665
- * { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
666
- * { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
823
+ * { path: 'images/a.jpg', mimeType: 'image/jpeg', size: 204800 },
824
+ * { path: 'images/b.jpg', mimeType: 'image/jpeg', size: 153600 },
667
825
  * ]);
668
- *
669
- * // Upload each file and confirm
670
826
  * for (const f of files) {
671
827
  * await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
672
828
  * await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
@@ -678,37 +834,35 @@ var StorageManager = class {
678
834
  `${this.basePath}/batch-upload-urls`,
679
835
  { method: "POST", body: { files }, headers: this.authHeaders }
680
836
  );
681
- return { files: result.files };
837
+ return {
838
+ files: result.succeeded.map((f) => ({
839
+ index: f.index,
840
+ uploadUrl: f.uploadUrl,
841
+ path: f.path,
842
+ mimeType: f.mimeType,
843
+ expiresAt: f.expiresAt,
844
+ expiresIn: f.expiresIn
845
+ }))
846
+ };
682
847
  }
683
848
  /**
684
849
  * Confirm multiple direct uploads at once.
850
+ * Returns both succeeded and failed results.
685
851
  */
686
852
  async batchConfirmUploads(items) {
687
853
  const result = await this.http.request(
688
854
  `${this.basePath}/batch-confirm`,
689
855
  { method: "POST", body: { files: items }, headers: this.authHeaders }
690
856
  );
691
- return result.files.map((r) => ({
692
- path: r.path,
693
- mimeType: r.mimeType,
694
- size: r.size,
695
- isPublic: r.isPublic,
696
- publicUrl: r.publicUrl,
697
- downloadUrl: r.downloadUrl
698
- }));
857
+ return {
858
+ succeeded: result.succeeded.map(toUploadResult),
859
+ failed: result.failed
860
+ };
699
861
  }
700
862
  // ─── Download ─────────────────────────────────────────────────────────────
701
863
  /**
702
864
  * Download a private file as an ArrayBuffer.
703
865
  * For public files, use the `publicUrl` directly — no SDK needed.
704
- *
705
- * @example
706
- * ```ts
707
- * const buffer = await storage.download('docs/contract.pdf');
708
- * const blob = new Blob([buffer], { type: 'application/pdf' });
709
- * // Open in browser:
710
- * window.open(URL.createObjectURL(blob));
711
- * ```
712
866
  */
713
867
  async download(path) {
714
868
  const encoded = path.split("/").map(encodeURIComponent).join("/");
@@ -718,11 +872,16 @@ var StorageManager = class {
718
872
  });
719
873
  }
720
874
  /**
721
- * Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
875
+ * Download multiple files at once (max 20).
876
+ * Returns a structured result with succeeded and failed items.
877
+ * Each succeeded item includes the file content as a base64 string.
722
878
  *
723
879
  * @example
724
880
  * ```ts
725
- * const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
881
+ * const { succeeded, failed } = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
882
+ * for (const f of succeeded) {
883
+ * const bytes = Buffer.from(f.content, 'base64');
884
+ * }
726
885
  * ```
727
886
  */
728
887
  async batchDownload(paths) {
@@ -730,24 +889,30 @@ var StorageManager = class {
730
889
  `${this.basePath}/batch-download`,
731
890
  { method: "POST", body: { paths }, headers: this.authHeaders }
732
891
  );
733
- return result.files;
892
+ return {
893
+ succeeded: result.succeeded.map((f) => ({
894
+ index: f.index,
895
+ path: f.path,
896
+ mimeType: f.mimeType,
897
+ size: f.size,
898
+ content: f.content
899
+ })),
900
+ failed: result.failed.map((f) => ({
901
+ index: f.index,
902
+ path: f.path,
903
+ error: f.error,
904
+ code: f.code
905
+ }))
906
+ };
734
907
  }
735
908
  // ─── List ─────────────────────────────────────────────────────────────────
736
909
  /**
737
910
  * List files and folders at a given path prefix.
911
+ * Returns separate `files` and `folders` arrays for easy consumption.
738
912
  *
739
913
  * @example
740
914
  * ```ts
741
- * // List everything in the root
742
- * const { files, folders } = await storage.list();
743
- *
744
- * // List a specific folder
745
- * const { files, folders, hasMore, nextCursor } = await storage.list({
746
- * prefix: 'avatars/',
747
- * limit: 20,
748
- * });
749
- *
750
- * // Next page
915
+ * const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/', limit: 20 });
751
916
  * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
752
917
  * ```
753
918
  */
@@ -756,28 +921,38 @@ var StorageManager = class {
756
921
  if (opts.prefix) params.set("prefix", opts.prefix);
757
922
  if (opts.limit) params.set("limit", String(opts.limit));
758
923
  if (opts.cursor) params.set("cursor", opts.cursor);
759
- if (opts.recursive) params.set("recursive", "true");
760
924
  const qs = params.toString() ? `?${params}` : "";
761
925
  const result = await this.http.request(`${this.basePath}/list${qs}`, {
762
926
  method: "GET",
763
927
  headers: this.authHeaders
764
928
  });
929
+ if (result.items !== void 0) {
930
+ const files = [];
931
+ const folders = [];
932
+ for (const item of result.items) {
933
+ if (item.type === "folder") {
934
+ folders.push(item.path);
935
+ } else {
936
+ files.push(item);
937
+ }
938
+ }
939
+ return {
940
+ files,
941
+ folders,
942
+ hasMore: result.pagination?.hasMore ?? false,
943
+ nextCursor: result.pagination?.nextCursor ?? void 0
944
+ };
945
+ }
765
946
  return {
766
- files: result.files,
767
- folders: result.folders,
768
- hasMore: result.hasMore,
769
- nextCursor: result.nextCursor
947
+ files: result.files ?? [],
948
+ folders: result.folders ?? [],
949
+ hasMore: result.hasMore ?? false,
950
+ nextCursor: result.nextCursor ?? void 0
770
951
  };
771
952
  }
772
953
  // ─── Metadata ─────────────────────────────────────────────────────────────
773
954
  /**
774
- * Get metadata for a file (size, MIME type, visibility, URLs).
775
- *
776
- * @example
777
- * ```ts
778
- * const meta = await storage.getMetadata('avatars/alice.jpg');
779
- * console.log(meta.size, meta.isPublic, meta.publicUrl);
780
- * ```
955
+ * Get metadata for a file: size, MIME type, visibility, URLs.
781
956
  */
782
957
  async getMetadata(path) {
783
958
  const encoded = path.split("/").map(encodeURIComponent).join("/");
@@ -796,22 +971,15 @@ var StorageManager = class {
796
971
  updatedAt: result.updatedAt
797
972
  };
798
973
  }
799
- // ─── Signed URL (time-limited share) ─────────────────────────────────────
974
+ // ─── Signed URL ───────────────────────────────────────────────────────────
800
975
  /**
801
976
  * Generate a time-limited download URL for a private file.
802
- * The URL can be shared externally without requiring an `X-Storage-Key`.
803
- *
804
- * > **Note:** Downloads via signed URLs bypass the server, so download stats
805
- * > are NOT tracked. Use `downloadUrl` for tracked downloads.
977
+ * Can be shared externally without requiring an `X-Storage-Key`.
806
978
  *
807
- * @param path Path to the file.
808
- * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
979
+ * > Note: Downloads via signed URLs bypass the server — download stats are NOT tracked.
809
980
  *
810
- * @example
811
- * ```ts
812
- * const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
813
- * // Share signedUrl with the recipient — it expires in 30 minutes.
814
- * ```
981
+ * @param path File path.
982
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
815
983
  */
816
984
  async getSignedUrl(path, expiresIn = 3600) {
817
985
  const result = await this.http.request(`${this.basePath}/signed-url`, {
@@ -829,17 +997,6 @@ var StorageManager = class {
829
997
  // ─── Visibility ───────────────────────────────────────────────────────────
830
998
  /**
831
999
  * Change a file's visibility between public and private after upload.
832
- *
833
- * @example
834
- * ```ts
835
- * // Make a file public
836
- * const result = await storage.setVisibility('avatars/alice.jpg', true);
837
- * console.log(result.publicUrl); // CDN URL
838
- *
839
- * // Make a file private
840
- * const result = await storage.setVisibility('avatars/alice.jpg', false);
841
- * console.log(result.downloadUrl); // Auth-required URL
842
- * ```
843
1000
  */
844
1001
  async setVisibility(path, isPublic) {
845
1002
  const result = await this.http.request(`${this.basePath}/visibility`, {
@@ -855,14 +1012,6 @@ var StorageManager = class {
855
1012
  };
856
1013
  }
857
1014
  // ─── Folder Operations ────────────────────────────────────────────────────
858
- /**
859
- * Create a folder (a GCS prefix placeholder).
860
- *
861
- * @example
862
- * ```ts
863
- * await storage.createFolder('uploads/2025/');
864
- * ```
865
- */
866
1015
  async createFolder(path) {
867
1016
  const result = await this.http.request(
868
1017
  `${this.basePath}/folder`,
@@ -871,14 +1020,6 @@ var StorageManager = class {
871
1020
  return { path: result.path };
872
1021
  }
873
1022
  // ─── File Operations ──────────────────────────────────────────────────────
874
- /**
875
- * Delete a single file.
876
- *
877
- * @example
878
- * ```ts
879
- * await storage.deleteFile('avatars/old-avatar.jpg');
880
- * ```
881
- */
882
1023
  async deleteFile(path) {
883
1024
  await this.http.request(`${this.basePath}/file`, {
884
1025
  method: "DELETE",
@@ -886,14 +1027,6 @@ var StorageManager = class {
886
1027
  headers: this.authHeaders
887
1028
  });
888
1029
  }
889
- /**
890
- * Delete a folder and all its contents recursively.
891
- *
892
- * @example
893
- * ```ts
894
- * await storage.deleteFolder('temp/');
895
- * ```
896
- */
897
1030
  async deleteFolder(path) {
898
1031
  await this.http.request(`${this.basePath}/folder`, {
899
1032
  method: "DELETE",
@@ -901,17 +1034,6 @@ var StorageManager = class {
901
1034
  headers: this.authHeaders
902
1035
  });
903
1036
  }
904
- /**
905
- * Move or rename a file.
906
- *
907
- * @example
908
- * ```ts
909
- * // Rename
910
- * await storage.move('docs/draft.pdf', 'docs/final.pdf');
911
- * // Move to a different folder
912
- * await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
913
- * ```
914
- */
915
1037
  async move(from, to) {
916
1038
  const result = await this.http.request(
917
1039
  `${this.basePath}/move`,
@@ -919,14 +1041,6 @@ var StorageManager = class {
919
1041
  );
920
1042
  return { from: result.from, to: result.to };
921
1043
  }
922
- /**
923
- * Copy a file to a new path.
924
- *
925
- * @example
926
- * ```ts
927
- * await storage.copy('templates/base.html', 'sites/my-site/index.html');
928
- * ```
929
- */
930
1044
  async copy(from, to) {
931
1045
  const result = await this.http.request(
932
1046
  `${this.basePath}/copy`,
@@ -935,15 +1049,6 @@ var StorageManager = class {
935
1049
  return { from: result.from, to: result.to };
936
1050
  }
937
1051
  // ─── Stats ────────────────────────────────────────────────────────────────
938
- /**
939
- * Get storage statistics for your key: total files, bytes, operation counts.
940
- *
941
- * @example
942
- * ```ts
943
- * const stats = await storage.getStats();
944
- * console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
945
- * ```
946
- */
947
1052
  async getStats() {
948
1053
  const result = await this.http.request(`${this.basePath}/stats`, {
949
1054
  method: "GET",
@@ -952,17 +1057,10 @@ var StorageManager = class {
952
1057
  return result.stats;
953
1058
  }
954
1059
  // ─── Info (no auth) ───────────────────────────────────────────────────────
955
- /**
956
- * Ping the storage service. No authentication required.
957
- *
958
- * @example
959
- * ```ts
960
- * const info = await storage.info();
961
- * // → { ok: true, storageRoot: 'hydrous-storage' }
962
- * ```
963
- */
964
1060
  async info() {
965
- return this.http.get(`${this.basePath}/info`);
1061
+ return this.http.request(`${this.basePath}/info`, {
1062
+ method: "GET"
1063
+ });
966
1064
  }
967
1065
  };
968
1066
 
@@ -970,85 +1068,105 @@ var StorageManager = class {
970
1068
  var ScopedStorage = class _ScopedStorage {
971
1069
  constructor(manager, prefix) {
972
1070
  this.manager = manager;
973
- this.prefix = prefix.replace(/\/+$/, "") + "/";
1071
+ this.prefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
974
1072
  }
975
- scopedPath(userPath) {
976
- return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
1073
+ p(path) {
1074
+ return `${this.prefix}${path.replace(/^\/+/, "")}`;
977
1075
  }
978
- /** Upload a file within the scoped folder. */
979
- upload(data, path, options) {
980
- return this.manager.upload(data, this.scopedPath(path), options);
1076
+ // ─── Upload ───────────────────────────────────────────────────────────────
1077
+ async upload(data, path, options = {}) {
1078
+ return this.manager.upload(data, this.p(path), options);
981
1079
  }
982
- /** Upload raw JSON or text within the scoped folder. */
983
- uploadRaw(data, path, options) {
984
- return this.manager.uploadRaw(data, this.scopedPath(path), options);
1080
+ async uploadRaw(data, path, options = {}) {
1081
+ return this.manager.uploadRaw(data, this.p(path), options);
985
1082
  }
986
- /** Get a signed upload URL for a file within the scoped folder. */
987
- getUploadUrl(opts) {
988
- return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
1083
+ async getUploadUrl(opts) {
1084
+ return this.manager.getUploadUrl({ ...opts, path: this.p(opts.path) });
989
1085
  }
990
- /** Confirm a direct upload within the scoped folder. */
991
- confirmUpload(opts) {
992
- return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
1086
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
1087
+ return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
993
1088
  }
994
- /** Download a file within the scoped folder. */
995
- download(path) {
996
- return this.manager.download(this.scopedPath(path));
1089
+ async confirmUpload(opts) {
1090
+ return this.manager.confirmUpload({ ...opts, path: this.p(opts.path) });
997
1091
  }
1092
+ async getBatchUploadUrls(files) {
1093
+ return this.manager.getBatchUploadUrls(
1094
+ files.map((f) => ({ ...f, path: this.p(f.path) }))
1095
+ );
1096
+ }
1097
+ async batchConfirmUploads(items) {
1098
+ return this.manager.batchConfirmUploads(
1099
+ items.map((i) => ({ ...i, path: this.p(i.path) }))
1100
+ );
1101
+ }
1102
+ // ─── Download ─────────────────────────────────────────────────────────────
1103
+ async download(path) {
1104
+ return this.manager.download(this.p(path));
1105
+ }
1106
+ async batchDownload(paths) {
1107
+ return this.manager.batchDownload(paths.map((p) => this.p(p)));
1108
+ }
1109
+ // ─── List ─────────────────────────────────────────────────────────────────
998
1110
  /**
999
- * List files within the scoped folder.
1000
- * `prefix` in options is relative to the scope.
1111
+ * List files within this scope. The `prefix` option is relative to the scope root.
1112
+ *
1113
+ * @example
1114
+ * ```ts
1115
+ * const userDocs = storage.scope('users/alice/');
1116
+ * // Lists users/alice/docs/
1117
+ * const { files } = await userDocs.list({ prefix: 'docs/' });
1118
+ * ```
1001
1119
  */
1002
- list(opts = {}) {
1003
- const scopedOpts = {
1120
+ async list(opts = {}) {
1121
+ return this.manager.list({
1004
1122
  ...opts,
1005
- prefix: this.scopedPath(opts.prefix ?? "")
1006
- };
1007
- return this.manager.list(scopedOpts);
1123
+ prefix: this.p(opts.prefix ?? "")
1124
+ });
1125
+ }
1126
+ // ─── Metadata & URLs ──────────────────────────────────────────────────────
1127
+ async getMetadata(path) {
1128
+ return this.manager.getMetadata(this.p(path));
1008
1129
  }
1009
- /** Get metadata for a file within the scoped folder. */
1010
- getMetadata(path) {
1011
- return this.manager.getMetadata(this.scopedPath(path));
1130
+ async getSignedUrl(path, expiresIn = 3600) {
1131
+ return this.manager.getSignedUrl(this.p(path), expiresIn);
1012
1132
  }
1013
- /** Get a time-limited signed URL for a file within the scoped folder. */
1014
- getSignedUrl(path, expiresIn) {
1015
- return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
1133
+ async setVisibility(path, isPublic) {
1134
+ return this.manager.setVisibility(this.p(path), isPublic);
1016
1135
  }
1017
- /** Change visibility of a file within the scoped folder. */
1018
- setVisibility(path, isPublic) {
1019
- return this.manager.setVisibility(this.scopedPath(path), isPublic);
1136
+ // ─── Folder Operations ────────────────────────────────────────────────────
1137
+ async createFolder(path) {
1138
+ return this.manager.createFolder(this.p(path));
1020
1139
  }
1021
- /** Delete a file within the scoped folder. */
1022
- deleteFile(path) {
1023
- return this.manager.deleteFile(this.scopedPath(path));
1140
+ // ─── File Operations ──────────────────────────────────────────────────────
1141
+ async deleteFile(path) {
1142
+ return this.manager.deleteFile(this.p(path));
1024
1143
  }
1025
- /** Delete a sub-folder within the scoped folder. */
1026
- deleteFolder(path) {
1027
- return this.manager.deleteFolder(this.scopedPath(path));
1144
+ async deleteFolder(path) {
1145
+ return this.manager.deleteFolder(this.p(path));
1028
1146
  }
1029
- /** Move a file within the scoped folder. */
1030
- move(from, to) {
1031
- return this.manager.move(this.scopedPath(from), this.scopedPath(to));
1147
+ async move(from, to) {
1148
+ return this.manager.move(this.p(from), this.p(to));
1032
1149
  }
1033
- /** Copy a file within the scoped folder. */
1034
- copy(from, to) {
1035
- return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
1150
+ async copy(from, to) {
1151
+ return this.manager.copy(this.p(from), this.p(to));
1036
1152
  }
1037
- /** Create a sub-folder within the scoped folder. */
1038
- createFolder(path) {
1039
- return this.manager.createFolder(this.scopedPath(path));
1153
+ // ─── Stats ────────────────────────────────────────────────────────────────
1154
+ async getStats() {
1155
+ return this.manager.getStats();
1040
1156
  }
1157
+ // ─── Nesting ──────────────────────────────────────────────────────────────
1041
1158
  /**
1042
- * Create a further-scoped instance nested within this scope.
1159
+ * Create a deeper scope within this one.
1043
1160
  *
1044
1161
  * @example
1045
1162
  * ```ts
1046
- * const uploads = db.storage.scope('user-uploads');
1047
- * const images = uploads.scope('images'); // → "user-uploads/images/"
1163
+ * const user = storage.scope('users/alice/');
1164
+ * const userDocs = user.scope('docs/');
1165
+ * // Effective prefix: users/alice/docs/
1048
1166
  * ```
1049
1167
  */
1050
1168
  scope(subPrefix) {
1051
- return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
1169
+ return new _ScopedStorage(this.manager, this.p(subPrefix));
1052
1170
  }
1053
1171
  };
1054
1172
 
@@ -1068,22 +1186,23 @@ var HydrousClient = class {
1068
1186
  if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
1069
1187
  throw new Error("[HydrousDB] storageKeys is required. Define at least one storage key from https://hydrousdb.com/dashboard.");
1070
1188
  }
1071
- const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1072
- this.http = new HttpClient(baseUrl);
1189
+ this.http = new HttpClient(config.baseUrl ?? DEFAULT_BASE_URL);
1073
1190
  this.authKey_ = config.authKey;
1074
1191
  this.bucketSecurityKey_ = config.bucketSecurityKey;
1075
1192
  this.storageKeys_ = config.storageKeys;
1076
1193
  }
1077
- // ─── Records ─────────────────────────────────────────────────────────────
1194
+ // ─── Records ──────────────────────────────────────────────────────────────
1078
1195
  /**
1079
1196
  * Get a typed records client for the named bucket.
1080
- * Uses your `bucketSecurityKey` automatically.
1081
1197
  *
1082
1198
  * @example
1083
1199
  * ```ts
1084
1200
  * interface Post { title: string; published: boolean }
1085
1201
  * const posts = db.records<Post>('blog-posts');
1086
- * const post = await posts.create({ title: 'Hello', published: false });
1202
+ * const post = await posts.create(
1203
+ * { title: 'Hello', published: false },
1204
+ * { queryableFields: ['published'] },
1205
+ * );
1087
1206
  * ```
1088
1207
  */
1089
1208
  records(bucketKey) {
@@ -1098,7 +1217,6 @@ var HydrousClient = class {
1098
1217
  // ─── Auth ─────────────────────────────────────────────────────────────────
1099
1218
  /**
1100
1219
  * Get an auth client for the named user bucket.
1101
- * Uses your `authKey` automatically.
1102
1220
  *
1103
1221
  * @example
1104
1222
  * ```ts
@@ -1115,7 +1233,6 @@ var HydrousClient = class {
1115
1233
  // ─── Analytics ────────────────────────────────────────────────────────────
1116
1234
  /**
1117
1235
  * Get an analytics client for the named bucket.
1118
- * Uses your `bucketSecurityKey` automatically.
1119
1236
  *
1120
1237
  * @example
1121
1238
  * ```ts
@@ -1135,21 +1252,16 @@ var HydrousClient = class {
1135
1252
  // ─── Storage ──────────────────────────────────────────────────────────────
1136
1253
  /**
1137
1254
  * 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.
1255
+ * The name must match a key defined in `storageKeys` when calling `createClient`.
1140
1256
  *
1141
- * @param keyName The name of the storage key (e.g. `"avatars"`, `"documents"`, `"main"`).
1257
+ * Attach `.scope(prefix)` to namespace all operations under a path prefix.
1142
1258
  *
1143
1259
  * @example
1144
1260
  * ```ts
1145
- * const avatars = db.storage('avatars');
1146
- * const documents = db.storage('documents');
1261
+ * const avatars = db.storage('avatars');
1262
+ * const userDocs = db.storage('documents').scope(`users/${userId}/`);
1147
1263
  *
1148
- * // Upload to avatars bucket
1149
1264
  * 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
1265
  * await userDocs.upload(pdfBuffer, 'contract.pdf');
1154
1266
  * ```
1155
1267
  */