hydrousdb 2.0.0 → 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,764 +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.securityKey = config.securityKey;
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
- /** Builds the base path for a given bucket: /api/:bucketKey/:securityKey */
172
- path(bucketKey) {
173
- return `/api/${bucketKey}/${this.http.securityKey}`;
174
- }
175
- // ── GET — single record ────────────────────────────────────────────────────
176
- /**
177
- * Fetch a single record by ID.
178
- *
179
- * @example
180
- * const { data } = await db.records.get('rec_abc123', { bucketKey: 'users' });
181
- * const { data, history } = await db.records.get('rec_abc123', { bucketKey: 'users', showHistory: true });
182
- */
183
- async get(recordId, options) {
184
- const { bucketKey, showHistory, ...rest } = options;
185
- const params = { recordId };
186
- if (showHistory) params["showHistory"] = "true";
187
- return this.http.get(this.path(bucketKey), params, rest);
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
+ };
188
151
  }
189
- // ── GET historical snapshot ──────────────────────────────────────────────
152
+ // ─── SIGN UP ───────────────────────────────────────────────────────────────
190
153
  /**
191
- * Fetch a specific historical version (generation) of a record.
154
+ * Create a new user account and return a session.
192
155
  *
193
156
  * @example
194
- * const { data } = await db.records.getSnapshot('rec_abc123', '1700000000000000', { bucketKey: 'users' });
157
+ * const { data, error } = await hydrous.auth.signUp({
158
+ * email: 'user@example.com',
159
+ * password: 'supersecret',
160
+ * });
195
161
  */
196
- async getSnapshot(recordId, generation, options) {
197
- const { bucketKey, ...rest } = options;
198
- return this.http.get(this.path(bucketKey), { recordId, generation }, rest);
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
+ }
199
176
  }
200
- // ── GET collection query ─────────────────────────────────────────────────
177
+ // ─── SIGN IN ───────────────────────────────────────────────────────────────
201
178
  /**
202
- * Query a collection with optional filters, sorting, and pagination.
179
+ * Sign in with email and password.
203
180
  *
204
181
  * @example
205
- * // Simple query
206
- * const { data, meta } = await db.records.query({ bucketKey: 'orders', limit: 50 });
207
- *
208
- * // Filtered query
209
- * const { data } = await db.records.query({
210
- * bucketKey: 'orders',
211
- * filters: [{ field: 'status', op: '==', value: 'active' }],
212
- * timeScope: '7d',
182
+ * const { data, error } = await hydrous.auth.signIn({
183
+ * email: 'user@example.com',
184
+ * password: 'supersecret',
213
185
  * });
214
- *
215
- * // Paginated
216
- * let cursor: string | null = null;
217
- * do {
218
- * const result = await db.records.query({ bucketKey: 'orders', limit: 100, cursor: cursor ?? undefined });
219
- * cursor = result.meta.nextCursor;
220
- * } while (cursor);
186
+ * if (data) console.log('Signed in as', data.user.email);
221
187
  */
222
- async query(options) {
223
- const { bucketKey, ...rest } = options;
224
- if (rest.filters) validateFilters(rest.filters);
225
- const params = buildQueryParams(rest);
226
- return this.http.get(this.path(bucketKey), params, rest);
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
+ }
227
202
  }
228
- // ── POST insert ──────────────────────────────────────────────────────────
203
+ // ─── SIGN OUT ──────────────────────────────────────────────────────────────
229
204
  /**
230
- * Insert a new record into the specified bucket.
231
- *
232
- * @example
233
- * const { data, meta } = await db.records.insert(
234
- * { values: { name: 'Alice', score: 99 }, queryableFields: ['name'] },
235
- * { bucketKey: 'users' }
236
- * );
205
+ * Sign out the current user and invalidate their session.
237
206
  */
238
- async insert(payload, options) {
239
- const { bucketKey, ...rest } = options;
240
- return this.http.post(this.path(bucketKey), payload, rest);
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
+ }
241
220
  }
