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