hydrousdb 2.0.3 → 3.0.0
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 +857 -1016
- package/dist/index.cjs +1647 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1540 -0
- package/dist/index.d.ts +1423 -437
- package/dist/index.js +1528 -951
- package/dist/index.js.map +1 -1
- package/package.json +48 -19
- package/dist/index.d.mts +0 -554
- package/dist/index.mjs +0 -1038
- package/dist/index.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1055 +1,1632 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
// src/utils/errors.ts
|
|
4
|
-
var
|
|
5
|
-
constructor(message, code
|
|
2
|
+
var HydrousError = class extends Error {
|
|
3
|
+
constructor(message, code, status, requestId, details) {
|
|
6
4
|
super(message);
|
|
7
|
-
this.name = "
|
|
5
|
+
this.name = "HydrousError";
|
|
8
6
|
this.code = code;
|
|
9
7
|
this.status = status;
|
|
8
|
+
this.requestId = requestId;
|
|
9
|
+
this.details = details;
|
|
10
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
11
|
+
}
|
|
12
|
+
toString() {
|
|
13
|
+
return `HydrousError [${this.code}]: ${this.message}`;
|
|
10
14
|
}
|
|
11
15
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
var AuthError = class extends HydrousError {
|
|
17
|
+
constructor(message, code, status, requestId, details) {
|
|
18
|
+
super(message, code, status, requestId, details);
|
|
19
|
+
this.name = "AuthError";
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
};
|
|
22
|
+
var RecordError = class extends HydrousError {
|
|
23
|
+
constructor(message, code, status, requestId, details) {
|
|
24
|
+
super(message, code, status, requestId, details);
|
|
25
|
+
this.name = "RecordError";
|
|
18
26
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// src/utils/http.ts
|
|
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;
|
|
33
|
-
}
|
|
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
|
-
);
|
|
27
|
+
};
|
|
28
|
+
var StorageError = class extends HydrousError {
|
|
29
|
+
constructor(message, code, status, requestId) {
|
|
30
|
+
super(message, code, status, requestId);
|
|
31
|
+
this.name = "StorageError";
|
|
41
32
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const [k, v] of Object.entries(params)) {
|
|
48
|
-
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
49
|
-
}
|
|
33
|
+
};
|
|
34
|
+
var AnalyticsError = class extends HydrousError {
|
|
35
|
+
constructor(message, code, status, requestId) {
|
|
36
|
+
super(message, code, status, requestId);
|
|
37
|
+
this.name = "AnalyticsError";
|
|
50
38
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 }));
|
|
39
|
+
};
|
|
40
|
+
var ValidationError = class extends HydrousError {
|
|
41
|
+
constructor(message, details) {
|
|
42
|
+
super(message, "VALIDATION_ERROR", 400, void 0, details);
|
|
43
|
+
this.name = "ValidationError";
|
|
85
44
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
}
|
|
45
|
+
};
|
|
46
|
+
var NetworkError = class extends HydrousError {
|
|
47
|
+
constructor(message, cause) {
|
|
48
|
+
super(message, "NETWORK_ERROR");
|
|
49
|
+
this.name = "NetworkError";
|
|
50
|
+
this.cause = cause;
|
|
103
51
|
}
|
|
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));
|
|
126
|
-
}
|
|
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
|
-
}
|
|
52
|
+
};
|
|
135
53
|
|
|
136
|
-
// src/
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.baseUrl =
|
|
141
|
-
this.
|
|
142
|
-
"Content-Type": "application/json",
|
|
143
|
-
"Authorization": `Bearer ${config.authKey}`
|
|
144
|
-
};
|
|
54
|
+
// src/utils/http.ts
|
|
55
|
+
var DEFAULT_BASE_URL = "https://db-api-82687684612.us-central1.run.app";
|
|
56
|
+
var HttpClient = class {
|
|
57
|
+
constructor(baseUrl, securityKey) {
|
|
58
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
59
|
+
this.securityKey = securityKey;
|
|
145
60
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
61
|
+
async request(path, opts = {}) {
|
|
62
|
+
const {
|
|
63
|
+
method = "GET",
|
|
64
|
+
body,
|
|
65
|
+
headers = {},
|
|
66
|
+
rawBody,
|
|
67
|
+
contentType = "application/json"
|
|
68
|
+
} = opts;
|
|
69
|
+
const url = `${this.baseUrl}${path}`;
|
|
70
|
+
const reqHeaders = {
|
|
71
|
+
"X-Api-Key": this.securityKey,
|
|
72
|
+
...headers
|
|
73
|
+
};
|
|
74
|
+
let reqBody = null;
|
|
75
|
+
if (rawBody !== void 0) {
|
|
76
|
+
reqBody = rawBody;
|
|
77
|
+
if (contentType) reqHeaders["Content-Type"] = contentType;
|
|
78
|
+
} else if (body !== void 0) {
|
|
79
|
+
reqBody = JSON.stringify(body);
|
|
80
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
159
81
|
}
|
|
160
|
-
|
|
161
|
-
/** Sign in with email and password */
|
|
162
|
-
async signIn(options) {
|
|
82
|
+
let response;
|
|
163
83
|
try {
|
|
164
|
-
|
|
165
|
-
method
|
|
166
|
-
headers:
|
|
167
|
-
body:
|
|
84
|
+
response = await fetch(url, {
|
|
85
|
+
method,
|
|
86
|
+
headers: reqHeaders,
|
|
87
|
+
body: reqBody
|
|
168
88
|
});
|
|
169
|
-
const json = await parseResponse(res);
|
|
170
|
-
this.session = json.data;
|
|
171
|
-
return { data: json.data, error: null };
|
|
172
89
|
} catch (err) {
|
|
173
|
-
|
|
90
|
+
throw new NetworkError(
|
|
91
|
+
`Network request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
err
|
|
93
|
+
);
|
|
174
94
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
} catch (err) {
|
|
187
|
-
return { data: null, error: toHydrousError(err) };
|
|
95
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
96
|
+
if (!ct.includes("application/json")) {
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new HydrousError(
|
|
99
|
+
`Request failed with status ${response.status}`,
|
|
100
|
+
`HTTP_${response.status}`,
|
|
101
|
+
response.status
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const buffer = await response.arrayBuffer();
|
|
105
|
+
return buffer;
|
|
188
106
|
}
|
|
189
|
-
|
|
190
|
-
/** Get the currently authenticated user */
|
|
191
|
-
async getUser() {
|
|
107
|
+
let responseData;
|
|
192
108
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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" } };
|
|
109
|
+
responseData = await response.json();
|
|
110
|
+
} catch {
|
|
111
|
+
throw new HydrousError(
|
|
112
|
+
"Failed to parse server response as JSON",
|
|
113
|
+
"PARSE_ERROR",
|
|
114
|
+
response.status
|
|
115
|
+
);
|
|
207
116
|
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} catch (err) {
|
|
218
|
-
return { data: null, error: toHydrousError(err) };
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const errData = responseData;
|
|
119
|
+
throw new HydrousError(
|
|
120
|
+
errData["error"] ?? `Request failed with status ${response.status}`,
|
|
121
|
+
errData["code"] ?? `HTTP_${response.status}`,
|
|
122
|
+
response.status,
|
|
123
|
+
errData["requestId"],
|
|
124
|
+
errData["details"]
|
|
125
|
+
);
|
|
219
126
|
}
|
|
127
|
+
return responseData;
|
|
220
128
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return this.session;
|
|
129
|
+
get(path, headers) {
|
|
130
|
+
return this.request(path, { method: "GET", headers });
|
|
224
131
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return ((_a = this.session) == null ? void 0 : _a.accessToken) ? { "X-Session-Token": this.session.accessToken } : {};
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// src/utils/query.ts
|
|
232
|
-
function serialiseQuery(opts = {}) {
|
|
233
|
-
var _a;
|
|
234
|
-
const params = {};
|
|
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";
|
|
241
|
-
}
|
|
242
|
-
const filters = opts.where ? Array.isArray(opts.where) ? opts.where : [opts.where] : [];
|
|
243
|
-
if (filters.length > 0) params["where"] = JSON.stringify(filters);
|
|
244
|
-
return params;
|
|
245
|
-
}
|
|
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 });
|
|
253
|
-
|
|
254
|
-
// src/records/client.ts
|
|
255
|
-
var RecordsClient = class {
|
|
256
|
-
constructor(config) {
|
|
257
|
-
this.baseUrl = config.url;
|
|
258
|
-
this.headers = {
|
|
259
|
-
"Content-Type": "application/json",
|
|
260
|
-
"Authorization": `Bearer ${config.bucketSecurityKey}`
|
|
261
|
-
};
|
|
132
|
+
post(path, body, headers) {
|
|
133
|
+
return this.request(path, { method: "POST", body, headers });
|
|
262
134
|
}
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
}
|
|
135
|
+
put(path, body, headers) {
|
|
136
|
+
return this.request(path, { method: "PUT", body, headers });
|
|
273
137
|
}
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
}
|
|
138
|
+
patch(path, body, headers) {
|
|
139
|
+
return this.request(path, { method: "PATCH", body, headers });
|
|
283
140
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
}
|
|
141
|
+
delete(path, body, headers) {
|
|
142
|
+
return this.request(path, { method: "DELETE", body, headers });
|
|
297
143
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
144
|
+
async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
145
|
+
if (typeof XMLHttpRequest !== "undefined" && onProgress) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const xhr = new XMLHttpRequest();
|
|
148
|
+
xhr.upload.onprogress = (e) => {
|
|
149
|
+
if (e.lengthComputable) {
|
|
150
|
+
onProgress(Math.round(e.loaded / e.total * 100));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
xhr.onload = () => {
|
|
154
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
155
|
+
resolve();
|
|
156
|
+
} else {
|
|
157
|
+
reject(new Error(`Upload failed: ${xhr.status}`));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
xhr.onerror = () => reject(new Error("Upload network error"));
|
|
161
|
+
xhr.open("PUT", signedUrl);
|
|
162
|
+
xhr.setRequestHeader("Content-Type", mimeType);
|
|
163
|
+
const payload = data instanceof Blob ? data : new Blob([data], { type: mimeType });
|
|
164
|
+
xhr.send(payload);
|
|
305
165
|
});
|
|
306
|
-
const json = await parseResponse(res);
|
|
307
|
-
return { data: json.data, error: null };
|
|
308
|
-
} catch (err) {
|
|
309
|
-
return { data: null, error: toHydrousError(err) };
|
|
310
166
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
await parseResponse(res);
|
|
320
|
-
return { data: void 0, error: null };
|
|
321
|
-
} catch (err) {
|
|
322
|
-
return { data: null, error: toHydrousError(err) };
|
|
167
|
+
const fetchBody = data instanceof Blob ? data : new Blob([data], { type: mimeType });
|
|
168
|
+
const response = await fetch(signedUrl, {
|
|
169
|
+
method: "PUT",
|
|
170
|
+
headers: { "Content-Type": mimeType },
|
|
171
|
+
body: fetchBody
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new NetworkError(`Signed URL upload failed with status ${response.status}`);
|
|
323
175
|
}
|
|
324
176
|
}
|
|
325
177
|
};
|
|
326
178
|
|
|
327
|
-
// src/
|
|
328
|
-
var
|
|
329
|
-
constructor(
|
|
330
|
-
this.
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
"Authorization": `Bearer ${config.bucketSecurityKey}`
|
|
334
|
-
};
|
|
335
|
-
}
|
|
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
|
-
}
|
|
350
|
-
}
|
|
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
|
-
}
|
|
179
|
+
// src/auth/client.ts
|
|
180
|
+
var AuthClient = class {
|
|
181
|
+
constructor(http, bucketKey) {
|
|
182
|
+
this.http = http;
|
|
183
|
+
this.bucketKey = bucketKey;
|
|
184
|
+
this.basePath = `/auth/${bucketKey}`;
|
|
368
185
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
186
|
+
// ─── Registration & Login ─────────────────────────────────────────────────
|
|
187
|
+
/**
|
|
188
|
+
* Register a new user in this bucket.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```ts
|
|
192
|
+
* const { user, session } = await auth.signup({
|
|
193
|
+
* email: 'alice@example.com',
|
|
194
|
+
* password: 'hunter2',
|
|
195
|
+
* fullName: 'Alice Wonderland',
|
|
196
|
+
* // Any extra fields are stored on the user record:
|
|
197
|
+
* plan: 'pro',
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async signup(options) {
|
|
202
|
+
const result = await this.http.post(`${this.basePath}/signup`, options);
|
|
203
|
+
return { user: result.user, session: result.session };
|
|
385
204
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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;
|
|
205
|
+
/**
|
|
206
|
+
* Authenticate an existing user and create a session.
|
|
207
|
+
* Sessions are valid for 24 hours; use `refreshSession` to extend.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const { user, session } = await auth.login({
|
|
212
|
+
* email: 'alice@example.com',
|
|
213
|
+
* password: 'hunter2',
|
|
214
|
+
* });
|
|
215
|
+
* // Store session.sessionId safely — you'll need it for other calls.
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async login(options) {
|
|
219
|
+
const result = await this.http.post(`${this.basePath}/login`, options);
|
|
220
|
+
return { user: result.user, session: result.session };
|
|
444
221
|
}
|
|
445
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
446
|
-
// UPLOAD — single file
|
|
447
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
448
222
|
/**
|
|
449
|
-
*
|
|
223
|
+
* Invalidate a session (sign out).
|
|
450
224
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* await auth.logout({ sessionId: session.sessionId });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
async logout({ sessionId }) {
|
|
231
|
+
await this.http.post(`${this.basePath}/logout`, { sessionId });
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Extend a session using its refresh token.
|
|
235
|
+
* Returns a new session object.
|
|
453
236
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* const newSession = await auth.refreshSession({ refreshToken: session.refreshToken });
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
async refreshSession({ refreshToken }) {
|
|
243
|
+
const result = await this.http.post(
|
|
244
|
+
`${this.basePath}/session/refresh`,
|
|
245
|
+
{ refreshToken }
|
|
246
|
+
);
|
|
247
|
+
return result.session;
|
|
248
|
+
}
|
|
249
|
+
// ─── User Profile ─────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Fetch a user by their ID.
|
|
456
252
|
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```ts
|
|
255
|
+
* const user = await auth.getUser({ userId: 'usr_abc123' });
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
async getUser({ userId }) {
|
|
259
|
+
const result = await this.http.get(`${this.basePath}/user/${userId}`);
|
|
260
|
+
return result.user;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Update fields on a user record. Requires a valid session.
|
|
460
264
|
*
|
|
461
265
|
* @example
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* setSpeed(`${p.bytesPerSecond} B/s`);
|
|
468
|
-
* setEta(`${p.eta}s remaining`);
|
|
469
|
-
* },
|
|
266
|
+
* ```ts
|
|
267
|
+
* const updated = await auth.updateUser({
|
|
268
|
+
* sessionId: session.sessionId,
|
|
269
|
+
* userId: user.id,
|
|
270
|
+
* data: { fullName: 'Alice Smith', plan: 'enterprise' },
|
|
470
271
|
* });
|
|
272
|
+
* ```
|
|
471
273
|
*/
|
|
472
|
-
async
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
}
|
|
274
|
+
async updateUser(options) {
|
|
275
|
+
const { sessionId, userId, data } = options;
|
|
276
|
+
const result = await this.http.patch(
|
|
277
|
+
`${this.basePath}/user`,
|
|
278
|
+
{ sessionId, userId, ...data }
|
|
279
|
+
);
|
|
280
|
+
return result.user;
|
|
559
281
|
}
|
|
560
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
561
|
-
// UPLOAD TEXT / JSON
|
|
562
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
563
282
|
/**
|
|
564
|
-
*
|
|
283
|
+
* Soft-delete a user. The record is marked deleted but not removed from storage.
|
|
284
|
+
* Requires a valid session (the user can delete themselves, or an admin can delete any user).
|
|
565
285
|
*
|
|
566
286
|
* @example
|
|
567
|
-
*
|
|
568
|
-
* await
|
|
569
|
-
*
|
|
570
|
-
* JSON.stringify({ theme: 'dark' }),
|
|
571
|
-
* { mimeType: 'application/json' }
|
|
572
|
-
* );
|
|
287
|
+
* ```ts
|
|
288
|
+
* await auth.deleteUser({ sessionId: session.sessionId, userId: user.id });
|
|
289
|
+
* ```
|
|
573
290
|
*/
|
|
574
|
-
async
|
|
575
|
-
|
|
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
|
-
}
|
|
291
|
+
async deleteUser({ sessionId, userId }) {
|
|
292
|
+
await this.http.delete(`${this.basePath}/user`, { sessionId, userId });
|
|
610
293
|
}
|
|
611
|
-
//
|
|
612
|
-
// BATCH UPLOAD
|
|
613
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
294
|
+
// ─── Admin Operations ─────────────────────────────────────────────────────
|
|
614
295
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
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.
|
|
296
|
+
* List all users in the bucket. **Admin session required.**
|
|
620
297
|
*
|
|
621
298
|
* @example
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* const { users, total } = await auth.listUsers({
|
|
301
|
+
* sessionId: adminSession.sessionId,
|
|
302
|
+
* limit: 50,
|
|
303
|
+
* offset: 0,
|
|
625
304
|
* });
|
|
305
|
+
* ```
|
|
626
306
|
*/
|
|
627
|
-
async
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
}
|
|
307
|
+
async listUsers(options) {
|
|
308
|
+
const { sessionId, limit = 50, offset = 0 } = options;
|
|
309
|
+
const result = await this.http.post(
|
|
310
|
+
`${this.basePath}/users/list`,
|
|
311
|
+
{ sessionId, limit, offset }
|
|
312
|
+
);
|
|
313
|
+
return { users: result.users, total: result.total, limit: result.limit, offset: result.offset };
|
|
701
314
|
}
|
|
702
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
703
|
-
// DOWNLOAD
|
|
704
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
705
315
|
/**
|
|
706
|
-
*
|
|
316
|
+
* Permanently hard-delete a user and all their data. **Admin session required.**
|
|
317
|
+
* This action is irreversible.
|
|
707
318
|
*
|
|
708
319
|
* @example
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
320
|
+
* ```ts
|
|
321
|
+
* await auth.hardDeleteUser({ sessionId: adminSession.sessionId, userId: user.id });
|
|
322
|
+
* ```
|
|
712
323
|
*/
|
|
713
|
-
async
|
|
714
|
-
|
|
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
|
-
}
|
|
324
|
+
async hardDeleteUser({ sessionId, userId }) {
|
|
325
|
+
await this.http.delete(`${this.basePath}/user/hard`, { sessionId, userId });
|
|
727
326
|
}
|
|
728
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
729
|
-
// BATCH DOWNLOAD
|
|
730
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
731
327
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
* Set `autoSave: true` (browser only) to trigger a Save dialog per file.
|
|
328
|
+
* Bulk delete multiple users. **Admin session required.**
|
|
735
329
|
*
|
|
736
330
|
* @example
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* const result = await auth.bulkDeleteUsers({
|
|
333
|
+
* sessionId: adminSession.sessionId,
|
|
334
|
+
* userIds: ['usr_a', 'usr_b'],
|
|
335
|
+
* });
|
|
336
|
+
* ```
|
|
741
337
|
*/
|
|
742
|
-
async
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
}
|
|
338
|
+
async bulkDeleteUsers({
|
|
339
|
+
sessionId,
|
|
340
|
+
userIds
|
|
341
|
+
}) {
|
|
342
|
+
const result = await this.http.post(
|
|
343
|
+
`${this.basePath}/users/bulk-delete`,
|
|
344
|
+
{ sessionId, userIds }
|
|
345
|
+
);
|
|
346
|
+
return { deleted: result.deleted, failed: result.failed };
|
|
795
347
|
}
|
|
796
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
797
|
-
// LIST
|
|
798
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
799
348
|
/**
|
|
800
|
-
*
|
|
349
|
+
* Lock a user account, preventing login. **Admin session required.**
|
|
350
|
+
*
|
|
351
|
+
* @param options.duration Lock duration in milliseconds. Defaults to 15 minutes.
|
|
801
352
|
*
|
|
802
353
|
* @example
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
354
|
+
* ```ts
|
|
355
|
+
* await auth.lockAccount({
|
|
356
|
+
* sessionId: adminSession.sessionId,
|
|
357
|
+
* userId: user.id,
|
|
358
|
+
* duration: 60 * 60 * 1000, // 1 hour
|
|
359
|
+
* });
|
|
360
|
+
* ```
|
|
807
361
|
*/
|
|
808
|
-
async
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
362
|
+
async lockAccount({
|
|
363
|
+
sessionId,
|
|
364
|
+
userId,
|
|
365
|
+
duration
|
|
366
|
+
}) {
|
|
367
|
+
const result = await this.http.post(`${this.basePath}/account/lock`, { sessionId, userId, duration });
|
|
368
|
+
return result.data;
|
|
828
369
|
}
|
|
829
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
830
|
-
// METADATA
|
|
831
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
832
370
|
/**
|
|
833
|
-
*
|
|
371
|
+
* Unlock a previously locked user account. **Admin session required.**
|
|
834
372
|
*
|
|
835
373
|
* @example
|
|
836
|
-
*
|
|
837
|
-
*
|
|
374
|
+
* ```ts
|
|
375
|
+
* await auth.unlockAccount({ sessionId: adminSession.sessionId, userId: user.id });
|
|
376
|
+
* ```
|
|
838
377
|
*/
|
|
839
|
-
async
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
}
|
|
849
|
-
}
|
|
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
|
-
}
|
|
866
|
-
}
|
|
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
|
-
}
|
|
880
|
-
}
|
|
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
|
-
}
|
|
894
|
-
}
|
|
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
|
-
}
|
|
911
|
-
}
|
|
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
|
-
}
|
|
378
|
+
async unlockAccount({
|
|
379
|
+
sessionId,
|
|
380
|
+
userId
|
|
381
|
+
}) {
|
|
382
|
+
await this.http.post(`${this.basePath}/account/unlock`, { sessionId, userId });
|
|
925
383
|
}
|
|
926
|
-
//
|
|
927
|
-
// SIGNED URL
|
|
928
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
384
|
+
// ─── Password Management ──────────────────────────────────────────────────
|
|
929
385
|
/**
|
|
930
|
-
*
|
|
386
|
+
* Change a user's password. The user must supply their current password.
|
|
931
387
|
*
|
|
932
388
|
* @example
|
|
933
|
-
*
|
|
934
|
-
*
|
|
389
|
+
* ```ts
|
|
390
|
+
* await auth.changePassword({
|
|
391
|
+
* sessionId: session.sessionId,
|
|
392
|
+
* userId: user.id,
|
|
393
|
+
* currentPassword: 'hunter2',
|
|
394
|
+
* newPassword: 'correcthorsebatterystaple',
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
935
397
|
*/
|
|
936
|
-
async
|
|
937
|
-
|
|
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
|
-
}
|
|
398
|
+
async changePassword(options) {
|
|
399
|
+
await this.http.post(`${this.basePath}/password/change`, options);
|
|
949
400
|
}
|
|
950
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
951
|
-
// STATS
|
|
952
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
953
401
|
/**
|
|
954
|
-
*
|
|
402
|
+
* Request a password reset email for a user.
|
|
403
|
+
* Always returns success to prevent user enumeration.
|
|
955
404
|
*
|
|
956
405
|
* @example
|
|
957
|
-
*
|
|
958
|
-
*
|
|
406
|
+
* ```ts
|
|
407
|
+
* await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
408
|
+
* ```
|
|
959
409
|
*/
|
|
960
|
-
async
|
|
961
|
-
|
|
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
|
-
}
|
|
970
|
-
}
|
|
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;
|
|
410
|
+
async requestPasswordReset({ email }) {
|
|
411
|
+
await this.http.post(`${this.basePath}/password/reset/request`, { email });
|
|
979
412
|
}
|
|
980
413
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
* @param keyName - Must match a property you declared in `storageKeys`
|
|
414
|
+
* Complete a password reset using the token from the reset email.
|
|
984
415
|
*
|
|
985
416
|
* @example
|
|
986
|
-
*
|
|
987
|
-
*
|
|
417
|
+
* ```ts
|
|
418
|
+
* await auth.confirmPasswordReset({
|
|
419
|
+
* resetToken: 'tok_from_email',
|
|
420
|
+
* newPassword: 'correcthorsebatterystaple',
|
|
421
|
+
* });
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
async confirmPasswordReset({
|
|
425
|
+
resetToken,
|
|
426
|
+
newPassword
|
|
427
|
+
}) {
|
|
428
|
+
await this.http.post(`${this.basePath}/password/reset/confirm`, {
|
|
429
|
+
resetToken,
|
|
430
|
+
newPassword
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// ─── Email Verification ───────────────────────────────────────────────────
|
|
434
|
+
/**
|
|
435
|
+
* Send (or resend) an email verification message to a user.
|
|
988
436
|
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```ts
|
|
439
|
+
* await auth.requestEmailVerification({ userId: user.id });
|
|
440
|
+
* ```
|
|
991
441
|
*/
|
|
992
|
-
|
|
993
|
-
|
|
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);
|
|
442
|
+
async requestEmailVerification({ userId }) {
|
|
443
|
+
await this.http.post(`${this.basePath}/email/verify/request`, { userId });
|
|
1006
444
|
}
|
|
1007
|
-
/**
|
|
1008
|
-
|
|
1009
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Confirm an email address using the token from the verification email.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```ts
|
|
450
|
+
* await auth.confirmEmailVerification({ verifyToken: 'tok_from_email' });
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
async confirmEmailVerification({ verifyToken }) {
|
|
454
|
+
await this.http.post(`${this.basePath}/email/verify/confirm`, { verifyToken });
|
|
1010
455
|
}
|
|
1011
456
|
};
|
|
1012
457
|
|
|
1013
|
-
// src/
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
458
|
+
// src/utils/query.ts
|
|
459
|
+
function buildQueryParams(options = {}) {
|
|
460
|
+
const params = new URLSearchParams();
|
|
461
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
462
|
+
if (options.offset !== void 0) params.set("offset", String(options.offset));
|
|
463
|
+
if (options.orderBy !== void 0) params.set("orderBy", options.orderBy);
|
|
464
|
+
if (options.order !== void 0) params.set("order", options.order);
|
|
465
|
+
if (options.fields !== void 0) params.set("fields", options.fields);
|
|
466
|
+
if (options.startAfter !== void 0) params.set("startAfter", options.startAfter);
|
|
467
|
+
if (options.startAt !== void 0) params.set("startAt", options.startAt);
|
|
468
|
+
if (options.endAt !== void 0) params.set("endAt", options.endAt);
|
|
469
|
+
if (options.dateRange?.start !== void 0)
|
|
470
|
+
params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
|
|
471
|
+
if (options.dateRange?.end !== void 0)
|
|
472
|
+
params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
|
|
473
|
+
if (options.filters && options.filters.length > 0) {
|
|
474
|
+
params.set("filters", JSON.stringify(options.filters));
|
|
1030
475
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
476
|
+
const str = params.toString();
|
|
477
|
+
return str ? `?${str}` : "";
|
|
478
|
+
}
|
|
479
|
+
function guessMimeType(filename) {
|
|
480
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
481
|
+
const map = {
|
|
482
|
+
jpg: "image/jpeg",
|
|
483
|
+
jpeg: "image/jpeg",
|
|
484
|
+
png: "image/png",
|
|
485
|
+
gif: "image/gif",
|
|
486
|
+
webp: "image/webp",
|
|
487
|
+
svg: "image/svg+xml",
|
|
488
|
+
pdf: "application/pdf",
|
|
489
|
+
mp4: "video/mp4",
|
|
490
|
+
webm: "video/webm",
|
|
491
|
+
mp3: "audio/mpeg",
|
|
492
|
+
wav: "audio/wav",
|
|
493
|
+
txt: "text/plain",
|
|
494
|
+
html: "text/html",
|
|
495
|
+
css: "text/css",
|
|
496
|
+
js: "application/javascript",
|
|
497
|
+
json: "application/json",
|
|
498
|
+
xml: "application/xml",
|
|
499
|
+
zip: "application/zip",
|
|
500
|
+
csv: "text/csv"
|
|
501
|
+
};
|
|
502
|
+
return map[ext ?? ""] ?? "application/octet-stream";
|
|
503
|
+
}
|
|
504
|
+
function assertSafeName(name, label = "name") {
|
|
505
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.\-]{0,200}$/.test(name)) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Invalid ${label} "${name}". Must start with a letter or underscore and contain only letters, numbers, underscores, dots, or hyphens.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/records/client.ts
|
|
513
|
+
var RecordsClient = class {
|
|
514
|
+
constructor(http, bucketKey) {
|
|
515
|
+
assertSafeName(bucketKey, "bucketKey");
|
|
516
|
+
this.http = http;
|
|
517
|
+
this.bucketKey = bucketKey;
|
|
518
|
+
this.basePath = `/records/${bucketKey}`;
|
|
519
|
+
}
|
|
520
|
+
// ─── Single Record Operations ─────────────────────────────────────────────
|
|
521
|
+
/**
|
|
522
|
+
* Create a new record.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* const user = await records.create({
|
|
527
|
+
* name: 'Alice',
|
|
528
|
+
* email: 'alice@example.com',
|
|
529
|
+
* score: 100,
|
|
530
|
+
* });
|
|
531
|
+
* console.log(user.id); // "rec_xxxxxxxx"
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
async create(data) {
|
|
535
|
+
const result = await this.http.post(this.basePath, data);
|
|
536
|
+
return result.record ?? result.data;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Fetch a single record by ID.
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* const post = await records.get('rec_abc123');
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
async get(id) {
|
|
547
|
+
const result = await this.http.get(`${this.basePath}/${id}`);
|
|
548
|
+
return result.record ?? result.data;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Overwrite a record entirely (full replace).
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```ts
|
|
555
|
+
* const updated = await records.set('rec_abc123', {
|
|
556
|
+
* name: 'Alice Updated',
|
|
557
|
+
* email: 'alice2@example.com',
|
|
558
|
+
* });
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
async set(id, data) {
|
|
562
|
+
const result = await this.http.put(`${this.basePath}/${id}`, data);
|
|
563
|
+
return result.record ?? result.data;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Partially update a record (merge by default).
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* // Merge: only the provided fields are updated
|
|
571
|
+
* const updated = await records.patch('rec_abc123', { score: 200 });
|
|
572
|
+
*
|
|
573
|
+
* // Replace: equivalent to set()
|
|
574
|
+
* const replaced = await records.patch('rec_abc123', { score: 200 }, { merge: false });
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
async patch(id, data, options = {}) {
|
|
578
|
+
const { merge = true } = options;
|
|
579
|
+
const result = await this.http.patch(
|
|
580
|
+
`${this.basePath}/${id}`,
|
|
581
|
+
{ ...data, _merge: merge }
|
|
582
|
+
);
|
|
583
|
+
return result.record ?? result.data;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Delete a record permanently.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* await records.delete('rec_abc123');
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
async delete(id) {
|
|
594
|
+
await this.http.delete(`${this.basePath}/${id}`);
|
|
595
|
+
}
|
|
596
|
+
// ─── Batch Operations ─────────────────────────────────────────────────────
|
|
597
|
+
/**
|
|
598
|
+
* Create multiple records in one request.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```ts
|
|
602
|
+
* const created = await records.batchCreate([
|
|
603
|
+
* { name: 'Alice', score: 100 },
|
|
604
|
+
* { name: 'Bob', score: 200 },
|
|
605
|
+
* ]);
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
async batchCreate(items) {
|
|
609
|
+
const result = await this.http.post(
|
|
610
|
+
`${this.basePath}/batch`,
|
|
611
|
+
{ records: items }
|
|
612
|
+
);
|
|
613
|
+
return result.records;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Delete multiple records by ID in one request.
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* ```ts
|
|
620
|
+
* await records.batchDelete(['rec_a', 'rec_b', 'rec_c']);
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
async batchDelete(ids) {
|
|
624
|
+
const result = await this.http.post(`${this.basePath}/batch/delete`, { ids });
|
|
625
|
+
return { deleted: result.deleted, failed: result.failed };
|
|
626
|
+
}
|
|
627
|
+
// ─── Querying ─────────────────────────────────────────────────────────────
|
|
628
|
+
/**
|
|
629
|
+
* Query records with optional filters, sorting, and pagination.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```ts
|
|
633
|
+
* // Simple query
|
|
634
|
+
* const { records } = await posts.query({ limit: 10 });
|
|
635
|
+
*
|
|
636
|
+
* // Filtered query with cursor pagination
|
|
637
|
+
* const page1 = await posts.query({
|
|
638
|
+
* filters: [
|
|
639
|
+
* { field: 'status', op: '==', value: 'published' },
|
|
640
|
+
* { field: 'views', op: '>', value: 1000 },
|
|
641
|
+
* ],
|
|
642
|
+
* orderBy: 'createdAt',
|
|
643
|
+
* order: 'desc',
|
|
644
|
+
* limit: 20,
|
|
645
|
+
* });
|
|
646
|
+
*
|
|
647
|
+
* const page2 = await posts.query({
|
|
648
|
+
* filters: [{ field: 'status', op: '==', value: 'published' }],
|
|
649
|
+
* limit: 20,
|
|
650
|
+
* startAfter: page1.nextCursor,
|
|
651
|
+
* });
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
async query(options = {}) {
|
|
655
|
+
const qs = buildQueryParams(options);
|
|
656
|
+
const result = await this.http.get(`${this.basePath}${qs}`);
|
|
657
|
+
return {
|
|
658
|
+
records: result.records,
|
|
659
|
+
total: result.total,
|
|
660
|
+
hasMore: result.hasMore,
|
|
661
|
+
nextCursor: result.nextCursor
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Convenience alias: get all records up to `limit` (default 100).
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```ts
|
|
669
|
+
* const allPosts = await posts.getAll({ limit: 500 });
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
async getAll(options = {}) {
|
|
673
|
+
const { records } = await this.query(options);
|
|
674
|
+
return records;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Count records matching optional filters.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```ts
|
|
681
|
+
* const total = await posts.count([{ field: 'status', op: '==', value: 'published' }]);
|
|
682
|
+
* ```
|
|
683
|
+
*/
|
|
684
|
+
async count(filters = []) {
|
|
685
|
+
const result = await this.http.post(
|
|
686
|
+
`${this.basePath}/count`,
|
|
687
|
+
{ filters }
|
|
688
|
+
);
|
|
689
|
+
return result.count;
|
|
690
|
+
}
|
|
691
|
+
// ─── Version History ──────────────────────────────────────────────────────
|
|
692
|
+
/**
|
|
693
|
+
* Retrieve the full version history of a record.
|
|
694
|
+
* Each write creates a new version stored in GCS.
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```ts
|
|
698
|
+
* const history = await records.getHistory('rec_abc123');
|
|
699
|
+
* console.log(history[0].data); // latest version
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
async getHistory(id) {
|
|
703
|
+
const result = await this.http.get(`${this.basePath}/${id}/history`);
|
|
704
|
+
return result.history;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Restore a record to a specific historical version.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```ts
|
|
711
|
+
* const history = await records.getHistory('rec_abc123');
|
|
712
|
+
* const restored = await records.restoreVersion('rec_abc123', history[2].version);
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
async restoreVersion(id, version) {
|
|
716
|
+
const result = await this.http.post(
|
|
717
|
+
`${this.basePath}/${id}/restore`,
|
|
718
|
+
{ version }
|
|
719
|
+
);
|
|
720
|
+
return result.record ?? result.data;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/analytics/client.ts
|
|
725
|
+
var AnalyticsClient = class {
|
|
726
|
+
constructor(http, bucketKey) {
|
|
727
|
+
assertSafeName(bucketKey, "bucketKey");
|
|
728
|
+
this.http = http;
|
|
729
|
+
this.bucketKey = bucketKey;
|
|
730
|
+
this.basePath = `/analytics/${bucketKey}`;
|
|
731
|
+
}
|
|
732
|
+
/** Internal dispatcher — all queries POST to the same endpoint. */
|
|
733
|
+
async run(query) {
|
|
734
|
+
const result = await this.http.post(this.basePath, query);
|
|
735
|
+
return result.data;
|
|
736
|
+
}
|
|
737
|
+
// ─── Count ────────────────────────────────────────────────────────────────
|
|
738
|
+
/**
|
|
739
|
+
* Count the total number of records in the bucket, with optional date filter.
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```ts
|
|
743
|
+
* const { count } = await analytics.count();
|
|
744
|
+
* // → { count: 4821 }
|
|
745
|
+
*
|
|
746
|
+
* // Count only this month's records
|
|
747
|
+
* const { count } = await analytics.count({
|
|
748
|
+
* dateRange: { start: new Date('2025-01-01').getTime(), end: Date.now() },
|
|
749
|
+
* });
|
|
750
|
+
* ```
|
|
751
|
+
*/
|
|
752
|
+
async count(opts = {}) {
|
|
753
|
+
return this.run({ queryType: "count", ...opts });
|
|
754
|
+
}
|
|
755
|
+
// ─── Distribution ─────────────────────────────────────────────────────────
|
|
756
|
+
/**
|
|
757
|
+
* Count how many records have each unique value for a given field.
|
|
758
|
+
* Great for pie charts and bar charts.
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```ts
|
|
762
|
+
* const rows = await analytics.distribution({
|
|
763
|
+
* field: 'status',
|
|
764
|
+
* limit: 10,
|
|
765
|
+
* order: 'desc',
|
|
766
|
+
* });
|
|
767
|
+
* // → [{ value: 'completed', count: 312 }, { value: 'pending', count: 88 }, ...]
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
async distribution(opts) {
|
|
771
|
+
assertSafeName(opts.field, "field");
|
|
772
|
+
return this.run({ queryType: "distribution", ...opts });
|
|
773
|
+
}
|
|
774
|
+
// ─── Sum ──────────────────────────────────────────────────────────────────
|
|
775
|
+
/**
|
|
776
|
+
* Sum a numeric field, optionally grouped by another field.
|
|
777
|
+
*
|
|
778
|
+
* @example
|
|
779
|
+
* ```ts
|
|
780
|
+
* // Total revenue
|
|
781
|
+
* const rows = await analytics.sum({ field: 'amount' });
|
|
782
|
+
* // → [{ sum: 198432.50 }]
|
|
783
|
+
*
|
|
784
|
+
* // Revenue by country
|
|
785
|
+
* const rows = await analytics.sum({ field: 'amount', groupBy: 'country', limit: 10 });
|
|
786
|
+
* // → [{ group: 'US', sum: 120000 }, { group: 'UK', sum: 45000 }, ...]
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
async sum(opts) {
|
|
790
|
+
assertSafeName(opts.field, "field");
|
|
791
|
+
if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
|
|
792
|
+
return this.run({ queryType: "sum", ...opts });
|
|
793
|
+
}
|
|
794
|
+
// ─── Time Series ──────────────────────────────────────────────────────────
|
|
795
|
+
/**
|
|
796
|
+
* Count records created over time, grouped by a time granularity.
|
|
797
|
+
* Perfect for line charts and activity graphs.
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```ts
|
|
801
|
+
* const rows = await analytics.timeSeries({
|
|
802
|
+
* granularity: 'day',
|
|
803
|
+
* dateRange: { start: Date.now() - 7 * 86400000, end: Date.now() },
|
|
804
|
+
* });
|
|
805
|
+
* // → [{ date: '2025-06-01', count: 42 }, { date: '2025-06-02', count: 67 }, ...]
|
|
806
|
+
* ```
|
|
807
|
+
*/
|
|
808
|
+
async timeSeries(opts = {}) {
|
|
809
|
+
return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
|
|
810
|
+
}
|
|
811
|
+
// ─── Field Time Series ────────────────────────────────────────────────────
|
|
812
|
+
/**
|
|
813
|
+
* Aggregate a numeric field over time (e.g. daily revenue, hourly signups).
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```ts
|
|
817
|
+
* const rows = await analytics.fieldTimeSeries({
|
|
818
|
+
* field: 'amount',
|
|
819
|
+
* aggregation: 'sum',
|
|
820
|
+
* granularity: 'week',
|
|
821
|
+
* });
|
|
822
|
+
* // → [{ date: '2025-W22', value: 14230.50 }, ...]
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
async fieldTimeSeries(opts) {
|
|
826
|
+
assertSafeName(opts.field, "field");
|
|
827
|
+
return this.run({
|
|
828
|
+
queryType: "fieldTimeSeries",
|
|
829
|
+
aggregation: "sum",
|
|
830
|
+
granularity: "day",
|
|
831
|
+
...opts
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
// ─── Top N ────────────────────────────────────────────────────────────────
|
|
835
|
+
/**
|
|
836
|
+
* Get the top N values by frequency for a field.
|
|
837
|
+
* Optionally pair with a `labelField` for human-readable labels.
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```ts
|
|
841
|
+
* // Top 5 most purchased products
|
|
842
|
+
* const rows = await analytics.topN({
|
|
843
|
+
* field: 'productId',
|
|
844
|
+
* labelField: 'productName',
|
|
845
|
+
* n: 5,
|
|
846
|
+
* });
|
|
847
|
+
* // → [{ value: 'prod_123', label: 'Widget Pro', count: 892 }, ...]
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
850
|
+
async topN(opts) {
|
|
851
|
+
assertSafeName(opts.field, "field");
|
|
852
|
+
if (opts.labelField) assertSafeName(opts.labelField, "labelField");
|
|
853
|
+
return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
|
|
854
|
+
}
|
|
855
|
+
// ─── Field Stats ──────────────────────────────────────────────────────────
|
|
856
|
+
/**
|
|
857
|
+
* Get statistical summary (min, max, avg, sum, count, stddev) for a numeric field.
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* ```ts
|
|
861
|
+
* const stats = await analytics.stats({ field: 'orderValue' });
|
|
862
|
+
* // → { min: 4.99, max: 9999.99, avg: 87.23, sum: 420948.27, count: 4823, stddev: 143.2 }
|
|
863
|
+
* ```
|
|
864
|
+
*/
|
|
865
|
+
async stats(opts) {
|
|
866
|
+
assertSafeName(opts.field, "field");
|
|
867
|
+
return this.run({ queryType: "stats", ...opts });
|
|
868
|
+
}
|
|
869
|
+
// ─── Filtered Records ─────────────────────────────────────────────────────
|
|
870
|
+
/**
|
|
871
|
+
* Query raw records with filters, field selection, and pagination.
|
|
872
|
+
* This is the analytics version of `records.query()` but powered by BigQuery.
|
|
873
|
+
*
|
|
874
|
+
* @example
|
|
875
|
+
* ```ts
|
|
876
|
+
* const { records } = await analytics.records({
|
|
877
|
+
* filters: [{ field: 'status', op: '==', value: 'refunded' }],
|
|
878
|
+
* selectFields: ['orderId', 'amount', 'createdAt'],
|
|
879
|
+
* limit: 50,
|
|
880
|
+
* orderBy: 'amount',
|
|
881
|
+
* order: 'desc',
|
|
882
|
+
* });
|
|
883
|
+
* ```
|
|
884
|
+
*/
|
|
885
|
+
async records(opts = {}) {
|
|
886
|
+
if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
|
|
887
|
+
if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
|
|
888
|
+
return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
|
|
889
|
+
}
|
|
890
|
+
// ─── Multi Metric ─────────────────────────────────────────────────────────
|
|
891
|
+
/**
|
|
892
|
+
* Calculate multiple aggregations in a single query.
|
|
893
|
+
* Ideal for dashboards that need several numbers at once.
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* ```ts
|
|
897
|
+
* const result = await analytics.multiMetric({
|
|
898
|
+
* metrics: [
|
|
899
|
+
* { field: 'amount', name: 'totalRevenue', aggregation: 'sum' },
|
|
900
|
+
* { field: 'amount', name: 'avgOrderValue', aggregation: 'avg' },
|
|
901
|
+
* { field: 'userId', name: 'uniqueCustomers', aggregation: 'count' },
|
|
902
|
+
* ],
|
|
903
|
+
* });
|
|
904
|
+
* // → { totalRevenue: 198432.50, avgOrderValue: 87.23, uniqueCustomers: 2275 }
|
|
905
|
+
* ```
|
|
906
|
+
*/
|
|
907
|
+
async multiMetric(opts) {
|
|
908
|
+
opts.metrics.forEach((m) => {
|
|
909
|
+
assertSafeName(m.field, "metric.field");
|
|
910
|
+
assertSafeName(m.name, "metric.name");
|
|
911
|
+
});
|
|
912
|
+
return this.run({ queryType: "multiMetric", ...opts });
|
|
913
|
+
}
|
|
914
|
+
// ─── Storage Stats ────────────────────────────────────────────────────────
|
|
915
|
+
/**
|
|
916
|
+
* Get storage statistics for this bucket: record counts, byte sizes.
|
|
917
|
+
*
|
|
918
|
+
* @example
|
|
919
|
+
* ```ts
|
|
920
|
+
* const stats = await analytics.storageStats();
|
|
921
|
+
* // → { totalRecords: 4821, totalBytes: 48293820, avgBytes: 10015, ... }
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
async storageStats(opts = {}) {
|
|
925
|
+
return this.run({ queryType: "storageStats", ...opts });
|
|
926
|
+
}
|
|
927
|
+
// ─── Cross-Bucket Comparison ──────────────────────────────────────────────
|
|
928
|
+
/**
|
|
929
|
+
* Compare the same field aggregation across multiple buckets in one query.
|
|
930
|
+
* Your security key must have read access to ALL listed buckets.
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```ts
|
|
934
|
+
* const rows = await analytics.crossBucket({
|
|
935
|
+
* bucketKeys: ['orders-us', 'orders-eu', 'orders-apac'],
|
|
936
|
+
* field: 'amount',
|
|
937
|
+
* aggregation: 'sum',
|
|
938
|
+
* });
|
|
939
|
+
* // → [
|
|
940
|
+
* // { bucket: 'orders-us', value: 120000 },
|
|
941
|
+
* // { bucket: 'orders-eu', value: 45000 },
|
|
942
|
+
* // { bucket: 'orders-apac', value: 33000 },
|
|
943
|
+
* // ]
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
946
|
+
async crossBucket(opts) {
|
|
947
|
+
assertSafeName(opts.field, "field");
|
|
948
|
+
opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
|
|
949
|
+
return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
|
|
950
|
+
}
|
|
951
|
+
// ─── Raw Query ────────────────────────────────────────────────────────────
|
|
952
|
+
/**
|
|
953
|
+
* Send a raw analytics query object. Use this when you need full control
|
|
954
|
+
* over the query shape or want to use a queryType not covered by the helpers.
|
|
955
|
+
*
|
|
956
|
+
* @example
|
|
957
|
+
* ```ts
|
|
958
|
+
* const result = await analytics.query({
|
|
959
|
+
* queryType: 'topN',
|
|
960
|
+
* field: 'category',
|
|
961
|
+
* n: 3,
|
|
962
|
+
* order: 'asc',
|
|
963
|
+
* });
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
async query(query) {
|
|
967
|
+
const data = await this.run(query);
|
|
968
|
+
return { queryType: query.queryType, data };
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/storage/manager.ts
|
|
973
|
+
var StorageManager = class {
|
|
974
|
+
constructor(http, storageKey) {
|
|
975
|
+
this.basePath = "/storage";
|
|
976
|
+
this.http = http;
|
|
977
|
+
this.storageKey = storageKey;
|
|
978
|
+
}
|
|
979
|
+
/** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
|
|
980
|
+
get authHeaders() {
|
|
981
|
+
return { "X-Storage-Key": this.storageKey };
|
|
982
|
+
}
|
|
983
|
+
// ─── Upload: Simple (server-buffered) ────────────────────────────────────
|
|
984
|
+
/**
|
|
985
|
+
* Upload a file to storage in one step (server-buffered, up to 500 MB).
|
|
986
|
+
* For files >10 MB or when you need upload progress, use `getUploadUrl()` instead.
|
|
987
|
+
*
|
|
988
|
+
* @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
|
|
989
|
+
* @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
|
|
990
|
+
* @param options Upload options: isPublic, overwrite, mimeType.
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* ```ts
|
|
994
|
+
* // Upload a public avatar
|
|
995
|
+
* const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
|
|
996
|
+
* console.log(result.publicUrl); // → https://...
|
|
997
|
+
*
|
|
998
|
+
* // Upload a private document
|
|
999
|
+
* const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
|
|
1000
|
+
* console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
|
|
1001
|
+
* ```
|
|
1002
|
+
*/
|
|
1003
|
+
async upload(data, path, options = {}) {
|
|
1004
|
+
const { isPublic = false, overwrite = false, mimeType } = options;
|
|
1005
|
+
const mime = mimeType ?? guessMimeType(path);
|
|
1006
|
+
const formData = new FormData();
|
|
1007
|
+
const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
|
|
1008
|
+
formData.append("file", blob, path.split("/").pop() ?? "file");
|
|
1009
|
+
formData.append("path", path);
|
|
1010
|
+
formData.append("mimeType", mime);
|
|
1011
|
+
formData.append("isPublic", String(isPublic));
|
|
1012
|
+
formData.append("overwrite", String(overwrite));
|
|
1013
|
+
const result = await this.http.request(`${this.basePath}/upload`, {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
rawBody: formData,
|
|
1016
|
+
headers: this.authHeaders
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
path: result.path,
|
|
1020
|
+
mimeType: result.mimeType,
|
|
1021
|
+
size: result.size,
|
|
1022
|
+
isPublic: result.isPublic,
|
|
1023
|
+
publicUrl: result.publicUrl,
|
|
1024
|
+
downloadUrl: result.downloadUrl
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Upload raw JSON or plain text data as a file.
|
|
1029
|
+
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```ts
|
|
1032
|
+
* const result = await storage.uploadRaw(
|
|
1033
|
+
* { config: { theme: 'dark' } },
|
|
1034
|
+
* 'settings/user-config.json',
|
|
1035
|
+
* { isPublic: false },
|
|
1036
|
+
* );
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
async uploadRaw(data, path, options = {}) {
|
|
1040
|
+
const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
|
|
1041
|
+
const body = typeof data === "string" ? data : JSON.stringify(data);
|
|
1042
|
+
const result = await this.http.request(`${this.basePath}/upload-raw`, {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
body: { path, data: body, mimeType, isPublic, overwrite },
|
|
1045
|
+
headers: this.authHeaders
|
|
1046
|
+
});
|
|
1047
|
+
return {
|
|
1048
|
+
path: result.path,
|
|
1049
|
+
mimeType: result.mimeType,
|
|
1050
|
+
size: result.size,
|
|
1051
|
+
isPublic: result.isPublic,
|
|
1052
|
+
publicUrl: result.publicUrl,
|
|
1053
|
+
downloadUrl: result.downloadUrl
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
// ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
|
|
1057
|
+
/**
|
|
1058
|
+
* Step 1 of the recommended upload flow.
|
|
1059
|
+
* Get a signed URL to upload directly to GCS from the client (supports progress).
|
|
1060
|
+
*
|
|
1061
|
+
* @example
|
|
1062
|
+
* ```ts
|
|
1063
|
+
* const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
|
|
1064
|
+
* path: 'videos/intro.mp4',
|
|
1065
|
+
* mimeType: 'video/mp4',
|
|
1066
|
+
* size: file.size,
|
|
1067
|
+
* isPublic: true,
|
|
1068
|
+
* });
|
|
1069
|
+
*
|
|
1070
|
+
* // Upload using XHR for progress tracking
|
|
1071
|
+
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
|
|
1072
|
+
* console.log(`${pct}% uploaded`);
|
|
1073
|
+
* });
|
|
1074
|
+
*
|
|
1075
|
+
* // Step 3: confirm
|
|
1076
|
+
* const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
|
|
1077
|
+
* ```
|
|
1078
|
+
*/
|
|
1079
|
+
async getUploadUrl(opts) {
|
|
1080
|
+
const result = await this.http.request(`${this.basePath}/upload-url`, {
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
body: { isPublic: false, overwrite: false, expiresInSeconds: 900, ...opts },
|
|
1083
|
+
headers: this.authHeaders
|
|
1084
|
+
});
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Upload data directly to a signed GCS URL (no auth headers needed).
|
|
1089
|
+
* Optionally tracks progress via a callback.
|
|
1090
|
+
*
|
|
1091
|
+
* @param signedUrl The URL returned by `getUploadUrl()`.
|
|
1092
|
+
* @param data File data.
|
|
1093
|
+
* @param mimeType Must match what was used in `getUploadUrl()`.
|
|
1094
|
+
* @param onProgress Optional callback called with 0–100 progress percentage.
|
|
1095
|
+
*/
|
|
1096
|
+
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
1097
|
+
await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Step 3 of the recommended upload flow.
|
|
1101
|
+
* Confirm a direct upload and register metadata on the server.
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const result = await storage.confirmUpload({
|
|
1106
|
+
* path: 'videos/intro.mp4',
|
|
1107
|
+
* mimeType: 'video/mp4',
|
|
1108
|
+
* isPublic: true,
|
|
1109
|
+
* });
|
|
1110
|
+
* console.log(result.publicUrl);
|
|
1111
|
+
* ```
|
|
1112
|
+
*/
|
|
1113
|
+
async confirmUpload(opts) {
|
|
1114
|
+
const result = await this.http.request(`${this.basePath}/confirm`, {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
body: { isPublic: false, ...opts },
|
|
1117
|
+
headers: this.authHeaders
|
|
1118
|
+
});
|
|
1119
|
+
return {
|
|
1120
|
+
path: result.path,
|
|
1121
|
+
mimeType: result.mimeType,
|
|
1122
|
+
size: result.size,
|
|
1123
|
+
isPublic: result.isPublic,
|
|
1124
|
+
publicUrl: result.publicUrl,
|
|
1125
|
+
downloadUrl: result.downloadUrl
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
// ─── Batch Upload ─────────────────────────────────────────────────────────
|
|
1129
|
+
/**
|
|
1130
|
+
* Get signed upload URLs for multiple files at once.
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* ```ts
|
|
1134
|
+
* const { files } = await storage.getBatchUploadUrls([
|
|
1135
|
+
* { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
|
|
1136
|
+
* { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
|
|
1137
|
+
* ]);
|
|
1138
|
+
*
|
|
1139
|
+
* // Upload each file and confirm
|
|
1140
|
+
* for (const f of files) {
|
|
1141
|
+
* await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
1142
|
+
* await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
|
|
1143
|
+
* }
|
|
1144
|
+
* ```
|
|
1145
|
+
*/
|
|
1146
|
+
async getBatchUploadUrls(files) {
|
|
1147
|
+
const result = await this.http.request(
|
|
1148
|
+
`${this.basePath}/batch-upload-urls`,
|
|
1149
|
+
{ method: "POST", body: { files }, headers: this.authHeaders }
|
|
1150
|
+
);
|
|
1151
|
+
return { files: result.files };
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Confirm multiple direct uploads at once.
|
|
1155
|
+
*/
|
|
1156
|
+
async batchConfirmUploads(items) {
|
|
1157
|
+
const result = await this.http.request(
|
|
1158
|
+
`${this.basePath}/batch-confirm`,
|
|
1159
|
+
{ method: "POST", body: { files: items }, headers: this.authHeaders }
|
|
1160
|
+
);
|
|
1161
|
+
return result.files.map((r) => ({
|
|
1162
|
+
path: r.path,
|
|
1163
|
+
mimeType: r.mimeType,
|
|
1164
|
+
size: r.size,
|
|
1165
|
+
isPublic: r.isPublic,
|
|
1166
|
+
publicUrl: r.publicUrl,
|
|
1167
|
+
downloadUrl: r.downloadUrl
|
|
1168
|
+
}));
|
|
1169
|
+
}
|
|
1170
|
+
// ─── Download ─────────────────────────────────────────────────────────────
|
|
1171
|
+
/**
|
|
1172
|
+
* Download a private file as an ArrayBuffer.
|
|
1173
|
+
* For public files, use the `publicUrl` directly — no SDK needed.
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* ```ts
|
|
1177
|
+
* const buffer = await storage.download('docs/contract.pdf');
|
|
1178
|
+
* const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
1179
|
+
* // Open in browser:
|
|
1180
|
+
* window.open(URL.createObjectURL(blob));
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
async download(path) {
|
|
1184
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1185
|
+
return this.http.request(`${this.basePath}/download/${encoded}`, {
|
|
1186
|
+
method: "GET",
|
|
1187
|
+
headers: this.authHeaders
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Download multiple files at once, returned as a JSON map of `{ path: base64 }`.
|
|
1192
|
+
*
|
|
1193
|
+
* @example
|
|
1194
|
+
* ```ts
|
|
1195
|
+
* const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
|
|
1196
|
+
* ```
|
|
1197
|
+
*/
|
|
1198
|
+
async batchDownload(paths) {
|
|
1199
|
+
const result = await this.http.request(
|
|
1200
|
+
`${this.basePath}/batch-download`,
|
|
1201
|
+
{ method: "POST", body: { paths }, headers: this.authHeaders }
|
|
1202
|
+
);
|
|
1203
|
+
return result.files;
|
|
1204
|
+
}
|
|
1205
|
+
// ─── List ─────────────────────────────────────────────────────────────────
|
|
1206
|
+
/**
|
|
1207
|
+
* List files and folders at a given path prefix.
|
|
1208
|
+
*
|
|
1209
|
+
* @example
|
|
1210
|
+
* ```ts
|
|
1211
|
+
* // List everything in the root
|
|
1212
|
+
* const { files, folders } = await storage.list();
|
|
1213
|
+
*
|
|
1214
|
+
* // List a specific folder
|
|
1215
|
+
* const { files, folders, hasMore, nextCursor } = await storage.list({
|
|
1216
|
+
* prefix: 'avatars/',
|
|
1217
|
+
* limit: 20,
|
|
1218
|
+
* });
|
|
1219
|
+
*
|
|
1220
|
+
* // Next page
|
|
1221
|
+
* const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
|
|
1222
|
+
* ```
|
|
1223
|
+
*/
|
|
1224
|
+
async list(opts = {}) {
|
|
1225
|
+
const params = new URLSearchParams();
|
|
1226
|
+
if (opts.prefix) params.set("prefix", opts.prefix);
|
|
1227
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
1228
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
1229
|
+
if (opts.recursive) params.set("recursive", "true");
|
|
1230
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
1231
|
+
const result = await this.http.request(`${this.basePath}/list${qs}`, {
|
|
1232
|
+
method: "GET",
|
|
1233
|
+
headers: this.authHeaders
|
|
1234
|
+
});
|
|
1235
|
+
return {
|
|
1236
|
+
files: result.files,
|
|
1237
|
+
folders: result.folders,
|
|
1238
|
+
hasMore: result.hasMore,
|
|
1239
|
+
nextCursor: result.nextCursor
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
// ─── Metadata ─────────────────────────────────────────────────────────────
|
|
1243
|
+
/**
|
|
1244
|
+
* Get metadata for a file (size, MIME type, visibility, URLs).
|
|
1245
|
+
*
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```ts
|
|
1248
|
+
* const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
1249
|
+
* console.log(meta.size, meta.isPublic, meta.publicUrl);
|
|
1250
|
+
* ```
|
|
1251
|
+
*/
|
|
1252
|
+
async getMetadata(path) {
|
|
1253
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1254
|
+
const result = await this.http.request(
|
|
1255
|
+
`${this.basePath}/metadata/${encoded}`,
|
|
1256
|
+
{ method: "GET", headers: this.authHeaders }
|
|
1257
|
+
);
|
|
1258
|
+
return {
|
|
1259
|
+
path: result.path,
|
|
1260
|
+
size: result.size,
|
|
1261
|
+
mimeType: result.mimeType,
|
|
1262
|
+
isPublic: result.isPublic,
|
|
1263
|
+
publicUrl: result.publicUrl,
|
|
1264
|
+
downloadUrl: result.downloadUrl,
|
|
1265
|
+
createdAt: result.createdAt,
|
|
1266
|
+
updatedAt: result.updatedAt
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
// ─── Signed URL (time-limited share) ─────────────────────────────────────
|
|
1270
|
+
/**
|
|
1271
|
+
* Generate a time-limited download URL for a private file.
|
|
1272
|
+
* The URL can be shared externally without requiring an `X-Storage-Key`.
|
|
1273
|
+
*
|
|
1274
|
+
* > **Note:** Downloads via signed URLs bypass the server, so download stats
|
|
1275
|
+
* > are NOT tracked. Use `downloadUrl` for tracked downloads.
|
|
1276
|
+
*
|
|
1277
|
+
* @param path Path to the file.
|
|
1278
|
+
* @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
|
|
1279
|
+
*
|
|
1280
|
+
* @example
|
|
1281
|
+
* ```ts
|
|
1282
|
+
* const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
|
|
1283
|
+
* // Share signedUrl with the recipient — it expires in 30 minutes.
|
|
1284
|
+
* ```
|
|
1285
|
+
*/
|
|
1286
|
+
async getSignedUrl(path, expiresIn = 3600) {
|
|
1287
|
+
const result = await this.http.request(`${this.basePath}/signed-url`, {
|
|
1288
|
+
method: "POST",
|
|
1289
|
+
body: { path, expiresIn },
|
|
1290
|
+
headers: this.authHeaders
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
signedUrl: result.signedUrl,
|
|
1294
|
+
expiresAt: result.expiresAt,
|
|
1295
|
+
expiresIn: result.expiresIn,
|
|
1296
|
+
path: result.path
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
// ─── Visibility ───────────────────────────────────────────────────────────
|
|
1300
|
+
/**
|
|
1301
|
+
* Change a file's visibility between public and private after upload.
|
|
1302
|
+
*
|
|
1303
|
+
* @example
|
|
1304
|
+
* ```ts
|
|
1305
|
+
* // Make a file public
|
|
1306
|
+
* const result = await storage.setVisibility('avatars/alice.jpg', true);
|
|
1307
|
+
* console.log(result.publicUrl); // CDN URL
|
|
1308
|
+
*
|
|
1309
|
+
* // Make a file private
|
|
1310
|
+
* const result = await storage.setVisibility('avatars/alice.jpg', false);
|
|
1311
|
+
* console.log(result.downloadUrl); // Auth-required URL
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
async setVisibility(path, isPublic) {
|
|
1315
|
+
const result = await this.http.request(`${this.basePath}/visibility`, {
|
|
1316
|
+
method: "PATCH",
|
|
1317
|
+
body: { path, isPublic },
|
|
1318
|
+
headers: this.authHeaders
|
|
1319
|
+
});
|
|
1320
|
+
return {
|
|
1321
|
+
path: result.path,
|
|
1322
|
+
isPublic: result.isPublic,
|
|
1323
|
+
publicUrl: result.publicUrl,
|
|
1324
|
+
downloadUrl: result.downloadUrl
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
1328
|
+
/**
|
|
1329
|
+
* Create a folder (a GCS prefix placeholder).
|
|
1330
|
+
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* await storage.createFolder('uploads/2025/');
|
|
1334
|
+
* ```
|
|
1335
|
+
*/
|
|
1336
|
+
async createFolder(path) {
|
|
1337
|
+
const result = await this.http.request(
|
|
1338
|
+
`${this.basePath}/folder`,
|
|
1339
|
+
{ method: "POST", body: { path }, headers: this.authHeaders }
|
|
1340
|
+
);
|
|
1341
|
+
return { path: result.path };
|
|
1342
|
+
}
|
|
1343
|
+
// ─── File Operations ──────────────────────────────────────────────────────
|
|
1344
|
+
/**
|
|
1345
|
+
* Delete a single file.
|
|
1346
|
+
*
|
|
1347
|
+
* @example
|
|
1348
|
+
* ```ts
|
|
1349
|
+
* await storage.deleteFile('avatars/old-avatar.jpg');
|
|
1350
|
+
* ```
|
|
1351
|
+
*/
|
|
1352
|
+
async deleteFile(path) {
|
|
1353
|
+
await this.http.request(`${this.basePath}/file`, {
|
|
1354
|
+
method: "DELETE",
|
|
1355
|
+
body: { path },
|
|
1356
|
+
headers: this.authHeaders
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Delete a folder and all its contents recursively.
|
|
1361
|
+
*
|
|
1362
|
+
* @example
|
|
1363
|
+
* ```ts
|
|
1364
|
+
* await storage.deleteFolder('temp/');
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
1367
|
+
async deleteFolder(path) {
|
|
1368
|
+
await this.http.request(`${this.basePath}/folder`, {
|
|
1369
|
+
method: "DELETE",
|
|
1370
|
+
body: { path },
|
|
1371
|
+
headers: this.authHeaders
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Move or rename a file.
|
|
1376
|
+
*
|
|
1377
|
+
* @example
|
|
1378
|
+
* ```ts
|
|
1379
|
+
* // Rename
|
|
1380
|
+
* await storage.move('docs/draft.pdf', 'docs/final.pdf');
|
|
1381
|
+
* // Move to a different folder
|
|
1382
|
+
* await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
|
|
1383
|
+
* ```
|
|
1384
|
+
*/
|
|
1385
|
+
async move(from, to) {
|
|
1386
|
+
const result = await this.http.request(
|
|
1387
|
+
`${this.basePath}/move`,
|
|
1388
|
+
{ method: "POST", body: { from, to }, headers: this.authHeaders }
|
|
1389
|
+
);
|
|
1390
|
+
return { from: result.from, to: result.to };
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Copy a file to a new path.
|
|
1394
|
+
*
|
|
1395
|
+
* @example
|
|
1396
|
+
* ```ts
|
|
1397
|
+
* await storage.copy('templates/base.html', 'sites/my-site/index.html');
|
|
1398
|
+
* ```
|
|
1399
|
+
*/
|
|
1400
|
+
async copy(from, to) {
|
|
1401
|
+
const result = await this.http.request(
|
|
1402
|
+
`${this.basePath}/copy`,
|
|
1403
|
+
{ method: "POST", body: { from, to }, headers: this.authHeaders }
|
|
1404
|
+
);
|
|
1405
|
+
return { from: result.from, to: result.to };
|
|
1406
|
+
}
|
|
1407
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
1408
|
+
/**
|
|
1409
|
+
* Get storage statistics for your key: total files, bytes, operation counts.
|
|
1410
|
+
*
|
|
1411
|
+
* @example
|
|
1412
|
+
* ```ts
|
|
1413
|
+
* const stats = await storage.getStats();
|
|
1414
|
+
* console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
|
|
1415
|
+
* ```
|
|
1416
|
+
*/
|
|
1417
|
+
async getStats() {
|
|
1418
|
+
const result = await this.http.request(`${this.basePath}/stats`, {
|
|
1419
|
+
method: "GET",
|
|
1420
|
+
headers: this.authHeaders
|
|
1421
|
+
});
|
|
1422
|
+
return result.stats;
|
|
1423
|
+
}
|
|
1424
|
+
// ─── Info (no auth) ───────────────────────────────────────────────────────
|
|
1425
|
+
/**
|
|
1426
|
+
* Ping the storage service. No authentication required.
|
|
1427
|
+
*
|
|
1428
|
+
* @example
|
|
1429
|
+
* ```ts
|
|
1430
|
+
* const info = await storage.info();
|
|
1431
|
+
* // → { ok: true, storageRoot: 'hydrous-storage' }
|
|
1432
|
+
* ```
|
|
1433
|
+
*/
|
|
1434
|
+
async info() {
|
|
1435
|
+
return this.http.get(`${this.basePath}/info`);
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/storage/scoped.ts
|
|
1440
|
+
var ScopedStorage = class _ScopedStorage {
|
|
1441
|
+
constructor(manager, prefix) {
|
|
1442
|
+
this.manager = manager;
|
|
1443
|
+
this.prefix = prefix.replace(/\/+$/, "") + "/";
|
|
1444
|
+
}
|
|
1445
|
+
scopedPath(userPath) {
|
|
1446
|
+
return `${this.prefix}${userPath.replace(/^\/+/, "")}`;
|
|
1447
|
+
}
|
|
1448
|
+
/** Upload a file within the scoped folder. */
|
|
1449
|
+
upload(data, path, options) {
|
|
1450
|
+
return this.manager.upload(data, this.scopedPath(path), options);
|
|
1451
|
+
}
|
|
1452
|
+
/** Upload raw JSON or text within the scoped folder. */
|
|
1453
|
+
uploadRaw(data, path, options) {
|
|
1454
|
+
return this.manager.uploadRaw(data, this.scopedPath(path), options);
|
|
1455
|
+
}
|
|
1456
|
+
/** Get a signed upload URL for a file within the scoped folder. */
|
|
1457
|
+
getUploadUrl(opts) {
|
|
1458
|
+
return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
|
|
1459
|
+
}
|
|
1460
|
+
/** Upload data directly to a signed GCS URL with optional progress tracking. */
|
|
1461
|
+
uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
1462
|
+
return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
1463
|
+
}
|
|
1464
|
+
/** Confirm a direct upload within the scoped folder. */
|
|
1465
|
+
confirmUpload(opts) {
|
|
1466
|
+
return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
|
|
1467
|
+
}
|
|
1468
|
+
/** Download a file within the scoped folder. */
|
|
1469
|
+
download(path) {
|
|
1470
|
+
return this.manager.download(this.scopedPath(path));
|
|
1471
|
+
}
|
|
1472
|
+
/** List files within the scoped folder. */
|
|
1473
|
+
list(opts = {}) {
|
|
1474
|
+
return this.manager.list({ ...opts, prefix: this.scopedPath(opts.prefix ?? "") });
|
|
1475
|
+
}
|
|
1476
|
+
/** Get metadata for a file within the scoped folder. */
|
|
1477
|
+
getMetadata(path) {
|
|
1478
|
+
return this.manager.getMetadata(this.scopedPath(path));
|
|
1479
|
+
}
|
|
1480
|
+
/** Get a time-limited signed URL for a file within the scoped folder. */
|
|
1481
|
+
getSignedUrl(path, expiresIn) {
|
|
1482
|
+
return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
|
|
1483
|
+
}
|
|
1484
|
+
/** Change visibility of a file within the scoped folder. */
|
|
1485
|
+
setVisibility(path, isPublic) {
|
|
1486
|
+
return this.manager.setVisibility(this.scopedPath(path), isPublic);
|
|
1487
|
+
}
|
|
1488
|
+
/** Delete a file within the scoped folder. */
|
|
1489
|
+
deleteFile(path) {
|
|
1490
|
+
return this.manager.deleteFile(this.scopedPath(path));
|
|
1491
|
+
}
|
|
1492
|
+
/** Delete a sub-folder within the scoped folder. */
|
|
1493
|
+
deleteFolder(path) {
|
|
1494
|
+
return this.manager.deleteFolder(this.scopedPath(path));
|
|
1495
|
+
}
|
|
1496
|
+
/** Move a file within the scoped folder. */
|
|
1497
|
+
move(from, to) {
|
|
1498
|
+
return this.manager.move(this.scopedPath(from), this.scopedPath(to));
|
|
1499
|
+
}
|
|
1500
|
+
/** Copy a file within the scoped folder. */
|
|
1501
|
+
copy(from, to) {
|
|
1502
|
+
return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
|
|
1503
|
+
}
|
|
1504
|
+
/** Create a sub-folder within the scoped folder. */
|
|
1505
|
+
createFolder(path) {
|
|
1506
|
+
return this.manager.createFolder(this.scopedPath(path));
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Create a further-scoped instance nested within this scope.
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* ```ts
|
|
1513
|
+
* const uploads = db.storage.scope('user-uploads');
|
|
1514
|
+
* const images = uploads.scope('images'); // → "user-uploads/images/"
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
scope(subPrefix) {
|
|
1518
|
+
return new _ScopedStorage(this.manager, this.scopedPath(subPrefix));
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// src/client.ts
|
|
1523
|
+
var HydrousClient = class {
|
|
1524
|
+
constructor(config) {
|
|
1525
|
+
this._storage = null;
|
|
1526
|
+
this._recordsCache = /* @__PURE__ */ new Map();
|
|
1527
|
+
this._authCache = /* @__PURE__ */ new Map();
|
|
1528
|
+
this._analyticsCache = /* @__PURE__ */ new Map();
|
|
1529
|
+
if (!config.securityKey) {
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
"[HydrousDB] securityKey is required. Get yours from https://hydrousdb.com/dashboard."
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
1535
|
+
this.http = new HttpClient(baseUrl, config.securityKey);
|
|
1536
|
+
this._storageKey = config.securityKey;
|
|
1537
|
+
}
|
|
1538
|
+
// ─── Records ─────────────────────────────────────────────────────────────
|
|
1539
|
+
/**
|
|
1540
|
+
* Get a typed records client for the given bucket.
|
|
1541
|
+
*
|
|
1542
|
+
* The generic type parameter `T` describes the shape of records in this
|
|
1543
|
+
* bucket. Leave it unset for a generic `Record<string, unknown>` shape.
|
|
1544
|
+
*
|
|
1545
|
+
* @param bucketKey The name of your bucket (must match what you created in the dashboard).
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* ```ts
|
|
1549
|
+
* interface Post { title: string; body: string; published: boolean }
|
|
1550
|
+
* const posts = db.records<Post>('blog-posts');
|
|
1551
|
+
*
|
|
1552
|
+
* const post = await posts.create({ title: 'Hello', body: '...', published: false });
|
|
1553
|
+
* // post.id, post.createdAt, post.updatedAt are added automatically
|
|
1554
|
+
* ```
|
|
1555
|
+
*/
|
|
1556
|
+
records(bucketKey) {
|
|
1557
|
+
if (!this._recordsCache.has(bucketKey)) {
|
|
1558
|
+
this._recordsCache.set(bucketKey, new RecordsClient(this.http, bucketKey));
|
|
1559
|
+
}
|
|
1560
|
+
return this._recordsCache.get(bucketKey);
|
|
1561
|
+
}
|
|
1562
|
+
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
1563
|
+
/**
|
|
1564
|
+
* Get an auth client for the given user bucket.
|
|
1565
|
+
*
|
|
1566
|
+
* @param bucketKey The name of your user bucket (e.g. `"app-users"`).
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* ```ts
|
|
1570
|
+
* const auth = db.auth('app-users');
|
|
1571
|
+
* const { user, session } = await auth.login({ email: '...', password: '...' });
|
|
1572
|
+
* ```
|
|
1573
|
+
*/
|
|
1574
|
+
auth(bucketKey) {
|
|
1575
|
+
if (!this._authCache.has(bucketKey)) {
|
|
1576
|
+
this._authCache.set(bucketKey, new AuthClient(this.http, bucketKey));
|
|
1577
|
+
}
|
|
1578
|
+
return this._authCache.get(bucketKey);
|
|
1579
|
+
}
|
|
1580
|
+
// ─── Analytics ────────────────────────────────────────────────────────────
|
|
1581
|
+
/**
|
|
1582
|
+
* Get an analytics client for the given bucket.
|
|
1583
|
+
*
|
|
1584
|
+
* @param bucketKey The name of the bucket to analyse.
|
|
1585
|
+
*
|
|
1586
|
+
* @example
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* const analytics = db.analytics('orders');
|
|
1589
|
+
* const { count } = await analytics.count();
|
|
1590
|
+
* ```
|
|
1591
|
+
*/
|
|
1592
|
+
analytics(bucketKey) {
|
|
1593
|
+
if (!this._analyticsCache.has(bucketKey)) {
|
|
1594
|
+
this._analyticsCache.set(bucketKey, new AnalyticsClient(this.http, bucketKey));
|
|
1595
|
+
}
|
|
1596
|
+
return this._analyticsCache.get(bucketKey);
|
|
1597
|
+
}
|
|
1598
|
+
// ─── Storage ──────────────────────────────────────────────────────────────
|
|
1599
|
+
/**
|
|
1600
|
+
* The storage manager for uploading, downloading, listing, and managing files.
|
|
1601
|
+
*
|
|
1602
|
+
* Scoped to your project — you can never access another project's files.
|
|
1603
|
+
*
|
|
1604
|
+
* @example
|
|
1605
|
+
* ```ts
|
|
1606
|
+
* // Upload a file
|
|
1607
|
+
* const result = await db.storage.upload(file, 'images/photo.jpg', { isPublic: true });
|
|
1608
|
+
*
|
|
1609
|
+
* // Scope to a folder
|
|
1610
|
+
* const avatars = db.storage.scope('user-avatars');
|
|
1611
|
+
* await avatars.upload(blob, `${userId}.jpg`, { isPublic: true });
|
|
1612
|
+
* ```
|
|
1613
|
+
*/
|
|
1614
|
+
get storage() {
|
|
1615
|
+
if (!this._storage) {
|
|
1616
|
+
this._storage = new StorageManager(this.http, this._storageKey);
|
|
1617
|
+
}
|
|
1618
|
+
const mgr = this._storage;
|
|
1619
|
+
const extended = mgr;
|
|
1620
|
+
if (!extended.scope) {
|
|
1621
|
+
extended.scope = (prefix) => new ScopedStorage(mgr, prefix);
|
|
1622
|
+
}
|
|
1623
|
+
return extended;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1034
1626
|
function createClient(config) {
|
|
1035
1627
|
return new HydrousClient(config);
|
|
1036
1628
|
}
|
|
1037
1629
|
|
|
1038
|
-
|
|
1039
|
-
exports.AuthClient = AuthClient;
|
|
1040
|
-
exports.HydrousClient = HydrousClient;
|
|
1041
|
-
exports.HydrousDBError = HydrousDBError;
|
|
1042
|
-
exports.RecordsClient = RecordsClient;
|
|
1043
|
-
exports.ScopedStorageClient = ScopedStorageClient;
|
|
1044
|
-
exports.StorageManager = StorageManager;
|
|
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;
|
|
1630
|
+
export { AnalyticsClient, AnalyticsError, AuthClient, AuthError, HydrousClient, HydrousError, NetworkError, RecordError, RecordsClient, ScopedStorage, StorageError, StorageManager, ValidationError, createClient };
|
|
1054
1631
|
//# sourceMappingURL=index.js.map
|
|
1055
1632
|
//# sourceMappingURL=index.js.map
|