242
- // ── PATCH update ─────────────────────────────────────────────────────────
243
- /**
244
- * Update an existing record.
245
- *
246
- * @example
247
- * await db.records.update(
248
- * { recordId: 'rec_abc123', values: { score: 100 }, track_record_history: true },
249
- * { bucketKey: 'users' }
250
- * );
251
- */
252
- async update(payload, options) {
253
- const { bucketKey, ...rest } = options;
254
- return this.http.patch(this.path(bucketKey), payload, rest);
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
+ }
255
234
  }
256
- // ── DELETE ─────────────────────────────────────────────────────────────────
235
+ // ─── REFRESH TOKEN ────────────────────────────────────────────────────────
257
236
  /**
258
- * Delete a record permanently.
259
- *
260
- * @example
261
- * await db.records.delete('rec_abc123', { bucketKey: 'users' });
237
+ * Refresh the access token using the stored refresh token.
238
+ * Called automatically by the SDK when a 401 is received.
262
239
  */
263
- async delete(recordId, options) {
264
- const { bucketKey, ...rest } = options;
265
- return this.http.delete(this.path(bucketKey), { recordId }, rest);
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
+ }
258
+ }
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
+ };
266
312
  }
267
- // ── HEAD — existence check ─────────────────────────────────────────────────
313
+ // ─── SELECT ────────────────────────────────────────────────────────────────
268
314
  /**
269
- * Check whether a record exists without fetching its full data.
270
- * Returns `null` if the record is not found.
315
+ * Query records from a collection.
316
+ *
317
+ * @param collection - Collection name (e.g. "users")
318
+ * @param options - Filters, ordering, pagination
271
319
  *
272
320
  * @example
273
- * const info = await db.records.exists('rec_abc123', { bucketKey: 'users' });
274
- * if (info?.exists) console.log('found at', info.updatedAt);
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,
325
+ * });
275
326
  */
276
- async exists(recordId, options) {
277
- const { bucketKey, ...rest } = options;
278
- const res = await this.http.head(this.path(bucketKey), { recordId }, rest);
279
- if (res.status === 404) return null;
280
- if (!res.ok) return null;
281
- return {
282
- exists: true,
283
- id: res.headers.get("X-Record-Id") ?? recordId,
284
- createdAt: res.headers.get("X-Created-At") ?? "",
285
- updatedAt: res.headers.get("X-Updated-At") ?? "",
286
- sizeBytes: res.headers.get("X-Size-Bytes") ?? ""
287
- };
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
+ }
288
337
  }
289
- // ── Batch update ─────────────────────────────────────────────────────────
338
+ // ─── GET ONE ───────────────────────────────────────────────────────────────
290
339
  /**
291
- * Update up to 500 records in a single request.
340
+ * Fetch a single record by its ID.
292
341
  *
293
342
  * @example
294
- * await db.records.batchUpdate(
295
- * { updates: [{ recordId: 'rec_1', values: { status: 'archived' } }] },
296
- * { bucketKey: 'orders' }
297
- * );
343
+ * const { data, error } = await hydrous.records.get('users', 'user_abc123');
298
344
  */
299
- async batchUpdate(payload, options) {
300
- const { bucketKey, ...rest } = options;
301
- return this.http.post(`${this.path(bucketKey)}/batch/update`, payload, rest);
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
+ }
302
354
  }
303
- // ── Batch — delete ─────────────────────────────────────────────────────────
355
+ // ─── INSERT ────────────────────────────────────────────────────────────────
304
356
  /**
305
- * Delete up to 500 records in a single request.
357
+ * Insert one or more records into a collection.
358
+ *
359
+ * @param collection - Collection name
360
+ * @param payload - A single record object or an array of record objects
306
361
  *
307
362
  * @example
308
- * await db.records.batchDelete(
309
- * { recordIds: ['rec_1', 'rec_2'] },
310
- * { bucketKey: 'orders' }
311
- * );
363
+ * // Single insert
364
+ * const { data, error } = await hydrous.records.insert('users', {
365
+ * name: 'Alice', email: 'alice@example.com'
366
+ * });
367
+ *
368
+ * // Bulk insert
369
+ * const { data, error } = await hydrous.records.insert('users', [
370
+ * { name: 'Alice' }, { name: 'Bob' }
371
+ * ]);
312
372
  */
