hydrousdb 2.0.1 → 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 +911 -515
- 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 +1339 -510
- package/dist/index.js +1427 -1100
- package/dist/index.js.map +1 -1
- package/package.json +31 -14
- package/dist/index.d.mts +0 -711
- package/dist/index.mjs +0 -1291
- package/dist/index.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1305 +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
|
-
|
|
27
|
+
};
|
|
28
|
+
var StorageError = class extends HydrousError {
|
|
29
|
+
constructor(message, code, status, requestId) {
|
|
30
|
+
super(message, code, status, requestId);
|
|
31
|
+
this.name = "StorageError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var AnalyticsError = class extends HydrousError {
|
|
35
|
+
constructor(message, code, status, requestId) {
|
|
36
|
+
super(message, code, status, requestId);
|
|
37
|
+
this.name = "AnalyticsError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var ValidationError = class extends HydrousError {
|
|
41
|
+
constructor(message, details) {
|
|
42
|
+
super(message, "VALIDATION_ERROR", 400, void 0, details);
|
|
43
|
+
this.name = "ValidationError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var NetworkError = class extends HydrousError {
|
|
47
|
+
constructor(message, cause) {
|
|
48
|
+
super(message, "NETWORK_ERROR");
|
|
49
|
+
this.name = "NetworkError";
|
|
50
|
+
this.cause = cause;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
24
53
|
|
|
25
54
|
// src/utils/http.ts
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
throw new HydrousSDKError(`HTTP ${res.status}`, "HTTP_ERROR", res.status);
|
|
33
|
-
}
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
const err = body;
|
|
38
|
-
throw new HydrousSDKError(
|
|
39
|
-
err.error || err.message || `HTTP ${res.status}`,
|
|
40
|
-
err.code || "HTTP_ERROR",
|
|
41
|
-
res.status
|
|
42
|
-
);
|
|
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;
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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";
|
|
53
81
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (line.startsWith("event:")) eventType = line.slice(6).trim();
|
|
76
|
-
if (line.startsWith("data:")) dataLine = line.slice(5).trim();
|
|
77
|
-
}
|
|
78
|
-
if (dataLine === null) continue;
|
|
79
|
-
try {
|
|
80
|
-
onEvent(eventType, JSON.parse(dataLine));
|
|
81
|
-
} catch (e) {
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await fetch(url, {
|
|
85
|
+
method,
|
|
86
|
+
headers: reqHeaders,
|
|
87
|
+
body: reqBody
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new NetworkError(
|
|
91
|
+
`Network request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
err
|
|
93
|
+
);
|
|
94
|
+
}
|
|
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
|
+
);
|
|
82
103
|
}
|
|
104
|
+
const buffer = await response.arrayBuffer();
|
|
105
|
+
return buffer;
|
|
83
106
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return new Promise((resolve, reject) => {
|
|
94
|
-
const xhr = new XMLHttpRequest();
|
|
95
|
-
xhr.open("POST", url);
|
|
96
|
-
for (const [key, val] of Object.entries(headers)) {
|
|
97
|
-
xhr.setRequestHeader(key, val);
|
|
107
|
+
let responseData;
|
|
108
|
+
try {
|
|
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
|
+
);
|
|
98
116
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
);
|
|
104
126
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
return responseData;
|
|
128
|
+
}
|
|
129
|
+
get(path, headers) {
|
|
130
|
+
return this.request(path, { method: "GET", headers });
|
|
131
|
+
}
|
|
132
|
+
post(path, body, headers) {
|
|
133
|
+
return this.request(path, { method: "POST", body, headers });
|
|
134
|
+
}
|
|
135
|
+
put(path, body, headers) {
|
|
136
|
+
return this.request(path, { method: "PUT", body, headers });
|
|
137
|
+
}
|
|
138
|
+
patch(path, body, headers) {
|
|
139
|
+
return this.request(path, { method: "PATCH", body, headers });
|
|
140
|
+
}
|
|
141
|
+
delete(path, body, headers) {
|
|
142
|
+
return this.request(path, { method: "DELETE", body, headers });
|
|
143
|
+
}
|
|
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);
|
|
165
|
+
});
|
|
133
166
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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}`);
|
|
138
175
|
}
|
|
139
176
|
}
|
|
140
|
-
}
|
|
177
|
+
};
|
|
141
178
|
|
|
142
179
|
// src/auth/client.ts
|
|
143
180
|
var AuthClient = class {
|
|
144
|
-
constructor(
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
148
|
-
"Content-Type": "application/json",
|
|
149
|
-
"Authorization": `Bearer ${config.apiKey}`
|
|
150
|
-
};
|
|
181
|
+
constructor(http, bucketKey) {
|
|
182
|
+
this.http = http;
|
|
183
|
+
this.bucketKey = bucketKey;
|
|
184
|
+
this.basePath = `/auth/${bucketKey}`;
|
|
151
185
|
}
|
|
152
|
-
// ───
|
|
186
|
+
// ─── Registration & Login ─────────────────────────────────────────────────
|
|
153
187
|
/**
|
|
154
|
-
*
|
|
188
|
+
* Register a new user in this bucket.
|
|
155
189
|
*
|
|
156
190
|
* @example
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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',
|
|
160
198
|
* });
|
|
199
|
+
* ```
|
|
161
200
|
*/
|
|
162
|
-
async
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const res = await fetch(url, {
|
|
166
|
-
method: "POST",
|
|
167
|
-
headers: this.headers,
|
|
168
|
-
body: JSON.stringify(options)
|
|
169
|
-
});
|
|
170
|
-
const json = await parseResponse(res);
|
|
171
|
-
this.session = json.data;
|
|
172
|
-
return { data: json.data, error: null };
|
|
173
|
-
} catch (err) {
|
|
174
|
-
return { data: null, error: toHydrousError(err) };
|
|
175
|
-
}
|
|
201
|
+
async signup(options) {
|
|
202
|
+
const result = await this.http.post(`${this.basePath}/signup`, options);
|
|
203
|
+
return { user: result.user, session: result.session };
|
|
176
204
|
}
|
|
177
|
-
// ─── SIGN IN ───────────────────────────────────────────────────────────────
|
|
178
205
|
/**
|
|
179
|
-
*
|
|
206
|
+
* Authenticate an existing user and create a session.
|
|
207
|
+
* Sessions are valid for 24 hours; use `refreshSession` to extend.
|
|
180
208
|
*
|
|
181
209
|
* @example
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
210
|
+
* ```ts
|
|
211
|
+
* const { user, session } = await auth.login({
|
|
212
|
+
* email: 'alice@example.com',
|
|
213
|
+
* password: 'hunter2',
|
|
185
214
|
* });
|
|
186
|
-
*
|
|
215
|
+
* // Store session.sessionId safely — you'll need it for other calls.
|
|
216
|
+
* ```
|
|
187
217
|
*/
|
|
188
|
-
async
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
218
|
+
async login(options) {
|
|
219
|
+
const result = await this.http.post(`${this.basePath}/login`, options);
|
|
220
|
+
return { user: result.user, session: result.session };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Invalidate a session (sign out).
|
|
224
|
+
*
|
|
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 });
|
|
202
232
|
}
|
|
203
|
-
// ─── SIGN OUT ──────────────────────────────────────────────────────────────
|
|
204
233
|
/**
|
|
205
|
-
*
|
|
234
|
+
* Extend a session using its refresh token.
|
|
235
|
+
* Returns a new session object.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* const newSession = await auth.refreshSession({ refreshToken: session.refreshToken });
|
|
240
|
+
* ```
|
|
206
241
|
*/
|
|
207
|
-
async
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
214
|
-
await parseResponse(res);
|
|
215
|
-
this.session = null;
|
|
216
|
-
return { data: void 0, error: null };
|
|
217
|
-
} catch (err) {
|
|
218
|
-
return { data: null, error: toHydrousError(err) };
|
|
219
|
-
}
|
|
242
|
+
async refreshSession({ refreshToken }) {
|
|
243
|
+
const result = await this.http.post(
|
|
244
|
+
`${this.basePath}/session/refresh`,
|
|
245
|
+
{ refreshToken }
|
|
246
|
+
);
|
|
247
|
+
return result.session;
|
|
220
248
|
}
|
|
221
|
-
// ───
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
249
|
+
// ─── User Profile ─────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Fetch a user by their ID.
|
|
252
|
+
*
|
|
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;
|
|
234
261
|
}
|
|
235
|
-
// ─── REFRESH TOKEN ────────────────────────────────────────────────────────
|
|
236
262
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
263
|
+
* Update fields on a user record. Requires a valid session.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* const updated = await auth.updateUser({
|
|
268
|
+
* sessionId: session.sessionId,
|
|
269
|
+
* userId: user.id,
|
|
270
|
+
* data: { fullName: 'Alice Smith', plan: 'enterprise' },
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
239
273
|
*/
|
|
240
|
-
async
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
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).
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```ts
|
|
288
|
+
* await auth.deleteUser({ sessionId: session.sessionId, userId: user.id });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
async deleteUser({ sessionId, userId }) {
|
|
292
|
+
await this.http.delete(`${this.basePath}/user`, { sessionId, userId });
|
|
293
|
+
}
|
|
294
|
+
// ─── Admin Operations ─────────────────────────────────────────────────────
|
|
295
|
+
/**
|
|
296
|
+
* List all users in the bucket. **Admin session required.**
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* const { users, total } = await auth.listUsers({
|
|
301
|
+
* sessionId: adminSession.sessionId,
|
|
302
|
+
* limit: 50,
|
|
303
|
+
* offset: 0,
|
|
304
|
+
* });
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
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 };
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Permanently hard-delete a user and all their data. **Admin session required.**
|
|
317
|
+
* This action is irreversible.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* await auth.hardDeleteUser({ sessionId: adminSession.sessionId, userId: user.id });
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
async hardDeleteUser({ sessionId, userId }) {
|
|
325
|
+
await this.http.delete(`${this.basePath}/user/hard`, { sessionId, userId });
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Bulk delete multiple users. **Admin session required.**
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```ts
|
|
332
|
+
* const result = await auth.bulkDeleteUsers({
|
|
333
|
+
* sessionId: adminSession.sessionId,
|
|
334
|
+
* userIds: ['usr_a', 'usr_b'],
|
|
335
|
+
* });
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
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 };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Lock a user account, preventing login. **Admin session required.**
|
|
350
|
+
*
|
|
351
|
+
* @param options.duration Lock duration in milliseconds. Defaults to 15 minutes.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* await auth.lockAccount({
|
|
356
|
+
* sessionId: adminSession.sessionId,
|
|
357
|
+
* userId: user.id,
|
|
358
|
+
* duration: 60 * 60 * 1000, // 1 hour
|
|
359
|
+
* });
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
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;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Unlock a previously locked user account. **Admin session required.**
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* await auth.unlockAccount({ sessionId: adminSession.sessionId, userId: user.id });
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
async unlockAccount({
|
|
379
|
+
sessionId,
|
|
380
|
+
userId
|
|
381
|
+
}) {
|
|
382
|
+
await this.http.post(`${this.basePath}/account/unlock`, { sessionId, userId });
|
|
258
383
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
384
|
+
// ─── Password Management ──────────────────────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* Change a user's password. The user must supply their current password.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```ts
|
|
390
|
+
* await auth.changePassword({
|
|
391
|
+
* sessionId: session.sessionId,
|
|
392
|
+
* userId: user.id,
|
|
393
|
+
* currentPassword: 'hunter2',
|
|
394
|
+
* newPassword: 'correcthorsebatterystaple',
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
async changePassword(options) {
|
|
399
|
+
await this.http.post(`${this.basePath}/password/change`, options);
|
|
262
400
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Request a password reset email for a user.
|
|
403
|
+
* Always returns success to prevent user enumeration.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* await auth.requestPasswordReset({ email: 'alice@example.com' });
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
async requestPasswordReset({ email }) {
|
|
411
|
+
await this.http.post(`${this.basePath}/password/reset/request`, { email });
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Complete a password reset using the token from the reset email.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
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.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```ts
|
|
439
|
+
* await auth.requestEmailVerification({ userId: user.id });
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
async requestEmailVerification({ userId }) {
|
|
443
|
+
await this.http.post(`${this.basePath}/email/verify/request`, { userId });
|
|
444
|
+
}
|
|
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 });
|
|
266
455
|
}
|
|
267
456
|
};
|
|
268
457
|
|
|
269
458
|
// src/utils/query.ts
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return { field, operator: "eq", value };
|
|
290
|
-
}
|
|
291
|
-
function neq(field, value) {
|
|
292
|
-
return { field, operator: "neq", value };
|
|
293
|
-
}
|
|
294
|
-
function gt(field, value) {
|
|
295
|
-
return { field, operator: "gt", value };
|
|
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));
|
|
475
|
+
}
|
|
476
|
+
const str = params.toString();
|
|
477
|
+
return str ? `?${str}` : "";
|
|
296
478
|
}
|
|
297
|
-
function
|
|
298
|
-
|
|
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";
|
|
299
503
|
}
|
|
300
|
-
function
|
|
301
|
-
|
|
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
|
+
}
|
|
302
510
|
}
|
|
303
511
|
|
|
304
512
|
// src/records/client.ts
|
|
305
513
|
var RecordsClient = class {
|
|
306
|
-
constructor(
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
};
|
|
514
|
+
constructor(http, bucketKey) {
|
|
515
|
+
assertSafeName(bucketKey, "bucketKey");
|
|
516
|
+
this.http = http;
|
|
517
|
+
this.bucketKey = bucketKey;
|
|
518
|
+
this.basePath = `/records/${bucketKey}`;
|
|
312
519
|
}
|
|
313
|
-
// ───
|
|
520
|
+
// ─── Single Record Operations ─────────────────────────────────────────────
|
|
314
521
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* @param collection - Collection name (e.g. "users")
|
|
318
|
-
* @param options - Filters, ordering, pagination
|
|
522
|
+
* Create a new record.
|
|
319
523
|
*
|
|
320
524
|
* @example
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
525
|
+
* ```ts
|
|
526
|
+
* const user = await records.create({
|
|
527
|
+
* name: 'Alice',
|
|
528
|
+
* email: 'alice@example.com',
|
|
529
|
+
* score: 100,
|
|
325
530
|
* });
|
|
531
|
+
* console.log(user.id); // "rec_xxxxxxxx"
|
|
532
|
+
* ```
|
|
326
533
|
*/
|
|
327
|
-
async
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const url = buildUrl(this.baseUrl, `records/${collection}`, params);
|
|
331
|
-
const res = await fetch(url, { headers: this.headers });
|
|
332
|
-
const json = await parseResponse(res);
|
|
333
|
-
return { data: json.data, count: json.count, error: null };
|
|
334
|
-
} catch (err) {
|
|
335
|
-
return { data: [], count: 0, error: toHydrousError(err) };
|
|
336
|
-
}
|
|
534
|
+
async create(data) {
|
|
535
|
+
const result = await this.http.post(this.basePath, data);
|
|
536
|
+
return result.record ?? result.data;
|
|
337
537
|
}
|
|
338
|
-
// ─── GET ONE ───────────────────────────────────────────────────────────────
|
|
339
538
|
/**
|
|
340
|
-
* Fetch a single record by
|
|
539
|
+
* Fetch a single record by ID.
|
|
341
540
|
*
|
|
342
541
|
* @example
|
|
343
|
-
*
|
|
542
|
+
* ```ts
|
|
543
|
+
* const post = await records.get('rec_abc123');
|
|
544
|
+
* ```
|
|
344
545
|
*/
|
|
345
|
-
async get(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const res = await fetch(url, { headers: this.headers });
|
|
349
|
-
const json = await parseResponse(res);
|
|
350
|
-
return { data: json.data, error: null };
|
|
351
|
-
} catch (err) {
|
|
352
|
-
return { data: null, error: toHydrousError(err) };
|
|
353
|
-
}
|
|
546
|
+
async get(id) {
|
|
547
|
+
const result = await this.http.get(`${this.basePath}/${id}`);
|
|
548
|
+
return result.record ?? result.data;
|
|
354
549
|
}
|
|
355
|
-
// ─── INSERT ────────────────────────────────────────────────────────────────
|
|
356
550
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* @param collection - Collection name
|
|
360
|
-
* @param payload - A single record object or an array of record objects
|
|
551
|
+
* Overwrite a record entirely (full replace).
|
|
361
552
|
*
|
|
362
553
|
* @example
|
|
363
|
-
*
|
|
364
|
-
* const
|
|
365
|
-
* name: 'Alice',
|
|
554
|
+
* ```ts
|
|
555
|
+
* const updated = await records.set('rec_abc123', {
|
|
556
|
+
* name: 'Alice Updated',
|
|
557
|
+
* email: 'alice2@example.com',
|
|
366
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).
|
|
367
567
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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 },
|
|
371
605
|
* ]);
|
|
606
|
+
* ```
|
|
372
607
|
*/
|
|
373
|
-
async
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 };
|
|
386
626
|
}
|
|
387
|
-
// ───
|
|
627
|
+
// ─── Querying ─────────────────────────────────────────────────────────────
|
|
388
628
|
/**
|
|
389
|
-
*
|
|
629
|
+
* Query records with optional filters, sorting, and pagination.
|
|
390
630
|
*
|
|
391
631
|
* @example
|
|
392
|
-
*
|
|
393
|
-
*
|
|
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,
|
|
394
651
|
* });
|
|
652
|
+
* ```
|
|
395
653
|
*/
|
|
396
|
-
async
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return { data: json.data, error: null };
|
|
406
|
-
} catch (err) {
|
|
407
|
-
return { data: null, error: toHydrousError(err) };
|
|
408
|
-
}
|
|
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
|
+
};
|
|
409
663
|
}
|
|
410
|
-
// ─── DELETE ────────────────────────────────────────────────────────────────
|
|
411
664
|
/**
|
|
412
|
-
*
|
|
665
|
+
* Convenience alias: get all records up to `limit` (default 100).
|
|
413
666
|
*
|
|
414
667
|
* @example
|
|
415
|
-
*
|
|
668
|
+
* ```ts
|
|
669
|
+
* const allPosts = await posts.getAll({ limit: 500 });
|
|
670
|
+
* ```
|
|
416
671
|
*/
|
|
417
|
-
async
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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;
|
|
426
721
|
}
|
|
427
722
|
};
|
|
428
723
|
|
|
429
724
|
// src/analytics/client.ts
|
|
430
725
|
var AnalyticsClient = class {
|
|
431
|
-
constructor(
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
};
|
|
726
|
+
constructor(http, bucketKey) {
|
|
727
|
+
assertSafeName(bucketKey, "bucketKey");
|
|
728
|
+
this.http = http;
|
|
729
|
+
this.bucketKey = bucketKey;
|
|
730
|
+
this.basePath = `/analytics/${bucketKey}`;
|
|
437
731
|
}
|
|
438
|
-
|
|
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 ────────────────────────────────────────────────────────────────
|
|
439
738
|
/**
|
|
440
|
-
*
|
|
739
|
+
* Count the total number of records in the bucket, with optional date filter.
|
|
441
740
|
*
|
|
442
741
|
* @example
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
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() },
|
|
447
749
|
* });
|
|
750
|
+
* ```
|
|
448
751
|
*/
|
|
449
|
-
async
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
const url = buildUrl(this.baseUrl, "analytics/track");
|
|
453
|
-
const res = await fetch(url, {
|
|
454
|
-
method: "POST",
|
|
455
|
-
headers: this.headers,
|
|
456
|
-
body: JSON.stringify({
|
|
457
|
-
...options,
|
|
458
|
-
timestamp: (_a = options.timestamp) != null ? _a : Date.now()
|
|
459
|
-
})
|
|
460
|
-
});
|
|
461
|
-
await parseResponse(res);
|
|
462
|
-
return { data: void 0, error: null };
|
|
463
|
-
} catch (err) {
|
|
464
|
-
return { data: null, error: toHydrousError(err) };
|
|
465
|
-
}
|
|
752
|
+
async count(opts = {}) {
|
|
753
|
+
return this.run({ queryType: "count", ...opts });
|
|
466
754
|
}
|
|
467
|
-
// ───
|
|
755
|
+
// ─── Distribution ─────────────────────────────────────────────────────────
|
|
468
756
|
/**
|
|
469
|
-
*
|
|
757
|
+
* Count how many records have each unique value for a given field.
|
|
758
|
+
* Great for pie charts and bar charts.
|
|
470
759
|
*
|
|
471
760
|
* @example
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
761
|
+
* ```ts
|
|
762
|
+
* const rows = await analytics.distribution({
|
|
763
|
+
* field: 'status',
|
|
764
|
+
* limit: 10,
|
|
765
|
+
* order: 'desc',
|
|
477
766
|
* });
|
|
767
|
+
* // → [{ value: 'completed', count: 312 }, { value: 'pending', count: 88 }, ...]
|
|
768
|
+
* ```
|
|
478
769
|
*/
|
|
479
|
-
async
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (options.event) params["event"] = options.event;
|
|
483
|
-
if (options.from) params["from"] = options.from;
|
|
484
|
-
if (options.to) params["to"] = options.to;
|
|
485
|
-
if (options.limit) params["limit"] = String(options.limit);
|
|
486
|
-
if (options.groupBy) params["groupBy"] = options.groupBy;
|
|
487
|
-
const url = buildUrl(this.baseUrl, "analytics/events", params);
|
|
488
|
-
const res = await fetch(url, { headers: this.headers });
|
|
489
|
-
const json = await parseResponse(res);
|
|
490
|
-
return { data: json.data, count: json.count, error: null };
|
|
491
|
-
} catch (err) {
|
|
492
|
-
return { data: [], count: 0, error: toHydrousError(err) };
|
|
493
|
-
}
|
|
770
|
+
async distribution(opts) {
|
|
771
|
+
assertSafeName(opts.field, "field");
|
|
772
|
+
return this.run({ queryType: "distribution", ...opts });
|
|
494
773
|
}
|
|
495
|
-
// ───
|
|
774
|
+
// ─── Sum ──────────────────────────────────────────────────────────────────
|
|
496
775
|
/**
|
|
497
|
-
*
|
|
498
|
-
* calling `track` in a loop).
|
|
776
|
+
* Sum a numeric field, optionally grouped by another field.
|
|
499
777
|
*
|
|
500
778
|
* @example
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* ]
|
|
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
|
+
* ```
|
|
505
788
|
*/
|
|
506
|
-
async
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
var _a;
|
|
511
|
-
return {
|
|
512
|
-
...e,
|
|
513
|
-
timestamp: (_a = e.timestamp) != null ? _a : Date.now()
|
|
514
|
-
};
|
|
515
|
-
});
|
|
516
|
-
const res = await fetch(url, {
|
|
517
|
-
method: "POST",
|
|
518
|
-
headers: this.headers,
|
|
519
|
-
body: JSON.stringify({ events: stamped })
|
|
520
|
-
});
|
|
521
|
-
await parseResponse(res);
|
|
522
|
-
return { data: void 0, error: null };
|
|
523
|
-
} catch (err) {
|
|
524
|
-
return { data: null, error: toHydrousError(err) };
|
|
525
|
-
}
|
|
789
|
+
async sum(opts) {
|
|
790
|
+
assertSafeName(opts.field, "field");
|
|
791
|
+
if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
|
|
792
|
+
return this.run({ queryType: "sum", ...opts });
|
|
526
793
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
function drainSSEProgress(rawText, onProgress) {
|
|
542
|
-
const results = [];
|
|
543
|
-
const errors = [];
|
|
544
|
-
parseSSEText(rawText, (eventType, data) => {
|
|
545
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
546
|
-
const d = data;
|
|
547
|
-
if (eventType === "progress" && onProgress) {
|
|
548
|
-
onProgress({
|
|
549
|
-
index: (_a = d["index"]) != null ? _a : 0,
|
|
550
|
-
total: (_b = d["total"]) != null ? _b : 1,
|
|
551
|
-
path: (_c = d["path"]) != null ? _c : "",
|
|
552
|
-
stage: (_d = d["stage"]) != null ? _d : "uploading",
|
|
553
|
-
bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
|
|
554
|
-
totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
|
|
555
|
-
percent: (_g = d["percent"]) != null ? _g : 0,
|
|
556
|
-
bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
|
|
557
|
-
eta: (_i = d["eta"]) != null ? _i : null,
|
|
558
|
-
result: d["result"],
|
|
559
|
-
error: d["error"],
|
|
560
|
-
code: d["code"]
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
if (eventType === "done") {
|
|
564
|
-
if (d["path"]) {
|
|
565
|
-
results.push(d);
|
|
566
|
-
} else if (Array.isArray(d["errors"])) {
|
|
567
|
-
const succeeded = (_j = d["succeeded"]) != null ? _j : [];
|
|
568
|
-
const errs = d["errors"];
|
|
569
|
-
results.push(...succeeded);
|
|
570
|
-
errors.push(...errs);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (eventType === "error") {
|
|
574
|
-
errors.push({
|
|
575
|
-
path: "",
|
|
576
|
-
error: (_k = d["error"]) != null ? _k : "Unknown error",
|
|
577
|
-
code: (_l = d["code"]) != null ? _l : "UNKNOWN"
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
return { results, errors };
|
|
582
|
-
}
|
|
583
|
-
var StorageClient = class {
|
|
584
|
-
constructor(config) {
|
|
585
|
-
this.baseUrl = config.url;
|
|
586
|
-
}
|
|
587
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
588
|
-
// UPLOAD
|
|
589
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
590
|
-
/**
|
|
591
|
-
* Upload a single file to a bucket.
|
|
592
|
-
*
|
|
593
|
-
* The bucket key **always comes first**.
|
|
594
|
-
* Supply an `onProgress` callback to receive live upload progress including
|
|
595
|
-
* bytes transferred, speed (bytes/sec), ETA, and lifecycle stage.
|
|
596
|
-
*
|
|
597
|
-
* ### Stages fired via `onProgress`
|
|
598
|
-
* | Stage | Meaning |
|
|
599
|
-
* |-------------|------------------------------------------|
|
|
600
|
-
* | `pending` | Queued, not yet started |
|
|
601
|
-
* | `compressing` | Server is compressing the file |
|
|
602
|
-
* | `uploading` | Bytes flowing to cloud storage |
|
|
603
|
-
* | `done` | Confirmed written to cloud storage |
|
|
604
|
-
* | `error` | Something went wrong |
|
|
605
|
-
*
|
|
606
|
-
* @param bucketKey Your storage bucket key (`ssk_…`)
|
|
607
|
-
* @param file A `File`, `Blob`, or `Buffer` (Node)
|
|
608
|
-
* @param options Path, overwrite flag, progress callback
|
|
609
|
-
*
|
|
610
|
-
* @example
|
|
611
|
-
* const { data, error } = await hydrous.storage.upload(
|
|
612
|
-
* 'ssk_my_bucket_key',
|
|
613
|
-
* file,
|
|
614
|
-
* {
|
|
615
|
-
* path: 'avatars/alice.jpg',
|
|
616
|
-
* overwrite: true,
|
|
617
|
-
* onProgress: (p) => {
|
|
618
|
-
* console.log(`${p.stage} — ${p.percent}% ${p.bytesPerSecond} B/s ETA ${p.eta}s`);
|
|
619
|
-
* },
|
|
620
|
-
* }
|
|
621
|
-
* );
|
|
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
|
+
* ```
|
|
622
807
|
*/
|
|
623
|
-
async
|
|
624
|
-
|
|
625
|
-
const { path, overwrite = false, onProgress } = options;
|
|
626
|
-
try {
|
|
627
|
-
const url = storageUrl(this.baseUrl, bucketKey, "upload");
|
|
628
|
-
const form = new FormData();
|
|
629
|
-
if (file instanceof Uint8Array) {
|
|
630
|
-
form.append("file", new Blob([file.buffer]), path != null ? path : "file");
|
|
631
|
-
} else if (file instanceof ArrayBuffer) {
|
|
632
|
-
form.append("file", new Blob([file]), path != null ? path : "file");
|
|
633
|
-
} else {
|
|
634
|
-
form.append("file", file, path != null ? path : file instanceof File ? file.name : "file");
|
|
635
|
-
}
|
|
636
|
-
if (path) form.append("path", path);
|
|
637
|
-
if (overwrite) form.append("overwrite", "true");
|
|
638
|
-
const headers = storageHeaders(bucketKey);
|
|
639
|
-
if (isBrowser) {
|
|
640
|
-
const totalBytes = file instanceof Blob ? file.size : file instanceof Uint8Array ? file.byteLength : file.byteLength;
|
|
641
|
-
const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
|
|
642
|
-
if (onProgress) {
|
|
643
|
-
onProgress({
|
|
644
|
-
index: 0,
|
|
645
|
-
total: 1,
|
|
646
|
-
path: path != null ? path : "",
|
|
647
|
-
stage: "uploading",
|
|
648
|
-
bytesUploaded: loaded,
|
|
649
|
-
totalBytes: total || totalBytes,
|
|
650
|
-
percent: Math.min(99, Math.round(loaded / (total || totalBytes) * 100)),
|
|
651
|
-
bytesPerSecond: null,
|
|
652
|
-
eta: null
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
const { results, errors } = drainSSEProgress(rawBody, onProgress);
|
|
657
|
-
if (errors.length > 0 && results.length === 0) {
|
|
658
|
-
return { data: null, error: { message: errors[0].error, code: errors[0].code } };
|
|
659
|
-
}
|
|
660
|
-
const result = (_a = results[0]) != null ? _a : null;
|
|
661
|
-
if (result && onProgress) {
|
|
662
|
-
onProgress({
|
|
663
|
-
index: 0,
|
|
664
|
-
total: 1,
|
|
665
|
-
path: result.path,
|
|
666
|
-
stage: "done",
|
|
667
|
-
bytesUploaded: totalBytes,
|
|
668
|
-
totalBytes,
|
|
669
|
-
percent: 100,
|
|
670
|
-
bytesPerSecond: null,
|
|
671
|
-
eta: 0,
|
|
672
|
-
result
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
return { data: result, error: null };
|
|
676
|
-
}
|
|
677
|
-
const res = await fetch(url, { method: "POST", headers, body: form });
|
|
678
|
-
if (!res.ok) {
|
|
679
|
-
const err = await res.json().catch(() => ({}));
|
|
680
|
-
throw new HydrousSDKError((_b = err.error) != null ? _b : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
|
|
681
|
-
}
|
|
682
|
-
let finalResult = null;
|
|
683
|
-
await readSSEStream(res, (eventType, data) => {
|
|
684
|
-
var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
685
|
-
const d = data;
|
|
686
|
-
if (eventType === "progress" && onProgress) {
|
|
687
|
-
onProgress({
|
|
688
|
-
index: (_a2 = d["index"]) != null ? _a2 : 0,
|
|
689
|
-
total: (_b2 = d["total"]) != null ? _b2 : 1,
|
|
690
|
-
path: (_d = (_c = d["path"]) != null ? _c : path) != null ? _d : "",
|
|
691
|
-
stage: (_e = d["stage"]) != null ? _e : "uploading",
|
|
692
|
-
bytesUploaded: (_f = d["bytesUploaded"]) != null ? _f : 0,
|
|
693
|
-
totalBytes: (_g = d["totalBytes"]) != null ? _g : 0,
|
|
694
|
-
percent: (_h = d["percent"]) != null ? _h : 0,
|
|
695
|
-
bytesPerSecond: (_i = d["bytesPerSecond"]) != null ? _i : null,
|
|
696
|
-
eta: (_j = d["eta"]) != null ? _j : null,
|
|
697
|
-
result: d["result"],
|
|
698
|
-
error: d["error"]
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
if (eventType === "done") finalResult = data;
|
|
702
|
-
if (eventType === "error") {
|
|
703
|
-
throw new HydrousSDKError(
|
|
704
|
-
(_k = d["error"]) != null ? _k : "Upload failed",
|
|
705
|
-
(_l = d["code"]) != null ? _l : "UPLOAD_ERROR"
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
return { data: finalResult, error: null };
|
|
710
|
-
} catch (err) {
|
|
711
|
-
return { data: null, error: toHydrousError(err) };
|
|
712
|
-
}
|
|
808
|
+
async timeSeries(opts = {}) {
|
|
809
|
+
return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
|
|
713
810
|
}
|
|
714
|
-
//
|
|
715
|
-
// UPLOAD RAW (text / JSON / binary from string)
|
|
716
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
811
|
+
// ─── Field Time Series ────────────────────────────────────────────────────
|
|
717
812
|
/**
|
|
718
|
-
*
|
|
719
|
-
* Great for saving generated content, config files, or JSON records.
|
|
813
|
+
* Aggregate a numeric field over time (e.g. daily revenue, hourly signups).
|
|
720
814
|
*
|
|
721
|
-
* @
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
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.
|
|
725
838
|
*
|
|
726
839
|
* @example
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
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
|
+
* ```
|
|
733
849
|
*/
|
|
734
|
-
async
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const url = storageUrl(this.baseUrl, bucketKey, "upload-raw");
|
|
739
|
-
const headers = { ...storageHeaders(bucketKey), "Content-Type": "application/json" };
|
|
740
|
-
const res = await fetch(url, {
|
|
741
|
-
method: "POST",
|
|
742
|
-
headers,
|
|
743
|
-
body: JSON.stringify({ path, content, mimeType, overwrite })
|
|
744
|
-
});
|
|
745
|
-
if (!res.ok) {
|
|
746
|
-
const e = await res.json().catch(() => ({}));
|
|
747
|
-
throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
|
|
748
|
-
}
|
|
749
|
-
let finalResult = null;
|
|
750
|
-
await readSSEStream(res, (eventType, data) => {
|
|
751
|
-
var _a2, _b, _c, _d, _e, _f;
|
|
752
|
-
const d = data;
|
|
753
|
-
if (eventType === "progress" && onProgress) {
|
|
754
|
-
onProgress({
|
|
755
|
-
index: 0,
|
|
756
|
-
total: 1,
|
|
757
|
-
path,
|
|
758
|
-
stage: (_a2 = d["stage"]) != null ? _a2 : "uploading",
|
|
759
|
-
bytesUploaded: (_b = d["bytesUploaded"]) != null ? _b : 0,
|
|
760
|
-
totalBytes: (_c = d["totalBytes"]) != null ? _c : 0,
|
|
761
|
-
percent: (_d = d["percent"]) != null ? _d : 0,
|
|
762
|
-
bytesPerSecond: (_e = d["bytesPerSecond"]) != null ? _e : null,
|
|
763
|
-
eta: (_f = d["eta"]) != null ? _f : null
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
if (eventType === "done") finalResult = data;
|
|
767
|
-
});
|
|
768
|
-
return { data: finalResult, error: null };
|
|
769
|
-
} catch (err) {
|
|
770
|
-
return { data: null, error: toHydrousError(err) };
|
|
771
|
-
}
|
|
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 });
|
|
772
854
|
}
|
|
773
|
-
//
|
|
774
|
-
// BATCH UPLOAD
|
|
775
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
855
|
+
// ─── Field Stats ──────────────────────────────────────────────────────────
|
|
776
856
|
/**
|
|
777
|
-
*
|
|
857
|
+
* Get statistical summary (min, max, avg, sum, count, stddev) for a numeric field.
|
|
778
858
|
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
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.
|
|
783
873
|
*
|
|
784
|
-
* @
|
|
785
|
-
*
|
|
786
|
-
*
|
|
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.
|
|
787
894
|
*
|
|
788
895
|
* @example
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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
|
+
* ```
|
|
799
906
|
*/
|
|
800
|
-
async
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const resolvedPaths = files.map(
|
|
807
|
-
(f, i) => {
|
|
808
|
-
var _a2;
|
|
809
|
-
return (_a2 = paths == null ? void 0 : paths[i]) != null ? _a2 : `${prefix}${f.name}`;
|
|
810
|
-
}
|
|
811
|
-
);
|
|
812
|
-
files.forEach((f) => form.append("files", f, f.name));
|
|
813
|
-
form.append("paths", JSON.stringify(resolvedPaths));
|
|
814
|
-
if (overwrite) form.append("overwrite", "true");
|
|
815
|
-
const headers = storageHeaders(bucketKey);
|
|
816
|
-
if (isBrowser) {
|
|
817
|
-
const totalBytes = files.reduce((s, f) => s + f.size, 0);
|
|
818
|
-
const rawBody = await xhrUpload(url, form, headers, (loaded, total) => {
|
|
819
|
-
if (onProgress) {
|
|
820
|
-
let cursor = 0;
|
|
821
|
-
for (let i = 0; i < files.length; i++) {
|
|
822
|
-
const share = files[i].size / (totalBytes || 1);
|
|
823
|
-
const myStart = cursor;
|
|
824
|
-
const myEnd = cursor + share;
|
|
825
|
-
const fileLoaded = Math.max(0, Math.min(
|
|
826
|
-
files[i].size,
|
|
827
|
-
(loaded / (total || totalBytes) - myStart) / share * files[i].size
|
|
828
|
-
));
|
|
829
|
-
onProgress({
|
|
830
|
-
index: i,
|
|
831
|
-
total: files.length,
|
|
832
|
-
path: resolvedPaths[i],
|
|
833
|
-
stage: "uploading",
|
|
834
|
-
bytesUploaded: Math.round(fileLoaded),
|
|
835
|
-
totalBytes: files[i].size,
|
|
836
|
-
percent: Math.min(99, Math.round(fileLoaded / files[i].size * 100)),
|
|
837
|
-
bytesPerSecond: null,
|
|
838
|
-
eta: null
|
|
839
|
-
});
|
|
840
|
-
cursor = myEnd;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
});
|
|
844
|
-
const { results, errors } = drainSSEProgress(rawBody, onProgress);
|
|
845
|
-
return {
|
|
846
|
-
data: {
|
|
847
|
-
succeeded: results,
|
|
848
|
-
failed: errors
|
|
849
|
-
},
|
|
850
|
-
error: null
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
const res = await fetch(url, { method: "POST", headers, body: form });
|
|
854
|
-
if (!res.ok) {
|
|
855
|
-
const e = await res.json().catch(() => ({}));
|
|
856
|
-
throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
|
|
857
|
-
}
|
|
858
|
-
const succeeded = [];
|
|
859
|
-
const failed = [];
|
|
860
|
-
await readSSEStream(res, (eventType, data) => {
|
|
861
|
-
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
862
|
-
const d = data;
|
|
863
|
-
if (eventType === "progress" && onProgress) {
|
|
864
|
-
onProgress({
|
|
865
|
-
index: (_a2 = d["index"]) != null ? _a2 : 0,
|
|
866
|
-
total: (_b = d["total"]) != null ? _b : files.length,
|
|
867
|
-
path: (_c = d["path"]) != null ? _c : "",
|
|
868
|
-
stage: (_d = d["stage"]) != null ? _d : "uploading",
|
|
869
|
-
bytesUploaded: (_e = d["bytesUploaded"]) != null ? _e : 0,
|
|
870
|
-
totalBytes: (_f = d["totalBytes"]) != null ? _f : 0,
|
|
871
|
-
percent: (_g = d["percent"]) != null ? _g : 0,
|
|
872
|
-
bytesPerSecond: (_h = d["bytesPerSecond"]) != null ? _h : null,
|
|
873
|
-
eta: (_i = d["eta"]) != null ? _i : null,
|
|
874
|
-
result: d["result"],
|
|
875
|
-
error: d["error"],
|
|
876
|
-
code: d["code"]
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
if (eventType === "done" && d["succeeded"]) {
|
|
880
|
-
succeeded.push(...d["succeeded"]);
|
|
881
|
-
failed.push(...(_j = d["errors"]) != null ? _j : []);
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
|
-
return { data: { succeeded, failed }, error: null };
|
|
885
|
-
} catch (err) {
|
|
886
|
-
return { data: null, error: toHydrousError(err) };
|
|
887
|
-
}
|
|
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 });
|
|
888
913
|
}
|
|
889
|
-
//
|
|
890
|
-
// DOWNLOAD
|
|
891
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
914
|
+
// ─── Storage Stats ────────────────────────────────────────────────────────
|
|
892
915
|
/**
|
|
893
|
-
*
|
|
916
|
+
* Get storage statistics for this bucket: record counts, byte sizes.
|
|
894
917
|
*
|
|
895
|
-
* @
|
|
896
|
-
*
|
|
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.
|
|
897
931
|
*
|
|
898
932
|
* @example
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
* '
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
* }
|
|
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
|
+
* ```
|
|
907
945
|
*/
|
|
908
|
-
async
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const res = await fetch(url, { headers: storageHeaders(bucketKey) });
|
|
913
|
-
if (!res.ok) {
|
|
914
|
-
const e = await res.json().catch(() => ({}));
|
|
915
|
-
throw new HydrousSDKError((_a = e.error) != null ? _a : `HTTP ${res.status}`, "HTTP_ERROR", res.status);
|
|
916
|
-
}
|
|
917
|
-
const buffer = await res.arrayBuffer();
|
|
918
|
-
return { data: buffer, error: null };
|
|
919
|
-
} catch (err) {
|
|
920
|
-
return { data: null, error: toHydrousError(err) };
|
|
921
|
-
}
|
|
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 });
|
|
922
950
|
}
|
|
923
|
-
//
|
|
924
|
-
// BATCH DOWNLOAD
|
|
925
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
951
|
+
// ─── Raw Query ────────────────────────────────────────────────────────────
|
|
926
952
|
/**
|
|
927
|
-
*
|
|
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.
|
|
928
955
|
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
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.
|
|
931
987
|
*
|
|
932
|
-
* @param
|
|
933
|
-
* @param
|
|
934
|
-
* @param options
|
|
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.
|
|
935
991
|
*
|
|
936
992
|
* @example
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
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 },
|
|
944
1036
|
* );
|
|
1037
|
+
* ```
|
|
945
1038
|
*/
|
|
946
|
-
async
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
var _a2, _b, _c, _d, _e, _f, _g, _h;
|
|
963
|
-
const d = data;
|
|
964
|
-
if (eventType === "file") {
|
|
965
|
-
const base64 = d["content"];
|
|
966
|
-
const mimeType = (_a2 = d["mimeType"]) != null ? _a2 : "application/octet-stream";
|
|
967
|
-
const path = (_b = d["path"]) != null ? _b : "";
|
|
968
|
-
const size = (_c = d["size"]) != null ? _c : 0;
|
|
969
|
-
const index = (_d = d["index"]) != null ? _d : 0;
|
|
970
|
-
const binary = atob(base64);
|
|
971
|
-
const bytes = new Uint8Array(binary.length);
|
|
972
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
973
|
-
const content = bytes.buffer;
|
|
974
|
-
downloadedFiles.push({ path, content, mimeType, size });
|
|
975
|
-
if (onProgress) {
|
|
976
|
-
onProgress({
|
|
977
|
-
index,
|
|
978
|
-
total: filePaths.length,
|
|
979
|
-
path,
|
|
980
|
-
status: "success",
|
|
981
|
-
size,
|
|
982
|
-
mimeType
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
if (autoSave && isBrowser) {
|
|
986
|
-
const blob = new Blob([content], { type: mimeType });
|
|
987
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
988
|
-
const a = document.createElement("a");
|
|
989
|
-
a.href = blobUrl;
|
|
990
|
-
a.download = (_e = path.split("/").pop()) != null ? _e : "download";
|
|
991
|
-
a.click();
|
|
992
|
-
setTimeout(() => URL.revokeObjectURL(blobUrl), 5e3);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
if (eventType === "error" && onProgress) {
|
|
996
|
-
const index = (_f = d["index"]) != null ? _f : 0;
|
|
997
|
-
onProgress({
|
|
998
|
-
index,
|
|
999
|
-
total: filePaths.length,
|
|
1000
|
-
path: (_g = filePaths[index]) != null ? _g : "",
|
|
1001
|
-
status: "error",
|
|
1002
|
-
error: (_h = d["error"]) != null ? _h : "Download failed"
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
return { data: downloadedFiles, error: null };
|
|
1007
|
-
} catch (err) {
|
|
1008
|
-
return { data: null, error: toHydrousError(err) };
|
|
1009
|
-
}
|
|
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
|
+
};
|
|
1010
1055
|
}
|
|
1011
|
-
//
|
|
1012
|
-
// LIST
|
|
1013
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1056
|
+
// ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
|
|
1014
1057
|
/**
|
|
1015
|
-
*
|
|
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
|
+
* });
|
|
1016
1069
|
*
|
|
1017
|
-
*
|
|
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.
|
|
1018
1090
|
*
|
|
1019
|
-
* @param
|
|
1020
|
-
* @param
|
|
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.
|
|
1021
1102
|
*
|
|
1022
1103
|
* @example
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const result = await storage.confirmUpload({
|
|
1106
|
+
* path: 'videos/intro.mp4',
|
|
1107
|
+
* mimeType: 'video/mp4',
|
|
1108
|
+
* isPublic: true,
|
|
1026
1109
|
* });
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
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 });
|
|
1029
1143
|
* }
|
|
1144
|
+
* ```
|
|
1030
1145
|
*/
|
|
1031
|
-
async
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
cursor: cursor || void 0
|
|
1038
|
-
};
|
|
1039
|
-
const url = buildUrl(
|
|
1040
|
-
this.baseUrl,
|
|
1041
|
-
`storage/${bucketFromKey(bucketKey)}/list`,
|
|
1042
|
-
params
|
|
1043
|
-
);
|
|
1044
|
-
const res = await fetch(url, { headers: storageHeaders(bucketKey) });
|
|
1045
|
-
const json = await parseResponse(res);
|
|
1046
|
-
return { data: json, error: null };
|
|
1047
|
-
} catch (err) {
|
|
1048
|
-
return { data: null, error: toHydrousError(err) };
|
|
1049
|
-
}
|
|
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 };
|
|
1050
1152
|
}
|
|
1051
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1052
|
-
// METADATA
|
|
1053
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1054
1153
|
/**
|
|
1055
|
-
*
|
|
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.
|
|
1056
1174
|
*
|
|
1057
|
-
* @
|
|
1058
|
-
*
|
|
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 }`.
|
|
1059
1192
|
*
|
|
1060
1193
|
* @example
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
* );
|
|
1065
|
-
* console.log(data.size, data.mimeType);
|
|
1194
|
+
* ```ts
|
|
1195
|
+
* const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
|
|
1196
|
+
* ```
|
|
1066
1197
|
*/
|
|
1067
|
-
async
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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 };
|
|
1076
1342
|
}
|
|
1077
|
-
//
|
|
1078
|
-
// DELETE FILE
|
|
1079
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1343
|
+
// ─── File Operations ──────────────────────────────────────────────────────
|
|
1080
1344
|
/**
|
|
1081
1345
|
* Delete a single file.
|
|
1082
1346
|
*
|
|
1083
|
-
* @
|
|
1084
|
-
*
|
|
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.
|
|
1085
1361
|
*
|
|
1086
1362
|
* @example
|
|
1087
|
-
*
|
|
1363
|
+
* ```ts
|
|
1364
|
+
* await storage.deleteFolder('temp/');
|
|
1365
|
+
* ```
|
|
1088
1366
|
*/
|
|
1089
|
-
async
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
body: JSON.stringify({ path: filePath })
|
|
1096
|
-
});
|
|
1097
|
-
await parseResponse(res);
|
|
1098
|
-
return { data: void 0, error: null };
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
return { data: null, error: toHydrousError(err) };
|
|
1101
|
-
}
|
|
1367
|
+
async deleteFolder(path) {
|
|
1368
|
+
await this.http.request(`${this.basePath}/folder`, {
|
|
1369
|
+
method: "DELETE",
|
|
1370
|
+
body: { path },
|
|
1371
|
+
headers: this.authHeaders
|
|
1372
|
+
});
|
|
1102
1373
|
}
|
|
1103
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1104
|
-
// DELETE FOLDER
|
|
1105
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1106
1374
|
/**
|
|
1107
|
-
*
|
|
1375
|
+
* Move or rename a file.
|
|
1108
1376
|
*
|
|
1109
|
-
* @
|
|
1110
|
-
*
|
|
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.
|
|
1111
1394
|
*
|
|
1112
1395
|
* @example
|
|
1113
|
-
*
|
|
1396
|
+
* ```ts
|
|
1397
|
+
* await storage.copy('templates/base.html', 'sites/my-site/index.html');
|
|
1398
|
+
* ```
|
|
1114
1399
|
*/
|
|
1115
|
-
async
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
body: JSON.stringify({ path: folderPath })
|
|
1122
|
-
});
|
|
1123
|
-
await parseResponse(res);
|
|
1124
|
-
return { data: void 0, error: null };
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
return { data: null, error: toHydrousError(err) };
|
|
1127
|
-
}
|
|
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 };
|
|
1128
1406
|
}
|
|
1129
|
-
//
|
|
1130
|
-
// CREATE FOLDER
|
|
1131
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1407
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
1132
1408
|
/**
|
|
1133
|
-
*
|
|
1409
|
+
* Get storage statistics for your key: total files, bytes, operation counts.
|
|
1134
1410
|
*
|
|
1135
|
-
* @
|
|
1136
|
-
*
|
|
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.
|
|
1137
1427
|
*
|
|
1138
1428
|
* @example
|
|
1139
|
-
*
|
|
1429
|
+
* ```ts
|
|
1430
|
+
* const info = await storage.info();
|
|
1431
|
+
* // → { ok: true, storageRoot: 'hydrous-storage' }
|
|
1432
|
+
* ```
|
|
1140
1433
|
*/
|
|
1141
|
-
async
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
+
);
|
|
1153
1533
|
}
|
|
1534
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
1535
|
+
this.http = new HttpClient(baseUrl, config.securityKey);
|
|
1536
|
+
this._storageKey = config.securityKey;
|
|
1154
1537
|
}
|
|
1155
|
-
//
|
|
1156
|
-
// MOVE
|
|
1157
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1538
|
+
// ─── Records ─────────────────────────────────────────────────────────────
|
|
1158
1539
|
/**
|
|
1159
|
-
*
|
|
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.
|
|
1160
1544
|
*
|
|
1161
|
-
* @param bucketKey
|
|
1162
|
-
* @param fromPath Current path of the file
|
|
1163
|
-
* @param toPath New path for the file
|
|
1545
|
+
* @param bucketKey The name of your bucket (must match what you created in the dashboard).
|
|
1164
1546
|
*
|
|
1165
1547
|
* @example
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
* );
|
|
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
|
+
* ```
|
|
1171
1555
|
*/
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
const res = await fetch(url, {
|
|
1176
|
-
method: "POST",
|
|
1177
|
-
headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
|
|
1178
|
-
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
1179
|
-
});
|
|
1180
|
-
await parseResponse(res);
|
|
1181
|
-
return { data: void 0, error: null };
|
|
1182
|
-
} catch (err) {
|
|
1183
|
-
return { data: null, error: toHydrousError(err) };
|
|
1556
|
+
records(bucketKey) {
|
|
1557
|
+
if (!this._recordsCache.has(bucketKey)) {
|
|
1558
|
+
this._recordsCache.set(bucketKey, new RecordsClient(this.http, bucketKey));
|
|
1184
1559
|
}
|
|
1560
|
+
return this._recordsCache.get(bucketKey);
|
|
1185
1561
|
}
|
|
1186
|
-
//
|
|
1187
|
-
// COPY
|
|
1188
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1562
|
+
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
1189
1563
|
/**
|
|
1190
|
-
*
|
|
1564
|
+
* Get an auth client for the given user bucket.
|
|
1191
1565
|
*
|
|
1192
|
-
* @param bucketKey
|
|
1193
|
-
* @param fromPath Source path
|
|
1194
|
-
* @param toPath Destination path
|
|
1566
|
+
* @param bucketKey The name of your user bucket (e.g. `"app-users"`).
|
|
1195
1567
|
*
|
|
1196
1568
|
* @example
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
* );
|
|
1569
|
+
* ```ts
|
|
1570
|
+
* const auth = db.auth('app-users');
|
|
1571
|
+
* const { user, session } = await auth.login({ email: '...', password: '...' });
|
|
1572
|
+
* ```
|
|
1202
1573
|
*/
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const res = await fetch(url, {
|
|
1207
|
-
method: "POST",
|
|
1208
|
-
headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
|
|
1209
|
-
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
1210
|
-
});
|
|
1211
|
-
await parseResponse(res);
|
|
1212
|
-
return { data: void 0, error: null };
|
|
1213
|
-
} catch (err) {
|
|
1214
|
-
return { data: null, error: toHydrousError(err) };
|
|
1574
|
+
auth(bucketKey) {
|
|
1575
|
+
if (!this._authCache.has(bucketKey)) {
|
|
1576
|
+
this._authCache.set(bucketKey, new AuthClient(this.http, bucketKey));
|
|
1215
1577
|
}
|
|
1578
|
+
return this._authCache.get(bucketKey);
|
|
1216
1579
|
}
|
|
1217
|
-
//
|
|
1218
|
-
// SIGNED URL
|
|
1219
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1580
|
+
// ─── Analytics ────────────────────────────────────────────────────────────
|
|
1220
1581
|
/**
|
|
1221
|
-
*
|
|
1582
|
+
* Get an analytics client for the given bucket.
|
|
1222
1583
|
*
|
|
1223
|
-
* @param bucketKey
|
|
1224
|
-
* @param filePath Path of the file
|
|
1225
|
-
* @param options `expiresIn` seconds (default: 3600)
|
|
1584
|
+
* @param bucketKey The name of the bucket to analyse.
|
|
1226
1585
|
*
|
|
1227
1586
|
* @example
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
* );
|
|
1233
|
-
* console.log(data.signedUrl); // share this URL
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* const analytics = db.analytics('orders');
|
|
1589
|
+
* const { count } = await analytics.count();
|
|
1590
|
+
* ```
|
|
1234
1591
|
*/
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const url = storageUrl(this.baseUrl, bucketKey, "signed-url");
|
|
1239
|
-
const res = await fetch(url, {
|
|
1240
|
-
method: "POST",
|
|
1241
|
-
headers: { ...storageHeaders(bucketKey), "Content-Type": "application/json" },
|
|
1242
|
-
body: JSON.stringify({ path: filePath, expiresInSeconds: expiresIn })
|
|
1243
|
-
});
|
|
1244
|
-
const json = await parseResponse(res);
|
|
1245
|
-
return { data: json, error: null };
|
|
1246
|
-
} catch (err) {
|
|
1247
|
-
return { data: null, error: toHydrousError(err) };
|
|
1592
|
+
analytics(bucketKey) {
|
|
1593
|
+
if (!this._analyticsCache.has(bucketKey)) {
|
|
1594
|
+
this._analyticsCache.set(bucketKey, new AnalyticsClient(this.http, bucketKey));
|
|
1248
1595
|
}
|
|
1596
|
+
return this._analyticsCache.get(bucketKey);
|
|
1249
1597
|
}
|
|
1250
|
-
//
|
|
1251
|
-
// STATS
|
|
1252
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1598
|
+
// ─── Storage ──────────────────────────────────────────────────────────────
|
|
1253
1599
|
/**
|
|
1254
|
-
*
|
|
1600
|
+
* The storage manager for uploading, downloading, listing, and managing files.
|
|
1255
1601
|
*
|
|
1256
|
-
*
|
|
1602
|
+
* Scoped to your project — you can never access another project's files.
|
|
1257
1603
|
*
|
|
1258
1604
|
* @example
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
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
|
+
* ```
|
|
1261
1613
|
*/
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const res = await fetch(url, { headers: storageHeaders(bucketKey) });
|
|
1266
|
-
const json = await parseResponse(res);
|
|
1267
|
-
return { data: json.data, error: null };
|
|
1268
|
-
} catch (err) {
|
|
1269
|
-
return { data: null, error: toHydrousError(err) };
|
|
1614
|
+
get storage() {
|
|
1615
|
+
if (!this._storage) {
|
|
1616
|
+
this._storage = new StorageManager(this.http, this._storageKey);
|
|
1270
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;
|
|
1271
1624
|
}
|
|
1272
1625
|
};
|
|
1273
|
-
|
|
1274
|
-
// src/client.ts
|
|
1275
|
-
var HydrousClient = class {
|
|
1276
|
-
constructor(config) {
|
|
1277
|
-
if (!config.url) throw new Error("[Hydrous] config.url is required");
|
|
1278
|
-
if (!config.apiKey) throw new Error("[Hydrous] config.apiKey is required");
|
|
1279
|
-
this.auth = new AuthClient(config);
|
|
1280
|
-
this.records = new RecordsClient(config);
|
|
1281
|
-
this.analytics = new AnalyticsClient(config);
|
|
1282
|
-
this.storage = new StorageClient(config);
|
|
1283
|
-
}
|
|
1284
|
-
};
|
|
1285
|
-
|
|
1286
|
-
// src/index.ts
|
|
1287
1626
|
function createClient(config) {
|
|
1288
1627
|
return new HydrousClient(config);
|
|
1289
1628
|
}
|
|
1290
1629
|
|
|
1291
|
-
|
|
1292
|
-
exports.AuthClient = AuthClient;
|
|
1293
|
-
exports.HydrousClient = HydrousClient;
|
|
1294
|
-
exports.HydrousSDKError = HydrousSDKError;
|
|
1295
|
-
exports.RecordsClient = RecordsClient;
|
|
1296
|
-
exports.StorageClient = StorageClient;
|
|
1297
|
-
exports.createClient = createClient;
|
|
1298
|
-
exports.eq = eq;
|
|
1299
|
-
exports.gt = gt;
|
|
1300
|
-
exports.inArray = inArray;
|
|
1301
|
-
exports.isHydrousError = isHydrousError;
|
|
1302
|
-
exports.lt = lt;
|
|
1303
|
-
exports.neq = neq;
|
|
1630
|
+
export { AnalyticsClient, AnalyticsError, AuthClient, AuthError, HydrousClient, HydrousError, NetworkError, RecordError, RecordsClient, ScopedStorage, StorageError, StorageManager, ValidationError, createClient };
|
|
1304
1631
|
//# sourceMappingURL=index.js.map
|
|
1305
1632
|
//# sourceMappingURL=index.js.map
|