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