313
- async batchDelete(payload, options) {
314
- const { bucketKey, ...rest } = options;
315
- return this.http.post(`${this.path(bucketKey)}/batch/delete`, payload, rest);
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
+ }
316
386
  }
317
- // ── Batch — insert ─────────────────────────────────────────────────────────
387
+ // ─── UPDATE ────────────────────────────────────────────────────────────────
318
388
  /**
319
- * Insert up to 500 records in a single request.
320
- * Returns HTTP 207 (multi-status) — check `meta.failed` for partial failures.
389
+ * Update a record by ID.
321
390
  *
322
391
  * @example
323
- * const result = await db.records.batchInsert(
324
- * { records: [{ name: 'Alice' }, { name: 'Bob' }], queryableFields: ['name'] },
325
- * { bucketKey: 'users' }
326
- * );
392
+ * const { data, error } = await hydrous.records.update('users', 'user_abc123', {
393
+ * name: 'Alice Smith'
394
+ * });
327
395
  */
328
- async batchInsert(payload, options) {
329
- const { bucketKey, ...rest } = options;
330
- return this.http.post(`${this.path(bucketKey)}/batch/insert`, payload, rest);
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
+ }
331
409
  }
332
- // ── Helpers ────────────────────────────────────────────────────────────────
410
+ // ─── DELETE ────────────────────────────────────────────────────────────────
333
411
  /**
334
- * Fetch ALL records matching a query, automatically following cursors.
335
- * Use with care on large collections — prefer `query()` with manual pagination.
412
+ * Delete a record by ID.
336
413
  *
337
414
  * @example
338
- * const allRecords = await db.records.queryAll({
339
- * bucketKey: 'orders',
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(
348
- cursor ? { ...options, cursor } : { ...options }
349
- );
350
- all.push(...result.data);
351
- cursor = result.meta.nextCursor ?? null;
352
- } while (cursor);
353
- 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
+ }
354
426
  }
355
427
  };
356
428
 
357
- // src/auth/client.ts
358
- var AuthClient = class {
359
- constructor(http) {
360
- this.http = http;
361
- }
362
- get path() {
363
- 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
+ };
364
437
  }
365
- // ── Sign-up ────────────────────────────────────────────────────────────────
438
+ // ─── TRACK ────────────────────────────────────────────────────────────────
366
439
  /**
367
- * Create a new user account. Returns the user and a session immediately.
440
+ * Track an analytics event.
368
441
  *
369
442
  * @example
370
- * const { data, session } = await db.auth.signUp({
371
- * email: 'alice@example.com',
372
- * password: 'Str0ngP@ss!',
373
- * fullName: 'Alice Smith',
443
+ * await hydrous.analytics.track({
444
+ * event: 'page_view',
445
+ * properties: { page: '/home', referrer: 'google.com' },
446
+ * userId: 'user_abc123',
374
447
  * });
375
- * // Store session.sessionId and session.refreshToken in your app
376
448
  */
377
- async signUp(payload, opts) {
378
- 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
+ }
379
466
  }
380
- // ── Sign-in ────────────────────────────────────────────────────────────────
467
+ // ─── QUERY ────────────────────────────────────────────────────────────────
381
468
  /**
382
- * Authenticate with email + password. Returns user data and a new session.
469
+ * Query recorded analytics events.
383
470
  *
384
471
  * @example
385
- * const { data, session } = await db.auth.signIn({
386
- * email: 'alice@example.com',
387
- * 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,
388
477
  * });
389
478
  */
390
- async signIn(payload, opts) {
391
- 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
+ }
392
494
  }
393
- // ── Sign-out ───────────────────────────────────────────────────────────────
495
+ // ─── BATCH TRACK ─────────────────────────────────────────────────────────
394
496
  /**
395
- * 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).
396
499
  *
397
500
  * @example
398
- * // Single device
399
- * await db.auth.signOut({ sessionId: 'sess_...' });
400
- *
401
- * // All devices
402
- * await db.auth.signOut({ allDevices: true, userId: 'user_...' });
501
+ * await hydrous.analytics.trackBatch([
502
+ * { event: 'signup', userId: 'u1' },
503
+ * { event: 'onboarded', userId: 'u1' },
504
+ * ]);
403
505
  */
