hydrousdb 1.1.1 → 2.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,799 +1,1289 @@
1
1
  'use strict';
2
2
 
3
3
  // src/utils/errors.ts
4
- var HydrousError = class extends Error {
5
- constructor(body, status) {
6
- super(body.error);
7
- this.name = "HydrousError";
4
+ var HydrousSDKError = class extends Error {
5
+ constructor(message, code = "SDK_ERROR", status) {
6
+ super(message);
7
+ this.name = "HydrousSDKError";
8
+ this.code = code;
8
9
  this.status = status;
9
- this.code = body.code;
10
- this.details = body.details ?? [];
11
- this.requestId = body.requestId;
12
10
  }
13
11
  };
14
- var HydrousNetworkError = class extends Error {
15
- constructor(message, cause) {
16
- super(message);
17
- this.name = "HydrousNetworkError";
18
- this.cause = cause;
12
+ function toHydrousError(err) {
13
+ if (err instanceof HydrousSDKError) {
14
+ return { message: err.message, code: err.code, status: err.status };
19
15
  }
20
- };
21
- var HydrousTimeoutError = class extends Error {
22
- constructor(timeoutMs) {
23
- super(`Request timed out after ${timeoutMs}ms`);
24
- this.name = "HydrousTimeoutError";
16
+ if (err instanceof Error) {
17
+ return { message: err.message, code: "UNKNOWN_ERROR" };
25
18
  }
26
- };
19
+ return { message: String(err), code: "UNKNOWN_ERROR" };
20
+ }
21
+ function isHydrousError(err) {
22
+ return err instanceof HydrousSDKError;
23
+ }
27
24
 
28
25
  // src/utils/http.ts
29
- var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
30
- var DEFAULT_TIMEOUT = 3e4;
31
- var DEFAULT_RETRIES = 2;
32
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
33
- var HttpClient = class {
34
- constructor(config) {
35
- this.authKey = config.authKey;
36
- this.bucketKey = config.bucketKey;
37
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
38
- this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
39
- this.retries = config.retries ?? DEFAULT_RETRIES;
40
- }
41
- // ── Core request ────────────────────────────────────────────────────────────
42
- async request(method, path, options) {
43
- const url = this.buildUrl(path, options?.params);
44
- const headers = {
45
- "Content-Type": "application/json",
46
- ...options?.headers
47
- };
48
- const init = {
49
- method,
50
- headers,
51
- signal: this.buildSignal(options?.signal)
52
- };
53
- if (options?.body !== void 0) {
54
- init.body = JSON.stringify(options.body);
26
+ async function parseResponse(res) {
27
+ let body;
28
+ try {
29
+ body = await res.json();
30
+ } catch (e) {
31
+ if (!res.ok) {
32
+ throw new HydrousSDKError(`HTTP ${res.status}`, "HTTP_ERROR", res.status);
55
33
  }
56
- return this.executeWithRetry(url, init, options?.raw ?? false, this.retries);
34
+ return void 0;
57
35
  }
58
- // ── Convenience methods ──────────────────────────────────────────────────────
59
- get(path, params, opts) {
60
- return this.request("GET", path, { params, ...opts });
61
- }
62
- post(path, body, opts) {
63
- return this.request("POST", path, { body, ...opts });
64
- }
65
- patch(path, body, opts) {
66
- return this.request("PATCH", path, { body, ...opts });
67
- }
68
- delete(path, params, opts) {
69
- return this.request("DELETE", path, { params, ...opts });
70
- }
71
- head(path, params, opts) {
72
- return this.request("HEAD", path, { params, raw: true, ...opts });
36
+ if (!res.ok) {
37
+ const err = body;
38
+ throw new HydrousSDKError(
39
+ err.error || err.message || `HTTP ${res.status}`,
40
+ err.code || "HTTP_ERROR",
41
+ res.status
42
+ );
73
43
  }
74
- // ── Internals ────────────────────────────────────────────────────────────────
75
- buildUrl(path, params) {
76
- const url = new URL(`${this.baseUrl}${path}`);
77
- if (params) {
78
- for (const [k, v] of Object.entries(params)) {
79
- if (v !== void 0 && v !== null && v !== "") {
80
- url.searchParams.set(k, String(v));
81
- }
44
+ return body;
45
+ }
46
+ function buildUrl(base, path, params) {
47
+ const url = new URL(path, base.endsWith("/") ? base : base + "/");
48
+ if (params) {
49
+ for (const [k, v] of Object.entries(params)) {
50
+ if (v !== void 0 && v !== null) {
51
+ url.searchParams.set(k, String(v));
82
52
  }
83
53
  }
84
- return url.toString();
85
54
  }
86
- buildSignal(userSignal) {
87
- const controller = new AbortController();
88
- const timeoutId = setTimeout(() => controller.abort("timeout"), this.timeout);
89
- userSignal?.addEventListener("abort", () => {
90
- clearTimeout(timeoutId);
91
- controller.abort(userSignal.reason);
92
- });
93
- return controller.signal;
94
- }
95
- async executeWithRetry(url, init, raw, retriesLeft) {
96
- try {
97
- const res = await fetch(url, init);
98
- if (raw) return res;
99
- if (res.ok) {
100
- if (res.status === 204) return void 0;
101
- return await res.json();
55
+ return url.toString();
56
+ }
57
+ function mergeHeaders(defaults, overrides) {
58
+ return { ...defaults, ...overrides };
59
+ }
60
+ async function readSSEStream(response, onEvent) {
61
+ if (!response.body) return;
62
+ const reader = response.body.getReader();
63
+ const decoder = new TextDecoder();
64
+ let buf = "";
65
+ const flush = (chunk) => {
66
+ var _a;
67
+ buf += chunk;
68
+ const blocks = buf.split("\n\n");
69
+ buf = (_a = blocks.pop()) != null ? _a : "";
70
+ for (const block of blocks) {
71
+ if (!block.trim()) continue;
72
+ let eventType = "message";
73
+ let dataLine = null;
74
+ for (const line of block.split("\n")) {
75
+ if (line.startsWith("event:")) eventType = line.slice(6).trim();
76
+ if (line.startsWith("data:")) dataLine = line.slice(5).trim();
102
77
  }
103
- let body;
78
+ if (dataLine === null) continue;
104
79
  try {
105
- body = await res.json();
106
- } catch {
107
- body = { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
108
- }
109
- if (retriesLeft > 0 && RETRYABLE_STATUSES.has(res.status)) {
110
- await sleep(500 * (this.retries - retriesLeft + 1));
111
- return this.executeWithRetry(url, init, raw, retriesLeft - 1);
112
- }
113
- throw new HydrousError(body, res.status);
114
- } catch (err) {
115
- if (err instanceof HydrousError) throw err;
116
- if (err instanceof Error && err.message === "timeout") {
117
- throw new HydrousTimeoutError(this.timeout);
80
+ onEvent(eventType, JSON.parse(dataLine));
81
+ } catch (e) {
118
82
  }
119
- if (retriesLeft > 0) {
120
- await sleep(500 * (this.retries - retriesLeft + 1));
121
- return this.executeWithRetry(url, init, raw, retriesLeft - 1);
122
- }
123
- throw new HydrousNetworkError(
124
- `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
125
- err
126
- );
127
83
  }
84
+ };
85
+ while (true) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ flush(decoder.decode(value, { stream: true }));
128
89
  }
129
- };
130
- function sleep(ms) {
131
- return new Promise((resolve) => setTimeout(resolve, ms));
90
+ if (buf.trim()) flush("");
132
91
  }
133
-
134
- // src/utils/query.ts
135
- function buildQueryParams(opts) {
136
- const params = {};
137
- if (opts.timeScope) params["timeScope"] = opts.timeScope;
138
- if (opts.year) params["year"] = opts.year;
139
- if (opts.sortOrder) params["sortOrder"] = opts.sortOrder;
140
- if (opts.limit) params["limit"] = opts.limit;
141
- if (opts.cursor) params["cursor"] = opts.cursor;
142
- if (opts.fields) params["fields"] = opts.fields;
143
- for (const filter of opts.filters ?? []) {
144
- const paramKey = filter.op === "==" ? filter.field : `${filter.field}[${filter.op}]`;
145
- params[paramKey] = filter.value;
146
- }
147
- return params;
92
+ function xhrUpload(url, body, headers, onXhrProgress) {
93
+ return new Promise((resolve, reject) => {
94
+ const xhr = new XMLHttpRequest();
95
+ xhr.open("POST", url);
96
+ for (const [key, val] of Object.entries(headers)) {
97
+ xhr.setRequestHeader(key, val);
98
+ }
99
+ xhr.responseType = "text";
100
+ if (onXhrProgress) {
101
+ xhr.upload.onprogress = (e) => {
102
+ if (e.lengthComputable) onXhrProgress(e.loaded, e.total);
103
+ };
104
+ }
105
+ xhr.onload = () => {
106
+ var _a;
107
+ if (xhr.status >= 200 && xhr.status < 300) {
108
+ resolve(xhr.responseText);
109
+ } else {
110
+ try {
111
+ const d = JSON.parse(xhr.responseText);
112
+ reject(new HydrousSDKError((_a = d.error) != null ? _a : `HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
113
+ } catch (e) {
114
+ reject(new HydrousSDKError(`HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
115
+ }
116
+ }
117
+ };
118
+ xhr.onerror = () => reject(new HydrousSDKError("Network error", "NETWORK_ERROR"));
119
+ xhr.onabort = () => reject(new HydrousSDKError("Upload aborted", "UPLOAD_ABORTED"));
120
+ xhr.ontimeout = () => reject(new HydrousSDKError("Upload timed out", "UPLOAD_TIMEOUT"));
121
+ xhr.send(body);
122
+ });
148
123
  }
149
- function validateFilters(filters) {
150
- const VALID_OPS = /* @__PURE__ */ new Set(["==", "!=", ">", "<", ">=", "<=", "contains"]);
151
- if (filters.length > 3) {
152
- throw new Error("HydrousDB supports a maximum of 3 filters per query.");
153
- }
154
- for (const f of filters) {
155
- if (!VALID_OPS.has(f.op)) {
156
- throw new Error(
157
- `Invalid filter operator "${f.op}". Valid operators: ${[...VALID_OPS].join(", ")}`
158
- );
124
+ function parseSSEText(text, onEvent) {
125
+ const blocks = text.split("\n\n");
126
+ for (const block of blocks) {
127
+ if (!block.trim()) continue;
128
+ let eventType = "message";
129
+ let dataLine = null;
130
+ for (const line of block.split("\n")) {
131
+ if (line.startsWith("event:")) eventType = line.slice(6).trim();
132
+ if (line.startsWith("data:")) dataLine = line.slice(5).trim();
159
133
  }
160
- if (!f.field || typeof f.field !== "string") {
161
- throw new Error('Each filter must have a non-empty "field" string.');
134
+ if (dataLine === null) continue;
135
+ try {
136
+ onEvent(eventType, JSON.parse(dataLine));
137
+ } catch (e) {
162
138
  }
163
139
  }
164
140
  }
165
141
 
166
- // src/records/client.ts
167
- var RecordsClient = class {
168
- constructor(http) {
169
- this.http = http;
170
- }
171
- get path() {
172
- return `/api/${this.http.bucketKey}`;
142
+ // src/auth/client.ts
143
+ var AuthClient = class {
144
+ constructor(config) {
145
+ this.session = null;
146
+ this.baseUrl = config.url;
147
+ this.headers = {
148
+ "Content-Type": "application/json",
149
+ "Authorization": `Bearer ${config.apiKey}`
150
+ };
173
151
  }
174
- // ── GET single record ────────────────────────────────────────────────────
152
+ // ─── SIGN UP ───────────────────────────────────────────────────────────────
175
153
  /**
176
- * Fetch a single record by ID.
154
+ * Create a new user account and return a session.
177
155
  *
178
156
  * @example
179
- * const { data } = await db.records.get('rec_abc123');
180
- * const { data, history } = await db.records.get('rec_abc123', { showHistory: true });
157
+ * const { data, error } = await hydrous.auth.signUp({
158
+ * email: 'user@example.com',
159
+ * password: 'supersecret',
160
+ * });
181
161
  */
182
- async get(recordId, options) {
183
- const params = { recordId };
184
- if (options?.showHistory) params["showHistory"] = "true";
185
- return this.http.get(this.path, params, options);
162
+ async signUp(options) {
163
+ try {
164
+ const url = buildUrl(this.baseUrl, "auth/signup");
165
+ const res = await fetch(url, {
166
+ method: "POST",
167
+ headers: this.headers,
168
+ body: JSON.stringify(options)
169
+ });
170
+ const json = await parseResponse(res);
171
+ this.session = json.data;
172
+ return { data: json.data, error: null };
173
+ } catch (err) {
174
+ return { data: null, error: toHydrousError(err) };
175
+ }
186
176
  }
187
- // ── GET historical snapshot ──────────────────────────────────────────────
177
+ // ─── SIGN IN ───────────────────────────────────────────────────────────────
188
178
  /**
189
- * Fetch a specific historical version (generation) of a record.
179
+ * Sign in with email and password.
190
180
  *
191
181
  * @example
192
- * const { data } = await db.records.getSnapshot('rec_abc123', '1700000000000000');
182
+ * const { data, error } = await hydrous.auth.signIn({
183
+ * email: 'user@example.com',
184
+ * password: 'supersecret',
185
+ * });
186
+ * if (data) console.log('Signed in as', data.user.email);
193
187
  */
194
- async getSnapshot(recordId, generation, opts) {
195
- return this.http.get(this.path, { recordId, generation }, opts);
188
+ async signIn(options) {
189
+ try {
190
+ const url = buildUrl(this.baseUrl, "auth/signin");
191
+ const res = await fetch(url, {
192
+ method: "POST",
193
+ headers: this.headers,
194
+ body: JSON.stringify(options)
195
+ });
196
+ const json = await parseResponse(res);
197
+ this.session = json.data;
198
+ return { data: json.data, error: null };
199
+ } catch (err) {
200
+ return { data: null, error: toHydrousError(err) };
201
+ }
196
202
  }
197
- // ── GET collection query ─────────────────────────────────────────────────
203
+ // ─── SIGN OUT ──────────────────────────────────────────────────────────────
198
204
  /**
199
- * Query a collection with optional filters, sorting, and pagination.
200
- *
201
- * @example
202
- * // Simple query
203
- * const { data, meta } = await db.records.query({ limit: 50, sortOrder: 'desc' });
204
- *
205
- * // Filtered query
206
- * const { data } = await db.records.query({
207
- * filters: [{ field: 'status', op: '==', value: 'active' }],
208
- * timeScope: '7d',
209
- * });
210
- *
211
- * // Paginated
212
- * let cursor: string | null = null;
213
- * do {
214
- * const result = await db.records.query({ limit: 100, cursor: cursor ?? undefined });
215
- * cursor = result.meta.nextCursor;
216
- * } while (cursor);
205
+ * Sign out the current user and invalidate their session.
217
206
  */
218
- async query(options) {
219
- if (options?.filters) validateFilters(options.filters);
220
- const params = buildQueryParams(options ?? {});
221
- return this.http.get(this.path, params, options);
207
+ async signOut() {
208
+ try {
209
+ const url = buildUrl(this.baseUrl, "auth/signout");
210
+ const res = await fetch(url, {
211
+ method: "POST",
212
+ headers: mergeHeaders(this.headers, this._sessionHeader())
213
+ });
214
+ await parseResponse(res);
215
+ this.session = null;
216
+ return { data: void 0, error: null };
217
+ } catch (err) {
218
+ return { data: null, error: toHydrousError(err) };
219
+ }
220
+ }
221
+ // ─── GET USER ──────────────────────────────────────────────────────────────
222
+ /** Return the currently authenticated user, or null if not signed in. */
223
+ async getUser() {
224
+ try {
225
+ const url = buildUrl(this.baseUrl, "auth/user");
226
+ const res = await fetch(url, {
227
+ headers: mergeHeaders(this.headers, this._sessionHeader())
228
+ });
229
+ const json = await parseResponse(res);
230
+ return { data: json.data, error: null };
231
+ } catch (err) {
232
+ return { data: null, error: toHydrousError(err) };
233
+ }
222
234
  }
223
- // ── POST insert ──────────────────────────────────────────────────────────
235
+ // ─── REFRESH TOKEN ────────────────────────────────────────────────────────
224
236
  /**
225
- * Insert a new record.
226
- *
227
- * @example
228
- * const { data, meta } = await db.records.insert({
229
- * values: { name: 'Alice', score: 99 },
230
- * queryableFields: ['name'],
231
- * userEmail: 'alice@example.com',
232
- * });
237
+ * Refresh the access token using the stored refresh token.
238
+ * Called automatically by the SDK when a 401 is received.
233
239
  */
234
- async insert(payload, opts) {
235
- return this.http.post(this.path, payload, opts);
240
+ async refreshSession() {
241
+ var _a;
242
+ if (!((_a = this.session) == null ? void 0 : _a.refreshToken)) {
243
+ return { data: null, error: { message: "No session", code: "NO_SESSION" } };
244
+ }
245
+ try {
246
+ const url = buildUrl(this.baseUrl, "auth/refresh");
247
+ const res = await fetch(url, {
248
+ method: "POST",
249
+ headers: this.headers,
250
+ body: JSON.stringify({ refreshToken: this.session.refreshToken })
251
+ });
252
+ const json = await parseResponse(res);
253
+ this.session = json.data;
254
+ return { data: json.data, error: null };
255
+ } catch (err) {
256
+ return { data: null, error: toHydrousError(err) };
257
+ }
236
258
  }
237
- // ── PATCH update ─────────────────────────────────────────────────────────
259
+ /** Return the current in-memory session (may be null). */
260
+ getSession() {
261
+ return this.session;
262
+ }
263
+ _sessionHeader() {
264
+ var _a;
265
+ return ((_a = this.session) == null ? void 0 : _a.accessToken) ? { "X-Session-Token": this.session.accessToken } : {};
266
+ }
267
+ };
268
+
269
+ // src/utils/query.ts
270
+ function serialiseQuery(opts = {}) {
271
+ var _a;
272
+ const params = {};
273
+ if (opts.limit !== void 0) params["limit"] = String(opts.limit);
274
+ if (opts.offset !== void 0) params["offset"] = String(opts.offset);
275
+ if (opts.select && opts.select.length > 0) {
276
+ params["select"] = opts.select.join(",");
277
+ }
278
+ if (opts.orderBy) {
279
+ params["orderBy"] = opts.orderBy.field;
280
+ params["direction"] = (_a = opts.orderBy.direction) != null ? _a : "asc";
281
+ }
282
+ const filters = opts.where ? Array.isArray(opts.where) ? opts.where : [opts.where] : [];
283
+ if (filters.length > 0) {
284
+ params["where"] = JSON.stringify(filters);
285
+ }
286
+ return params;
287
+ }
288
+ function eq(field, value) {
289
+ return { field, operator: "eq", value };
290
+ }
291
+ function neq(field, value) {
292
+ return { field, operator: "neq", value };
293
+ }
294
+ function gt(field, value) {
295
+ return { field, operator: "gt", value };
296
+ }
297
+ function lt(field, value) {
298
+ return { field, operator: "lt", value };
299
+ }
300
+ function inArray(field, value) {
301
+ return { field, operator: "in", value };
302
+ }
303
+
304
+ // src/records/client.ts
305
+ var RecordsClient = class {
306
+ constructor(config) {
307
+ this.baseUrl = config.url;
308
+ this.headers = {
309
+ "Content-Type": "application/json",
310
+ "Authorization": `Bearer ${config.apiKey}`
311
+ };
312
+ }
313
+ // ─── SELECT ────────────────────────────────────────────────────────────────
238
314
  /**
239
- * Update an existing record.
315
+ * Query records from a collection.
316
+ *
317
+ * @param collection - Collection name (e.g. "users")
318
+ * @param options - Filters, ordering, pagination
240
319
  *
241
320
  * @example
242
- * await db.records.update({
243
- * recordId: 'rec_abc123',
244
- * values: { score: 100 },
245
- * track_record_history: true,
321
+ * const { data, error } = await hydrous.records.select('users', {
322
+ * where: { field: 'role', operator: 'eq', value: 'admin' },
323
+ * orderBy: { field: 'createdAt', direction: 'desc' },
324
+ * limit: 20,
246
325
  * });
247
326
  */
248
- async update(payload, opts) {
249
- return this.http.patch(this.path, payload, opts);
327
+ async select(collection, options = {}) {
328
+ try {
329
+ const params = serialiseQuery(options);
330
+ const url = buildUrl(this.baseUrl, `records/${collection}`, params);
331
+ const res = await fetch(url, { headers: this.headers });
332
+ const json = await parseResponse(res);
333
+ return { data: json.data, count: json.count, error: null };
334
+ } catch (err) {
335
+ return { data: [], count: 0, error: toHydrousError(err) };
336
+ }
250
337
  }
251
- // ── DELETE ─────────────────────────────────────────────────────────────────
338
+ // ─── GET ONE ───────────────────────────────────────────────────────────────
252
339
  /**
253
- * Delete a record permanently.
340
+ * Fetch a single record by its ID.
254
341
  *
255
342
  * @example
256
- * await db.records.delete('rec_abc123');
343
+ * const { data, error } = await hydrous.records.get('users', 'user_abc123');
257
344
  */
258
- async delete(recordId, opts) {
259
- return this.http.delete(this.path, { recordId }, opts);
345
+ async get(collection, id) {
346
+ try {
347
+ const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
348
+ const res = await fetch(url, { headers: this.headers });
349
+ const json = await parseResponse(res);
350
+ return { data: json.data, error: null };
351
+ } catch (err) {
352
+ return { data: null, error: toHydrousError(err) };
353
+ }
260
354
  }
261
- // ── HEAD — existence check ─────────────────────────────────────────────────
355
+ // ─── INSERT ────────────────────────────────────────────────────────────────
262
356
  /**
263
- * Check whether a record exists without fetching its full data.
264
- * Returns `null` if the record is not found.
357
+ * Insert one or more records into a collection.
265
358
  *
266
- * @example
267
- * const info = await db.records.exists('rec_abc123');
268
- * if (info?.exists) console.log('found at', info.updatedAt);
269
- */
270
- async exists(recordId, opts) {
271
- const res = await this.http.head(this.path, { recordId }, opts);
272
- if (res.status === 404) return null;
273
- if (!res.ok) return null;
274
- return {
275
- exists: true,
276
- id: res.headers.get("X-Record-Id") ?? recordId,
277
- createdAt: res.headers.get("X-Created-At") ?? "",
278
- updatedAt: res.headers.get("X-Updated-At") ?? "",
279
- sizeBytes: res.headers.get("X-Size-Bytes") ?? ""
280
- };
281
- }
282
- // ── Batch — update ─────────────────────────────────────────────────────────
283
- /**
284
- * Update up to 500 records in a single request.
359
+ * @param collection - Collection name
360
+ * @param payload - A single record object or an array of record objects
285
361
  *
286
362
  * @example
287
- * await db.records.batchUpdate({
288
- * updates: [
289
- * { recordId: 'rec_1', values: { status: 'archived' } },
290
- * { recordId: 'rec_2', values: { status: 'archived' } },
291
- * ],
363
+ * // Single insert
364
+ * const { data, error } = await hydrous.records.insert('users', {
365
+ * name: 'Alice', email: 'alice@example.com'
292
366
  * });
293
- */
294
- async batchUpdate(payload, opts) {
295
- return this.http.post(
296
- `${this.path}/batch/update`,
297
- payload,
298
- opts
299
- );
300
- }
301
- // ── Batch — delete ─────────────────────────────────────────────────────────
302
- /**
303
- * Delete up to 500 records in a single request.
304
367
  *
305
- * @example
306
- * await db.records.batchDelete({ recordIds: ['rec_1', 'rec_2', 'rec_3'] });
368
+ * // Bulk insert
369
+ * const { data, error } = await hydrous.records.insert('users', [
370
+ * { name: 'Alice' }, { name: 'Bob' }
371
+ * ]);
307
372
  */
308
- async batchDelete(payload, opts) {
309
- return this.http.post(
310
- `${this.path}/batch/delete`,
311
- payload,
312
- opts
313
- );
373
+ async insert(collection, payload) {
374
+ try {
375
+ const url = buildUrl(this.baseUrl, `records/${collection}`);
376
+ const res = await fetch(url, {
377
+ method: "POST",
378
+ headers: this.headers,
379
+ body: JSON.stringify(payload)
380
+ });
381
+ const json = await parseResponse(res);
382
+ return { data: json.data, count: json.count, error: null };
383
+ } catch (err) {
384
+ return { data: [], count: 0, error: toHydrousError(err) };
385
+ }
314
386
  }
315
- // ── Batch — insert ─────────────────────────────────────────────────────────
387
+ // ─── UPDATE ────────────────────────────────────────────────────────────────
316
388
  /**
317
- * Insert up to 500 records in a single request.
318
- * Returns HTTP 207 (multi-status) — check `meta.failed` for partial failures.
389
+ * Update a record by ID.
319
390
  *
320
391
  * @example
321
- * const result = await db.records.batchInsert({
322
- * records: [{ name: 'Alice' }, { name: 'Bob' }],
323
- * queryableFields: ['name'],
392
+ * const { data, error } = await hydrous.records.update('users', 'user_abc123', {
393
+ * name: 'Alice Smith'
324
394
  * });
325
395
  */
326
- async batchInsert(payload, opts) {
327
- return this.http.post(
328
- `${this.path}/batch/insert`,
329
- payload,
330
- opts
331
- );
396
+ async update(collection, id, payload) {
397
+ try {
398
+ const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
399
+ const res = await fetch(url, {
400
+ method: "PATCH",
401
+ headers: this.headers,
402
+ body: JSON.stringify(payload)
403
+ });
404
+ const json = await parseResponse(res);
405
+ return { data: json.data, error: null };
406
+ } catch (err) {
407
+ return { data: null, error: toHydrousError(err) };
408
+ }
332
409
  }
333
- // ── Helpers ────────────────────────────────────────────────────────────────
410
+ // ─── DELETE ────────────────────────────────────────────────────────────────
334
411
  /**
335
- * Fetch ALL records matching a query, automatically following cursors.
336
- * Use with care on large collections — prefer `query()` with manual pagination.
412
+ * Delete a record by ID.
337
413
  *
338
414
  * @example
339
- * const allRecords = await db.records.queryAll({
340
- * filters: [{ field: 'type', op: '==', value: 'invoice' }],
341
- * });
415
+ * const { error } = await hydrous.records.delete('users', 'user_abc123');
342
416
  */
343
- async queryAll(options) {
344
- const all = [];
345
- let cursor = null;
346
- do {
347
- const result = await this.query(cursor ? { ...options, cursor } : { ...options });
348
- all.push(...result.data);
349
- cursor = result.meta.nextCursor ?? null;
350
- } while (cursor);
351
- return all;
417
+ async delete(collection, id) {
418
+ try {
419
+ const url = buildUrl(this.baseUrl, `records/${collection}/${id}`);
420
+ const res = await fetch(url, { method: "DELETE", headers: this.headers });
421
+ await parseResponse(res);
422
+ return { data: void 0, error: null };
423
+ } catch (err) {
424
+ return { data: null, error: toHydrousError(err) };
425
+ }
352
426
  }
353
427
  };
354
428
 
355
- // src/auth/client.ts
356
- var AuthClient = class {
357
- constructor(http) {
358
- this.http = http;
359
- }
360
- get path() {
361
- return `/auth/${this.http.authKey}`;
429
+ // src/analytics/client.ts
430
+ var AnalyticsClient = class {
431
+ constructor(config) {
432
+ this.baseUrl = config.url;
433
+ this.headers = {
434
+ "Content-Type": "application/json",
435
+ "Authorization": `Bearer ${config.apiKey}`
436
+ };
362
437
  }
363
- // ── Sign-up ────────────────────────────────────────────────────────────────
438
+ // ─── TRACK ────────────────────────────────────────────────────────────────
364
439
  /**
365
- * Create a new user account. Returns the user and a session immediately.
440
+ * Track an analytics event.
366
441
  *
367
442
  * @example
368
- * const { data, session } = await db.auth.signUp({
369
- * email: 'alice@example.com',
370
- * password: 'Str0ngP@ss!',
371
- * fullName: 'Alice Smith',
443
+ * await hydrous.analytics.track({
444
+ * event: 'page_view',
445
+ * properties: { page: '/home', referrer: 'google.com' },
446
+ * userId: 'user_abc123',
372
447
  * });
373
- * // Store session.sessionId and session.refreshToken in your app
374
448
  */
375
- async signUp(payload, opts) {
376
- return this.http.post(`${this.path}/signup`, payload, opts);
449
+ async track(options) {
450
+ var _a;
451
+ try {
452
+ const url = buildUrl(this.baseUrl, "analytics/track");
453
+ const res = await fetch(url, {
454
+ method: "POST",
455
+ headers: this.headers,
456
+ body: JSON.stringify({
457
+ ...options,
458
+ timestamp: (_a = options.timestamp) != null ? _a : Date.now()
459
+ })
460
+ });
461
+ await parseResponse(res);
462
+ return { data: void 0, error: null };
463
+ } catch (err) {
464
+ return { data: null, error: toHydrousError(err) };
465
+ }
377
466
  }
378
- // ── Sign-in ────────────────────────────────────────────────────────────────
467
+ // ─── QUERY ────────────────────────────────────────────────────────────────
379
468
  /**
380
- * Authenticate with email + password. Returns user data and a new session.
469
+ * Query recorded analytics events.
381
470
  *
382
471
  * @example
383
- * const { data, session } = await db.auth.signIn({
384
- * email: 'alice@example.com',
385
- * password: 'Str0ngP@ss!',
472
+ * const { data } = await hydrous.analytics.query({
473
+ * event: 'page_view',
474
+ * from: '2024-01-01',
475
+ * to: '2024-01-31',
476
+ * limit: 100,
386
477
  * });
387
478
  */
388
- async signIn(payload, opts) {
389
- return this.http.post(`${this.path}/signin`, payload, opts);
479
+ async query(options = {}) {
480
+ try {
481
+ const params = {};
482
+ if (options.event) params["event"] = options.event;
483
+ if (options.from) params["from"] = options.from;
484
+ if (options.to) params["to"] = options.to;
485
+ if (options.limit) params["limit"] = String(options.limit);
486
+ if (options.groupBy) params["groupBy"] = options.groupBy;
487
+ const url = buildUrl(this.baseUrl, "analytics/events", params);
488
+ const res = await fetch(url, { headers: this.headers });
489
+ const json = await parseResponse(res);
490
+ return { data: json.data, count: json.count, error: null };
491
+ } catch (err) {
492
+ return { data: [], count: 0, error: toHydrousError(err) };
493
+ }
390
494
  }
391
- // ── Sign-out ───────────────────────────────────────────────────────────────
495
+ // ─── BATCH TRACK ─────────────────────────────────────────────────────────
392
496
  /**
393
- * Revoke a session (or all sessions for a user).
497
+ * Track multiple events in a single request (more efficient than
498
+ * calling `track` in a loop).
394
499
  *
395
500
  * @example
396
- * // Single device
397
- * await db.auth.signOut({ sessionId: 'sess_...' });
398
- *
399
- * // All devices
400
- * await db.auth.signOut({ allDevices: true, userId: 'user_...' });
501
+ * await hydrous.analytics.trackBatch([
502
+ * { event: 'signup', userId: 'u1' },
503
+ * { event: 'onboarded', userId: 'u1' },
504
+ * ]);
401
505
  */
402
- async signOut(payload, opts) {
403
- return this.http.post(`${this.path}/signout`, payload, opts);
506
+ async trackBatch(events) {
507
+ try {
508
+ const url = buildUrl(this.baseUrl, "analytics/track/batch");
509
+ const stamped = events.map((e) => {
510
+ var _a;
511
+ return {
512
+ ...e,
513
+ timestamp: (_a = e.timestamp) != null ? _a : Date.now()
514
+ };
515
+ });
516
+ const res = await fetch(url, {
517
+ method: "POST",
518
+ headers: this.headers,
519
+ body: JSON.stringify({ events: stamped })
520
+ });
521
+ await parseResponse(res);
522
+ return { data: void 0, error: null };
523
+ } catch (err) {
524
+ return { data: null, error: toHydrousError(err) };
525
+ }
404
526
  }
405
- // ── Session: validate ──────────────────────────────────────────────────────
406
- /**
407
- * Validate a session token — use this on every protected request in your backend.
408
- *
409
- * @example
410
- * try {
411
- * const { data } = await db.auth.validateSession(sessionId);
412
- * // data is the authenticated user
413
- * } catch (err) {
414
- * // Session expired or invalid
415
- * }
416
- */
417
- async validateSession(sessionId, opts) {
418
- return this.http.post(`${this.path}/session/validate`, { sessionId }, opts);
527
+ };
528
+
529
+ // src/storage/client.ts
530
+ var isBrowser = typeof window !== "undefined" && typeof XMLHttpRequest !== "undefined";
531
+ function bucketFromKey(key) {
532
+ return encodeURIComponent(key);
533
+ }
534
+ function storageUrl(base, bucketKey, path) {
535
+ const bucket = bucketFromKey(bucketKey);
536
+ return `${base.replace(/\/$/, "")}/storage/${bucket}/${path.replace(/^\//, "")}`;
537
+ }
538
+ function storageHeaders(bucketKey) {
539
+ return { "X-Storage-Key": bucketKey };
540
+ }
541
+ function drainSSEProgress(rawText, onProgress) {
542
+ const results = [];
543
+ const errors = [];
544
+ parseSSEText(rawText, (eventType, data) => {
545
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
546
+ const d = data;
547
+ if (eventType === "progress" && onProgress) {
548
+ onProgress({
549
+ index: (_a = d["index"]) != null ? _a : 0,
550
+ total: (_b = d["total"]) != null ? _b : 1,
551
+ path: (_c = d["path"]) != null ? _c : "",
552
+ stage: (_d = d["stage"]) != null ? _d : "uploading",
553
+ bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
554
+ totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
555
+ percent: (_g = d["percent"]) != null ? _g : 0,
556
+ bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
557
+ eta: (_i = d["eta"]) != null ? _i : null,
558
+ result: d["result"],
559
+ error: d["error"],
560
+ code: d["code"]
561
+ });
562
+ }
563
+ if (eventType === "done") {
564
+ if (d["path"]) {
565
+ results.push(d);
566
+ } else if (Array.isArray(d["errors"])) {
567
+ const succeeded = (_j = d["succeeded"]) != null ? _j : [];
568
+ const errs = d["errors"];
569
+ results.push(...succeeded);
570
+ errors.push(...errs);
571
+ }
572
+ }
573
+ if (eventType === "error") {
574
+ errors.push({
575
+ path: "",
576
+ error: (_k = d["error"]) != null ? _k : "Unknown error",
577
+ code: (_l = d["code"]) != null ? _l : "UNKNOWN"
578
+ });
579
+ }
580
+ });
581
+ return { results, errors };
582
+ }
583
+ var StorageClient = class {
584
+ constructor(config) {
585
+ this.baseUrl = config.url;
419
586
  }
420
- // ── Session: refresh ───────────────────────────────────────────────────────
587
+ // ══════════════════════════════════════════════════════════════════════════
588
+ // UPLOAD
589
+ // ══════════════════════════════════════════════════════════════════════════
421
590
  /**
422
- * Exchange a refresh token for a new session (rotation).
423
- * The old session is revoked.
591
+ * Upload a single file to a bucket.
424
592
  *
425
- * @example
426
- * const { session } = await db.auth.refreshSession(refreshToken);
427
- */
428
- async refreshSession(refreshToken, opts) {
429
- return this.http.post(`${this.path}/session/refresh`, { refreshToken }, opts);
430
- }
431
- // ── User: get ──────────────────────────────────────────────────────────────
432
- /**
433
- * Fetch a user by their ID.
593
+ * The bucket key **always comes first**.
594
+ * Supply an `onProgress` callback to receive live upload progress including
595
+ * bytes transferred, speed (bytes/sec), ETA, and lifecycle stage.
434
596
  *
435
- * @example
436
- * const { data } = await db.auth.getUser('user_abc123');
437
- */
438
- async getUser(userId, opts) {
439
- return this.http.get(`${this.path}/user`, { userId }, opts);
440
- }
441
- // ── User: list ─────────────────────────────────────────────────────────────
442
- /**
443
- * List users with cursor-based pagination.
597
+ * ### Stages fired via `onProgress`
598
+ * | Stage | Meaning |
599
+ * |-------------|------------------------------------------|
600
+ * | `pending` | Queued, not yet started |
601
+ * | `compressing` | Server is compressing the file |
602
+ * | `uploading` | Bytes flowing to cloud storage |
603
+ * | `done` | Confirmed written to cloud storage |
604
+ * | `error` | Something went wrong |
444
605
  *
445
- * @example
446
- * const { data, meta } = await db.auth.listUsers({ limit: 50 });
447
- */
448
- async listUsers(options) {
449
- const params = {
450
- limit: options?.limit
451
- };
452
- if (options?.cursor) params["cursor"] = options.cursor;
453
- return this.http.get(`${this.path}/users`, params, options);
454
- }
455
- // ── User: update ───────────────────────────────────────────────────────────
456
- /**
457
- * Update user profile fields.
606
+ * @param bucketKey Your storage bucket key (`ssk_…`)
607
+ * @param file A `File`, `Blob`, or `Buffer` (Node)
608
+ * @param options Path, overwrite flag, progress callback
458
609
  *
459
610
  * @example
460
- * await db.auth.updateUser({ userId: 'user_abc', updates: { fullName: 'Bob Smith' } });
611
+ * const { data, error } = await hydrous.storage.upload(
612
+ * 'ssk_my_bucket_key',
613
+ * file,
614
+ * {
615
+ * path: 'avatars/alice.jpg',
616
+ * overwrite: true,
617
+ * onProgress: (p) => {
618
+ * console.log(`${p.stage} — ${p.percent}% ${p.bytesPerSecond} B/s ETA ${p.eta}s`);
619
+ * },
620
+ * }
621
+ * );
461
622
  */
462
- async updateUser(payload, opts) {
463
- return this.http.patch(`${this.path}/user`, payload, opts);
623
+ async upload(bucketKey, file, options = {}) {
624
+ var _a, _b;
625
+ const { path, overwrite = false, onProgress } = options;
626
+ try {
627
+ const url = storageUrl(this.baseUrl, bucketKey, "upload");
628
+ const form = new FormData();
629
+ if (file instanceof Uint8Array) {
630
+ form.append("file", new Blob([file.buffer]), path != null ? path : "file");
631
+ } else if (file instanceof ArrayBuffer) {
632
+ form.append("file", new Blob([file]), path != null ? path : "file");
633
+ } else {
634
+ form.append("file", file, path != null ? path : file instanceof File ? file.name : "file");
635
+ }
636
+ if (path) form.append("path", path);
637
+ if (overwrite) form.append("overwrite", "true");
638
+ const headers = storageHeaders(bucketKey);
639
+ if (isBrowser) {
640
+ const totalBytes = file instanceof Blob ? file.size : file instanceof Uint8Array ? file.byteLength : file.byteLength;
641
+ const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
642
+ if (onProgress) {
643
+ onProgress({
644
+ index: 0,
645
+ total: 1,
646
+ path: path != null ? path : "",
647
+ stage: "uploading",
648
+ bytesUploaded: loaded,
649
+ totalBytes: total || totalBytes,
650
+ percent: Math.min(99, Math.round(loaded / (total || totalBytes) * 100)),
651
+ bytesPerSecond: null,
652
+ eta: null
653
+ });
654
+ }
655
+ });
656
+ const { results, errors } = drainSSEProgress(rawBody, onProgress);
657
+ if (errors.length > 0 && results.length === 0) {
658
+ return { data: null, error: { message: errors[0].error, code: errors[0].code } };
659
+ }
660
+ const result = (_a = results[0]) != null ? _a : null;
661
+ if (result && onProgress) {
662
+ onProgress({
663
+ index: 0,
664
+ total: 1,
665
+ path: result.path,
666
+ stage: "done",
667
+ bytesUploaded: totalBytes,
668
+ totalBytes,
669
+ percent: 100,
670
+ bytesPerSecond: null,
671
+ eta: 0,
672
+ result
673
+ });
674
+ }
675
+ return { data: result, error: null };
676
+ }
677
+ const res = await fetch(url, { method: "POST", headers, body: form });
678
+ if (!res.ok) {
679
+ const err = await res.json().catch(() => ({}));
680
+ throw new HydrousSDKError((_b = err.error) != null ? _b : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
681
+ }
682
+ let finalResult = null;
683
+ await readSSEStream(res, (eventType, data) => {
684
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
685
+ const d = data;
686
+ if (eventType === "progress" && onProgress) {
687
+ onProgress({
688
+ index: (_a2 = d["index"]) != null ? _a2 : 0,
689
+ total: (_b2 = d["total"]) != null ? _b2 : 1,
690
+ path: (_d = (_c = d["path"]) != null ? _c : path) != null ? _d : "",
691
+ stage: (_e = d["stage"]) != null ? _e : "uploading",
692
+ bytesUploaded: (_f = d["bytesUploaded"]) != null ? _f : 0,
693
+ totalBytes: (_g = d["totalBytes"]) != null ? _g : 0,
694
+ percent: (_h = d["percent"]) != null ? _h : 0,
695
+ bytesPerSecond: (_i = d["bytesPerSecond"]) != null ? _i : null,
696
+ eta: (_j = d["eta"]) != null ? _j : null,
697
+ result: d["result"],
698
+ error: d["error"]
699
+ });
700
+ }
701
+ if (eventType === "done") finalResult = data;
702
+ if (eventType === "error") {
703
+ throw new HydrousSDKError(
704
+ (_k = d["error"]) != null ? _k : "Upload failed",
705
+ (_l = d["code"]) != null ? _l : "UPLOAD_ERROR"
706
+ );
707
+ }
708
+ });
709
+ return { data: finalResult, error: null };
710
+ } catch (err) {
711
+ return { data: null, error: toHydrousError(err) };
712
+ }
464
713
  }
465
- // ── User: delete ───────────────────────────────────────────────────────────
714
+ // ══════════════════════════════════════════════════════════════════════════
715
+ // UPLOAD RAW (text / JSON / binary from string)
716
+ // ══════════════════════════════════════════════════════════════════════════
466
717
  /**
467
- * Soft-delete a user. All their sessions are revoked automatically.
718
+ * Upload raw text or JSON content directly no `File` object needed.
719
+ * Great for saving generated content, config files, or JSON records.
468
720
  *
469
- * @example
470
- * await db.auth.deleteUser('user_abc123');
471
- */
472
- async deleteUser(userId, opts) {
473
- return this.http.delete(`${this.path}/user`, { userId }, opts);
474
- }
475
- // ── Password: change ───────────────────────────────────────────────────────
476
- /**
477
- * Change password for a signed-in user (requires old password).
478
- * All sessions are revoked after success.
721
+ * @param bucketKey Your storage bucket key (`ssk_…`)
722
+ * @param path Destination path (e.g. `"configs/settings.json"`)
723
+ * @param content String content to store
724
+ * @param options `mimeType`, `overwrite`, `onProgress`
479
725
  *
480
726
  * @example
481
- * await db.auth.changePassword({
482
- * userId: 'user_abc',
483
- * oldPassword: 'Old@Pass1',
484
- * newPassword: 'New@Pass2',
485
- * });
727
+ * await hydrous.storage.uploadText(
728
+ * 'ssk_my_bucket_key',
729
+ * 'reports/summary.txt',
730
+ * 'Hello from Hydrous!',
731
+ * { mimeType: 'text/plain' }
732
+ * );
486
733
  */
487
- async changePassword(payload, opts) {
488
- return this.http.post(`${this.path}/password/change`, payload, opts);
734
+ async uploadText(bucketKey, path, content, options = {}) {
735
+ var _a;
736
+ const { mimeType = "text/plain", overwrite = false, onProgress } = options;
737
+ try {
738
+ const url = storageUrl(this.baseUrl, bucketKey, "upload-raw");
739
+ const headers = { ...storageHeaders(bucketKey), "Content-Type": "application/json" };
740
+ const res = await fetch(url, {
741
+ method: "POST",
742
+ headers,
743
+ body: JSON.stringify({ path, content, mimeType, overwrite })
744
+ });
745
+ if (!res.ok) {
746
+ const e = await res.json().catch(() => ({}));
747
+ throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
748
+ }
749
+ let finalResult = null;
750
+ await readSSEStream(res, (eventType, data) => {
751
+ var _a2, _b, _c, _d, _e, _f;
752
+ const d = data;
753
+ if (eventType === "progress" && onProgress) {
754
+ onProgress({
755
+ index: 0,
756
+ total: 1,
757
+ path,
758
+ stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
759
+ bytesUploaded: (_b = d["bytesUploaded"]) != null ? _b : 0,
760
+ totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
761
+ percent: (_d = d["percent"]) != null ? _d : 0,
762
+ bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
763
+ eta: (_f = d["eta"]) != null ? _f : null
764
+ });
765
+ }
766
+ if (eventType === "done") finalResult = data;
767
+ });
768
+ return { data: finalResult, error: null };
769
+ } catch (err) {
770
+ return { data: null, error: toHydrousError(err) };
771
+ }
489
772
  }
490
- // ── Password: reset request ────────────────────────────────────────────────
773
+ // ══════════════════════════════════════════════════════════════════════════
774
+ // BATCH UPLOAD
775
+ // ══════════════════════════════════════════════════════════════════════════
491
776
  /**
492
- * Request a password reset email.
493
- * Always returns success to prevent email enumeration.
777
+ * Upload multiple files in one request.
494
778
  *
495
- * @example
496
- * await db.auth.requestPasswordReset({ email: 'alice@example.com' });
497
- */
498
- async requestPasswordReset(payload, opts) {
499
- return this.http.post(`${this.path}/password/reset/request`, payload, opts);
500
- }
501
- // ── Password: reset confirm ────────────────────────────────────────────────
502
- /**
503
- * Confirm a password reset using the token from the reset email.
779
+ * `onProgress` fires for **every file individually** — the `index` field
780
+ * tells you which file the event belongs to (0-based, same order as `files`).
781
+ * All files receive a `pending` event upfront before any uploads start,
782
+ * so you can render all progress bars immediately.
504
783
  *
505
- * @example
506
- * await db.auth.confirmPasswordReset({ resetToken: 'tok_...', newPassword: 'New@Pass2' });
507
- */
508
- async confirmPasswordReset(payload, opts) {
509
- return this.http.post(`${this.path}/password/reset/confirm`, payload, opts);
510
- }
511
- // ── Email: verify request ──────────────────────────────────────────────────
512
- /**
513
- * Send a verification email to the user.
784
+ * @param bucketKey Your storage bucket key (`ssk_…`)
785
+ * @param files Array of `File` objects (browser) or `{ name, data }` objects (Node)
786
+ * @param options Prefix, per-file paths, overwrite, concurrency, onProgress
514
787
  *
515
788
  * @example
516
- * await db.auth.requestEmailVerification({ userId: 'user_abc' });
789
+ * await hydrous.storage.batchUpload(
790
+ * 'ssk_my_bucket_key',
791
+ * fileArray,
792
+ * {
793
+ * prefix: 'uploads/2024/',
794
+ * onProgress: (p) => {
795
+ * console.log(`File ${p.index}: ${p.stage} ${p.percent}%`);
796
+ * },
797
+ * }
798
+ * );
517
799
  */
518
- async requestEmailVerification(payload, opts) {
519
- return this.http.post(`${this.path}/email/verify/request`, payload, opts);
800
+ async batchUpload(bucketKey, files, options = {}) {
801
+ var _a;
802
+ const { prefix = "", paths, overwrite = false, onProgress } = options;
803
+ try {
804
+ const url = storageUrl(this.baseUrl, bucketKey, "batch-upload");
805
+ const form = new FormData();
806
+ const resolvedPaths = files.map(
807
+ (f, i) => {
808
+ var _a2;
809
+ return (_a2 = paths == null ? void 0 : paths[i]) != null ? _a2 : `${prefix}${f.name}`;
810
+ }
811
+ );
812
+ files.forEach((f) => form.append("files", f, f.name));
813
+ form.append("paths", JSON.stringify(resolvedPaths));
814
+ if (overwrite) form.append("overwrite", "true");
815
+ const headers = storageHeaders(bucketKey);
816
+ if (isBrowser) {
817
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
818
+ const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
819
+ if (onProgress) {
820
+ let cursor = 0;
821
+ for (let i = 0; i < files.length; i++) {
822
+ const share = files[i].size / (totalBytes || 1);
823
+ const myStart = cursor;
824
+ const myEnd = cursor + share;
825
+ const fileLoaded = Math.max(0, Math.min(
826
+ files[i].size,
827
+ (loaded / (total || totalBytes) - myStart) / share * files[i].size
828
+ ));
829
+ onProgress({
830
+ index: i,
831
+ total: files.length,
832
+ path: resolvedPaths[i],
833
+ stage: "uploading",
834
+ bytesUploaded: Math.round(fileLoaded),
835
+ totalBytes: files[i].size,
836
+ percent: Math.min(99, Math.round(fileLoaded / files[i].size * 100)),
837
+ bytesPerSecond: null,
838
+ eta: null
839
+ });
840
+ cursor = myEnd;
841
+ }
842
+ }
843
+ });
844
+ const { results, errors } = drainSSEProgress(rawBody, onProgress);
845
+ return {
846
+ data: {
847
+ succeeded: results,
848
+ failed: errors
849
+ },
850
+ error: null
851
+ };
852
+ }
853
+ const res = await fetch(url, { method: "POST", headers, body: form });
854
+ if (!res.ok) {
855
+ const e = await res.json().catch(() => ({}));
856
+ throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
857
+ }
858
+ const succeeded = [];
859
+ const failed = [];
860
+ await readSSEStream(res, (eventType, data) => {
861
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
862
+ const d = data;
863
+ if (eventType === "progress" && onProgress) {
864
+ onProgress({
865
+ index: (_a2 = d["index"]) != null ? _a2 : 0,
866
+ total: (_b = d["total"]) != null ? _b : files.length,
867
+ path: (_c = d["path"]) != null ? _c : "",
868
+ stage: (_d = d["stage"]) != null ? _d : "uploading",
869
+ bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
870
+ totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
871
+ percent: (_g = d["percent"]) != null ? _g : 0,
872
+ bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
873
+ eta: (_i = d["eta"]) != null ? _i : null,
874
+ result: d["result"],
875
+ error: d["error"],
876
+ code: d["code"]
877
+ });
878
+ }
879
+ if (eventType === "done" && d["succeeded"]) {
880
+ succeeded.push(...d["succeeded"]);
881
+ failed.push(...(_j = d["errors"]) != null ? _j : []);
882
+ }
883
+ });
884
+ return { data: { succeeded, failed }, error: null };
885
+ } catch (err) {
886
+ return { data: null, error: toHydrousError(err) };
887
+ }
520
888
  }
521
- // ── Email: verify confirm ──────────────────────────────────────────────────
889
+ // ══════════════════════════════════════════════════════════════════════════
890
+ // DOWNLOAD
891
+ // ══════════════════════════════════════════════════════════════════════════
522
892
  /**
523
- * Confirm email address using the token from the verification email.
893
+ * Download a single file and return its content as an `ArrayBuffer`.
894
+ *
895
+ * @param bucketKey Your storage bucket key (`ssk_…`)
896
+ * @param filePath Path of the file within your bucket
524
897
  *
525
898
  * @example
526
- * await db.auth.confirmEmailVerification({ verifyToken: 'tok_...' });
899
+ * const { data, error } = await hydrous.storage.download(
900
+ * 'ssk_my_bucket_key',
901
+ * 'avatars/alice.jpg'
902
+ * );
903
+ * if (data) {
904
+ * const blob = new Blob([data]);
905
+ * const url = URL.createObjectURL(blob);
906
+ * }
527
907
  */
528
- async confirmEmailVerification(payload, opts) {
529
- return this.http.post(`${this.path}/email/verify/confirm`, payload, opts);
908
+ async download(bucketKey, filePath) {
909
+ var _a;
910
+ try {
911
+ const url = storageUrl(this.baseUrl, bucketKey, `download/${filePath}`);
912
+ const res = await fetch(url, { headers: storageHeaders(bucketKey) });
913
+ if (!res.ok) {
914
+ const e = await res.json().catch(() => ({}));
915
+ throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
916
+ }
917
+ const buffer = await res.arrayBuffer();
918
+ return { data: buffer, error: null };
919
+ } catch (err) {
920
+ return { data: null, error: toHydrousError(err) };
921
+ }
530
922
  }
531
- // ── Account: lock ──────────────────────────────────────────────────────────
923
+ // ══════════════════════════════════════════════════════════════════════════
924
+ // BATCH DOWNLOAD
925
+ // ══════════════════════════════════════════════════════════════════════════
532
926
  /**
533
- * Lock a user account for a given duration (default: 15 min).
927
+ * Download multiple files in one request.
928
+ *
929
+ * When `autoSave: true` (browser only) each file is automatically saved
930
+ * to the user's Downloads folder as it arrives.
931
+ *
932
+ * @param bucketKey Your storage bucket key (`ssk_…`)
933
+ * @param filePaths Array of file paths within your bucket
934
+ * @param options Concurrency, onProgress, autoSave
534
935
  *
535
936
  * @example
536
- * await db.auth.lockAccount({ userId: 'user_abc', duration: 30 * 60 * 1000 });
937
+ * const { data } = await hydrous.storage.batchDownload(
938
+ * 'ssk_my_bucket_key',
939
+ * ['reports/jan.pdf', 'reports/feb.pdf'],
940
+ * {
941
+ * onProgress: (p) => console.log(p.path, p.status),
942
+ * autoSave: true, // triggers browser file-save dialog per file
943
+ * }
944
+ * );
537
945
  */
538
- async lockAccount(payload, opts) {
539
- return this.http.post(`${this.path}/account/lock`, payload, opts);
946
+ async batchDownload(bucketKey, filePaths, options = {}) {
947
+ var _a;
948
+ const { concurrency = 5, onProgress, autoSave = false } = options;
949
+ try {
950
+ const url = storageUrl(this.baseUrl, bucketKey, "batch-download");
951
+ const res = await fetch(url, {
952
+ method: "POST",
953
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
954
+ body: JSON.stringify({ paths: filePaths, concurrency })
955
+ });
956
+ if (!res.ok) {
957
+ const e = await res.json().catch(() => ({}));
958
+ throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
959
+ }
960
+ const downloadedFiles = [];
961
+ await readSSEStream(res, (eventType, data) => {
962
+ var _a2, _b, _c, _d, _e, _f, _g, _h;
963
+ const d = data;
964
+ if (eventType === "file") {
965
+ const base64 = d["content"];
966
+ const mimeType = (_a2 = d["mimeType"]) != null ? _a2 : "application/octet-stream";
967
+ const path = (_b = d["path"]) != null ? _b : "";
968
+ const size = (_c = d["size"]) != null ? _c : 0;
969
+ const index = (_d = d["index"]) != null ? _d : 0;
970
+ const binary = atob(base64);
971
+ const bytes = new Uint8Array(binary.length);
972
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
973
+ const content = bytes.buffer;
974
+ downloadedFiles.push({ path, content, mimeType, size });
975
+ if (onProgress) {
976
+ onProgress({
977
+ index,
978
+ total: filePaths.length,
979
+ path,
980
+ status: "success",
981
+ size,
982
+ mimeType
983
+ });
984
+ }
985
+ if (autoSave && isBrowser) {
986
+ const blob = new Blob([content], { type: mimeType });
987
+ const blobUrl = URL.createObjectURL(blob);
988
+ const a = document.createElement("a");
989
+ a.href = blobUrl;
990
+ a.download = (_e = path.split("/").pop()) != null ? _e : "download";
991
+ a.click();
992
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 5e3);
993
+ }
994
+ }
995
+ if (eventType === "error" && onProgress) {
996
+ const index = (_f = d["index"]) != null ? _f : 0;
997
+ onProgress({
998
+ index,
999
+ total: filePaths.length,
1000
+ path: (_g = filePaths[index]) != null ? _g : "",
1001
+ status: "error",
1002
+ error: (_h = d["error"]) != null ? _h : "Download failed"
1003
+ });
1004
+ }
1005
+ });
1006
+ return { data: downloadedFiles, error: null };
1007
+ } catch (err) {
1008
+ return { data: null, error: toHydrousError(err) };
1009
+ }
540
1010
  }
541
- // ── Account: unlock ────────────────────────────────────────────────────────
1011
+ // ══════════════════════════════════════════════════════════════════════════
1012
+ // LIST
1013
+ // ══════════════════════════════════════════════════════════════════════════
542
1014
  /**
543
- * Unlock a previously locked user account.
1015
+ * List files and folders inside a bucket (or a folder within it).
1016
+ *
1017
+ * Results are paginated — use `pagination.nextCursor` to fetch the next page.
1018
+ *
1019
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1020
+ * @param options `prefix`, `limit`, `cursor`
544
1021
  *
545
1022
  * @example
546
- * await db.auth.unlockAccount('user_abc');
1023
+ * const { data } = await hydrous.storage.list('ssk_my_bucket_key', {
1024
+ * prefix: 'avatars/',
1025
+ * limit: 50,
1026
+ * });
1027
+ * for (const item of data.items) {
1028
+ * console.log(item.type, item.path);
1029
+ * }
547
1030
  */
548
- async unlockAccount(userId, opts) {
549
- return this.http.post(`${this.path}/account/unlock`, { userId }, opts);
1031
+ async list(bucketKey, options = {}) {
1032
+ const { prefix = "", limit = 50, cursor } = options;
1033
+ try {
1034
+ const params = {
1035
+ prefix: prefix || void 0,
1036
+ limit,
1037
+ cursor: cursor || void 0
1038
+ };
1039
+ const url = buildUrl(
1040
+ this.baseUrl,
1041
+ `storage/${bucketFromKey(bucketKey)}/list`,
1042
+ params
1043
+ );
1044
+ const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1045
+ const json = await parseResponse(res);
1046
+ return { data: json, error: null };
1047
+ } catch (err) {
1048
+ return { data: null, error: toHydrousError(err) };
1049
+ }
550
1050
  }
551
- // ── Helpers ────────────────────────────────────────────────────────────────
1051
+ // ══════════════════════════════════════════════════════════════════════════
1052
+ // METADATA
1053
+ // ══════════════════════════════════════════════════════════════════════════
552
1054
  /**
553
- * Fetch all users, automatically following cursors.
1055
+ * Get metadata for a specific file (size, MIME type, compression info, etc.)
1056
+ *
1057
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1058
+ * @param filePath Path of the file within your bucket
554
1059
  *
555
1060
  * @example
556
- * const users = await db.auth.listAllUsers();
1061
+ * const { data } = await hydrous.storage.metadata(
1062
+ * 'ssk_my_bucket_key',
1063
+ * 'avatars/alice.jpg'
1064
+ * );
1065
+ * console.log(data.size, data.mimeType);
557
1066
  */
558
- async listAllUsers(opts) {
559
- const all = [];
560
- let cursor = null;
561
- do {
562
- const result = await this.listUsers(cursor ? { cursor, ...opts } : { ...opts });
563
- all.push(...result.data);
564
- cursor = result.meta.nextCursor ?? null;
565
- } while (cursor);
566
- return all;
567
- }
568
- };
569
-
570
- // src/analytics/client.ts
571
- var AnalyticsClient = class {
572
- constructor(http) {
573
- this.http = http;
574
- }
575
- get path() {
576
- return `/api/analytics/${this.http.bucketKey}/${this.http.bucketKey}`;
577
- }
578
- // ── Raw query ─────────────────────────────────────────────────────────────
579
- /**
580
- * Run any analytics query with full control over the payload.
581
- * Prefer the typed convenience methods below for everyday use.
582
- */
583
- async query(payload, opts) {
584
- return this.http.post(this.path, payload, opts);
1067
+ async metadata(bucketKey, filePath) {
1068
+ try {
1069
+ const url = storageUrl(this.baseUrl, bucketKey, `metadata/${filePath}`);
1070
+ const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1071
+ const json = await parseResponse(res);
1072
+ return { data: json.data, error: null };
1073
+ } catch (err) {
1074
+ return { data: null, error: toHydrousError(err) };
1075
+ }
585
1076
  }
586
- // ── count ──────────────────────────────────────────────────────────────────
1077
+ // ══════════════════════════════════════════════════════════════════════════
1078
+ // DELETE FILE
1079
+ // ══════════════════════════════════════════════════════════════════════════
587
1080
  /**
588
- * Total record count, optionally scoped to a date range.
589
- * Server `queryType`: **"count"**
1081
+ * Delete a single file.
590
1082
  *
591
- * @example
592
- * const { data } = await db.analytics.count();
593
- * console.log(data.count); // 1234
1083
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1084
+ * @param filePath Path of the file to delete
594
1085
  *
595
- * // With date range
596
- * const { data } = await db.analytics.count({
597
- * dateRange: { startDate: '2025-01-01', endDate: '2025-12-31' }
598
- * });
1086
+ * @example
1087
+ * await hydrous.storage.deleteFile('ssk_my_bucket_key', 'avatars/old.jpg');
599
1088
  */
600
- async count(options) {
601
- return this.query({ queryType: "count", dateRange: options?.dateRange }, options);
1089
+ async deleteFile(bucketKey, filePath) {
1090
+ try {
1091
+ const url = storageUrl(this.baseUrl, bucketKey, "file");
1092
+ const res = await fetch(url, {
1093
+ method: "DELETE",
1094
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1095
+ body: JSON.stringify({ path: filePath })
1096
+ });
1097
+ await parseResponse(res);
1098
+ return { data: void 0, error: null };
1099
+ } catch (err) {
1100
+ return { data: null, error: toHydrousError(err) };
1101
+ }
602
1102
  }
603
- // ── distribution ───────────────────────────────────────────────────────────
1103
+ // ══════════════════════════════════════════════════════════════════════════
1104
+ // DELETE FOLDER
1105
+ // ══════════════════════════════════════════════════════════════════════════
604
1106
  /**
605
- * Value distribution (histogram) for a field.
606
- * Server `queryType`: **"distribution"**
1107
+ * Recursively delete a folder and all of its contents.
607
1108
  *
608
- * @example
609
- * const { data } = await db.analytics.distribution('status');
610
- * // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
611
- */
612
- async distribution(field, options) {
613
- return this.query({
614
- queryType: "distribution",
615
- field,
616
- limit: options?.limit,
617
- order: options?.order,
618
- dateRange: options?.dateRange
619
- }, options);
620
- }
621
- // ── sum ────────────────────────────────────────────────────────────────────
622
- /**
623
- * Sum a numeric field, with optional group-by.
624
- * Server `queryType`: **"sum"**
1109
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1110
+ * @param folderPath Folder path to delete (e.g. `"old-uploads/"`)
625
1111
  *
626
1112
  * @example
627
- * const { data } = await db.analytics.sum('revenue', { groupBy: 'region' });
1113
+ * await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
628
1114
  */
629
- async sum(field, options) {
630
- return this.query({
631
- queryType: "sum",
632
- field,
633
- groupBy: options?.groupBy,
634
- limit: options?.limit,
635
- dateRange: options?.dateRange
636
- }, options);
637
- }
638
- // ── timeSeries ─────────────────────────────────────────────────────────────
1115
+ async deleteFolder(bucketKey, folderPath) {
1116
+ try {
1117
+ const url = storageUrl(this.baseUrl, bucketKey, "folder");
1118
+ const res = await fetch(url, {
1119
+ method: "DELETE",
1120
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1121
+ body: JSON.stringify({ path: folderPath })
1122
+ });
1123
+ await parseResponse(res);
1124
+ return { data: void 0, error: null };
1125
+ } catch (err) {
1126
+ return { data: null, error: toHydrousError(err) };
1127
+ }
1128
+ }
1129
+ // ══════════════════════════════════════════════════════════════════════════
1130
+ // CREATE FOLDER
1131
+ // ══════════════════════════════════════════════════════════════════════════
639
1132
  /**
640
- * Record count grouped over time.
641
- * Server `queryType`: **"timeSeries"**
1133
+ * Create an empty folder.
642
1134
  *
643
- * @example
644
- * const { data } = await db.analytics.timeSeries({ granularity: 'day' });
645
- * // [{ date: '2025-01-01', count: 42 }, ...]
646
- */
647
- async timeSeries(options) {
648
- return this.query({
649
- queryType: "timeSeries",
650
- granularity: options?.granularity,
651
- dateRange: options?.dateRange
652
- }, options);
653
- }
654
- // ── fieldTimeSeries ────────────────────────────────────────────────────────
655
- /**
656
- * Aggregate a numeric field over time.
657
- * Server `queryType`: **"fieldTimeSeries"**
1135
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1136
+ * @param folderPath Path for the new folder (e.g. `"avatars/2024/"`)
658
1137
  *
659
1138
  * @example
660
- * const { data } = await db.analytics.fieldTimeSeries('revenue', {
661
- * granularity: 'month',
662
- * aggregation: 'sum',
663
- * });
1139
+ * await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
664
1140
  */
665
- async fieldTimeSeries(field, options) {
666
- return this.query({
667
- queryType: "fieldTimeSeries",
668
- field,
669
- aggregation: options?.aggregation,
670
- granularity: options?.granularity,
671
- dateRange: options?.dateRange
672
- }, options);
673
- }
674
- // ── topN ───────────────────────────────────────────────────────────────────
1141
+ async createFolder(bucketKey, folderPath) {
1142
+ try {
1143
+ const url = storageUrl(this.baseUrl, bucketKey, "folder");
1144
+ const res = await fetch(url, {
1145
+ method: "POST",
1146
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1147
+ body: JSON.stringify({ path: folderPath })
1148
+ });
1149
+ await parseResponse(res);
1150
+ return { data: void 0, error: null };
1151
+ } catch (err) {
1152
+ return { data: null, error: toHydrousError(err) };
1153
+ }
1154
+ }
1155
+ // ══════════════════════════════════════════════════════════════════════════
1156
+ // MOVE
1157
+ // ══════════════════════════════════════════════════════════════════════════
675
1158
  /**
676
- * Top N most frequent values for a field.
677
- * Server `queryType`: **"topN"**
1159
+ * Move (rename) a file to a new path.
678
1160
  *
679
- * @example
680
- * const { data } = await db.analytics.topN('country', 5);
681
- * // [{ label: 'US', value: 'US', count: 500 }, ...]
682
- */
683
- async topN(field, n = 10, options) {
684
- return this.query({
685
- queryType: "topN",
686
- field,
687
- n,
688
- labelField: options?.labelField,
689
- order: options?.order,
690
- dateRange: options?.dateRange
691
- }, options);
692
- }
693
- // ── stats ──────────────────────────────────────────────────────────────────
694
- /**
695
- * Statistical summary for a numeric field: min, max, avg, stddev, p50, p90, p99.
696
- * Server `queryType`: **"stats"**
1161
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1162
+ * @param fromPath Current path of the file
1163
+ * @param toPath New path for the file
697
1164
  *
698
1165
  * @example
699
- * const { data } = await db.analytics.stats('score');
700
- * console.log(data.avg, data.p99);
1166
+ * await hydrous.storage.move(
1167
+ * 'ssk_my_bucket_key',
1168
+ * 'drafts/report.pdf',
1169
+ * 'published/report.pdf'
1170
+ * );
701
1171
  */
702
- async stats(field, options) {
703
- return this.query({ queryType: "stats", field, dateRange: options?.dateRange }, options);
1172
+ async move(bucketKey, fromPath, toPath) {
1173
+ try {
1174
+ const url = storageUrl(this.baseUrl, bucketKey, "move");
1175
+ const res = await fetch(url, {
1176
+ method: "POST",
1177
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1178
+ body: JSON.stringify({ from: fromPath, to: toPath })
1179
+ });
1180
+ await parseResponse(res);
1181
+ return { data: void 0, error: null };
1182
+ } catch (err) {
1183
+ return { data: null, error: toHydrousError(err) };
1184
+ }
704
1185
  }
705
- // ── records ────────────────────────────────────────────────────────────────
1186
+ // ══════════════════════════════════════════════════════════════════════════
1187
+ // COPY
1188
+ // ══════════════════════════════════════════════════════════════════════════
706
1189
  /**
707
- * Filtered, paginated raw records with optional field projection.
708
- * Supports filter ops: == != > < >= <= CONTAINS
709
- * Server `queryType`: **"records"**
1190
+ * Copy a file to a new path (original is kept).
710
1191
  *
711
- * @example
712
- * const { data } = await db.analytics.records({
713
- * filters: [{ field: 'role', op: '==', value: 'admin' }],
714
- * selectFields: ['email', 'createdAt'],
715
- * limit: 25,
716
- * });
717
- * console.log(data.data, data.hasMore);
718
- */
719
- async records(options) {
720
- return this.query({
721
- queryType: "records",
722
- filters: options?.filters,
723
- selectFields: options?.selectFields,
724
- limit: options?.limit,
725
- offset: options?.offset,
726
- orderBy: options?.orderBy,
727
- order: options?.order,
728
- dateRange: options?.dateRange
729
- }, options);
730
- }
731
- // ── multiMetric ────────────────────────────────────────────────────────────
732
- /**
733
- * Multiple aggregations in a single BigQuery call — ideal for dashboard stat cards.
734
- * Server `queryType`: **"multiMetric"**
1192
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1193
+ * @param fromPath Source path
1194
+ * @param toPath Destination path
735
1195
  *
736
1196
  * @example
737
- * const { data } = await db.analytics.multiMetric([
738
- * { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
739
- * { name: 'avgScore', field: 'score', aggregation: 'avg' },
740
- * { name: 'userCount', field: 'userId', aggregation: 'count' },
741
- * ]);
742
- * console.log(data.totalRevenue, data.avgScore, data.userCount);
1197
+ * await hydrous.storage.copy(
1198
+ * 'ssk_my_bucket_key',
1199
+ * 'templates/invoice.pdf',
1200
+ * 'invoices/invoice-001.pdf'
1201
+ * );
743
1202
  */
744
- async multiMetric(metrics, options) {
745
- return this.query({
746
- queryType: "multiMetric",
747
- metrics,
748
- dateRange: options?.dateRange
749
- }, options);
750
- }
751
- // ── storageStats ───────────────────────────────────────────────────────────
1203
+ async copy(bucketKey, fromPath, toPath) {
1204
+ try {
1205
+ const url = storageUrl(this.baseUrl, bucketKey, "copy");
1206
+ const res = await fetch(url, {
1207
+ method: "POST",
1208
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1209
+ body: JSON.stringify({ from: fromPath, to: toPath })
1210
+ });
1211
+ await parseResponse(res);
1212
+ return { data: void 0, error: null };
1213
+ } catch (err) {
1214
+ return { data: null, error: toHydrousError(err) };
1215
+ }
1216
+ }
1217
+ // ══════════════════════════════════════════════════════════════════════════
1218
+ // SIGNED URL
1219
+ // ══════════════════════════════════════════════════════════════════════════
752
1220
  /**
753
- * Storage statistics for the bucket total records, bytes, avg/min/max size.
754
- * Server `queryType`: **"storageStats"**
1221
+ * Generate a time-limited public URL for a private file.
1222
+ *
1223
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1224
+ * @param filePath Path of the file
1225
+ * @param options `expiresIn` seconds (default: 3600)
755
1226
  *
756
1227
  * @example
757
- * const { data } = await db.analytics.storageStats();
758
- * console.log(data.totalRecords, data.totalBytes);
1228
+ * const { data } = await hydrous.storage.signedUrl(
1229
+ * 'ssk_my_bucket_key',
1230
+ * 'private/contract.pdf',
1231
+ * { expiresIn: 300 } // 5 minutes
1232
+ * );
1233
+ * console.log(data.signedUrl); // share this URL
759
1234
  */
760
- async storageStats(options) {
761
- return this.query({ queryType: "storageStats", dateRange: options?.dateRange }, options);
1235
+ async signedUrl(bucketKey, filePath, options = {}) {
1236
+ const { expiresIn = 3600 } = options;
1237
+ try {
1238
+ const url = storageUrl(this.baseUrl, bucketKey, "signed-url");
1239
+ const res = await fetch(url, {
1240
+ method: "POST",
1241
+ headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
1242
+ body: JSON.stringify({ path: filePath, expiresInSeconds: expiresIn })
1243
+ });
1244
+ const json = await parseResponse(res);
1245
+ return { data: json, error: null };
1246
+ } catch (err) {
1247
+ return { data: null, error: toHydrousError(err) };
1248
+ }
762
1249
  }
763
- // ── crossBucket ────────────────────────────────────────────────────────────
1250
+ // ══════════════════════════════════════════════════════════════════════════
1251
+ // STATS
1252
+ // ══════════════════════════════════════════════════════════════════════════
764
1253
  /**
765
- * Compare a metric across multiple buckets in one query.
766
- * Server `queryType`: **"crossBucket"**
1254
+ * Get usage and billing statistics for this bucket key.
1255
+ *
1256
+ * @param bucketKey Your storage bucket key (`ssk_…`)
767
1257
  *
768
1258
  * @example
769
- * const { data } = await db.analytics.crossBucket({
770
- * bucketKeys: ['sales', 'refunds', 'trials'],
771
- * field: 'amount',
772
- * aggregation: 'sum',
773
- * });
1259
+ * const { data } = await hydrous.storage.stats('ssk_my_bucket_key');
1260
+ * console.log(`${data.totalFiles} files, ${data.totalSizeBytes} bytes stored`);
774
1261
  */
775
- async crossBucket(options) {
776
- return this.query({
777
- queryType: "crossBucket",
778
- bucketKeys: options.bucketKeys,
779
- field: options.field,
780
- aggregation: options.aggregation,
781
- dateRange: options.dateRange
782
- }, options);
1262
+ async stats(bucketKey) {
1263
+ try {
1264
+ const url = buildUrl(this.baseUrl, `storage/${bucketFromKey(bucketKey)}/stats`);
1265
+ const res = await fetch(url, { headers: storageHeaders(bucketKey) });
1266
+ const json = await parseResponse(res);
1267
+ return { data: json.data, error: null };
1268
+ } catch (err) {
1269
+ return { data: null, error: toHydrousError(err) };
1270
+ }
783
1271
  }
784
1272
  };
785
1273
 
786
1274
  // src/client.ts
787
1275
  var HydrousClient = class {
788
1276
  constructor(config) {
789
- if (!config.authKey) throw new Error("[hydrousdb] authKey is required");
790
- if (!config.bucketKey) throw new Error("[hydrousdb] bucketKey is required");
791
- this.http = new HttpClient(config);
792
- this.records = new RecordsClient(this.http);
793
- this.auth = new AuthClient(this.http);
794
- this.analytics = new AnalyticsClient(this.http);
1277
+ if (!config.url) throw new Error("[Hydrous] config.url is required");
1278
+ if (!config.apiKey) throw new Error("[Hydrous] config.apiKey is required");
1279
+ this.auth = new AuthClient(config);
1280
+ this.records = new RecordsClient(config);
1281
+ this.analytics = new AnalyticsClient(config);
1282
+ this.storage = new StorageClient(config);
795
1283
  }
796
1284
  };
1285
+
1286
+ // src/index.ts
797
1287
  function createClient(config) {
798
1288
  return new HydrousClient(config);
799
1289
  }
@@ -801,10 +1291,15 @@ function createClient(config) {
801
1291
  exports.AnalyticsClient = AnalyticsClient;
802
1292
  exports.AuthClient = AuthClient;
803
1293
  exports.HydrousClient = HydrousClient;
804
- exports.HydrousError = HydrousError;
805
- exports.HydrousNetworkError = HydrousNetworkError;
806
- exports.HydrousTimeoutError = HydrousTimeoutError;
1294
+ exports.HydrousSDKError = HydrousSDKError;
807
1295
  exports.RecordsClient = RecordsClient;
1296
+ exports.StorageClient = StorageClient;
808
1297
  exports.createClient = createClient;
1298
+ exports.eq = eq;
1299
+ exports.gt = gt;
1300
+ exports.inArray = inArray;
1301
+ exports.isHydrousError = isHydrousError;
1302
+ exports.lt = lt;
1303
+ exports.neq = neq;
809
1304
  //# sourceMappingURL=index.js.map
810
1305
  //# sourceMappingURL=index.js.map