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