404
- async signOut(payload, opts) {
405
- 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
+ }
406
526
  }
407
- // ── Session: validate ──────────────────────────────────────────────────────
408
- /**
409
- * Validate a session token — use this on every protected request in your backend.
410
- *
411
- * @example
412
- * try {
413
- * const { data } = await db.auth.validateSession(sessionId);
414
- * // data is the authenticated user
415
- * } catch (err) {
416
- * // Session expired or invalid
417
- * }
418
- */
419
- async validateSession(sessionId, opts) {
420
- 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;
421
586
  }
422
- // ── Session: refresh ───────────────────────────────────────────────────────
587
+ // ══════════════════════════════════════════════════════════════════════════
588
+ // UPLOAD
589
+ // ══════════════════════════════════════════════════════════════════════════
423
590
  /**
424
- * Exchange a refresh token for a new session (rotation).
425
- * The old session is revoked.
591
+ * Upload a single file to a bucket.
426
592
  *
427
- * @example
428
- * const { session } = await db.auth.refreshSession(refreshToken);
429
- */
430
- async refreshSession(refreshToken, opts) {
431
- return this.http.post(`${this.path}/session/refresh`, { refreshToken }, opts);
432
- }
433
- // ── User: get ──────────────────────────────────────────────────────────────
434
- /**
435
- * 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.
436
596
  *
437
- * @example
438
- * const { data } = await db.auth.getUser('user_abc123');
439
- */
440
- async getUser(userId, opts) {
441
- return this.http.get(`${this.path}/user`, { userId }, opts);
442
- }
443
- // ── User: list ─────────────────────────────────────────────────────────────
444
- /**
445
- * 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 |
446
605
  *
447
- * @example
448
- * const { data, meta } = await db.auth.listUsers({ limit: 50 });
449
- */
450
- async listUsers(options) {
451
- const params = {
452
- limit: options?.limit
453
- };
454
- if (options?.cursor) params["cursor"] = options.cursor;
455
- return this.http.get(`${this.path}/users`, params, options);
456
- }
457
- // ── User: update ───────────────────────────────────────────────────────────
458
- /**
459
- * 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
460
609
  *
461
610
  * @example
462
- * 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
+ * );
463
622
  */
464
- async updateUser(payload, opts) {
465
- 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
+ }
466
713
  }
467
- // ── User: delete ───────────────────────────────────────────────────────────
714
+ // ══════════════════════════════════════════════════════════════════════════
715
+ // UPLOAD RAW (text / JSON / binary from string)
716
+ // ══════════════════════════════════════════════════════════════════════════
468
717
  /**
469
- * 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.
470
720
  *
471
- * @example
472
- * await db.auth.deleteUser('user_abc123');
473
- */
474
- async deleteUser(userId, opts) {
475
- return this.http.delete(`${this.path}/user`, { userId }, opts);
476
- }
477
- // ── Password: change ───────────────────────────────────────────────────────
478
- /**
479
- * Change password for a signed-in user (requires old password).
480
- * 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`
481
725
  *
482
726
  * @example
483
- * await db.auth.changePassword({
484
- * userId: 'user_abc',
485
- * oldPassword: 'Old@Pass1',
486
- * newPassword: 'New@Pass2',
487
- * });
727
+ * await hydrous.storage.uploadText(
728
+ * 'ssk_my_bucket_key',
729
+ * 'reports/summary.txt',
730
+ * 'Hello from Hydrous!',
731
+ * { mimeType: 'text/plain' }
732
+ * );
488
733
  */
489
- async changePassword(payload, opts) {
490
- 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
+ }
491
772
  }
