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