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