492
- // ── Password: reset request ────────────────────────────────────────────────
773
+ // ══════════════════════════════════════════════════════════════════════════
774
+ // BATCH UPLOAD
775
+ // ══════════════════════════════════════════════════════════════════════════
493
776
  /**
494
- * Request a password reset email.
495
- * Always returns success to prevent email enumeration.
777
+ * Upload multiple files in one request.
496
778
  *
497
- * @example
498
- * await db.auth.requestPasswordReset({ email: 'alice@example.com' });
499
- */
500
- async requestPasswordReset(payload, opts) {
501
- return this.http.post(`${this.path}/password/reset/request`, payload, opts);
502
- }
503
- // ── Password: reset confirm ────────────────────────────────────────────────
504
- /**
505
- * 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.
506
783
  *
507
- * @example
508
- * await db.auth.confirmPasswordReset({ resetToken: 'tok_...', newPassword: 'New@Pass2' });
509
- */
510
- async confirmPasswordReset(payload, opts) {
511
- return this.http.post(`${this.path}/password/reset/confirm`, payload, opts);
512
- }
513
- // ── Email: verify request ──────────────────────────────────────────────────
514
- /**
515
- * 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
516
787
  *
517
788
  * @example
518
- * 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
+ * );
519
799
  */
520
- async requestEmailVerification(payload, opts) {
521
- 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
+ }
522
888
  }
523
- // ── Email: verify confirm ──────────────────────────────────────────────────
889
+ // ══════════════════════════════════════════════════════════════════════════
890
+ // DOWNLOAD
891
+ // ══════════════════════════════════════════════════════════════════════════
524
892
  /**
525
- * Confirm email address using the token from the verification email.
893
+ * Download a single file and return its content as an `ArrayBuffer`.
526
894
  *
527
- * @example
528
- * await db.auth.confirmEmailVerification({ verifyToken: 'tok_...' });
529
- */
530
- async confirmEmailVerification(payload, opts) {
531
- return this.http.post(`${this.path}/email/verify/confirm`, payload, opts);
532
- }
533
- // ── Account: lock ──────────────────────────────────────────────────────────
534
- /**
535
- * Lock a user account for a given duration (default: 15 min).
895
+ * @param bucketKey Your storage bucket key (`ssk_…`)
896
+ * @param filePath Path of the file within your bucket
536
897
  *
537
898
  * @example
538
- * await db.auth.lockAccount({ userId: 'user_abc', duration: 30 * 60 * 1000 });
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
+ * }
539
907
  */
540
- async lockAccount(payload, opts) {
541
- return this.http.post(`${this.path}/account/lock`, 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
+ }
542
922
  }
543
- // ── Account: unlock ────────────────────────────────────────────────────────
923
+ // ══════════════════════════════════════════════════════════════════════════
924
+ // BATCH DOWNLOAD
925
+ // ══════════════════════════════════════════════════════════════════════════
544
926
  /**
545
- * Unlock a previously locked user account.
927
+ * Download multiple files in one request.
546
928
  *
547
- * @example
548
- * await db.auth.unlockAccount('user_abc');
549
- */
550
- async unlockAccount(userId, opts) {
551
- return this.http.post(`${this.path}/account/unlock`, { userId }, opts);
552
- }
553
- // ── Helpers ────────────────────────────────────────────────────────────────
554
- /**
555
- * Fetch all users, automatically following cursors.
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
556
935
  *
557
936
  * @example
558
- * const users = await db.auth.listAllUsers();
559
- */
560
- async listAllUsers(opts) {
561
- const all = [];
562
- let cursor = null;
563
- do {
564
- const result = await this.listUsers(cursor ? { cursor, ...opts } : { ...opts });
565
- all.push(...result.data);
566
- cursor = result.meta.nextCursor ?? null;
567
- } while (cursor);
568
- return all;
569
- }
570
- };
571
-
572
- // src/analytics/client.ts
573
- var AnalyticsClient = class {
574
- constructor(http) {
575
- this.http = http;
576
- }
577
- /** Builds the path for a given bucket: /api/analytics/:bucketKey/:securityKey */
578
- path(bucketKey) {
579
- return `/api/analytics/${bucketKey}/${this.http.securityKey}`;
580
- }
581
- // ── Raw query ─────────────────────────────────────────────────────────────
582
- /**
583
- * Run any analytics query with full control over the payload.
584
- * Prefer the typed convenience methods below for everyday use.
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
+ * );
585
945
  */
586
- async query(payload, options) {
587
- const { bucketKey, ...rest } = options;
588
- return this.http.post(this.path(bucketKey), payload, rest);
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
+ }
589
1010
  }
