hydrousdb 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1055 +1,1179 @@
1
- 'use strict';
2
-
3
1
  // src/utils/errors.ts
4
- var HydrousDBError = class extends Error {
5
- constructor(message, code = "SDK_ERROR", status) {
2
+ var HydrousError = class extends Error {
3
+ constructor(message, code, status, requestId, details) {
6
4
  super(message);
7
- this.name = "HydrousDBError";
5
+ this.name = "HydrousError";
8
6
  this.code = code;
9
7
  this.status = status;
8
+ this.requestId = requestId;
9
+ this.details = details;
10
+ Object.setPrototypeOf(this, new.target.prototype);
11
+ }
12
+ toString() {
13
+ return `HydrousError [${this.code}]: ${this.message}`;
10
14
  }
11
15
  };
12
- function toHydrousError(err) {
13
- if (err instanceof HydrousDBError) {
14
- return { message: err.message, code: err.code, status: err.status };
16
+ var AuthError = class extends HydrousError {
17
+ constructor(message, code, status, requestId, details) {
18
+ super(message, code, status, requestId, details);
19
+ this.name = "AuthError";
15
20
  }
16
- if (err instanceof Error) {
17
- return { message: err.message, code: "UNKNOWN_ERROR" };
21
+ };
22
+ var RecordError = class extends HydrousError {
23
+ constructor(message, code, status, requestId, details) {
24
+ super(message, code, status, requestId, details);
25
+ this.name = "RecordError";
18
26
  }
19
- return { message: String(err), code: "UNKNOWN_ERROR" };
20
- }
21
- function isHydrousError(err) {
22
- return err instanceof HydrousDBError;
23
- }
27
+ };
28
+ var StorageError = class extends HydrousError {
29
+ constructor(message, code, status, requestId) {
30
+ super(message, code, status, requestId);
31
+ this.name = "StorageError";
32
+ }
33
+ };
34
+ var AnalyticsError = class extends HydrousError {
35
+ constructor(message, code, status, requestId) {
36
+ super(message, code, status, requestId);
37
+ this.name = "AnalyticsError";
38
+ }
39
+ };
40
+ var ValidationError = class extends HydrousError {
41
+ constructor(message, details) {
42
+ super(message, "VALIDATION_ERROR", 400, void 0, details);
43
+ this.name = "ValidationError";
44
+ }
45
+ };
46
+ var NetworkError = class extends HydrousError {
47
+ constructor(message, cause) {
48
+ super(message, "NETWORK_ERROR");
49
+ this.name = "NetworkError";
50
+ if (cause) this.cause = cause;
51
+ }
52
+ };
24
53
 
25
54
  // src/utils/http.ts
26
- async function parseResponse(res) {
27
- let body;
28
- try {
29
- body = await res.json();
30
- } catch (e) {
31
- if (!res.ok) throw new HydrousDBError(`HTTP ${res.status}`, "HTTP_ERROR", res.status);
32
- return void 0;
33
- }
34
- if (!res.ok) {
35
- const e = body;
36
- throw new HydrousDBError(
37
- e.error || e.message || `HTTP ${res.status}`,
38
- e.code || "HTTP_ERROR",
39
- res.status
40
- );
55
+ var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
56
+ var HttpClient = class {
57
+ constructor(baseUrl) {
58
+ this.baseUrl = baseUrl.replace(/\/$/, "");
41
59
  }
42
- return body;
43
- }
44
- function buildUrl(base, path, params) {
45
- const url = new URL(path, base.endsWith("/") ? base : base + "/");
46
- if (params) {
47
- for (const [k, v] of Object.entries(params)) {
48
- if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
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;
49
69
  }
50
- }
51
- return url.toString();
52
- }
53
- function mergeHeaders(a, b) {
54
- return { ...a, ...b };
55
- }
56
- async function readSSEStream(response, onEvent) {
57
- if (!response.body) return;
58
- const reader = response.body.getReader();
59
- const decoder = new TextDecoder();
60
- let buf = "";
61
- const flush = (chunk) => {
62
- var _a;
63
- buf += chunk;
64
- const blocks = buf.split("\n\n");
65
- buf = (_a = blocks.pop()) != null ? _a : "";
66
- for (const block of blocks) {
67
- if (!block.trim()) continue;
68
- let eventType = "message";
69
- let dataLine = null;
70
- for (const line of block.split("\n")) {
71
- if (line.startsWith("event:")) eventType = line.slice(6).trim();
72
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
73
- }
74
- if (dataLine === null) continue;
75
- try {
76
- onEvent(eventType, JSON.parse(dataLine));
77
- } catch (e) {
78
- }
70
+ const {
71
+ method = "GET",
72
+ body,
73
+ headers = {},
74
+ rawBody,
75
+ contentType = "application/json"
76
+ } = resolvedOpts;
77
+ 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";
79
89
  }
80
- };
81
- while (true) {
82
- const { done, value } = await reader.read();
83
- if (done) break;
84
- flush(decoder.decode(value, { stream: true }));
85
- }
86
- if (buf.trim()) flush("");
87
- }
88
- function parseSSEText(text, onEvent) {
89
- const blocks = text.split("\n\n");
90
- for (const block of blocks) {
91
- if (!block.trim()) continue;
92
- let eventType = "message";
93
- let dataLine = null;
94
- for (const line of block.split("\n")) {
95
- if (line.startsWith("event:")) eventType = line.slice(6).trim();
96
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
90
+ let response;
91
+ try {
92
+ response = await fetch(url, {
93
+ method,
94
+ headers: reqHeaders,
95
+ body: reqBody
96
+ });
97
+ } catch (err) {
98
+ throw new NetworkError(
99
+ `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
100
+ err
101
+ );
102
+ }
103
+ const ct = response.headers.get("content-type") ?? "";
104
+ if (!ct.includes("application/json")) {
105
+ if (!response.ok) {
106
+ throw new HydrousError(
107
+ `Request failed with status ${response.status}`,
108
+ `HTTP_${response.status}`,
109
+ response.status
110
+ );
111
+ }
112
+ const buffer = await response.arrayBuffer();
113
+ return buffer;
97
114
  }
98
- if (dataLine === null) continue;
115
+ let responseData;
99
116
  try {
100
- onEvent(eventType, JSON.parse(dataLine));
101
- } catch (e) {
117
+ responseData = await response.json();
118
+ } catch {
119
+ throw new HydrousError(
120
+ "Failed to parse server response as JSON",
121
+ "PARSE_ERROR",
122
+ response.status
123
+ );
124
+ }
125
+ if (!response.ok) {
126
+ const errData = responseData;
127
+ throw new HydrousError(
128
+ errData["error"] ?? `Request failed with status ${response.status}`,
129
+ errData["code"] ?? `HTTP_${response.status}`,
130
+ response.status,
131
+ errData["requestId"],
132
+ errData["details"]
133
+ );
102
134
  }
135
+ return responseData;
103
136
  }
104
- }
105
- function xhrUpload(url, body, headers, onProgress) {
106
- return new Promise((resolve, reject) => {
107
- const xhr = new XMLHttpRequest();
108
- xhr.open("POST", url);
109
- for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
110
- xhr.responseType = "text";
111
- if (onProgress) {
112
- xhr.upload.onprogress = (e) => {
113
- if (e.lengthComputable) onProgress(e.loaded, e.total);
114
- };
137
+ get(path, apiKey, headers) {
138
+ return this.request(path, apiKey, { method: "GET", headers });
139
+ }
140
+ post(path, apiKey, body, headers) {
141
+ return this.request(path, apiKey, { method: "POST", body, headers });
142
+ }
143
+ put(path, apiKey, body, headers) {
144
+ return this.request(path, apiKey, { method: "PUT", body, headers });
145
+ }
146
+ patch(path, apiKey, body, headers) {
147
+ return this.request(path, apiKey, { method: "PATCH", body, headers });
148
+ }
149
+ delete(path, apiKey, body, headers) {
150
+ return this.request(path, apiKey, { method: "DELETE", body, headers });
151
+ }
152
+ 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();
157
+ xhr.upload.onprogress = (e) => {
158
+ if (e.lengthComputable) {
159
+ onProgress(Math.round(e.loaded / e.total * 100));
160
+ }
161
+ };
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"));
170
+ xhr.open("PUT", signedUrl);
171
+ xhr.setRequestHeader("Content-Type", mimeType);
172
+ const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
173
+ xhr.send(payload);
174
+ });
115
175
  }
116
- xhr.onload = () => {
117
- var _a;
118
- if (xhr.status >= 200 && xhr.status < 300) {
119
- resolve(xhr.responseText);
120
- } else {
121
- try {
122
- const d = JSON.parse(xhr.responseText);
123
- reject(new HydrousDBError((_a = d.error) != null ? _a : `HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
124
- } catch (e) {
125
- reject(new HydrousDBError(`HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
126
- }
127
- }
128
- };
129
- xhr.onerror = () => reject(new HydrousDBError("Network error", "NETWORK_ERROR"));
130
- xhr.onabort = () => reject(new HydrousDBError("Upload aborted", "UPLOAD_ABORTED"));
131
- xhr.ontimeout = () => reject(new HydrousDBError("Upload timed out", "UPLOAD_TIMEOUT"));
132
- xhr.send(body);
133
- });
134
- }
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}`);
184
+ }
185
+ }
186
+ };
135
187
 
136
188
  // src/auth/client.ts
137
189
  var AuthClient = class {
138
- constructor(config) {
139
- this.session = null;
140
- this.baseUrl = config.url;
141
- this.headers = {
142
- "Content-Type": "application/json",
143
- "Authorization": `Bearer ${config.authKey}`
144
- };
190
+ constructor(http, authKey, bucketKey) {
191
+ this.http = http;
192
+ this.authKey = authKey;
193
+ this.basePath = `/auth/${bucketKey}`;
145
194
  }
146
- /** Create a new user account */
147
- async signUp(options) {
148
- try {
149
- const res = await fetch(buildUrl(this.baseUrl, "auth/signup"), {
150
- method: "POST",
151
- headers: this.headers,
152
- body: JSON.stringify(options)
153
- });
154
- const json = await parseResponse(res);
155
- this.session = json.data;
156
- return { data: json.data, error: null };
157
- } catch (err) {
158
- return { data: null, error: toHydrousError(err) };
159
- }
195
+ post(path, body) {
196
+ return this.http.post(path, this.authKey, body);
160
197
  }
161
- /** Sign in with email and password */
162
- async signIn(options) {
163
- try {
164
- const res = await fetch(buildUrl(this.baseUrl, "auth/signin"), {
165
- method: "POST",
166
- headers: this.headers,
167
- body: JSON.stringify(options)
168
- });
169
- const json = await parseResponse(res);
170
- this.session = json.data;
171
- return { data: json.data, error: null };
172
- } catch (err) {
173
- return { data: null, error: toHydrousError(err) };
174
- }
198
+ get(path) {
199
+ return this.http.get(path, this.authKey);
175
200
  }
176
- /** Sign out and invalidate the current session */
177
- async signOut() {
178
- try {
179
- const res = await fetch(buildUrl(this.baseUrl, "auth/signout"), {
180
- method: "POST",
181
- headers: mergeHeaders(this.headers, this._sessionHeader())
182
- });
183
- await parseResponse(res);
184
- this.session = null;
185
- return { data: void 0, error: null };
186
- } catch (err) {
187
- return { data: null, error: toHydrousError(err) };
188
- }
201
+ patch(path, body) {
202
+ return this.http.patch(path, this.authKey, body);
189
203
  }
190
- /** Get the currently authenticated user */
191
- async getUser() {
192
- try {
193
- const res = await fetch(buildUrl(this.baseUrl, "auth/user"), {
194
- headers: mergeHeaders(this.headers, this._sessionHeader())
195
- });
196
- const json = await parseResponse(res);
197
- return { data: json.data, error: null };
198
- } catch (err) {
199
- return { data: null, error: toHydrousError(err) };
200
- }
204
+ delete(path, body) {
205
+ return this.http.delete(path, this.authKey, body);
201
206
  }
202
- /** Refresh the access token using the stored refresh token */
203
- async refreshSession() {
204
- var _a;
205
- if (!((_a = this.session) == null ? void 0 : _a.refreshToken)) {
206
- return { data: null, error: { message: "No active session", code: "NO_SESSION" } };
207
- }
208
- try {
209
- const res = await fetch(buildUrl(this.baseUrl, "auth/refresh"), {
210
- method: "POST",
211
- headers: this.headers,
212
- body: JSON.stringify({ refreshToken: this.session.refreshToken })
213
- });
214
- const json = await parseResponse(res);
215
- this.session = json.data;
216
- return { data: json.data, error: null };
217
- } catch (err) {
218
- return { data: null, error: toHydrousError(err) };
219
- }
207
+ // ─── Registration & Login ─────────────────────────────────────────────────
208
+ async signup(options) {
209
+ const result = await this.post(`${this.basePath}/signup`, options);
210
+ return { user: result.user, session: result.session };
220
211
  }
221
- /** Return the current in-memory session (may be null) */
222
- getSession() {
223
- return this.session;
212
+ async login(options) {
213
+ const result = await this.post(`${this.basePath}/login`, options);
214
+ return { user: result.user, session: result.session };
224
215
  }
225
- _sessionHeader() {
226
- var _a;
227
- return ((_a = this.session) == null ? void 0 : _a.accessToken) ? { "X-Session-Token": this.session.accessToken } : {};
216
+ async logout({ sessionId }) {
217
+ await this.post(`${this.basePath}/logout`, { sessionId });
218
+ }
219
+ async refreshSession({ refreshToken }) {
220
+ const result = await this.post(`${this.basePath}/session/refresh`, { refreshToken });
221
+ return result.session;
222
+ }
223
+ // ─── User Profile ─────────────────────────────────────────────────────────
224
+ async getUser({ userId }) {
225
+ const result = await this.get(`${this.basePath}/user/${userId}`);
226
+ return result.user;
227
+ }
228
+ 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;
232
+ }
233
+ async deleteUser({ sessionId, userId }) {
234
+ await this.delete(`${this.basePath}/user`, { sessionId, userId });
235
+ }
236
+ // ─── Admin Operations ─────────────────────────────────────────────────────
237
+ 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 }
242
+ );
243
+ return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
244
+ }
245
+ async hardDeleteUser({ sessionId, userId }) {
246
+ await this.delete(`${this.basePath}/user/hard`, { sessionId, userId });
247
+ }
248
+ async bulkDeleteUsers({ sessionId, userIds }) {
249
+ const result = await this.post(
250
+ `${this.basePath}/users/bulk-delete`,
251
+ { sessionId, userIds }
252
+ );
253
+ return { deleted: result.deleted, failed: result.failed };
254
+ }
255
+ async lockAccount({ sessionId, userId, duration }) {
256
+ const result = await this.post(
257
+ `${this.basePath}/account/lock`,
258
+ { sessionId, userId, duration }
259
+ );
260
+ return result.data;
261
+ }
262
+ async unlockAccount({ sessionId, userId }) {
263
+ await this.post(`${this.basePath}/account/unlock`, { sessionId, userId });
264
+ }
265
+ // ─── Password Management ──────────────────────────────────────────────────
266
+ async changePassword(options) {
267
+ await this.post(`${this.basePath}/password/change`, options);
268
+ }
269
+ async requestPasswordReset({ email }) {
270
+ await this.post(`${this.basePath}/password/reset/request`, { email });
271
+ }
272
+ async confirmPasswordReset({ resetToken, newPassword }) {
273
+ await this.post(`${this.basePath}/password/reset/confirm`, { resetToken, newPassword });
274
+ }
275
+ // ─── Email Verification ───────────────────────────────────────────────────
276
+ async requestEmailVerification({ userId }) {
277
+ await this.post(`${this.basePath}/email/verify/request`, { userId });
278
+ }
279
+ async confirmEmailVerification({ verifyToken }) {
280
+ await this.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
228
281
  }
229
282
  };
230
283
 
231
284
  // src/utils/query.ts
232
- function serialiseQuery(opts = {}) {
233
- var _a;
234
- const params = {};
235
- if (opts.limit !== void 0) params["limit"] = String(opts.limit);
236
- if (opts.offset !== void 0) params["offset"] = String(opts.offset);
237
- if (opts.select && opts.select.length > 0) params["select"] = opts.select.join(",");
238
- if (opts.orderBy) {
239
- params["orderBy"] = opts.orderBy.field;
240
- params["direction"] = (_a = opts.orderBy.direction) != null ? _a : "asc";
241
- }
242
- const filters = opts.where ? Array.isArray(opts.where) ? opts.where : [opts.where] : [];
243
- if (filters.length > 0) params["where"] = JSON.stringify(filters);
244
- return params;
285
+ function buildQueryParams(options = {}) {
286
+ const params = new URLSearchParams();
287
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
288
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
289
+ if (options.orderBy !== void 0) params.set("orderBy", options.orderBy);
290
+ if (options.order !== void 0) params.set("order", options.order);
291
+ if (options.fields !== void 0) params.set("fields", options.fields);
292
+ if (options.startAfter !== void 0) params.set("startAfter", options.startAfter);
293
+ if (options.startAt !== void 0) params.set("startAt", options.startAt);
294
+ if (options.endAt !== void 0) params.set("endAt", options.endAt);
295
+ if (options.dateRange?.start !== void 0)
296
+ params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
297
+ if (options.dateRange?.end !== void 0)
298
+ params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
299
+ if (options.filters && options.filters.length > 0) {
300
+ params.set("filters", JSON.stringify(options.filters));
301
+ }
302
+ const str = params.toString();
303
+ return str ? `?${str}` : "";
304
+ }
305
+ function guessMimeType(filename) {
306
+ const ext = filename.split(".").pop()?.toLowerCase();
307
+ const map = {
308
+ jpg: "image/jpeg",
309
+ jpeg: "image/jpeg",
310
+ png: "image/png",
311
+ gif: "image/gif",
312
+ webp: "image/webp",
313
+ svg: "image/svg+xml",
314
+ pdf: "application/pdf",
315
+ mp4: "video/mp4",
316
+ webm: "video/webm",
317
+ mp3: "audio/mpeg",
318
+ wav: "audio/wav",
319
+ txt: "text/plain",
320
+ html: "text/html",
321
+ css: "text/css",
322
+ js: "application/javascript",
323
+ json: "application/json",
324
+ xml: "application/xml",
325
+ zip: "application/zip",
326
+ csv: "text/csv"
327
+ };
328
+ return map[ext ?? ""] ?? "application/octet-stream";
329
+ }
330
+ function assertSafeName(name, label = "name") {
331
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
332
+ throw new Error(
333
+ `Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
334
+ );
335
+ }
245
336
  }
246
- var eq = (field, value) => ({ field, operator: "eq", value });
247
- var neq = (field, value) => ({ field, operator: "neq", value });
248
- var gt = (field, value) => ({ field, operator: "gt", value });
249
- var lt = (field, value) => ({ field, operator: "lt", value });
250
- var gte = (field, value) => ({ field, operator: "gte", value });
251
- var lte = (field, value) => ({ field, operator: "lte", value });
252
- var inArray = (field, value) => ({ field, operator: "in", value });
253
337
 
254
338
  // src/records/client.ts
255
339
  var RecordsClient = class {
256
- constructor(config) {
257
- this.baseUrl = config.url;
258
- this.headers = {
259
- "Content-Type": "application/json",
260
- "Authorization": `Bearer ${config.bucketSecurityKey}`
261
- };
340
+ constructor(http, bucketSecurityKey, bucketKey) {
341
+ assertSafeName(bucketKey, "bucketKey");
342
+ this.http = http;
343
+ this.bucketKey = bucketKey;
344
+ this.bucketKey_ = bucketSecurityKey;
345
+ this.basePath = `/records/${bucketKey}`;
262
346
  }
263
- /** Query records from a collection */
264
- async select(collection, options = {}) {
265
- try {
266
- const url = buildUrl(this.baseUrl, `records/${collection}`, serialiseQuery(options));
267
- const res = await fetch(url, { headers: this.headers });
268
- const json = await parseResponse(res);
269
- return { data: json.data, count: json.count, error: null };
270
- } catch (err) {
271
- return { data: [], count: 0, error: toHydrousError(err) };
272
- }
347
+ get key() {
348
+ return this.bucketKey_;
273
349
  }
274
- /** Fetch a single record by ID */
275
- async get(collection, id) {
276
- try {
277
- const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), { headers: this.headers });
278
- const json = await parseResponse(res);
279
- return { data: json.data, error: null };
280
- } catch (err) {
281
- return { data: null, error: toHydrousError(err) };
282
- }
350
+ // ─── 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;
283
354
  }
284
- /** Insert one or more records */
285
- async insert(collection, payload) {
286
- try {
287
- const res = await fetch(buildUrl(this.baseUrl, `records/${collection}`), {
288
- method: "POST",
289
- headers: this.headers,
290
- body: JSON.stringify(payload)
291
- });
292
- const json = await parseResponse(res);
293
- return { data: json.data, count: json.count, error: null };
294
- } catch (err) {
295
- return { data: [], count: 0, error: toHydrousError(err) };
296
- }
355
+ async get(id) {
356
+ const result = await this.http.get(`${this.basePath}/${id}`, this.key);
357
+ return result.record ?? result.data;
297
358
  }
298
- /** Update a record by ID */
299
- async update(collection, id, payload) {
300
- try {
301
- const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), {
302
- method: "PATCH",
303
- headers: this.headers,
304
- body: JSON.stringify(payload)
305
- });
306
- const json = await parseResponse(res);
307
- return { data: json.data, error: null };
308
- } catch (err) {
309
- return { data: null, error: toHydrousError(err) };
310
- }
359
+ async set(id, data) {
360
+ const result = await this.http.put(`${this.basePath}/${id}`, this.key, data);
361
+ return result.record ?? result.data;
311
362
  }
312
- /** Delete a record by ID */
313
- async delete(collection, id) {
314
- try {
315
- const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), {
316
- method: "DELETE",
317
- headers: this.headers
318
- });
319
- await parseResponse(res);
320
- return { data: void 0, error: null };
321
- } catch (err) {
322
- return { data: null, error: toHydrousError(err) };
323
- }
363
+ async patch(id, data, options = {}) {
364
+ const { merge = true } = options;
365
+ const result = await this.http.patch(
366
+ `${this.basePath}/${id}`,
367
+ this.key,
368
+ { ...data, _merge: merge }
369
+ );
370
+ return result.record ?? result.data;
371
+ }
372
+ async delete(id) {
373
+ await this.http.delete(`${this.basePath}/${id}`, this.key);
374
+ }
375
+ // ─── Batch Operations ─────────────────────────────────────────────────────
376
+ async batchCreate(items) {
377
+ const result = await this.http.post(
378
+ `${this.basePath}/batch`,
379
+ this.key,
380
+ { records: items }
381
+ );
382
+ return result.records;
383
+ }
384
+ async batchDelete(ids) {
385
+ const result = await this.http.post(
386
+ `${this.basePath}/batch/delete`,
387
+ this.key,
388
+ { ids }
389
+ );
390
+ return { deleted: result.deleted, failed: result.failed };
391
+ }
392
+ // ─── Querying ─────────────────────────────────────────────────────────────
393
+ async query(options = {}) {
394
+ 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
+ };
402
+ }
403
+ async getAll(options = {}) {
404
+ const { records } = await this.query(options);
405
+ return records;
406
+ }
407
+ async count(filters = []) {
408
+ const result = await this.http.post(
409
+ `${this.basePath}/count`,
410
+ this.key,
411
+ { filters }
412
+ );
413
+ return result.count;
414
+ }
415
+ // ─── Version History ──────────────────────────────────────────────────────
416
+ async getHistory(id) {
417
+ const result = await this.http.get(`${this.basePath}/${id}/history`, this.key);
418
+ return result.history;
419
+ }
420
+ async restoreVersion(id, version) {
421
+ const result = await this.http.post(
422
+ `${this.basePath}/${id}/restore`,
423
+ this.key,
424
+ { version }
425
+ );
426
+ return result.record ?? result.data;
324
427
  }
325
428
  };
326
429
 
327
430
  // src/analytics/client.ts
328
431
  var AnalyticsClient = class {
329
- constructor(config) {
330
- this.baseUrl = config.url;
331
- this.headers = {
332
- "Content-Type": "application/json",
333
- "Authorization": `Bearer ${config.bucketSecurityKey}`
334
- };
432
+ constructor(http, bucketSecurityKey, bucketKey) {
433
+ assertSafeName(bucketKey, "bucketKey");
434
+ this.http = http;
435
+ this.bucketSecurityKey = bucketSecurityKey;
436
+ this.basePath = `/analytics/${bucketKey}`;
335
437
  }
336
- /** Track a single analytics event */
337
- async track(options) {
338
- var _a;
339
- try {
340
- const res = await fetch(buildUrl(this.baseUrl, "analytics/track"), {
341
- method: "POST",
342
- headers: this.headers,
343
- body: JSON.stringify({ ...options, timestamp: (_a = options.timestamp) != null ? _a : Date.now() })
344
- });
345
- await parseResponse(res);
346
- return { data: void 0, error: null };
347
- } catch (err) {
348
- return { data: null, error: toHydrousError(err) };
349
- }
438
+ async run(query) {
439
+ const result = await this.http.post(
440
+ this.basePath,
441
+ this.bucketSecurityKey,
442
+ query
443
+ );
444
+ return result.data;
350
445
  }
351
- /** Track many events in one request */
352
- async trackBatch(events) {
353
- try {
354
- const stamped = events.map((e) => {
355
- var _a;
356
- return { ...e, timestamp: (_a = e.timestamp) != null ? _a : Date.now() };
357
- });
358
- const res = await fetch(buildUrl(this.baseUrl, "analytics/track/batch"), {
359
- method: "POST",
360
- headers: this.headers,
361
- body: JSON.stringify({ events: stamped })
362
- });
363
- await parseResponse(res);
364
- return { data: void 0, error: null };
365
- } catch (err) {
366
- return { data: null, error: toHydrousError(err) };
367
- }
446
+ async count(opts = {}) {
447
+ return this.run({ queryType: "count", ...opts });
368
448
  }
369
- /** Query recorded analytics events */
370
- async query(options = {}) {
371
- try {
372
- const params = {};
373
- if (options.event) params["event"] = options.event;
374
- if (options.from) params["from"] = options.from;
375
- if (options.to) params["to"] = options.to;
376
- if (options.limit) params["limit"] = String(options.limit);
377
- if (options.groupBy) params["groupBy"] = options.groupBy;
378
- const url = buildUrl(this.baseUrl, "analytics/events", params);
379
- const res = await fetch(url, { headers: this.headers });
380
- const json = await parseResponse(res);
381
- return { data: json.data, count: json.count, error: null };
382
- } catch (err) {
383
- return { data: [], count: 0, error: toHydrousError(err) };
384
- }
449
+ async distribution(opts) {
450
+ assertSafeName(opts.field, "field");
451
+ return this.run({ queryType: "distribution", ...opts });
452
+ }
453
+ async sum(opts) {
454
+ assertSafeName(opts.field, "field");
455
+ if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
456
+ return this.run({ queryType: "sum", ...opts });
457
+ }
458
+ async timeSeries(opts = {}) {
459
+ return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
460
+ }
461
+ async fieldTimeSeries(opts) {
462
+ assertSafeName(opts.field, "field");
463
+ return this.run({ queryType: "fieldTimeSeries", aggregation: "sum", granularity: "day", ...opts });
464
+ }
465
+ async topN(opts) {
466
+ assertSafeName(opts.field, "field");
467
+ if (opts.labelField) assertSafeName(opts.labelField, "labelField");
468
+ return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
469
+ }
470
+ async stats(opts) {
471
+ assertSafeName(opts.field, "field");
472
+ return this.run({ queryType: "stats", ...opts });
473
+ }
474
+ async records(opts = {}) {
475
+ if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
476
+ if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
477
+ return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
478
+ }
479
+ async multiMetric(opts) {
480
+ opts.metrics.forEach((m) => {
481
+ assertSafeName(m.field, "metric.field");
482
+ assertSafeName(m.name, "metric.name");
483
+ });
484
+ return this.run({ queryType: "multiMetric", ...opts });
485
+ }
486
+ async storageStats(opts = {}) {
487
+ return this.run({ queryType: "storageStats", ...opts });
488
+ }
489
+ async crossBucket(opts) {
490
+ assertSafeName(opts.field, "field");
491
+ opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
492
+ return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
493
+ }
494
+ async query(query) {
495
+ const data = await this.run(query);
496
+ return { queryType: query.queryType, data };
385
497
  }
386
498
  };
387
499
 
388
- // src/storage/scoped.ts
389
- var isBrowser = typeof window !== "undefined" && typeof XMLHttpRequest !== "undefined";
390
- function storageBase(url, bucketKey) {
391
- return `${url.replace(/\/$/, "")}/storage/${encodeURIComponent(bucketKey)}`;
392
- }
393
- function storageHeaders(bucketKey) {
394
- return { "X-Storage-Key": bucketKey };
395
- }
396
- function jsonHeaders(bucketKey) {
397
- return { "X-Storage-Key": bucketKey, "Content-Type": "application/json" };
398
- }
399
- function drainSSE(raw, onProgress) {
400
- const results = [];
401
- const errors = [];
402
- parseSSEText(raw, (eventType, data) => {
403
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
404
- const d = data;
405
- if (eventType === "progress" && onProgress) {
406
- onProgress({
407
- index: (_a = d["index"]) != null ? _a : 0,
408
- total: (_b = d["total"]) != null ? _b : 1,
409
- path: (_c = d["path"]) != null ? _c : "",
410
- stage: (_d = d["stage"]) != null ? _d : "uploading",
411
- bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
412
- totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
413
- percent: (_g = d["percent"]) != null ? _g : 0,
414
- bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
415
- eta: (_i = d["eta"]) != null ? _i : null,
416
- result: d["result"],
417
- error: d["error"],
418
- code: d["code"]
419
- });
420
- }
421
- if (eventType === "done") {
422
- if (d["path"]) {
423
- results.push(d);
424
- } else if (Array.isArray(d["succeeded"])) {
425
- results.push(...d["succeeded"]);
426
- errors.push(...(_j = d["errors"]) != null ? _j : []);
427
- }
428
- }
429
- if (eventType === "error") {
430
- errors.push({
431
- path: "",
432
- error: (_k = d["error"]) != null ? _k : "Unknown error",
433
- code: (_l = d["code"]) != null ? _l : "UNKNOWN"
434
- });
435
- }
436
- });
437
- return { results, errors };
438
- }
439
- var ScopedStorageClient = class {
440
- constructor(baseUrl, keyName, bucketKey) {
441
- this.base = storageBase(baseUrl, bucketKey);
442
- this.key = bucketKey;
443
- this.keyName = keyName;
444
- }
445
- // ══════════════════════════════════════════════════════════════════════════
446
- // UPLOAD — single file
447
- // ══════════════════════════════════════════════════════════════════════════
500
+ // src/storage/manager.ts
501
+ var StorageManager = class {
502
+ constructor(http, storageKey) {
503
+ this.basePath = "/storage";
504
+ this.http = http;
505
+ this.storageKey = storageKey;
506
+ }
507
+ /** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
508
+ get authHeaders() {
509
+ return { "X-Storage-Key": this.storageKey };
510
+ }
511
+ // ─── Upload: Simple (server-buffered) ────────────────────────────────────
448
512
  /**
449
- * Upload a single file.
513
+ * 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.
450
515
  *
451
- * Supply `onProgress` to receive live upload ticks including bytes
452
- * transferred, speed (bytes/sec), ETA, and lifecycle stage.
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.
453
519
  *
454
- * **Stage sequence:**
455
- * `pending → compressing → uploading → done | error`
520
+ * @example
521
+ * ```ts
522
+ * // Upload a public avatar
523
+ * const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
524
+ * console.log(result.publicUrl); // → https://...
456
525
  *
457
- * In browsers the progress is tracked at the network level via XHR, so
458
- * `percent` reflects actual bytes leaving the device. `done` only fires
459
- * after the server confirms the write to cloud storage, so 100% is real.
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
529
+ * ```
530
+ */
531
+ async upload(data, path, options = {}) {
532
+ const { isPublic = false, overwrite = false, mimeType } = options;
533
+ const mime = mimeType ?? guessMimeType(path);
534
+ const formData = new FormData();
535
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
536
+ formData.append("file", blob, path.split("/").pop() ?? "file");
537
+ formData.append("path", path);
538
+ formData.append("mimeType", mime);
539
+ formData.append("isPublic", String(isPublic));
540
+ formData.append("overwrite", String(overwrite));
541
+ const result = await this.http.request(`${this.basePath}/upload`, {
542
+ method: "POST",
543
+ rawBody: formData,
544
+ headers: this.authHeaders
545
+ });
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
+ };
554
+ }
555
+ /**
556
+ * Upload raw JSON or plain text data as a file.
460
557
  *
461
558
  * @example
462
- * const { data, error } = await db.storage('avatars').upload(file, {
463
- * path: 'users/alice.jpg',
464
- * overwrite: true,
465
- * onProgress: (p) => {
466
- * setProgress(p.percent); // e.g. drive a <progress> bar
467
- * setSpeed(`${p.bytesPerSecond} B/s`);
468
- * setEta(`${p.eta}s remaining`);
469
- * },
559
+ * ```ts
560
+ * const result = await storage.uploadRaw(
561
+ * { config: { theme: 'dark' } },
562
+ * 'settings/user-config.json',
563
+ * { isPublic: false },
564
+ * );
565
+ * ```
566
+ */
567
+ async uploadRaw(data, path, options = {}) {
568
+ const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
569
+ const body = typeof data === "string" ? data : JSON.stringify(data);
570
+ const result = await this.http.request(`${this.basePath}/upload-raw`, {
571
+ method: "POST",
572
+ body: { path, data: body, mimeType, isPublic, overwrite },
573
+ headers: this.authHeaders
574
+ });
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
+ };
583
+ }
584
+ // ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
585
+ /**
586
+ * Step 1 of the recommended upload flow.
587
+ * Get a signed URL to upload directly to GCS from the client (supports progress).
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
592
+ * path: 'videos/intro.mp4',
593
+ * mimeType: 'video/mp4',
594
+ * size: file.size,
595
+ * isPublic: true,
470
596
  * });
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
604
+ * const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
605
+ * ```
471
606
  */
472
- async upload(file, options = {}) {
473
- var _a, _b;
474
- const { path, overwrite = false, onProgress } = options;
475
- try {
476
- const url = `${this.base}/upload`;
477
- const form = new FormData();
478
- if (file instanceof Uint8Array) {
479
- form.append("file", new Blob([file.buffer]), path != null ? path : "file");
480
- } else if (file instanceof ArrayBuffer) {
481
- form.append("file", new Blob([file]), path != null ? path : "file");
482
- } else {
483
- form.append("file", file, path != null ? path : file instanceof File ? file.name : "file");
484
- }
485
- if (path) form.append("path", path);
486
- if (overwrite) form.append("overwrite", "true");
487
- const headers = storageHeaders(this.key);
488
- if (isBrowser) {
489
- const totalBytes = file instanceof Blob ? file.size : file instanceof Uint8Array ? file.byteLength : file.byteLength;
490
- const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
491
- onProgress == null ? void 0 : onProgress({
492
- index: 0,
493
- total: 1,
494
- path: path != null ? path : "",
495
- stage: "uploading",
496
- bytesUploaded: loaded,
497
- totalBytes: total || totalBytes,
498
- percent: Math.min(99, Math.round(loaded / (total || totalBytes) * 100)),
499
- bytesPerSecond: null,
500
- eta: null
501
- });
502
- });
503
- const { results, errors } = drainSSE(rawBody, onProgress);
504
- if (errors.length > 0 && results.length === 0) {
505
- return { data: null, error: { message: errors[0].error, code: errors[0].code } };
506
- }
507
- const result = (_a = results[0]) != null ? _a : null;
508
- if (result && onProgress) {
509
- onProgress({
510
- index: 0,
511
- total: 1,
512
- path: result.path,
513
- stage: "done",
514
- bytesUploaded: totalBytes,
515
- totalBytes,
516
- percent: 100,
517
- bytesPerSecond: null,
518
- eta: 0,
519
- result
520
- });
521
- }
522
- return { data: result, error: null };
523
- }
524
- const res = await fetch(url, { method: "POST", headers, body: form });
525
- if (!res.ok) {
526
- const e = await res.json().catch(() => ({}));
527
- throw new HydrousDBError((_b = e.error) != null ? _b : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
528
- }
529
- let finalResult = null;
530
- await readSSEStream(res, (eventType, data) => {
531
- var _a2, _b2, _c, _d, _e, _f, _g, _h;
532
- const d = data;
533
- if (eventType === "progress" && onProgress) {
534
- onProgress({
535
- index: 0,
536
- total: 1,
537
- path: path != null ? path : "",
538
- stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
539
- bytesUploaded: (_b2 = d["bytesUploaded"]) != null ? _b2 : 0,
540
- totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
541
- percent: (_d = d["percent"]) != null ? _d : 0,
542
- bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
543
- eta: (_f = d["eta"]) != null ? _f : null,
544
- result: d["result"]
545
- });
546
- }
547
- if (eventType === "done") finalResult = data;
548
- if (eventType === "error") {
549
- throw new HydrousDBError(
550
- (_g = d["error"]) != null ? _g : "Upload failed",
551
- (_h = d["code"]) != null ? _h : "UPLOAD_ERROR"
552
- );
553
- }
554
- });
555
- return { data: finalResult, error: null };
556
- } catch (err) {
557
- return { data: null, error: toHydrousError(err) };
558
- }
607
+ async getUploadUrl(opts) {
608
+ const result = await this.http.request(`${this.basePath}/upload-url`, {
609
+ method: "POST",
610
+ body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
611
+ headers: this.authHeaders
612
+ });
613
+ return result;
614
+ }
615
+ /**
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.
623
+ */
624
+ async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
625
+ await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
559
626
  }
560
- // ══════════════════════════════════════════════════════════════════════════
561
- // UPLOAD TEXT / JSON
562
- // ══════════════════════════════════════════════════════════════════════════
563
627
  /**
564
- * Upload raw text or JSON content directly — no File object needed.
628
+ * Step 3 of the recommended upload flow.
629
+ * Confirm a direct upload and register metadata on the server.
565
630
  *
566
631
  * @example
567
- * // Save a JSON config
568
- * await db.storage('configs').uploadText(
569
- * 'settings/app.json',
570
- * JSON.stringify({ theme: 'dark' }),
571
- * { mimeType: 'application/json' }
572
- * );
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
+ * ```
573
640
  */
574
- async uploadText(path, content, options = {}) {
575
- var _a;
576
- const { mimeType = "text/plain", overwrite = false, onProgress } = options;
577
- try {
578
- const res = await fetch(`${this.base}/upload-raw`, {
579
- method: "POST",
580
- headers: jsonHeaders(this.key),
581
- body: JSON.stringify({ path, content, mimeType, overwrite })
582
- });
583
- if (!res.ok) {
584
- const e = await res.json().catch(() => ({}));
585
- throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
586
- }
587
- let finalResult = null;
588
- await readSSEStream(res, (eventType, data) => {
589
- var _a2, _b, _c, _d, _e, _f;
590
- const d = data;
591
- if (eventType === "progress" && onProgress) {
592
- onProgress({
593
- index: 0,
594
- total: 1,
595
- path,
596
- stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
597
- bytesUploaded: (_b = d["bytesUploaded"]) != null ? _b : 0,
598
- totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
599
- percent: (_d = d["percent"]) != null ? _d : 0,
600
- bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
601
- eta: (_f = d["eta"]) != null ? _f : null
602
- });
603
- }
604
- if (eventType === "done") finalResult = data;
605
- });
606
- return { data: finalResult, error: null };
607
- } catch (err) {
608
- return { data: null, error: toHydrousError(err) };
609
- }
641
+ async confirmUpload(opts) {
642
+ const result = await this.http.request(`${this.basePath}/confirm`, {
643
+ method: "POST",
644
+ body: { isPublic: false, ...opts },
645
+ headers: this.authHeaders
646
+ });
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
+ };
610
655
  }
611
- // ══════════════════════════════════════════════════════════════════════════
612
- // BATCH UPLOAD
613
- // ══════════════════════════════════════════════════════════════════════════
656
+ // ─── Batch Upload ─────────────────────────────────────────────────────────
614
657
  /**
615
- * Upload multiple files in one request.
658
+ * Get signed upload URLs for multiple files at once.
616
659
  *
617
- * `onProgress` fires per file — use `p.index` to identify which file.
618
- * All files receive a `pending` event upfront so you can render progress
619
- * bars immediately before any data is sent.
660
+ * @example
661
+ * ```ts
662
+ * 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 },
665
+ * ]);
666
+ *
667
+ * // Upload each file and confirm
668
+ * for (const f of files) {
669
+ * await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
670
+ * await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
671
+ * }
672
+ * ```
673
+ */
674
+ async getBatchUploadUrls(files) {
675
+ const result = await this.http.request(
676
+ `${this.basePath}/batch-upload-urls`,
677
+ { method: "POST", body: { files }, headers: this.authHeaders }
678
+ );
679
+ return { files: result.files };
680
+ }
681
+ /**
682
+ * Confirm multiple direct uploads at once.
683
+ */
684
+ async batchConfirmUploads(items) {
685
+ const result = await this.http.request(
686
+ `${this.basePath}/batch-confirm`,
687
+ { method: "POST", body: { files: items }, headers: this.authHeaders }
688
+ );
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
+ }));
697
+ }
698
+ // ─── Download ─────────────────────────────────────────────────────────────
699
+ /**
700
+ * Download a private file as an ArrayBuffer.
701
+ * 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
+ */
711
+ async download(path) {
712
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
713
+ return this.http.request(`${this.basePath}/download/${encoded}`, {
714
+ method: "GET",
715
+ headers: this.authHeaders
716
+ });
717
+ }
718
+ /**
719
+ * Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
620
720
  *
621
721
  * @example
622
- * await db.storage('documents').batchUpload(files, {
623
- * prefix: 'reports/2024/',
624
- * onProgress: (p) => updateBar(p.index, p.percent),
722
+ * ```ts
723
+ * const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
724
+ * ```
725
+ */
726
+ async batchDownload(paths) {
727
+ const result = await this.http.request(
728
+ `${this.basePath}/batch-download`,
729
+ { method: "POST", body: { paths }, headers: this.authHeaders }
730
+ );
731
+ return result.files;
732
+ }
733
+ // ─── List ─────────────────────────────────────────────────────────────────
734
+ /**
735
+ * List files and folders at a given path prefix.
736
+ *
737
+ * @example
738
+ * ```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,
625
746
  * });
747
+ *
748
+ * // Next page
749
+ * const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
750
+ * ```
626
751
  */
627
- async batchUpload(files, options = {}) {
628
- var _a;
629
- const { prefix = "", paths, overwrite = false, onProgress } = options;
630
- try {
631
- const url = `${this.base}/batch-upload`;
632
- const resolvedPaths = files.map((f, i) => {
633
- var _a2;
634
- return (_a2 = paths == null ? void 0 : paths[i]) != null ? _a2 : `${prefix}${f.name}`;
635
- });
636
- const form = new FormData();
637
- files.forEach((f) => form.append("files", f, f.name));
638
- form.append("paths", JSON.stringify(resolvedPaths));
639
- if (overwrite) form.append("overwrite", "true");
640
- const headers = storageHeaders(this.key);
641
- if (isBrowser) {
642
- const totalBytes = files.reduce((s, f) => s + f.size, 0);
643
- const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
644
- if (!onProgress) return;
645
- let cursor = 0;
646
- for (let i = 0; i < files.length; i++) {
647
- const share = files[i].size / (totalBytes || 1);
648
- const fileLoaded = Math.max(0, Math.min(
649
- files[i].size,
650
- (loaded / (total || totalBytes) - cursor) / share * files[i].size
651
- ));
652
- onProgress({
653
- index: i,
654
- total: files.length,
655
- path: resolvedPaths[i],
656
- stage: "uploading",
657
- bytesUploaded: Math.round(fileLoaded),
658
- totalBytes: files[i].size,
659
- percent: Math.min(99, Math.round(fileLoaded / files[i].size * 100)),
660
- bytesPerSecond: null,
661
- eta: null
662
- });
663
- cursor += share;
664
- }
665
- });
666
- const { results, errors } = drainSSE(rawBody, onProgress);
667
- return { data: { succeeded: results, failed: errors }, error: null };
668
- }
669
- const res = await fetch(url, { method: "POST", headers, body: form });
670
- if (!res.ok) {
671
- const e = await res.json().catch(() => ({}));
672
- throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
673
- }
674
- const succeeded = [];
675
- const failed = [];
676
- await readSSEStream(res, (eventType, data) => {
677
- var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
678
- const d = data;
679
- if (eventType === "progress" && onProgress) {
680
- onProgress({
681
- index: (_a2 = d["index"]) != null ? _a2 : 0,
682
- total: (_b = d["total"]) != null ? _b : files.length,
683
- path: (_c = d["path"]) != null ? _c : "",
684
- stage: (_d = d["stage"]) != null ? _d : "uploading",
685
- bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
686
- totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
687
- percent: (_g = d["percent"]) != null ? _g : 0,
688
- bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
689
- eta: (_i = d["eta"]) != null ? _i : null
690
- });
691
- }
692
- if (eventType === "done" && d["succeeded"]) {
693
- succeeded.push(...d["succeeded"]);
694
- failed.push(...(_j = d["errors"]) != null ? _j : []);
695
- }
696
- });
697
- return { data: { succeeded, failed }, error: null };
698
- } catch (err) {
699
- return { data: null, error: toHydrousError(err) };
700
- }
752
+ async list(opts = {}) {
753
+ const params = new URLSearchParams();
754
+ if (opts.prefix) params.set("prefix", opts.prefix);
755
+ if (opts.limit) params.set("limit", String(opts.limit));
756
+ if (opts.cursor) params.set("cursor", opts.cursor);
757
+ if (opts.recursive) params.set("recursive", "true");
758
+ const qs = params.toString() ? `?${params}` : "";
759
+ const result = await this.http.request(`${this.basePath}/list${qs}`, {
760
+ method: "GET",
761
+ headers: this.authHeaders
762
+ });
763
+ return {
764
+ files: result.files,
765
+ folders: result.folders,
766
+ hasMore: result.hasMore,
767
+ nextCursor: result.nextCursor
768
+ };
701
769
  }
702
- // ══════════════════════════════════════════════════════════════════════════
703
- // DOWNLOAD
704
- // ══════════════════════════════════════════════════════════════════════════
770
+ // ─── Metadata ─────────────────────────────────────────────────────────────
705
771
  /**
706
- * Download a single file and return its content as an `ArrayBuffer`.
772
+ * Get metadata for a file (size, MIME type, visibility, URLs).
707
773
  *
708
774
  * @example
709
- * const { data } = await db.storage('avatars').download('users/alice.jpg');
710
- * const blob = new Blob([data!]);
711
- * img.src = URL.createObjectURL(blob);
775
+ * ```ts
776
+ * const meta = await storage.getMetadata('avatars/alice.jpg');
777
+ * console.log(meta.size, meta.isPublic, meta.publicUrl);
778
+ * ```
712
779
  */
713
- async download(filePath) {
714
- var _a;
715
- try {
716
- const res = await fetch(`${this.base}/download/${filePath}`, {
717
- headers: storageHeaders(this.key)
718
- });
719
- if (!res.ok) {
720
- const e = await res.json().catch(() => ({}));
721
- throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
722
- }
723
- return { data: await res.arrayBuffer(), error: null };
724
- } catch (err) {
725
- return { data: null, error: toHydrousError(err) };
726
- }
780
+ async getMetadata(path) {
781
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
782
+ const result = await this.http.request(
783
+ `${this.basePath}/metadata/${encoded}`,
784
+ { method: "GET", headers: this.authHeaders }
785
+ );
786
+ return {
787
+ path: result.path,
788
+ size: result.size,
789
+ mimeType: result.mimeType,
790
+ isPublic: result.isPublic,
791
+ publicUrl: result.publicUrl,
792
+ downloadUrl: result.downloadUrl,
793
+ createdAt: result.createdAt,
794
+ updatedAt: result.updatedAt
795
+ };
727
796
  }
728
- // ══════════════════════════════════════════════════════════════════════════
729
- // BATCH DOWNLOAD
730
- // ══════════════════════════════════════════════════════════════════════════
797
+ // ─── Signed URL (time-limited share) ─────────────────────────────────────
731
798
  /**
732
- * Download multiple files in one request.
799
+ * 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.
733
804
  *
734
- * Set `autoSave: true` (browser only) to trigger a Save dialog per file.
805
+ * @param path Path to the file.
806
+ * @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
735
807
  *
736
808
  * @example
737
- * const { data } = await db.storage('reports').batchDownload(
738
- * ['jan.pdf', 'feb.pdf'],
739
- * { autoSave: true, onProgress: (p) => console.log(p.path, p.status) }
740
- * );
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
+ * ```
741
813
  */
742
- async batchDownload(filePaths, options = {}) {
743
- var _a;
744
- const { concurrency = 5, onProgress, autoSave = false } = options;
745
- try {
746
- const res = await fetch(`${this.base}/batch-download`, {
747
- method: "POST",
748
- headers: jsonHeaders(this.key),
749
- body: JSON.stringify({ paths: filePaths, concurrency })
750
- });
751
- if (!res.ok) {
752
- const e = await res.json().catch(() => ({}));
753
- throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
754
- }
755
- const downloadedFiles = [];
756
- await readSSEStream(res, (eventType, data) => {
757
- var _a2, _b, _c, _d, _e, _f, _g, _h;
758
- const d = data;
759
- if (eventType === "file") {
760
- const base64 = d["content"];
761
- const mimeType = (_a2 = d["mimeType"]) != null ? _a2 : "application/octet-stream";
762
- const path = (_b = d["path"]) != null ? _b : "";
763
- const size = (_c = d["size"]) != null ? _c : 0;
764
- const index = (_d = d["index"]) != null ? _d : 0;
765
- const binary = atob(base64);
766
- const bytes = new Uint8Array(binary.length);
767
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
768
- downloadedFiles.push({ path, content: bytes.buffer, mimeType, size });
769
- onProgress == null ? void 0 : onProgress({ index, total: filePaths.length, path, status: "success", size, mimeType });
770
- if (autoSave && isBrowser) {
771
- const blob = new Blob([bytes.buffer], { type: mimeType });
772
- const blobUrl = URL.createObjectURL(blob);
773
- const a = document.createElement("a");
774
- a.href = blobUrl;
775
- a.download = (_e = path.split("/").pop()) != null ? _e : "download";
776
- a.click();
777
- setTimeout(() => URL.revokeObjectURL(blobUrl), 5e3);
778
- }
779
- }
780
- if (eventType === "error" && onProgress) {
781
- const index = (_f = d["index"]) != null ? _f : 0;
782
- onProgress({
783
- index,
784
- total: filePaths.length,
785
- path: (_g = filePaths[index]) != null ? _g : "",
786
- status: "error",
787
- error: (_h = d["error"]) != null ? _h : "Download failed"
788
- });
789
- }
790
- });
791
- return { data: downloadedFiles, error: null };
792
- } catch (err) {
793
- return { data: null, error: toHydrousError(err) };
794
- }
814
+ async getSignedUrl(path, expiresIn = 3600) {
815
+ const result = await this.http.request(`${this.basePath}/signed-url`, {
816
+ method: "POST",
817
+ body: { path, expiresIn },
818
+ headers: this.authHeaders
819
+ });
820
+ return {
821
+ signedUrl: result.signedUrl,
822
+ expiresAt: result.expiresAt,
823
+ expiresIn: result.expiresIn,
824
+ path: result.path
825
+ };
795
826
  }
796
- // ══════════════════════════════════════════════════════════════════════════
797
- // LIST
798
- // ══════════════════════════════════════════════════════════════════════════
827
+ // ─── Visibility ───────────────────────────────────────────────────────────
799
828
  /**
800
- * List files and folders (paginated).
829
+ * Change a file's visibility between public and private after upload.
801
830
  *
802
831
  * @example
803
- * const { data } = await db.storage('avatars').list({ prefix: 'users/' });
804
- * for (const item of data!.items) {
805
- * console.log(item.type, item.path, item.size);
806
- * }
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
+ * ```
807
841
  */
808
- async list(options = {}) {
809
- const { prefix = "", limit = 50, cursor } = options;
810
- try {
811
- const url = buildUrl(this.base, "list", {
812
- prefix: prefix || void 0,
813
- limit,
814
- cursor: cursor || void 0
815
- });
816
- const res = await fetch(url.replace(this.base + "/list", `${this.base}/list`), {
817
- headers: storageHeaders(this.key)
818
- });
819
- const u = `${this.base}/list?${new URLSearchParams(
820
- Object.entries({ prefix: prefix || "", limit: String(limit), ...cursor ? { cursor } : {} }).filter(([, v]) => v !== "")
821
- ).toString()}`;
822
- const r = await fetch(u, { headers: storageHeaders(this.key) });
823
- const json = await parseResponse(r);
824
- return { data: json, error: null };
825
- } catch (err) {
826
- return { data: null, error: toHydrousError(err) };
827
- }
842
+ async setVisibility(path, isPublic) {
843
+ const result = await this.http.request(`${this.basePath}/visibility`, {
844
+ method: "PATCH",
845
+ body: { path, isPublic },
846
+ headers: this.authHeaders
847
+ });
848
+ return {
849
+ path: result.path,
850
+ isPublic: result.isPublic,
851
+ publicUrl: result.publicUrl,
852
+ downloadUrl: result.downloadUrl
853
+ };
828
854
  }
829
- // ══════════════════════════════════════════════════════════════════════════
830
- // METADATA
831
- // ══════════════════════════════════════════════════════════════════════════
855
+ // ─── Folder Operations ────────────────────────────────────────────────────
832
856
  /**
833
- * Get metadata for a file (size, MIME type, compression, etc.)
857
+ * Create a folder (a GCS prefix placeholder).
834
858
  *
835
859
  * @example
836
- * const { data: meta } = await db.storage('docs').metadata('report.pdf');
837
- * console.log(meta!.size, meta!.mimeType, meta!.isCompressed);
860
+ * ```ts
861
+ * await storage.createFolder('uploads/2025/');
862
+ * ```
838
863
  */
839
- async metadata(filePath) {
840
- try {
841
- const res = await fetch(`${this.base}/metadata/${filePath}`, {
842
- headers: storageHeaders(this.key)
843
- });
844
- const json = await parseResponse(res);
845
- return { data: json.data, error: null };
846
- } catch (err) {
847
- return { data: null, error: toHydrousError(err) };
848
- }
864
+ async createFolder(path) {
865
+ const result = await this.http.request(
866
+ `${this.basePath}/folder`,
867
+ { method: "POST", body: { path }, headers: this.authHeaders }
868
+ );
869
+ return { path: result.path };
849
870
  }
850
- // ══════════════════════════════════════════════════════════════════════════
851
- // DELETE
852
- // ══════════════════════════════════════════════════════════════════════════
853
- /** Delete a single file */
854
- async deleteFile(filePath) {
855
- try {
856
- const res = await fetch(`${this.base}/file`, {
857
- method: "DELETE",
858
- headers: jsonHeaders(this.key),
859
- body: JSON.stringify({ path: filePath })
860
- });
861
- await parseResponse(res);
862
- return { data: void 0, error: null };
863
- } catch (err) {
864
- return { data: null, error: toHydrousError(err) };
865
- }
871
+ // ─── File Operations ──────────────────────────────────────────────────────
872
+ /**
873
+ * Delete a single file.
874
+ *
875
+ * @example
876
+ * ```ts
877
+ * await storage.deleteFile('avatars/old-avatar.jpg');
878
+ * ```
879
+ */
880
+ async deleteFile(path) {
881
+ await this.http.request(`${this.basePath}/file`, {
882
+ method: "DELETE",
883
+ body: { path },
884
+ headers: this.authHeaders
885
+ });
866
886
  }
867
- /** Recursively delete a folder and all its contents */
868
- async deleteFolder(folderPath) {
869
- try {
870
- const res = await fetch(`${this.base}/folder`, {
871
- method: "DELETE",
872
- headers: jsonHeaders(this.key),
873
- body: JSON.stringify({ path: folderPath })
874
- });
875
- await parseResponse(res);
876
- return { data: void 0, error: null };
877
- } catch (err) {
878
- return { data: null, error: toHydrousError(err) };
879
- }
887
+ /**
888
+ * Delete a folder and all its contents recursively.
889
+ *
890
+ * @example
891
+ * ```ts
892
+ * await storage.deleteFolder('temp/');
893
+ * ```
894
+ */
895
+ async deleteFolder(path) {
896
+ await this.http.request(`${this.basePath}/folder`, {
897
+ method: "DELETE",
898
+ body: { path },
899
+ headers: this.authHeaders
900
+ });
880
901
  }
881
- /** Create an empty folder */
882
- async createFolder(folderPath) {
883
- try {
884
- const res = await fetch(`${this.base}/folder`, {
885
- method: "POST",
886
- headers: jsonHeaders(this.key),
887
- body: JSON.stringify({ path: folderPath })
888
- });
889
- await parseResponse(res);
890
- return { data: void 0, error: null };
891
- } catch (err) {
892
- return { data: null, error: toHydrousError(err) };
893
- }
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
+ async move(from, to) {
914
+ const result = await this.http.request(
915
+ `${this.basePath}/move`,
916
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
917
+ );
918
+ return { from: result.from, to: result.to };
894
919
  }
895
- // ══════════════════════════════════════════════════════════════════════════
896
- // MOVE & COPY
897
- // ══════════════════════════════════════════════════════════════════════════
898
- /** Move (rename) a file */
899
- async move(fromPath, toPath) {
900
- try {
901
- const res = await fetch(`${this.base}/move`, {
902
- method: "POST",
903
- headers: jsonHeaders(this.key),
904
- body: JSON.stringify({ from: fromPath, to: toPath })
905
- });
906
- await parseResponse(res);
907
- return { data: void 0, error: null };
908
- } catch (err) {
909
- return { data: null, error: toHydrousError(err) };
910
- }
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
+ async copy(from, to) {
929
+ const result = await this.http.request(
930
+ `${this.basePath}/copy`,
931
+ { method: "POST", body: { from, to }, headers: this.authHeaders }
932
+ );
933
+ return { from: result.from, to: result.to };
911
934
  }
912
- /** Copy a file (original is kept) */
913
- async copy(fromPath, toPath) {
914
- try {
915
- const res = await fetch(`${this.base}/copy`, {
916
- method: "POST",
917
- headers: jsonHeaders(this.key),
918
- body: JSON.stringify({ from: fromPath, to: toPath })
919
- });
920
- await parseResponse(res);
921
- return { data: void 0, error: null };
922
- } catch (err) {
923
- return { data: null, error: toHydrousError(err) };
924
- }
935
+ // ─── 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
+ async getStats() {
946
+ const result = await this.http.request(`${this.basePath}/stats`, {
947
+ method: "GET",
948
+ headers: this.authHeaders
949
+ });
950
+ return result.stats;
925
951
  }
926
- // ══════════════════════════════════════════════════════════════════════════
927
- // SIGNED URL
928
- // ══════════════════════════════════════════════════════════════════════════
952
+ // ─── Info (no auth) ───────────────────────────────────────────────────────
929
953
  /**
930
- * Generate a time-limited public URL for a private file.
954
+ * Ping the storage service. No authentication required.
931
955
  *
932
956
  * @example
933
- * const { data } = await db.storage('contracts').signedUrl('nda.pdf', { expiresIn: 300 });
934
- * console.log(data!.signedUrl); // share this
957
+ * ```ts
958
+ * const info = await storage.info();
959
+ * // → { ok: true, storageRoot: 'hydrous-storage' }
960
+ * ```
935
961
  */
936
- async signedUrl(filePath, options = {}) {
937
- const { expiresIn = 3600 } = options;
938
- try {
939
- const res = await fetch(`${this.base}/signed-url`, {
940
- method: "POST",
941
- headers: jsonHeaders(this.key),
942
- body: JSON.stringify({ path: filePath, expiresInSeconds: expiresIn })
943
- });
944
- const json = await parseResponse(res);
945
- return { data: json, error: null };
946
- } catch (err) {
947
- return { data: null, error: toHydrousError(err) };
948
- }
962
+ async info() {
963
+ return this.http.get(`${this.basePath}/info`);
964
+ }
965
+ };
966
+
967
+ // src/storage/scoped.ts
968
+ var ScopedStorage = class _ScopedStorage {
969
+ constructor(manager, prefix) {
970
+ this.manager = manager;
971
+ this.prefix = prefix.replace(/\/+$/, "") + "/";
972
+ }
973
+ scopedPath(userPath) {
974
+ return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
975
+ }
976
+ /** Upload a file within the scoped folder. */
977
+ upload(data, path, options) {
978
+ return this.manager.upload(data, this.scopedPath(path), options);
979
+ }
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);
983
+ }
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) });
987
+ }
988
+ /** Confirm a direct upload within the scoped folder. */
989
+ confirmUpload(opts) {
990
+ return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
991
+ }
992
+ /** Download a file within the scoped folder. */
993
+ download(path) {
994
+ return this.manager.download(this.scopedPath(path));
995
+ }
996
+ /**
997
+ * List files within the scoped folder.
998
+ * `prefix` in options is relative to the scope.
999
+ */
1000
+ list(opts = {}) {
1001
+ const scopedOpts = {
1002
+ ...opts,
1003
+ prefix: this.scopedPath(opts.prefix ?? "")
1004
+ };
1005
+ return this.manager.list(scopedOpts);
1006
+ }
1007
+ /** Get metadata for a file within the scoped folder. */
1008
+ getMetadata(path) {
1009
+ return this.manager.getMetadata(this.scopedPath(path));
1010
+ }
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);
1014
+ }
1015
+ /** Change visibility of a file within the scoped folder. */
1016
+ setVisibility(path, isPublic) {
1017
+ return this.manager.setVisibility(this.scopedPath(path), isPublic);
1018
+ }
1019
+ /** Delete a file within the scoped folder. */
1020
+ deleteFile(path) {
1021
+ return this.manager.deleteFile(this.scopedPath(path));
1022
+ }
1023
+ /** Delete a sub-folder within the scoped folder. */
1024
+ deleteFolder(path) {
1025
+ return this.manager.deleteFolder(this.scopedPath(path));
1026
+ }
1027
+ /** Move a file within the scoped folder. */
1028
+ move(from, to) {
1029
+ return this.manager.move(this.scopedPath(from), this.scopedPath(to));
1030
+ }
1031
+ /** Copy a file within the scoped folder. */
1032
+ copy(from, to) {
1033
+ return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
1034
+ }
1035
+ /** Create a sub-folder within the scoped folder. */
1036
+ createFolder(path) {
1037
+ return this.manager.createFolder(this.scopedPath(path));
949
1038
  }
950
- // ══════════════════════════════════════════════════════════════════════════
951
- // STATS
952
- // ══════════════════════════════════════════════════════════════════════════
953
1039
  /**
954
- * Get usage and billing stats for this storage key.
1040
+ * Create a further-scoped instance nested within this scope.
955
1041
  *
956
1042
  * @example
957
- * const { data } = await db.storage('main').stats();
958
- * console.log(data!.totalFiles, data!.totalSizeBytes);
1043
+ * ```ts
1044
+ * const uploads = db.storage.scope('user-uploads');
1045
+ * const images = uploads.scope('images'); // → "user-uploads/images/"
1046
+ * ```
959
1047
  */
960
- async stats() {
961
- try {
962
- const res = await fetch(`${this.base}/stats`, {
963
- headers: storageHeaders(this.key)
964
- });
965
- const json = await parseResponse(res);
966
- return { data: json.data, error: null };
967
- } catch (err) {
968
- return { data: null, error: toHydrousError(err) };
969
- }
1048
+ scope(subPrefix) {
1049
+ return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
970
1050
  }
971
1051
  };
972
1052
 
973
- // src/storage/manager.ts
974
- var StorageManager = class {
1053
+ // src/client.ts
1054
+ var HydrousClient = class {
975
1055
  constructor(config) {
976
- this.cache = /* @__PURE__ */ new Map();
977
- this.baseUrl = config.url;
978
- this._keys = config.storageKeys;
1056
+ this._recordsCache = /* @__PURE__ */ new Map();
1057
+ this._authCache = /* @__PURE__ */ new Map();
1058
+ this._analyticsCache = /* @__PURE__ */ new Map();
1059
+ this._storageCache = /* @__PURE__ */ new Map();
1060
+ if (!config.authKey) {
1061
+ throw new Error("[HydrousDB] authKey is required. Get yours from https://hydrousdb.com/dashboard.");
1062
+ }
1063
+ if (!config.bucketSecurityKey) {
1064
+ throw new Error("[HydrousDB] bucketSecurityKey is required. Get yours from https://hydrousdb.com/dashboard.");
1065
+ }
1066
+ if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
1067
+ throw new Error("[HydrousDB] storageKeys is required. Define at least one storage key from https://hydrousdb.com/dashboard.");
1068
+ }
1069
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1070
+ this.http = new HttpClient(baseUrl);
1071
+ this.authKey_ = config.authKey;
1072
+ this.bucketSecurityKey_ = config.bucketSecurityKey;
1073
+ this.storageKeys_ = config.storageKeys;
979
1074
  }
1075
+ // ─── Records ─────────────────────────────────────────────────────────────
980
1076
  /**
981
- * Get a storage client scoped to a named key.
982
- *
983
- * @param keyName - Must match a property you declared in `storageKeys`
1077
+ * Get a typed records client for the named bucket.
1078
+ * Uses your `bucketSecurityKey` automatically.
984
1079
  *
985
1080
  * @example
986
- * const avatarStore = db.storage('avatars');
987
- * const documentStore = db.storage('documents');
988
- *
989
- * await avatarStore.upload(file, { path: 'users/alice.jpg' });
990
- * const list = await documentStore.list({ prefix: 'invoices/' });
1081
+ * ```ts
1082
+ * interface Post { title: string; published: boolean }
1083
+ * const posts = db.records<Post>('blog-posts');
1084
+ * const post = await posts.create({ title: 'Hello', published: false });
1085
+ * ```
991
1086
  */
992
- use(keyName) {
993
- const bucketKey = this._keys[keyName];
994
- if (!bucketKey) {
995
- const available = Object.keys(this._keys).join(", ");
996
- throw new HydrousDBError(
997
- `Storage key "${keyName}" is not defined.
998
- Available: ${available || "(none)"}`,
999
- "UNKNOWN_STORAGE_KEY"
1087
+ records(bucketKey) {
1088
+ if (!this._recordsCache.has(bucketKey)) {
1089
+ this._recordsCache.set(
1090
+ bucketKey,
1091
+ new RecordsClient(this.http, this.bucketSecurityKey_, bucketKey)
1000
1092
  );
1001
1093
  }
1002
- if (!this.cache.has(keyName)) {
1003
- this.cache.set(keyName, new ScopedStorageClient(this.baseUrl, keyName, bucketKey));
1094
+ return this._recordsCache.get(bucketKey);
1095
+ }
1096
+ // ─── Auth ─────────────────────────────────────────────────────────────────
1097
+ /**
1098
+ * Get an auth client for the named user bucket.
1099
+ * Uses your `authKey` automatically.
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * const auth = db.auth('app-users');
1104
+ * const { user, session } = await auth.login({ email: '…', password: '…' });
1105
+ * ```
1106
+ */
1107
+ auth(bucketKey) {
1108
+ if (!this._authCache.has(bucketKey)) {
1109
+ this._authCache.set(bucketKey, new AuthClient(this.http, this.authKey_, bucketKey));
1004
1110
  }
1005
- return this.cache.get(keyName);
1111
+ return this._authCache.get(bucketKey);
1006
1112
  }
1007
- /** Return the names of all configured storage keys */
1008
- keyNames() {
1009
- return Object.keys(this._keys);
1113
+ // ─── Analytics ────────────────────────────────────────────────────────────
1114
+ /**
1115
+ * Get an analytics client for the named bucket.
1116
+ * Uses your `bucketSecurityKey` automatically.
1117
+ *
1118
+ * @example
1119
+ * ```ts
1120
+ * const analytics = db.analytics('orders');
1121
+ * const { count } = await analytics.count();
1122
+ * ```
1123
+ */
1124
+ analytics(bucketKey) {
1125
+ if (!this._analyticsCache.has(bucketKey)) {
1126
+ this._analyticsCache.set(
1127
+ bucketKey,
1128
+ new AnalyticsClient(this.http, this.bucketSecurityKey_, bucketKey)
1129
+ );
1130
+ }
1131
+ return this._analyticsCache.get(bucketKey);
1010
1132
  }
1011
- };
1012
-
1013
- // src/client.ts
1014
- var HydrousClient = class {
1015
- constructor(config) {
1016
- if (!config.url) throw new Error("[HydrousDB] config.url is required");
1017
- if (!config.authKey) throw new Error("[HydrousDB] config.authKey is required");
1018
- if (!config.bucketSecurityKey) throw new Error("[HydrousDB] config.bucketSecurityKey is required");
1019
- if (!config.storageKeys || typeof config.storageKeys !== "object") {
1020
- throw new Error("[HydrousDB] config.storageKeys must be an object of named keys");
1133
+ // ─── Storage ──────────────────────────────────────────────────────────────
1134
+ /**
1135
+ * Get a storage manager for the named storage key.
1136
+ * The name must match a key you defined in `storageKeys` when calling `createClient`.
1137
+ * Uses the corresponding `ssk_…` key automatically via `X-Storage-Key` header.
1138
+ *
1139
+ * @param keyName The name of the storage key (e.g. `"avatars"`, `"documents"`, `"main"`).
1140
+ *
1141
+ * @example
1142
+ * ```ts
1143
+ * const avatars = db.storage('avatars');
1144
+ * const documents = db.storage('documents');
1145
+ *
1146
+ * // Upload to avatars bucket
1147
+ * await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
1148
+ *
1149
+ * // Scope to a sub-folder
1150
+ * const userDocs = db.storage('documents').scope(`users/${userId}`);
1151
+ * await userDocs.upload(pdfBuffer, 'contract.pdf');
1152
+ * ```
1153
+ */
1154
+ storage(keyName) {
1155
+ const ssk = this.storageKeys_[keyName];
1156
+ if (!ssk) {
1157
+ const available = Object.keys(this.storageKeys_).join(", ");
1158
+ throw new Error(
1159
+ `[HydrousDB] Unknown storage key name "${keyName}". Available keys: ${available}. Add it to storageKeys in your createClient() config.`
1160
+ );
1161
+ }
1162
+ if (!this._storageCache.has(keyName)) {
1163
+ this._storageCache.set(keyName, new StorageManager(this.http, ssk));
1021
1164
  }
1022
- this.auth = new AuthClient(config);
1023
- this.records = new RecordsClient(config);
1024
- this.analytics = new AnalyticsClient(config);
1025
- const manager = new StorageManager(config);
1026
- const fn = (keyName) => manager.use(keyName);
1027
- fn.use = (keyName) => manager.use(keyName);
1028
- fn.keyNames = () => manager.keyNames();
1029
- this.storage = fn;
1165
+ const mgr = this._storageCache.get(keyName);
1166
+ const extended = mgr;
1167
+ if (!extended.scope) {
1168
+ extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
1169
+ }
1170
+ return extended;
1030
1171
  }
1031
1172
  };
1032
-
1033
- // src/index.ts
1034
1173
  function createClient(config) {
1035
1174
  return new HydrousClient(config);
1036
1175
  }
1037
1176
 
1038
- exports.AnalyticsClient = AnalyticsClient;
1039
- exports.AuthClient = AuthClient;
1040
- exports.HydrousClient = HydrousClient;
1041
- exports.HydrousDBError = HydrousDBError;
1042
- exports.RecordsClient = RecordsClient;
1043
- exports.ScopedStorageClient = ScopedStorageClient;
1044
- exports.StorageManager = StorageManager;
1045
- exports.createClient = createClient;
1046
- exports.eq = eq;
1047
- exports.gt = gt;
1048
- exports.gte = gte;
1049
- exports.inArray = inArray;
1050
- exports.isHydrousError = isHydrousError;
1051
- exports.lt = lt;
1052
- exports.lte = lte;
1053
- exports.neq = neq;
1177
+ export { AnalyticsClient, AnalyticsError, AuthClient, AuthError, HydrousClient, HydrousError, NetworkError, RecordError, RecordsClient, ScopedStorage, StorageError, StorageManager, ValidationError, createClient };
1054
1178
  //# sourceMappingURL=index.js.map
1055
1179
  //# sourceMappingURL=index.js.map