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