590
- // ── count ──────────────────────────────────────────────────────────────────
1011
+ // ══════════════════════════════════════════════════════════════════════════
1012
+ // LIST
1013
+ // ══════════════════════════════════════════════════════════════════════════
591
1014
  /**
592
- * Total record count, optionally scoped to a date range.
1015
+ * List files and folders inside a bucket (or a folder within it).
593
1016
  *
594
- * @example
595
- * const { data } = await db.analytics.count({ bucketKey: 'orders' });
596
- * console.log(data.count);
1017
+ * Results are paginated — use `pagination.nextCursor` to fetch the next page.
597
1018
  *
598
- * const { data } = await db.analytics.count({
599
- * bucketKey: 'orders',
600
- * dateRange: { startDate: '2025-01-01', endDate: '2025-12-31' },
601
- * });
602
- */
603
- async count(options) {
604
- const { dateRange, ...rest } = options;
605
- return this.query({ queryType: "count", dateRange }, rest);
606
- }
607
- // ── distribution ───────────────────────────────────────────────────────────
608
- /**
609
- * Value distribution (histogram) for a field.
1019
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1020
+ * @param options `prefix`, `limit`, `cursor`
610
1021
  *
611
1022
  * @example
612
- * const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
613
- * // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
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
+ * }
614
1030
  */
615
- async distribution(field, options) {
616
- const { limit, order, dateRange, ...rest } = options;
617
- return this.query({ queryType: "distribution", field, limit, order, dateRange }, rest);
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
+ }
618
1050
  }
619
- // ── sum ────────────────────────────────────────────────────────────────────
1051
+ // ══════════════════════════════════════════════════════════════════════════
1052
+ // METADATA
1053
+ // ══════════════════════════════════════════════════════════════════════════
620
1054
  /**
621
- * Sum a numeric field, with optional group-by.
1055
+ * Get metadata for a specific file (size, MIME type, compression info, etc.)
622
1056
  *
623
- * @example
624
- * const { data } = await db.analytics.sum('revenue', { bucketKey: 'orders', groupBy: 'region' });
625
- */
626
- async sum(field, options) {
627
- const { groupBy, limit, dateRange, ...rest } = options;
628
- return this.query({ queryType: "sum", field, groupBy, limit, dateRange }, rest);
629
- }
630
- // ── timeSeries ─────────────────────────────────────────────────────────────
631
- /**
632
- * Record count grouped over time.
1057
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1058
+ * @param filePath Path of the file within your bucket
633
1059
  *
634
1060
  * @example
635
- * const { data } = await db.analytics.timeSeries({ bucketKey: 'orders', granularity: 'day' });
636
- * // [{ date: '2025-01-01', count: 42 }, ...]
1061
+ * const { data } = await hydrous.storage.metadata(
1062
+ * 'ssk_my_bucket_key',
1063
+ * 'avatars/alice.jpg'
1064
+ * );
1065
+ * console.log(data.size, data.mimeType);
637
1066
  */
638
- async timeSeries(options) {
639
- const { granularity, dateRange, ...rest } = options;
640
- return this.query({ queryType: "timeSeries", granularity, dateRange }, rest);
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
+ }
641
1076
  }
642
- // ── fieldTimeSeries ────────────────────────────────────────────────────────
1077
+ // ══════════════════════════════════════════════════════════════════════════
1078
+ // DELETE FILE
1079
+ // ══════════════════════════════════════════════════════════════════════════
643
1080
  /**
644
- * Aggregate a numeric field over time.
1081
+ * Delete a single file.
1082
+ *
1083
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1084
+ * @param filePath Path of the file to delete
645
1085
  *
646
1086
  * @example
647
- * const { data } = await db.analytics.fieldTimeSeries('revenue', {
648
- * bucketKey: 'orders',
649
- * granularity: 'month',
650
- * aggregation: 'sum',
651
- * });
1087
+ * await hydrous.storage.deleteFile('ssk_my_bucket_key', 'avatars/old.jpg');
652
1088
  */
