hydrousdb 2.0.0 → 2.0.3

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.mjs CHANGED
@@ -1,766 +1,1038 @@
1
1
  // src/utils/errors.ts
2
- var HydrousError = class extends Error {
3
- constructor(body, status) {
4
- super(body.error);
5
- this.name = "HydrousError";
2
+ var HydrousDBError = class extends Error {
3
+ constructor(message, code = "SDK_ERROR", status) {
4
+ super(message);
5
+ this.name = "HydrousDBError";
6
+ this.code = code;
6
7
  this.status = status;
7
- this.code = body.code;
8
- this.details = body.details ?? [];
9
- this.requestId = body.requestId;
10
8
  }
11
9
  };
12
- var HydrousNetworkError = class extends Error {
13
- constructor(message, cause) {
14
- super(message);
15
- this.name = "HydrousNetworkError";
16
- this.cause = cause;
10
+ function toHydrousError(err) {
11
+ if (err instanceof HydrousDBError) {
12
+ return { message: err.message, code: err.code, status: err.status };
17
13
  }
18
- };
19
- var HydrousTimeoutError = class extends Error {
20
- constructor(timeoutMs) {
21
- super(`Request timed out after ${timeoutMs}ms`);
22
- this.name = "HydrousTimeoutError";
14
+ if (err instanceof Error) {
15
+ return { message: err.message, code: "UNKNOWN_ERROR" };
23
16
  }
24
- };
17
+ return { message: String(err), code: "UNKNOWN_ERROR" };
18
+ }
19
+ function isHydrousError(err) {
20
+ return err instanceof HydrousDBError;
21
+ }
25
22
 
26
23
  // src/utils/http.ts
27
- var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
28
- var DEFAULT_TIMEOUT = 3e4;
29
- var DEFAULT_RETRIES = 2;
30
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
31
- var HttpClient = class {
32
- constructor(config) {
33
- this.authKey = config.authKey;
34
- this.securityKey = config.securityKey;
35
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
36
- this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
37
- this.retries = config.retries ?? DEFAULT_RETRIES;
38
- }
39
- // ── Core request ────────────────────────────────────────────────────────────
40
- async request(method, path, options) {
41
- const url = this.buildUrl(path, options?.params);
42
- const headers = {
43
- "Content-Type": "application/json",
44
- ...options?.headers
45
- };
46
- const init = {
47
- method,
48
- headers,
49
- signal: this.buildSignal(options?.signal)
50
- };
51
- if (options?.body !== void 0) {
52
- init.body = JSON.stringify(options.body);
53
- }
54
- return this.executeWithRetry(url, init, options?.raw ?? false, this.retries);
24
+ async function parseResponse(res) {
25
+ let body;
26
+ try {
27
+ body = await res.json();
28
+ } catch (e) {
29
+ if (!res.ok) throw new HydrousDBError(`HTTP ${res.status}`, "HTTP_ERROR", res.status);
30
+ return void 0;
55
31
  }
56
- // ── Convenience methods ──────────────────────────────────────────────────────
57
- get(path, params, opts) {
58
- return this.request("GET", path, { params, ...opts });
59
- }
60
- post(path, body, opts) {
61
- return this.request("POST", path, { body, ...opts });
32
+ if (!res.ok) {
33
+ const e = body;
34
+ throw new HydrousDBError(
35
+ e.error || e.message || `HTTP ${res.status}`,
36
+ e.code || "HTTP_ERROR",
37
+ res.status
38
+ );
62
39
  }
63
- patch(path, body, opts) {
64
- return this.request("PATCH", path, { body, ...opts });
40
+ return body;
41
+ }
42
+ function buildUrl(base, path, params) {
43
+ const url = new URL(path, base.endsWith("/") ? base : base + "/");
44
+ if (params) {
45
+ for (const [k, v] of Object.entries(params)) {
46
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
47
+ }
65
48
  }
66
- delete(path, params, opts) {
67
- return this.request("DELETE", path, { params, ...opts });
49
+ return url.toString();
50
+ }
51
+ function mergeHeaders(a, b) {
52
+ return { ...a, ...b };
53
+ }
54
+ async function readSSEStream(response, onEvent) {
55
+ if (!response.body) return;
56
+ const reader = response.body.getReader();
57
+ const decoder = new TextDecoder();
58
+ let buf = "";
59
+ const flush = (chunk) => {
60
+ var _a;
61
+ buf += chunk;
62
+ const blocks = buf.split("\n\n");
63
+ buf = (_a = blocks.pop()) != null ? _a : "";
64
+ for (const block of blocks) {
65
+ if (!block.trim()) continue;
66
+ let eventType = "message";
67
+ let dataLine = null;
68
+ for (const line of block.split("\n")) {
69
+ if (line.startsWith("event:")) eventType = line.slice(6).trim();
70
+ if (line.startsWith("data:")) dataLine = line.slice(5).trim();
71
+ }
72
+ if (dataLine === null) continue;
73
+ try {
74
+ onEvent(eventType, JSON.parse(dataLine));
75
+ } catch (e) {
76
+ }
77
+ }
78
+ };
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+ flush(decoder.decode(value, { stream: true }));
68
83
  }
69
- head(path, params, opts) {
70
- return this.request("HEAD", path, { params, raw: true, ...opts });
84
+ if (buf.trim()) flush("");
85
+ }
86
+ function parseSSEText(text, onEvent) {
87
+ const blocks = text.split("\n\n");
88
+ for (const block of blocks) {
89
+ if (!block.trim()) continue;
90
+ let eventType = "message";
91
+ let dataLine = null;
92
+ for (const line of block.split("\n")) {
93
+ if (line.startsWith("event:")) eventType = line.slice(6).trim();
94
+ if (line.startsWith("data:")) dataLine = line.slice(5).trim();
95
+ }
96
+ if (dataLine === null) continue;
97
+ try {
98
+ onEvent(eventType, JSON.parse(dataLine));
99
+ } catch (e) {
100
+ }
71
101
  }
72
- // ── Internals ────────────────────────────────────────────────────────────────
73
- buildUrl(path, params) {
74
- const url = new URL(`${this.baseUrl}${path}`);
75
- if (params) {
76
- for (const [k, v] of Object.entries(params)) {
77
- if (v !== void 0 && v !== null && v !== "") {
78
- url.searchParams.set(k, String(v));
102
+ }
103
+ function xhrUpload(url, body, headers, onProgress) {
104
+ return new Promise((resolve, reject) => {
105
+ const xhr = new XMLHttpRequest();
106
+ xhr.open("POST", url);
107
+ for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
108
+ xhr.responseType = "text";
109
+ if (onProgress) {
110
+ xhr.upload.onprogress = (e) => {
111
+ if (e.lengthComputable) onProgress(e.loaded, e.total);
112
+ };
113
+ }
114
+ xhr.onload = () => {
115
+ var _a;
116
+ if (xhr.status >= 200 && xhr.status < 300) {
117
+ resolve(xhr.responseText);
118
+ } else {
119
+ try {
120
+ const d = JSON.parse(xhr.responseText);
121
+ reject(new HydrousDBError((_a = d.error) != null ? _a : `HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
122
+ } catch (e) {
123
+ reject(new HydrousDBError(`HTTP ${xhr.status}`, "HTTP_ERROR", xhr.status));
79
124
  }
80
125
  }
126
+ };
127
+ xhr.onerror = () => reject(new HydrousDBError("Network error", "NETWORK_ERROR"));
128
+ xhr.onabort = () => reject(new HydrousDBError("Upload aborted", "UPLOAD_ABORTED"));
129
+ xhr.ontimeout = () => reject(new HydrousDBError("Upload timed out", "UPLOAD_TIMEOUT"));
130
+ xhr.send(body);
131
+ });
132
+ }
133
+
134
+ // src/auth/client.ts
135
+ var AuthClient = class {
136
+ constructor(config) {
137
+ this.session = null;
138
+ this.baseUrl = config.url;
139
+ this.headers = {
140
+ "Content-Type": "application/json",
141
+ "Authorization": `Bearer ${config.authKey}`
142
+ };
143
+ }
144
+ /** Create a new user account */
145
+ async signUp(options) {
146
+ try {
147
+ const res = await fetch(buildUrl(this.baseUrl, "auth/signup"), {
148
+ method: "POST",
149
+ headers: this.headers,
150
+ body: JSON.stringify(options)
151
+ });
152
+ const json = await parseResponse(res);
153
+ this.session = json.data;
154
+ return { data: json.data, error: null };
155
+ } catch (err) {
156
+ return { data: null, error: toHydrousError(err) };
81
157
  }
82
- return url.toString();
83
158
  }
84
- buildSignal(userSignal) {
85
- const controller = new AbortController();
86
- const timeoutId = setTimeout(() => controller.abort("timeout"), this.timeout);
87
- userSignal?.addEventListener("abort", () => {
88
- clearTimeout(timeoutId);
89
- controller.abort(userSignal.reason);
90
- });
91
- return controller.signal;
159
+ /** Sign in with email and password */
160
+ async signIn(options) {
161
+ try {
162
+ const res = await fetch(buildUrl(this.baseUrl, "auth/signin"), {
163
+ method: "POST",
164
+ headers: this.headers,
165
+ body: JSON.stringify(options)
166
+ });
167
+ const json = await parseResponse(res);
168
+ this.session = json.data;
169
+ return { data: json.data, error: null };
170
+ } catch (err) {
171
+ return { data: null, error: toHydrousError(err) };
172
+ }
92
173
  }
93
- async executeWithRetry(url, init, raw, retriesLeft) {
174
+ /** Sign out and invalidate the current session */
175
+ async signOut() {
94
176
  try {
95
- const res = await fetch(url, init);
96
- if (raw) return res;
97
- if (res.ok) {
98
- if (res.status === 204) return void 0;
99
- return await res.json();
100
- }
101
- let body;
102
- try {
103
- body = await res.json();
104
- } catch {
105
- body = { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
106
- }
107
- if (retriesLeft > 0 && RETRYABLE_STATUSES.has(res.status)) {
108
- await sleep(500 * (this.retries - retriesLeft + 1));
109
- return this.executeWithRetry(url, init, raw, retriesLeft - 1);
110
- }
111
- throw new HydrousError(body, res.status);
177
+ const res = await fetch(buildUrl(this.baseUrl, "auth/signout"), {
178
+ method: "POST",
179
+ headers: mergeHeaders(this.headers, this._sessionHeader())
180
+ });
181
+ await parseResponse(res);
182
+ this.session = null;
183
+ return { data: void 0, error: null };
112
184
  } catch (err) {
113
- if (err instanceof HydrousError) throw err;
114
- if (err instanceof Error && err.message === "timeout") {
115
- throw new HydrousTimeoutError(this.timeout);
116
- }
117
- if (retriesLeft > 0) {
118
- await sleep(500 * (this.retries - retriesLeft + 1));
119
- return this.executeWithRetry(url, init, raw, retriesLeft - 1);
120
- }
121
- throw new HydrousNetworkError(
122
- `Network request failed: ${err instanceof Error ? err.message : String(err)}`,
123
- err
124
- );
185
+ return { data: null, error: toHydrousError(err) };
125
186
  }
126
187
  }
188
+ /** Get the currently authenticated user */
189
+ async getUser() {
190
+ try {
191
+ const res = await fetch(buildUrl(this.baseUrl, "auth/user"), {
192
+ headers: mergeHeaders(this.headers, this._sessionHeader())
193
+ });
194
+ const json = await parseResponse(res);
195
+ return { data: json.data, error: null };
196
+ } catch (err) {
197
+ return { data: null, error: toHydrousError(err) };
198
+ }
199
+ }
200
+ /** Refresh the access token using the stored refresh token */
201
+ async refreshSession() {
202
+ var _a;
203
+ if (!((_a = this.session) == null ? void 0 : _a.refreshToken)) {
204
+ return { data: null, error: { message: "No active session", code: "NO_SESSION" } };
205
+ }
206
+ try {
207
+ const res = await fetch(buildUrl(this.baseUrl, "auth/refresh"), {
208
+ method: "POST",
209
+ headers: this.headers,
210
+ body: JSON.stringify({ refreshToken: this.session.refreshToken })
211
+ });
212
+ const json = await parseResponse(res);
213
+ this.session = json.data;
214
+ return { data: json.data, error: null };
215
+ } catch (err) {
216
+ return { data: null, error: toHydrousError(err) };
217
+ }
218
+ }
219
+ /** Return the current in-memory session (may be null) */
220
+ getSession() {
221
+ return this.session;
222
+ }
223
+ _sessionHeader() {
224
+ var _a;
225
+ return ((_a = this.session) == null ? void 0 : _a.accessToken) ? { "X-Session-Token": this.session.accessToken } : {};
226
+ }
127
227
  };
128
- function sleep(ms) {
129
- return new Promise((resolve) => setTimeout(resolve, ms));
130
- }
131
228
 
132
229
  // src/utils/query.ts
133
- function buildQueryParams(opts) {
230
+ function serialiseQuery(opts = {}) {
231
+ var _a;
134
232
  const params = {};
135
- if (opts.timeScope) params["timeScope"] = opts.timeScope;
136
- if (opts.year) params["year"] = opts.year;
137
- if (opts.sortOrder) params["sortOrder"] = opts.sortOrder;
138
- if (opts.limit) params["limit"] = opts.limit;
139
- if (opts.cursor) params["cursor"] = opts.cursor;
140
- if (opts.fields) params["fields"] = opts.fields;
141
- for (const filter of opts.filters ?? []) {
142
- const paramKey = filter.op === "==" ? filter.field : `${filter.field}[${filter.op}]`;
143
- params[paramKey] = filter.value;
233
+ if (opts.limit !== void 0) params["limit"] = String(opts.limit);
234
+ if (opts.offset !== void 0) params["offset"] = String(opts.offset);
235
+ if (opts.select && opts.select.length > 0) params["select"] = opts.select.join(",");
236
+ if (opts.orderBy) {
237
+ params["orderBy"] = opts.orderBy.field;
238
+ params["direction"] = (_a = opts.orderBy.direction) != null ? _a : "asc";
144
239
  }
240
+ const filters = opts.where ? Array.isArray(opts.where) ? opts.where : [opts.where] : [];
241
+ if (filters.length > 0) params["where"] = JSON.stringify(filters);
145
242
  return params;
146
243
  }
147
- function validateFilters(filters) {
148
- const VALID_OPS = /* @__PURE__ */ new Set(["==", "!=", ">", "<", ">=", "<=", "contains"]);
149
- if (filters.length > 3) {
150
- throw new Error("HydrousDB supports a maximum of 3 filters per query.");
151
- }
152
- for (const f of filters) {
153
- if (!VALID_OPS.has(f.op)) {
154
- throw new Error(
155
- `Invalid filter operator "${f.op}". Valid operators: ${[...VALID_OPS].join(", ")}`
156
- );
157
- }
158
- if (!f.field || typeof f.field !== "string") {
159
- throw new Error('Each filter must have a non-empty "field" string.');
160
- }
161
- }
162
- }
244
+ var eq = (field, value) => ({ field, operator: "eq", value });
245
+ var neq = (field, value) => ({ field, operator: "neq", value });
246
+ var gt = (field, value) => ({ field, operator: "gt", value });
247
+ var lt = (field, value) => ({ field, operator: "lt", value });
248
+ var gte = (field, value) => ({ field, operator: "gte", value });
249
+ var lte = (field, value) => ({ field, operator: "lte", value });
250
+ var inArray = (field, value) => ({ field, operator: "in", value });
163
251
 
164
252
  // src/records/client.ts
165
253
  var RecordsClient = class {
166
- constructor(http) {
167
- this.http = http;
168
- }
169
- /** Builds the base path for a given bucket: /api/:bucketKey/:securityKey */
170
- path(bucketKey) {
171
- return `/api/${bucketKey}/${this.http.securityKey}`;
172
- }
173
- // ── GET — single record ────────────────────────────────────────────────────
174
- /**
175
- * Fetch a single record by ID.
176
- *
177
- * @example
178
- * const { data } = await db.records.get('rec_abc123', { bucketKey: 'users' });
179
- * const { data, history } = await db.records.get('rec_abc123', { bucketKey: 'users', showHistory: true });
180
- */
181
- async get(recordId, options) {
182
- const { bucketKey, showHistory, ...rest } = options;
183
- const params = { recordId };
184
- if (showHistory) params["showHistory"] = "true";
185
- return this.http.get(this.path(bucketKey), params, rest);
254
+ constructor(config) {
255
+ this.baseUrl = config.url;
256
+ this.headers = {
257
+ "Content-Type": "application/json",
258
+ "Authorization": `Bearer ${config.bucketSecurityKey}`
259
+ };
186
260
  }
187
- // ── GET historical snapshot ──────────────────────────────────────────────
188
- /**
189
- * Fetch a specific historical version (generation) of a record.
190
- *
191
- * @example
192
- * const { data } = await db.records.getSnapshot('rec_abc123', '1700000000000000', { bucketKey: 'users' });
193
- */
194
- async getSnapshot(recordId, generation, options) {
195
- const { bucketKey, ...rest } = options;
196
- return this.http.get(this.path(bucketKey), { recordId, generation }, rest);
261
+ /** Query records from a collection */
262
+ async select(collection, options = {}) {
263
+ try {
264
+ const url = buildUrl(this.baseUrl, `records/${collection}`, serialiseQuery(options));
265
+ const res = await fetch(url, { headers: this.headers });
266
+ const json = await parseResponse(res);
267
+ return { data: json.data, count: json.count, error: null };
268
+ } catch (err) {
269
+ return { data: [], count: 0, error: toHydrousError(err) };
270
+ }
197
271
  }
198
- // ── GET collection query ─────────────────────────────────────────────────
199
- /**
200
- * Query a collection with optional filters, sorting, and pagination.
201
- *
202
- * @example
203
- * // Simple query
204
- * const { data, meta } = await db.records.query({ bucketKey: 'orders', limit: 50 });
205
- *
206
- * // Filtered query
207
- * const { data } = await db.records.query({
208
- * bucketKey: 'orders',
209
- * filters: [{ field: 'status', op: '==', value: 'active' }],
210
- * timeScope: '7d',
211
- * });
212
- *
213
- * // Paginated
214
- * let cursor: string | null = null;
215
- * do {
216
- * const result = await db.records.query({ bucketKey: 'orders', limit: 100, cursor: cursor ?? undefined });
217
- * cursor = result.meta.nextCursor;
218
- * } while (cursor);
219
- */
220
- async query(options) {
221
- const { bucketKey, ...rest } = options;
222
- if (rest.filters) validateFilters(rest.filters);
223
- const params = buildQueryParams(rest);
224
- return this.http.get(this.path(bucketKey), params, rest);
272
+ /** Fetch a single record by ID */
273
+ async get(collection, id) {
274
+ try {
275
+ const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), { headers: this.headers });
276
+ const json = await parseResponse(res);
277
+ return { data: json.data, error: null };
278
+ } catch (err) {
279
+ return { data: null, error: toHydrousError(err) };
280
+ }
225
281
  }
226
- // ── POST insert ──────────────────────────────────────────────────────────
227
- /**
228
- * Insert a new record into the specified bucket.
229
- *
230
- * @example
231
- * const { data, meta } = await db.records.insert(
232
- * { values: { name: 'Alice', score: 99 }, queryableFields: ['name'] },
233
- * { bucketKey: 'users' }
234
- * );
235
- */
236
- async insert(payload, options) {
237
- const { bucketKey, ...rest } = options;
238
- return this.http.post(this.path(bucketKey), payload, rest);
282
+ /** Insert one or more records */
283
+ async insert(collection, payload) {
284
+ try {
285
+ const res = await fetch(buildUrl(this.baseUrl, `records/${collection}`), {
286
+ method: "POST",
287
+ headers: this.headers,
288
+ body: JSON.stringify(payload)
289
+ });
290
+ const json = await parseResponse(res);
291
+ return { data: json.data, count: json.count, error: null };
292
+ } catch (err) {
293
+ return { data: [], count: 0, error: toHydrousError(err) };
294
+ }
239
295
  }
240
- // ── PATCH update ─────────────────────────────────────────────────────────
241
- /**
242
- * Update an existing record.
243
- *
244
- * @example
245
- * await db.records.update(
246
- * { recordId: 'rec_abc123', values: { score: 100 }, track_record_history: true },
247
- * { bucketKey: 'users' }
248
- * );
249
- */
250
- async update(payload, options) {
251
- const { bucketKey, ...rest } = options;
252
- return this.http.patch(this.path(bucketKey), payload, rest);
296
+ /** Update a record by ID */
297
+ async update(collection, id, payload) {
298
+ try {
299
+ const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), {
300
+ method: "PATCH",
301
+ headers: this.headers,
302
+ body: JSON.stringify(payload)
303
+ });
304
+ const json = await parseResponse(res);
305
+ return { data: json.data, error: null };
306
+ } catch (err) {
307
+ return { data: null, error: toHydrousError(err) };
308
+ }
253
309
  }
254
- // ── DELETE ─────────────────────────────────────────────────────────────────
255
- /**
256
- * Delete a record permanently.
257
- *
258
- * @example
259
- * await db.records.delete('rec_abc123', { bucketKey: 'users' });
260
- */
261
- async delete(recordId, options) {
262
- const { bucketKey, ...rest } = options;
263
- return this.http.delete(this.path(bucketKey), { recordId }, rest);
310
+ /** Delete a record by ID */
311
+ async delete(collection, id) {
312
+ try {
313
+ const res = await fetch(buildUrl(this.baseUrl, `records/${collection}/${id}`), {
314
+ method: "DELETE",
315
+ headers: this.headers
316
+ });
317
+ await parseResponse(res);
318
+ return { data: void 0, error: null };
319
+ } catch (err) {
320
+ return { data: null, error: toHydrousError(err) };
321
+ }
264
322
  }
265
- // ── HEAD — existence check ─────────────────────────────────────────────────
266
- /**
267
- * Check whether a record exists without fetching its full data.
268
- * Returns `null` if the record is not found.
269
- *
270
- * @example
271
- * const info = await db.records.exists('rec_abc123', { bucketKey: 'users' });
272
- * if (info?.exists) console.log('found at', info.updatedAt);
273
- */
274
- async exists(recordId, options) {
275
- const { bucketKey, ...rest } = options;
276
- const res = await this.http.head(this.path(bucketKey), { recordId }, rest);
277
- if (res.status === 404) return null;
278
- if (!res.ok) return null;
279
- return {
280
- exists: true,
281
- id: res.headers.get("X-Record-Id") ?? recordId,
282
- createdAt: res.headers.get("X-Created-At") ?? "",
283
- updatedAt: res.headers.get("X-Updated-At") ?? "",
284
- sizeBytes: res.headers.get("X-Size-Bytes") ?? ""
323
+ };
324
+
325
+ // src/analytics/client.ts
326
+ var AnalyticsClient = class {
327
+ constructor(config) {
328
+ this.baseUrl = config.url;
329
+ this.headers = {
330
+ "Content-Type": "application/json",
331
+ "Authorization": `Bearer ${config.bucketSecurityKey}`
285
332
  };
286
333
  }
287
- // ── Batch update ─────────────────────────────────────────────────────────
288
- /**
289
- * Update up to 500 records in a single request.
290
- *
291
- * @example
292
- * await db.records.batchUpdate(
293
- * { updates: [{ recordId: 'rec_1', values: { status: 'archived' } }] },
294
- * { bucketKey: 'orders' }
295
- * );
296
- */
297
- async batchUpdate(payload, options) {
298
- const { bucketKey, ...rest } = options;
299
- return this.http.post(`${this.path(bucketKey)}/batch/update`, payload, rest);
300
- }
301
- // ── Batch — delete ─────────────────────────────────────────────────────────
302
- /**
303
- * Delete up to 500 records in a single request.
304
- *
305
- * @example
306
- * await db.records.batchDelete(
307
- * { recordIds: ['rec_1', 'rec_2'] },
308
- * { bucketKey: 'orders' }
309
- * );
310
- */
311
- async batchDelete(payload, options) {
312
- const { bucketKey, ...rest } = options;
313
- return this.http.post(`${this.path(bucketKey)}/batch/delete`, payload, rest);
334
+ /** Track a single analytics event */
335
+ async track(options) {
336
+ var _a;
337
+ try {
338
+ const res = await fetch(buildUrl(this.baseUrl, "analytics/track"), {
339
+ method: "POST",
340
+ headers: this.headers,
341
+ body: JSON.stringify({ ...options, timestamp: (_a = options.timestamp) != null ? _a : Date.now() })
342
+ });
343
+ await parseResponse(res);
344
+ return { data: void 0, error: null };
345
+ } catch (err) {
346
+ return { data: null, error: toHydrousError(err) };
347
+ }
314
348
  }
315
- // ── Batch insert ─────────────────────────────────────────────────────────
316
- /**
317
- * Insert up to 500 records in a single request.
318
- * Returns HTTP 207 (multi-status) check `meta.failed` for partial failures.
319
- *
320
- * @example
321
- * const result = await db.records.batchInsert(
322
- * { records: [{ name: 'Alice' }, { name: 'Bob' }], queryableFields: ['name'] },
323
- * { bucketKey: 'users' }
324
- * );
325
- */
326
- async batchInsert(payload, options) {
327
- const { bucketKey, ...rest } = options;
328
- return this.http.post(`${this.path(bucketKey)}/batch/insert`, payload, rest);
349
+ /** Track many events in one request */
350
+ async trackBatch(events) {
351
+ try {
352
+ const stamped = events.map((e) => {
353
+ var _a;
354
+ return { ...e, timestamp: (_a = e.timestamp) != null ? _a : Date.now() };
355
+ });
356
+ const res = await fetch(buildUrl(this.baseUrl, "analytics/track/batch"), {
357
+ method: "POST",
358
+ headers: this.headers,
359
+ body: JSON.stringify({ events: stamped })
360
+ });
361
+ await parseResponse(res);
362
+ return { data: void 0, error: null };
363
+ } catch (err) {
364
+ return { data: null, error: toHydrousError(err) };
365
+ }
329
366
  }
330
- // ── Helpers ────────────────────────────────────────────────────────────────
331
- /**
332
- * Fetch ALL records matching a query, automatically following cursors.
333
- * Use with care on large collections — prefer `query()` with manual pagination.
334
- *
335
- * @example
336
- * const allRecords = await db.records.queryAll({
337
- * bucketKey: 'orders',
338
- * filters: [{ field: 'type', op: '==', value: 'invoice' }],
339
- * });
340
- */
341
- async queryAll(options) {
342
- const all = [];
343
- let cursor = null;
344
- do {
345
- const result = await this.query(
346
- cursor ? { ...options, cursor } : { ...options }
347
- );
348
- all.push(...result.data);
349
- cursor = result.meta.nextCursor ?? null;
350
- } while (cursor);
351
- return all;
367
+ /** Query recorded analytics events */
368
+ async query(options = {}) {
369
+ try {
370
+ const params = {};
371
+ if (options.event) params["event"] = options.event;
372
+ if (options.from) params["from"] = options.from;
373
+ if (options.to) params["to"] = options.to;
374
+ if (options.limit) params["limit"] = String(options.limit);
375
+ if (options.groupBy) params["groupBy"] = options.groupBy;
376
+ const url = buildUrl(this.baseUrl, "analytics/events", params);
377
+ const res = await fetch(url, { headers: this.headers });
378
+ const json = await parseResponse(res);
379
+ return { data: json.data, count: json.count, error: null };
380
+ } catch (err) {
381
+ return { data: [], count: 0, error: toHydrousError(err) };
382
+ }
352
383
  }
353
384
  };
354
385
 
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}`;
362
- }
363
- // ── Sign-up ────────────────────────────────────────────────────────────────
364
- /**
365
- * Create a new user account. Returns the user and a session immediately.
366
- *
367
- * @example
368
- * const { data, session } = await db.auth.signUp({
369
- * email: 'alice@example.com',
370
- * password: 'Str0ngP@ss!',
371
- * fullName: 'Alice Smith',
372
- * });
373
- * // Store session.sessionId and session.refreshToken in your app
374
- */
375
- async signUp(payload, opts) {
376
- return this.http.post(`${this.path}/signup`, payload, opts);
377
- }
378
- // ── Sign-in ────────────────────────────────────────────────────────────────
379
- /**
380
- * Authenticate with email + password. Returns user data and a new session.
381
- *
382
- * @example
383
- * const { data, session } = await db.auth.signIn({
384
- * email: 'alice@example.com',
385
- * password: 'Str0ngP@ss!',
386
- * });
387
- */
388
- async signIn(payload, opts) {
389
- return this.http.post(`${this.path}/signin`, payload, opts);
390
- }
391
- // ── Sign-out ───────────────────────────────────────────────────────────────
392
- /**
393
- * Revoke a session (or all sessions for a user).
394
- *
395
- * @example
396
- * // Single device
397
- * await db.auth.signOut({ sessionId: 'sess_...' });
398
- *
399
- * // All devices
400
- * await db.auth.signOut({ allDevices: true, userId: 'user_...' });
401
- */
402
- async signOut(payload, opts) {
403
- return this.http.post(`${this.path}/signout`, payload, opts);
386
+ // src/storage/scoped.ts
387
+ var isBrowser = typeof window !== "undefined" && typeof XMLHttpRequest !== "undefined";
388
+ function storageBase(url, bucketKey) {
389
+ return `${url.replace(/\/$/, "")}/storage/${encodeURIComponent(bucketKey)}`;
390
+ }
391
+ function storageHeaders(bucketKey) {
392
+ return { "X-Storage-Key": bucketKey };
393
+ }
394
+ function jsonHeaders(bucketKey) {
395
+ return { "X-Storage-Key": bucketKey, "Content-Type": "application/json" };
396
+ }
397
+ function drainSSE(raw, onProgress) {
398
+ const results = [];
399
+ const errors = [];
400
+ parseSSEText(raw, (eventType, data) => {
401
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
402
+ const d = data;
403
+ if (eventType === "progress" && onProgress) {
404
+ onProgress({
405
+ index: (_a = d["index"]) != null ? _a : 0,
406
+ total: (_b = d["total"]) != null ? _b : 1,
407
+ path: (_c = d["path"]) != null ? _c : "",
408
+ stage: (_d = d["stage"]) != null ? _d : "uploading",
409
+ bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
410
+ totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
411
+ percent: (_g = d["percent"]) != null ? _g : 0,
412
+ bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
413
+ eta: (_i = d["eta"]) != null ? _i : null,
414
+ result: d["result"],
415
+ error: d["error"],
416
+ code: d["code"]
417
+ });
418
+ }
419
+ if (eventType === "done") {
420
+ if (d["path"]) {
421
+ results.push(d);
422
+ } else if (Array.isArray(d["succeeded"])) {
423
+ results.push(...d["succeeded"]);
424
+ errors.push(...(_j = d["errors"]) != null ? _j : []);
425
+ }
426
+ }
427
+ if (eventType === "error") {
428
+ errors.push({
429
+ path: "",
430
+ error: (_k = d["error"]) != null ? _k : "Unknown error",
431
+ code: (_l = d["code"]) != null ? _l : "UNKNOWN"
432
+ });
433
+ }
434
+ });
435
+ return { results, errors };
436
+ }
437
+ var ScopedStorageClient = class {
438
+ constructor(baseUrl, keyName, bucketKey) {
439
+ this.base = storageBase(baseUrl, bucketKey);
440
+ this.key = bucketKey;
441
+ this.keyName = keyName;
404
442
  }
405
- // ── Session: validate ──────────────────────────────────────────────────────
443
+ // ══════════════════════════════════════════════════════════════════════════
444
+ // UPLOAD — single file
445
+ // ══════════════════════════════════════════════════════════════════════════
406
446
  /**
407
- * Validate a session token — use this on every protected request in your backend.
447
+ * Upload a single file.
408
448
  *
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);
419
- }
420
- // ── Session: refresh ───────────────────────────────────────────────────────
421
- /**
422
- * Exchange a refresh token for a new session (rotation).
423
- * The old session is revoked.
449
+ * Supply `onProgress` to receive live upload ticks including bytes
450
+ * transferred, speed (bytes/sec), ETA, and lifecycle stage.
424
451
  *
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.
452
+ * **Stage sequence:**
453
+ * `pending compressing uploading done | error`
434
454
  *
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.
455
+ * In browsers the progress is tracked at the network level via XHR, so
456
+ * `percent` reflects actual bytes leaving the device. `done` only fires
457
+ * after the server confirms the write to cloud storage, so 100% is real.
444
458
  *
445
459
  * @example
446
- * const { data, meta } = await db.auth.listUsers({ limit: 50 });
460
+ * const { data, error } = await db.storage('avatars').upload(file, {
461
+ * path: 'users/alice.jpg',
462
+ * overwrite: true,
463
+ * onProgress: (p) => {
464
+ * setProgress(p.percent); // e.g. drive a <progress> bar
465
+ * setSpeed(`${p.bytesPerSecond} B/s`);
466
+ * setEta(`${p.eta}s remaining`);
467
+ * },
468
+ * });
447
469
  */
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);
470
+ async upload(file, options = {}) {
471
+ var _a, _b;
472
+ const { path, overwrite = false, onProgress } = options;
473
+ try {
474
+ const url = `${this.base}/upload`;
475
+ const form = new FormData();
476
+ if (file instanceof Uint8Array) {
477
+ form.append("file", new Blob([file.buffer]), path != null ? path : "file");
478
+ } else if (file instanceof ArrayBuffer) {
479
+ form.append("file", new Blob([file]), path != null ? path : "file");
480
+ } else {
481
+ form.append("file", file, path != null ? path : file instanceof File ? file.name : "file");
482
+ }
483
+ if (path) form.append("path", path);
484
+ if (overwrite) form.append("overwrite", "true");
485
+ const headers = storageHeaders(this.key);
486
+ if (isBrowser) {
487
+ const totalBytes = file instanceof Blob ? file.size : file instanceof Uint8Array ? file.byteLength : file.byteLength;
488
+ const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
489
+ onProgress == null ? void 0 : onProgress({
490
+ index: 0,
491
+ total: 1,
492
+ path: path != null ? path : "",
493
+ stage: "uploading",
494
+ bytesUploaded: loaded,
495
+ totalBytes: total || totalBytes,
496
+ percent: Math.min(99, Math.round(loaded / (total || totalBytes) * 100)),
497
+ bytesPerSecond: null,
498
+ eta: null
499
+ });
500
+ });
501
+ const { results, errors } = drainSSE(rawBody, onProgress);
502
+ if (errors.length > 0 && results.length === 0) {
503
+ return { data: null, error: { message: errors[0].error, code: errors[0].code } };
504
+ }
505
+ const result = (_a = results[0]) != null ? _a : null;
506
+ if (result && onProgress) {
507
+ onProgress({
508
+ index: 0,
509
+ total: 1,
510
+ path: result.path,
511
+ stage: "done",
512
+ bytesUploaded: totalBytes,
513
+ totalBytes,
514
+ percent: 100,
515
+ bytesPerSecond: null,
516
+ eta: 0,
517
+ result
518
+ });
519
+ }
520
+ return { data: result, error: null };
521
+ }
522
+ const res = await fetch(url, { method: "POST", headers, body: form });
523
+ if (!res.ok) {
524
+ const e = await res.json().catch(() => ({}));
525
+ throw new HydrousDBError((_b = e.error) != null ? _b : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
526
+ }
527
+ let finalResult = null;
528
+ await readSSEStream(res, (eventType, data) => {
529
+ var _a2, _b2, _c, _d, _e, _f, _g, _h;
530
+ const d = data;
531
+ if (eventType === "progress" && onProgress) {
532
+ onProgress({
533
+ index: 0,
534
+ total: 1,
535
+ path: path != null ? path : "",
536
+ stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
537
+ bytesUploaded: (_b2 = d["bytesUploaded"]) != null ? _b2 : 0,
538
+ totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
539
+ percent: (_d = d["percent"]) != null ? _d : 0,
540
+ bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
541
+ eta: (_f = d["eta"]) != null ? _f : null,
542
+ result: d["result"]
543
+ });
544
+ }
545
+ if (eventType === "done") finalResult = data;
546
+ if (eventType === "error") {
547
+ throw new HydrousDBError(
548
+ (_g = d["error"]) != null ? _g : "Upload failed",
549
+ (_h = d["code"]) != null ? _h : "UPLOAD_ERROR"
550
+ );
551
+ }
552
+ });
553
+ return { data: finalResult, error: null };
554
+ } catch (err) {
555
+ return { data: null, error: toHydrousError(err) };
556
+ }
454
557
  }
455
- // ── User: update ───────────────────────────────────────────────────────────
558
+ // ══════════════════════════════════════════════════════════════════════════
559
+ // UPLOAD TEXT / JSON
560
+ // ══════════════════════════════════════════════════════════════════════════
456
561
  /**
457
- * Update user profile fields.
562
+ * Upload raw text or JSON content directly — no File object needed.
458
563
  *
459
564
  * @example
460
- * await db.auth.updateUser({ userId: 'user_abc', updates: { fullName: 'Bob Smith' } });
565
+ * // Save a JSON config
566
+ * await db.storage('configs').uploadText(
567
+ * 'settings/app.json',
568
+ * JSON.stringify({ theme: 'dark' }),
569
+ * { mimeType: 'application/json' }
570
+ * );
461
571
  */
462
- async updateUser(payload, opts) {
463
- return this.http.patch(`${this.path}/user`, payload, opts);
572
+ async uploadText(path, content, options = {}) {
573
+ var _a;
574
+ const { mimeType = "text/plain", overwrite = false, onProgress } = options;
575
+ try {
576
+ const res = await fetch(`${this.base}/upload-raw`, {
577
+ method: "POST",
578
+ headers: jsonHeaders(this.key),
579
+ body: JSON.stringify({ path, content, mimeType, overwrite })
580
+ });
581
+ if (!res.ok) {
582
+ const e = await res.json().catch(() => ({}));
583
+ throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
584
+ }
585
+ let finalResult = null;
586
+ await readSSEStream(res, (eventType, data) => {
587
+ var _a2, _b, _c, _d, _e, _f;
588
+ const d = data;
589
+ if (eventType === "progress" && onProgress) {
590
+ onProgress({
591
+ index: 0,
592
+ total: 1,
593
+ path,
594
+ stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
595
+ bytesUploaded: (_b = d["bytesUploaded"]) != null ? _b : 0,
596
+ totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
597
+ percent: (_d = d["percent"]) != null ? _d : 0,
598
+ bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
599
+ eta: (_f = d["eta"]) != null ? _f : null
600
+ });
601
+ }
602
+ if (eventType === "done") finalResult = data;
603
+ });
604
+ return { data: finalResult, error: null };
605
+ } catch (err) {
606
+ return { data: null, error: toHydrousError(err) };
607
+ }
464
608
  }
465
- // ── User: delete ───────────────────────────────────────────────────────────
609
+ // ══════════════════════════════════════════════════════════════════════════
610
+ // BATCH UPLOAD
611
+ // ══════════════════════════════════════════════════════════════════════════
466
612
  /**
467
- * Soft-delete a user. All their sessions are revoked automatically.
613
+ * Upload multiple files in one request.
468
614
  *
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.
615
+ * `onProgress` fires per file — use `p.index` to identify which file.
616
+ * All files receive a `pending` event upfront so you can render progress
617
+ * bars immediately before any data is sent.
479
618
  *
480
619
  * @example
481
- * await db.auth.changePassword({
482
- * userId: 'user_abc',
483
- * oldPassword: 'Old@Pass1',
484
- * newPassword: 'New@Pass2',
620
+ * await db.storage('documents').batchUpload(files, {
621
+ * prefix: 'reports/2024/',
622
+ * onProgress: (p) => updateBar(p.index, p.percent),
485
623
  * });
486
624
  */
487
- async changePassword(payload, opts) {
488
- return this.http.post(`${this.path}/password/change`, payload, opts);
489
- }
490
- // ── Password: reset request ────────────────────────────────────────────────
491
- /**
492
- * Request a password reset email.
493
- * Always returns success to prevent email enumeration.
494
- *
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.
504
- *
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);
625
+ async batchUpload(files, options = {}) {
626
+ var _a;
627
+ const { prefix = "", paths, overwrite = false, onProgress } = options;
628
+ try {
629
+ const url = `${this.base}/batch-upload`;
630
+ const resolvedPaths = files.map((f, i) => {
631
+ var _a2;
632
+ return (_a2 = paths == null ? void 0 : paths[i]) != null ? _a2 : `${prefix}${f.name}`;
633
+ });
634
+ const form = new FormData();
635
+ files.forEach((f) => form.append("files", f, f.name));
636
+ form.append("paths", JSON.stringify(resolvedPaths));
637
+ if (overwrite) form.append("overwrite", "true");
638
+ const headers = storageHeaders(this.key);
639
+ if (isBrowser) {
640
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
641
+ const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
642
+ if (!onProgress) return;
643
+ let cursor = 0;
644
+ for (let i = 0; i < files.length; i++) {
645
+ const share = files[i].size / (totalBytes || 1);
646
+ const fileLoaded = Math.max(0, Math.min(
647
+ files[i].size,
648
+ (loaded / (total || totalBytes) - cursor) / share * files[i].size
649
+ ));
650
+ onProgress({
651
+ index: i,
652
+ total: files.length,
653
+ path: resolvedPaths[i],
654
+ stage: "uploading",
655
+ bytesUploaded: Math.round(fileLoaded),
656
+ totalBytes: files[i].size,
657
+ percent: Math.min(99, Math.round(fileLoaded / files[i].size * 100)),
658
+ bytesPerSecond: null,
659
+ eta: null
660
+ });
661
+ cursor += share;
662
+ }
663
+ });
664
+ const { results, errors } = drainSSE(rawBody, onProgress);
665
+ return { data: { succeeded: results, failed: errors }, error: null };
666
+ }
667
+ const res = await fetch(url, { method: "POST", headers, body: form });
668
+ if (!res.ok) {
669
+ const e = await res.json().catch(() => ({}));
670
+ throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
671
+ }
672
+ const succeeded = [];
673
+ const failed = [];
674
+ await readSSEStream(res, (eventType, data) => {
675
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
676
+ const d = data;
677
+ if (eventType === "progress" && onProgress) {
678
+ onProgress({
679
+ index: (_a2 = d["index"]) != null ? _a2 : 0,
680
+ total: (_b = d["total"]) != null ? _b : files.length,
681
+ path: (_c = d["path"]) != null ? _c : "",
682
+ stage: (_d = d["stage"]) != null ? _d : "uploading",
683
+ bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
684
+ totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
685
+ percent: (_g = d["percent"]) != null ? _g : 0,
686
+ bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
687
+ eta: (_i = d["eta"]) != null ? _i : null
688
+ });
689
+ }
690
+ if (eventType === "done" && d["succeeded"]) {
691
+ succeeded.push(...d["succeeded"]);
692
+ failed.push(...(_j = d["errors"]) != null ? _j : []);
693
+ }
694
+ });
695
+ return { data: { succeeded, failed }, error: null };
696
+ } catch (err) {
697
+ return { data: null, error: toHydrousError(err) };
698
+ }
510
699
  }
511
- // ── Email: verify request ──────────────────────────────────────────────────
700
+ // ══════════════════════════════════════════════════════════════════════════
701
+ // DOWNLOAD
702
+ // ══════════════════════════════════════════════════════════════════════════
512
703
  /**
513
- * Send a verification email to the user.
704
+ * Download a single file and return its content as an `ArrayBuffer`.
514
705
  *
515
706
  * @example
516
- * await db.auth.requestEmailVerification({ userId: 'user_abc' });
707
+ * const { data } = await db.storage('avatars').download('users/alice.jpg');
708
+ * const blob = new Blob([data!]);
709
+ * img.src = URL.createObjectURL(blob);
517
710
  */
518
- async requestEmailVerification(payload, opts) {
519
- return this.http.post(`${this.path}/email/verify/request`, payload, opts);
711
+ async download(filePath) {
712
+ var _a;
713
+ try {
714
+ const res = await fetch(`${this.base}/download/${filePath}`, {
715
+ headers: storageHeaders(this.key)
716
+ });
717
+ if (!res.ok) {
718
+ const e = await res.json().catch(() => ({}));
719
+ throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
720
+ }
721
+ return { data: await res.arrayBuffer(), error: null };
722
+ } catch (err) {
723
+ return { data: null, error: toHydrousError(err) };
724
+ }
520
725
  }
521
- // ── Email: verify confirm ──────────────────────────────────────────────────
726
+ // ══════════════════════════════════════════════════════════════════════════
727
+ // BATCH DOWNLOAD
728
+ // ══════════════════════════════════════════════════════════════════════════
522
729
  /**
523
- * Confirm email address using the token from the verification email.
730
+ * Download multiple files in one request.
524
731
  *
525
- * @example
526
- * await db.auth.confirmEmailVerification({ verifyToken: 'tok_...' });
527
- */
528
- async confirmEmailVerification(payload, opts) {
529
- return this.http.post(`${this.path}/email/verify/confirm`, payload, opts);
530
- }
531
- // ── Account: lock ──────────────────────────────────────────────────────────
532
- /**
533
- * Lock a user account for a given duration (default: 15 min).
732
+ * Set `autoSave: true` (browser only) to trigger a Save dialog per file.
534
733
  *
535
734
  * @example
536
- * await db.auth.lockAccount({ userId: 'user_abc', duration: 30 * 60 * 1000 });
735
+ * const { data } = await db.storage('reports').batchDownload(
736
+ * ['jan.pdf', 'feb.pdf'],
737
+ * { autoSave: true, onProgress: (p) => console.log(p.path, p.status) }
738
+ * );
537
739
  */
538
- async lockAccount(payload, opts) {
539
- return this.http.post(`${this.path}/account/lock`, payload, opts);
740
+ async batchDownload(filePaths, options = {}) {
741
+ var _a;
742
+ const { concurrency = 5, onProgress, autoSave = false } = options;
743
+ try {
744
+ const res = await fetch(`${this.base}/batch-download`, {
745
+ method: "POST",
746
+ headers: jsonHeaders(this.key),
747
+ body: JSON.stringify({ paths: filePaths, concurrency })
748
+ });
749
+ if (!res.ok) {
750
+ const e = await res.json().catch(() => ({}));
751
+ throw new HydrousDBError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
752
+ }
753
+ const downloadedFiles = [];
754
+ await readSSEStream(res, (eventType, data) => {
755
+ var _a2, _b, _c, _d, _e, _f, _g, _h;
756
+ const d = data;
757
+ if (eventType === "file") {
758
+ const base64 = d["content"];
759
+ const mimeType = (_a2 = d["mimeType"]) != null ? _a2 : "application/octet-stream";
760
+ const path = (_b = d["path"]) != null ? _b : "";
761
+ const size = (_c = d["size"]) != null ? _c : 0;
762
+ const index = (_d = d["index"]) != null ? _d : 0;
763
+ const binary = atob(base64);
764
+ const bytes = new Uint8Array(binary.length);
765
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
766
+ downloadedFiles.push({ path, content: bytes.buffer, mimeType, size });
767
+ onProgress == null ? void 0 : onProgress({ index, total: filePaths.length, path, status: "success", size, mimeType });
768
+ if (autoSave && isBrowser) {
769
+ const blob = new Blob([bytes.buffer], { type: mimeType });
770
+ const blobUrl = URL.createObjectURL(blob);
771
+ const a = document.createElement("a");
772
+ a.href = blobUrl;
773
+ a.download = (_e = path.split("/").pop()) != null ? _e : "download";
774
+ a.click();
775
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 5e3);
776
+ }
777
+ }
778
+ if (eventType === "error" && onProgress) {
779
+ const index = (_f = d["index"]) != null ? _f : 0;
780
+ onProgress({
781
+ index,
782
+ total: filePaths.length,
783
+ path: (_g = filePaths[index]) != null ? _g : "",
784
+ status: "error",
785
+ error: (_h = d["error"]) != null ? _h : "Download failed"
786
+ });
787
+ }
788
+ });
789
+ return { data: downloadedFiles, error: null };
790
+ } catch (err) {
791
+ return { data: null, error: toHydrousError(err) };
792
+ }
540
793
  }
541
- // ── Account: unlock ────────────────────────────────────────────────────────
794
+ // ══════════════════════════════════════════════════════════════════════════
795
+ // LIST
796
+ // ══════════════════════════════════════════════════════════════════════════
542
797
  /**
543
- * Unlock a previously locked user account.
798
+ * List files and folders (paginated).
544
799
  *
545
800
  * @example
546
- * await db.auth.unlockAccount('user_abc');
801
+ * const { data } = await db.storage('avatars').list({ prefix: 'users/' });
802
+ * for (const item of data!.items) {
803
+ * console.log(item.type, item.path, item.size);
804
+ * }
547
805
  */
548
- async unlockAccount(userId, opts) {
549
- return this.http.post(`${this.path}/account/unlock`, { userId }, opts);
806
+ async list(options = {}) {
807
+ const { prefix = "", limit = 50, cursor } = options;
808
+ try {
809
+ const url = buildUrl(this.base, "list", {
810
+ prefix: prefix || void 0,
811
+ limit,
812
+ cursor: cursor || void 0
813
+ });
814
+ const res = await fetch(url.replace(this.base + "/list", `${this.base}/list`), {
815
+ headers: storageHeaders(this.key)
816
+ });
817
+ const u = `${this.base}/list?${new URLSearchParams(
818
+ Object.entries({ prefix: prefix || "", limit: String(limit), ...cursor ? { cursor } : {} }).filter(([, v]) => v !== "")
819
+ ).toString()}`;
820
+ const r = await fetch(u, { headers: storageHeaders(this.key) });
821
+ const json = await parseResponse(r);
822
+ return { data: json, error: null };
823
+ } catch (err) {
824
+ return { data: null, error: toHydrousError(err) };
825
+ }
550
826
  }
551
- // ── Helpers ────────────────────────────────────────────────────────────────
827
+ // ══════════════════════════════════════════════════════════════════════════
828
+ // METADATA
829
+ // ══════════════════════════════════════════════════════════════════════════
552
830
  /**
553
- * Fetch all users, automatically following cursors.
831
+ * Get metadata for a file (size, MIME type, compression, etc.)
554
832
  *
555
833
  * @example
556
- * const users = await db.auth.listAllUsers();
834
+ * const { data: meta } = await db.storage('docs').metadata('report.pdf');
835
+ * console.log(meta!.size, meta!.mimeType, meta!.isCompressed);
557
836
  */
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
- /** Builds the path for a given bucket: /api/analytics/:bucketKey/:securityKey */
576
- path(bucketKey) {
577
- return `/api/analytics/${bucketKey}/${this.http.securityKey}`;
837
+ async metadata(filePath) {
838
+ try {
839
+ const res = await fetch(`${this.base}/metadata/${filePath}`, {
840
+ headers: storageHeaders(this.key)
841
+ });
842
+ const json = await parseResponse(res);
843
+ return { data: json.data, error: null };
844
+ } catch (err) {
845
+ return { data: null, error: toHydrousError(err) };
846
+ }
578
847
  }
579
- // ── Raw query ─────────────────────────────────────────────────────────────
580
- /**
581
- * Run any analytics query with full control over the payload.
582
- * Prefer the typed convenience methods below for everyday use.
583
- */
584
- async query(payload, options) {
585
- const { bucketKey, ...rest } = options;
586
- return this.http.post(this.path(bucketKey), payload, rest);
848
+ // ══════════════════════════════════════════════════════════════════════════
849
+ // DELETE
850
+ // ══════════════════════════════════════════════════════════════════════════
851
+ /** Delete a single file */
852
+ async deleteFile(filePath) {
853
+ try {
854
+ const res = await fetch(`${this.base}/file`, {
855
+ method: "DELETE",
856
+ headers: jsonHeaders(this.key),
857
+ body: JSON.stringify({ path: filePath })
858
+ });
859
+ await parseResponse(res);
860
+ return { data: void 0, error: null };
861
+ } catch (err) {
862
+ return { data: null, error: toHydrousError(err) };
863
+ }
587
864
  }
588
- // ── count ──────────────────────────────────────────────────────────────────
589
- /**
590
- * Total record count, optionally scoped to a date range.
591
- *
592
- * @example
593
- * const { data } = await db.analytics.count({ bucketKey: 'orders' });
594
- * console.log(data.count);
595
- *
596
- * const { data } = await db.analytics.count({
597
- * bucketKey: 'orders',
598
- * dateRange: { startDate: '2025-01-01', endDate: '2025-12-31' },
599
- * });
600
- */
601
- async count(options) {
602
- const { dateRange, ...rest } = options;
603
- return this.query({ queryType: "count", dateRange }, rest);
865
+ /** Recursively delete a folder and all its contents */
866
+ async deleteFolder(folderPath) {
867
+ try {
868
+ const res = await fetch(`${this.base}/folder`, {
869
+ method: "DELETE",
870
+ headers: jsonHeaders(this.key),
871
+ body: JSON.stringify({ path: folderPath })
872
+ });
873
+ await parseResponse(res);
874
+ return { data: void 0, error: null };
875
+ } catch (err) {
876
+ return { data: null, error: toHydrousError(err) };
877
+ }
604
878
  }
605
- // ── distribution ───────────────────────────────────────────────────────────
606
- /**
607
- * Value distribution (histogram) for a field.
608
- *
609
- * @example
610
- * const { data } = await db.analytics.distribution('status', { bucketKey: 'orders' });
611
- * // [{ value: 'active', count: 80 }, { value: 'archived', count: 20 }]
612
- */
613
- async distribution(field, options) {
614
- const { limit, order, dateRange, ...rest } = options;
615
- return this.query({ queryType: "distribution", field, limit, order, dateRange }, rest);
879
+ /** Create an empty folder */
880
+ async createFolder(folderPath) {
881
+ try {
882
+ const res = await fetch(`${this.base}/folder`, {
883
+ method: "POST",
884
+ headers: jsonHeaders(this.key),
885
+ body: JSON.stringify({ path: folderPath })
886
+ });
887
+ await parseResponse(res);
888
+ return { data: void 0, error: null };
889
+ } catch (err) {
890
+ return { data: null, error: toHydrousError(err) };
891
+ }
616
892
  }
617
- // ── sum ────────────────────────────────────────────────────────────────────
618
- /**
619
- * Sum a numeric field, with optional group-by.
620
- *
621
- * @example
622
- * const { data } = await db.analytics.sum('revenue', { bucketKey: 'orders', groupBy: 'region' });
623
- */
624
- async sum(field, options) {
625
- const { groupBy, limit, dateRange, ...rest } = options;
626
- return this.query({ queryType: "sum", field, groupBy, limit, dateRange }, rest);
893
+ // ══════════════════════════════════════════════════════════════════════════
894
+ // MOVE & COPY
895
+ // ══════════════════════════════════════════════════════════════════════════
896
+ /** Move (rename) a file */
897
+ async move(fromPath, toPath) {
898
+ try {
899
+ const res = await fetch(`${this.base}/move`, {
900
+ method: "POST",
901
+ headers: jsonHeaders(this.key),
902
+ body: JSON.stringify({ from: fromPath, to: toPath })
903
+ });
904
+ await parseResponse(res);
905
+ return { data: void 0, error: null };
906
+ } catch (err) {
907
+ return { data: null, error: toHydrousError(err) };
908
+ }
627
909
  }
628
- // ── timeSeries ─────────────────────────────────────────────────────────────
629
- /**
630
- * Record count grouped over time.
631
- *
632
- * @example
633
- * const { data } = await db.analytics.timeSeries({ bucketKey: 'orders', granularity: 'day' });
634
- * // [{ date: '2025-01-01', count: 42 }, ...]
635
- */
636
- async timeSeries(options) {
637
- const { granularity, dateRange, ...rest } = options;
638
- return this.query({ queryType: "timeSeries", granularity, dateRange }, rest);
910
+ /** Copy a file (original is kept) */
911
+ async copy(fromPath, toPath) {
912
+ try {
913
+ const res = await fetch(`${this.base}/copy`, {
914
+ method: "POST",
915
+ headers: jsonHeaders(this.key),
916
+ body: JSON.stringify({ from: fromPath, to: toPath })
917
+ });
918
+ await parseResponse(res);
919
+ return { data: void 0, error: null };
920
+ } catch (err) {
921
+ return { data: null, error: toHydrousError(err) };
922
+ }
639
923
  }
640
- // ── fieldTimeSeries ────────────────────────────────────────────────────────
924
+ // ══════════════════════════════════════════════════════════════════════════
925
+ // SIGNED URL
926
+ // ══════════════════════════════════════════════════════════════════════════
641
927
  /**
642
- * Aggregate a numeric field over time.
928
+ * Generate a time-limited public URL for a private file.
643
929
  *
644
930
  * @example
645
- * const { data } = await db.analytics.fieldTimeSeries('revenue', {
646
- * bucketKey: 'orders',
647
- * granularity: 'month',
648
- * aggregation: 'sum',
649
- * });
931
+ * const { data } = await db.storage('contracts').signedUrl('nda.pdf', { expiresIn: 300 });
932
+ * console.log(data!.signedUrl); // share this
650
933
  */
651
- async fieldTimeSeries(field, options) {
652
- const { aggregation, granularity, dateRange, ...rest } = options;
653
- return this.query({ queryType: "fieldTimeSeries", field, aggregation, granularity, dateRange }, rest);
934
+ async signedUrl(filePath, options = {}) {
935
+ const { expiresIn = 3600 } = options;
936
+ try {
937
+ const res = await fetch(`${this.base}/signed-url`, {
938
+ method: "POST",
939
+ headers: jsonHeaders(this.key),
940
+ body: JSON.stringify({ path: filePath, expiresInSeconds: expiresIn })
941
+ });
942
+ const json = await parseResponse(res);
943
+ return { data: json, error: null };
944
+ } catch (err) {
945
+ return { data: null, error: toHydrousError(err) };
946
+ }
654
947
  }
655
- // ── topN ───────────────────────────────────────────────────────────────────
948
+ // ══════════════════════════════════════════════════════════════════════════
949
+ // STATS
950
+ // ══════════════════════════════════════════════════════════════════════════
656
951
  /**
657
- * Top N most frequent values for a field.
952
+ * Get usage and billing stats for this storage key.
658
953
  *
659
954
  * @example
660
- * const { data } = await db.analytics.topN('country', 5, { bucketKey: 'users' });
661
- * // [{ label: 'US', value: 'US', count: 500 }, ...]
955
+ * const { data } = await db.storage('main').stats();
956
+ * console.log(data!.totalFiles, data!.totalSizeBytes);
662
957
  */
663
- async topN(field, n = 10, options) {
664
- const { labelField, order, dateRange, ...rest } = options;
665
- return this.query({ queryType: "topN", field, n, labelField, order, dateRange }, rest);
958
+ async stats() {
959
+ try {
960
+ const res = await fetch(`${this.base}/stats`, {
961
+ headers: storageHeaders(this.key)
962
+ });
963
+ const json = await parseResponse(res);
964
+ return { data: json.data, error: null };
965
+ } catch (err) {
966
+ return { data: null, error: toHydrousError(err) };
967
+ }
666
968
  }
667
- // ── stats ──────────────────────────────────────────────────────────────────
668
- /**
669
- * Statistical summary for a numeric field: min, max, avg, stddev, p50, p90, p99.
670
- *
671
- * @example
672
- * const { data } = await db.analytics.stats('score', { bucketKey: 'users' });
673
- * console.log(data.avg, data.p99);
674
- */
675
- async stats(field, options) {
676
- const { dateRange, ...rest } = options;
677
- return this.query({ queryType: "stats", field, dateRange }, rest);
969
+ };
970
+
971
+ // src/storage/manager.ts
972
+ var StorageManager = class {
973
+ constructor(config) {
974
+ this.cache = /* @__PURE__ */ new Map();
975
+ this.baseUrl = config.url;
976
+ this._keys = config.storageKeys;
678
977
  }
679
- // ── records ────────────────────────────────────────────────────────────────
680
978
  /**
681
- * Filtered, paginated raw records with optional field projection.
682
- * Supports filter ops: == != > < >= <= CONTAINS
979
+ * Get a storage client scoped to a named key.
683
980
  *
684
- * @example
685
- * const { data } = await db.analytics.records({
686
- * bucketKey: 'users',
687
- * filters: [{ field: 'role', op: '==', value: 'admin' }],
688
- * selectFields: ['email', 'createdAt'],
689
- * limit: 25,
690
- * });
691
- */
692
- async records(options) {
693
- const { filters, selectFields, limit, offset, orderBy, order, dateRange, ...rest } = options;
694
- return this.query(
695
- { queryType: "records", filters, selectFields, limit, offset, orderBy, order, dateRange },
696
- rest
697
- );
698
- }
699
- // ── multiMetric ────────────────────────────────────────────────────────────
700
- /**
701
- * Multiple aggregations in a single call — ideal for dashboard stat cards.
981
+ * @param keyName - Must match a property you declared in `storageKeys`
702
982
  *
703
983
  * @example
704
- * const { data } = await db.analytics.multiMetric(
705
- * [
706
- * { name: 'totalRevenue', field: 'amount', aggregation: 'sum' },
707
- * { name: 'avgScore', field: 'score', aggregation: 'avg' },
708
- * ],
709
- * { bucketKey: 'orders' }
710
- * );
711
- * console.log(data.totalRevenue, data.avgScore);
712
- */
713
- async multiMetric(metrics, options) {
714
- const { dateRange, ...rest } = options;
715
- return this.query({ queryType: "multiMetric", metrics, dateRange }, rest);
716
- }
717
- // ── storageStats ───────────────────────────────────────────────────────────
718
- /**
719
- * Storage statistics for a bucket — total records, bytes, avg/min/max size.
984
+ * const avatarStore = db.storage('avatars');
985
+ * const documentStore = db.storage('documents');
720
986
  *
721
- * @example
722
- * const { data } = await db.analytics.storageStats({ bucketKey: 'orders' });
723
- * console.log(data.totalRecords, data.totalBytes);
987
+ * await avatarStore.upload(file, { path: 'users/alice.jpg' });
988
+ * const list = await documentStore.list({ prefix: 'invoices/' });
724
989
  */
725
- async storageStats(options) {
726
- const { dateRange, ...rest } = options;
727
- return this.query({ queryType: "storageStats", dateRange }, rest);
990
+ use(keyName) {
991
+ const bucketKey = this._keys[keyName];
992
+ if (!bucketKey) {
993
+ const available = Object.keys(this._keys).join(", ");
994
+ throw new HydrousDBError(
995
+ `Storage key "${keyName}" is not defined.
996
+ Available: ${available || "(none)"}`,
997
+ "UNKNOWN_STORAGE_KEY"
998
+ );
999
+ }
1000
+ if (!this.cache.has(keyName)) {
1001
+ this.cache.set(keyName, new ScopedStorageClient(this.baseUrl, keyName, bucketKey));
1002
+ }
1003
+ return this.cache.get(keyName);
728
1004
  }
729
- // ── crossBucket ────────────────────────────────────────────────────────────
730
- /**
731
- * Compare a metric across multiple buckets in one query.
732
- * Note: `bucketKey` here is used only for auth — the actual buckets
733
- * compared are specified via `bucketKeys`.
734
- *
735
- * @example
736
- * const { data } = await db.analytics.crossBucket({
737
- * bucketKey: 'sales-2025', // auth bucket
738
- * bucketKeys: ['sales-2024', 'sales-2025'],
739
- * field: 'amount',
740
- * aggregation: 'sum',
741
- * });
742
- */
743
- async crossBucket(options) {
744
- const { bucketKeys, field, aggregation, dateRange, ...rest } = options;
745
- return this.query({ queryType: "crossBucket", bucketKeys, field, aggregation, dateRange }, rest);
1005
+ /** Return the names of all configured storage keys */
1006
+ keyNames() {
1007
+ return Object.keys(this._keys);
746
1008
  }
747
1009
  };
748
1010
 
749
1011
  // src/client.ts
750
1012
  var HydrousClient = class {
751
1013
  constructor(config) {
752
- if (!config.authKey) throw new Error("[hydrousdb] authKey is required");
753
- if (!config.securityKey) throw new Error("[hydrousdb] securityKey is required");
754
- this.http = new HttpClient(config);
755
- this.records = new RecordsClient(this.http);
756
- this.auth = new AuthClient(this.http);
757
- this.analytics = new AnalyticsClient(this.http);
1014
+ if (!config.url) throw new Error("[HydrousDB] config.url is required");
1015
+ if (!config.authKey) throw new Error("[HydrousDB] config.authKey is required");
1016
+ if (!config.bucketSecurityKey) throw new Error("[HydrousDB] config.bucketSecurityKey is required");
1017
+ if (!config.storageKeys || typeof config.storageKeys !== "object") {
1018
+ throw new Error("[HydrousDB] config.storageKeys must be an object of named keys");
1019
+ }
1020
+ this.auth = new AuthClient(config);
1021
+ this.records = new RecordsClient(config);
1022
+ this.analytics = new AnalyticsClient(config);
1023
+ const manager = new StorageManager(config);
1024
+ const fn = (keyName) => manager.use(keyName);
1025
+ fn.use = (keyName) => manager.use(keyName);
1026
+ fn.keyNames = () => manager.keyNames();
1027
+ this.storage = fn;
758
1028
  }
759
1029
  };
1030
+
1031
+ // src/index.ts
760
1032
  function createClient(config) {
761
1033
  return new HydrousClient(config);
762
1034
  }
763
1035
 
764
- export { AnalyticsClient, AuthClient, HydrousClient, HydrousError, HydrousNetworkError, HydrousTimeoutError, RecordsClient, createClient };
1036
+ export { AnalyticsClient, AuthClient, HydrousClient, HydrousDBError, RecordsClient, ScopedStorageClient, StorageManager, createClient, eq, gt, gte, inArray, isHydrousError, lt, lte, neq };
765
1037
  //# sourceMappingURL=index.mjs.map
766
1038
  //# sourceMappingURL=index.mjs.map