653
- async fieldTimeSeries(field, options) {
654
- const { aggregation, granularity, dateRange, ...rest } = options;
655
- return this.query({ queryType: "fieldTimeSeries", field, aggregation, granularity, dateRange }, rest);
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
+ }
656
1102
  }
657
- // ── topN ───────────────────────────────────────────────────────────────────
1103
+ // ══════════════════════════════════════════════════════════════════════════
1104
+ // DELETE FOLDER
1105
+ // ══════════════════════════════════════════════════════════════════════════
658
1106
  /**
659
- * Top N most frequent values for a field.
1107
+ * Recursively delete a folder and all of its contents.
1108
+ *
1109
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1110
+ * @param folderPath Folder path to delete (e.g. `"old-uploads/"`)
660
1111
  *
661
1112
  * @example
662
- * const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
663
- * // [{ label: 'US', value: 'US', count: 500 }, ...]
1113
+ * await hydrous.storage.deleteFolder('ssk_my_bucket_key', 'temp/');
664
1114
  */
665
- async topN(field, n = 10, options) {
666
- const { labelField, order, dateRange, ...rest } = options;
667
- return this.query({ queryType: "topN", field, n, labelField, order, dateRange }, rest);
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
+ }
668
1128
  }
669
- // ── stats ──────────────────────────────────────────────────────────────────
1129
+ // ══════════════════════════════════════════════════════════════════════════
1130
+ // CREATE FOLDER
1131
+ // ══════════════════════════════════════════════════════════════════════════
670
1132
  /**
671
- * Statistical summary for a numeric field: min, max, avg, stddev, p50, p90, p99.
1133
+ * Create an empty folder.
1134
+ *
1135
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1136
+ * @param folderPath Path for the new folder (e.g. `"avatars/2024/"`)
672
1137
  *
673
1138
  * @example
674
- * const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
675
- * console.log(data.avg, data.p99);
1139
+ * await hydrous.storage.createFolder('ssk_my_bucket_key', 'avatars/2024/');
676
1140
  */
677
- async stats(field, options) {
678
- const { dateRange, ...rest } = options;
679
- return this.query({ queryType: "stats", field, dateRange }, rest);
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
+ }
680
1154
  }
681
- // ── records ────────────────────────────────────────────────────────────────
1155
+ // ══════════════════════════════════════════════════════════════════════════
1156
+ // MOVE
1157
+ // ══════════════════════════════════════════════════════════════════════════
682
1158
  /**
683
- * Filtered, paginated raw records with optional field projection.
684
- * Supports filter ops: == != > < >= <= CONTAINS
1159
+ * Move (rename) a file to a new path.
1160
+ *
1161
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1162
+ * @param fromPath Current path of the file
1163
+ * @param toPath New path for the file
685
1164
  *
686
1165
  * @example
687
- * const { data } = await db.analytics.records({
688
- * bucketKey: 'users',
689
- * filters: [{ field: 'role', op: '==', value: 'admin' }],
690
- * selectFields: ['email', 'createdAt'],
691
- * limit: 25,
692
- * });
1166
+ * await hydrous.storage.move(
1167
+ * 'ssk_my_bucket_key',
1168
+ * 'drafts/report.pdf',
1169
+ * 'published/report.pdf'
1170
+ * );
693
1171
  */
694
- async records(options) {
695
- const { filters, selectFields, limit, offset, orderBy, order, dateRange, ...rest } = options;
696
- return this.query(
697
- { queryType: "records", filters, selectFields, limit, offset, orderBy, order, dateRange },
698
- rest
699
- );
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
+ }
700
1185
  }
701
- // ── multiMetric ────────────────────────────────────────────────────────────
1186
+ // ══════════════════════════════════════════════════════════════════════════
1187
+ // COPY
1188
+ // ══════════════════════════════════════════════════════════════════════════
702
1189
  /**
703
- * Multiple aggregations in a single call ideal for dashboard stat cards.
1190
+ * Copy a file to a new path (original is kept).
1191
+ *
1192
+ * @param bucketKey Your storage bucket key (`ssk_…`)
1193
+ * @param fromPath Source path
1194
+ * @param toPath Destination path
704
1195
  *
705
1196
  * @example
706
- * const { data } = await db.analytics.multiMetric(
707
- * [
708
- * { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
709
- * { name: 'avgScore', field: 'score', aggregation: 'avg' },
710
- * ],
711
- * { bucketKey: 'orders' }
1197
+ * await hydrous.storage.copy(
1198
+ * 'ssk_my_bucket_key',
1199
+ * 'templates/invoice.pdf',
1200
+ * 'invoices/invoice-001.pdf'
712
1201
  * );
713
- * console.log(data.totalRevenue, data.avgScore);
714
1202
  */
715
- async multiMetric(metrics, options) {
716
- const { dateRange, ...rest } = options;
717
- return this.query({ queryType: "multiMetric", metrics, dateRange }, rest);
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
+ }
718
1216
  }
719
- // ── storageStats ───────────────────────────────────────────────────────────
1217
+ // ══════════════════════════════════════════════════════════════════════════
1218
+ // SIGNED URL
1219
+ // ══════════════════════════════════════════════════════════════════════════
720
1220
  /**
721
- * Storage statistics for a bucket total records, bytes, avg/min/max size.
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)
722
1226
  *
723
1227
  * @example
724
- * const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
725
- * 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
726
1234
  */
727
- async storageStats(options) {
728
- const { dateRange, ...rest } = options;
729
- return this.query({ queryType: "storageStats", dateRange }, rest);
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
+ }
730
1249
  }
731
- // ── crossBucket ────────────────────────────────────────────────────────────
1250
+ // ══════════════════════════════════════════════════════════════════════════
1251
+ // STATS
1252
+ // ══════════════════════════════════════════════════════════════════════════
732
1253
  /**
733
- * Compare a metric across multiple buckets in one query.
734
- * Note: `bucketKey` here is used only for auth — the actual buckets
735
- * compared are specified via `bucketKeys`.
1254
+ * Get usage and billing statistics for this bucket key.
1255
+ *
1256
+ * @param bucketKey Your storage bucket key (`ssk_…`)
736
1257
  *
737
1258
  * @example
738
- * const { data } = await db.analytics.crossBucket({
739
- * bucketKey: 'sales-2025', // auth bucket
740
- * bucketKeys: ['sales-2024', 'sales-2025'],
741
- * field: 'amount',
742
- * aggregation: 'sum',
743
- * });
1259
+ * const { data } = await hydrous.storage.stats('ssk_my_bucket_key');
1260
+ * console.log(`${data.totalFiles} files, ${data.totalSizeBytes} bytes stored`);
744
1261
  */
745
- async crossBucket(options) {
746
- const { bucketKeys, field, aggregation, dateRange, ...rest } = options;
747
- return this.query({ queryType: "crossBucket", bucketKeys, field, aggregation, dateRange }, rest);
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
+ }
748
1271
  }
749
1272
  };
750
1273
 
751
1274
  // src/client.ts
752
1275
  var HydrousClient = class {
753
1276
  constructor(config) {
754
- if (!config.authKey) throw new Error("[hydrousdb] authKey is required");
755
- if (!config.securityKey) throw new Error("[hydrousdb] securityKey is required");
756
- this.http = new HttpClient(config);
757
- this.records = new RecordsClient(this.http);
758
- this.auth = new AuthClient(this.http);
759
- 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);
760
1283
  }
761
1284
  };
1285
+
1286
+ // src/index.ts
762
1287
  function createClient(config) {
763
1288
  return new HydrousClient(config);
764
1289
  }
@@ -766,10 +1291,15 @@ function createClient(config) {
766
1291
  exports.AnalyticsClient = AnalyticsClient;
767
1292
  exports.AuthClient = AuthClient;
768
1293
  exports.HydrousClient = HydrousClient;
769
- exports.HydrousError = HydrousError;
770
- exports.HydrousNetworkError = HydrousNetworkError;
771
- exports.HydrousTimeoutError = HydrousTimeoutError;
1294
+ exports.HydrousSDKError = HydrousSDKError;
772
1295
  exports.RecordsClient = RecordsClient;
1296
+ exports.StorageClient = StorageClient;
773
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;
774
1304
  //# sourceMappingURL=index.js.map
775
1305
  //# sourceMappingURL=index.js.map