hydrousdb 3.0.2 → 3.5.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/README.md +350 -975
- package/dist/index.cjs +942 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +643 -398
- package/dist/index.d.ts +643 -398
- package/dist/index.mjs +1581 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -6
- package/dist/index.js +0 -1179
- package/dist/index.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -49,7 +49,7 @@ var NetworkError = class extends HydrousError {
|
|
|
49
49
|
constructor(message, cause) {
|
|
50
50
|
super(message, "NETWORK_ERROR");
|
|
51
51
|
this.name = "NetworkError";
|
|
52
|
-
if (cause) this.cause = cause;
|
|
52
|
+
if (cause !== void 0) this.cause = cause;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
|
|
@@ -59,42 +59,23 @@ var HttpClient = class {
|
|
|
59
59
|
constructor(baseUrl) {
|
|
60
60
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
let resolvedOpts;
|
|
65
|
-
if (typeof apiKeyOrOpts === "string") {
|
|
66
|
-
apiKey = apiKeyOrOpts;
|
|
67
|
-
resolvedOpts = opts;
|
|
68
|
-
} else {
|
|
69
|
-
apiKey = void 0;
|
|
70
|
-
resolvedOpts = apiKeyOrOpts ?? opts;
|
|
71
|
-
}
|
|
72
|
-
const {
|
|
73
|
-
method = "GET",
|
|
74
|
-
body,
|
|
75
|
-
headers = {},
|
|
76
|
-
rawBody,
|
|
77
|
-
contentType = "application/json"
|
|
78
|
-
} = resolvedOpts;
|
|
62
|
+
// ─── Core request ──────────────────────────────────────────────────────────
|
|
63
|
+
async request(path, options) {
|
|
79
64
|
const url = `${this.baseUrl}${path}`;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (contentType) reqHeaders["Content-Type"] = contentType;
|
|
88
|
-
} else if (body !== void 0) {
|
|
89
|
-
reqBody = JSON.stringify(body);
|
|
90
|
-
reqHeaders["Content-Type"] = "application/json";
|
|
65
|
+
const headers = { ...options.headers };
|
|
66
|
+
let body;
|
|
67
|
+
if (options.rawBody !== void 0) {
|
|
68
|
+
body = options.rawBody;
|
|
69
|
+
} else if (options.body !== void 0) {
|
|
70
|
+
headers["Content-Type"] = "application/json";
|
|
71
|
+
body = JSON.stringify(options.body);
|
|
91
72
|
}
|
|
92
73
|
let response;
|
|
93
74
|
try {
|
|
94
75
|
response = await fetch(url, {
|
|
95
|
-
method,
|
|
96
|
-
headers
|
|
97
|
-
body
|
|
76
|
+
method: options.method,
|
|
77
|
+
headers,
|
|
78
|
+
body
|
|
98
79
|
});
|
|
99
80
|
} catch (err) {
|
|
100
81
|
throw new NetworkError(
|
|
@@ -102,184 +83,471 @@ var HttpClient = class {
|
|
|
102
83
|
err
|
|
103
84
|
);
|
|
104
85
|
}
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
86
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
87
|
+
if (!contentType.includes("application/json")) {
|
|
107
88
|
if (!response.ok) {
|
|
108
89
|
throw new HydrousError(
|
|
109
90
|
`Request failed with status ${response.status}`,
|
|
110
|
-
|
|
91
|
+
"REQUEST_FAILED",
|
|
111
92
|
response.status
|
|
112
93
|
);
|
|
113
94
|
}
|
|
114
|
-
|
|
115
|
-
return buffer;
|
|
95
|
+
return response.arrayBuffer();
|
|
116
96
|
}
|
|
117
|
-
let
|
|
97
|
+
let data;
|
|
118
98
|
try {
|
|
119
|
-
|
|
99
|
+
data = await response.json();
|
|
120
100
|
} catch {
|
|
121
|
-
throw new HydrousError(
|
|
122
|
-
"Failed to parse server response as JSON",
|
|
123
|
-
"PARSE_ERROR",
|
|
124
|
-
response.status
|
|
125
|
-
);
|
|
101
|
+
throw new HydrousError("Failed to parse response JSON", "PARSE_ERROR", response.status);
|
|
126
102
|
}
|
|
127
103
|
if (!response.ok) {
|
|
128
|
-
const errData = responseData;
|
|
129
104
|
throw new HydrousError(
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
data["error"] || data["message"] || `Request failed with status ${response.status}`,
|
|
106
|
+
data["code"] || "REQUEST_FAILED",
|
|
132
107
|
response.status,
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
data["requestId"],
|
|
109
|
+
data["details"]
|
|
135
110
|
);
|
|
136
111
|
}
|
|
137
|
-
return
|
|
112
|
+
return data;
|
|
138
113
|
}
|
|
139
|
-
|
|
140
|
-
|
|
114
|
+
// ─── Convenience wrappers ─────────────────────────────────────────────────
|
|
115
|
+
get(path, apiKey, extraHeaders) {
|
|
116
|
+
return this.request(path, {
|
|
117
|
+
method: "GET",
|
|
118
|
+
headers: apiKey ? { "X-Api-Key": apiKey, ...extraHeaders } : { ...extraHeaders }
|
|
119
|
+
});
|
|
141
120
|
}
|
|
142
|
-
post(path, apiKey, body
|
|
143
|
-
return this.request(path,
|
|
121
|
+
post(path, apiKey, body) {
|
|
122
|
+
return this.request(path, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
body,
|
|
125
|
+
headers: { "X-Api-Key": apiKey }
|
|
126
|
+
});
|
|
144
127
|
}
|
|
145
|
-
put(path, apiKey, body
|
|
146
|
-
return this.request(path,
|
|
128
|
+
put(path, apiKey, body) {
|
|
129
|
+
return this.request(path, {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
body,
|
|
132
|
+
headers: { "X-Api-Key": apiKey }
|
|
133
|
+
});
|
|
147
134
|
}
|
|
148
|
-
patch(path, apiKey, body
|
|
149
|
-
return this.request(path,
|
|
135
|
+
patch(path, apiKey, body) {
|
|
136
|
+
return this.request(path, {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
body,
|
|
139
|
+
headers: { "X-Api-Key": apiKey }
|
|
140
|
+
});
|
|
150
141
|
}
|
|
151
|
-
delete(path, apiKey, body
|
|
152
|
-
return this.request(path,
|
|
142
|
+
delete(path, apiKey, body) {
|
|
143
|
+
return this.request(path, {
|
|
144
|
+
method: "DELETE",
|
|
145
|
+
body,
|
|
146
|
+
headers: { "X-Api-Key": apiKey }
|
|
147
|
+
});
|
|
153
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* PUT directly to a signed GCS URL.
|
|
151
|
+
* Uses XHR in browsers for onprogress support; fetch in Node.
|
|
152
|
+
*/
|
|
154
153
|
async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
const xhr = new
|
|
154
|
+
const body = data instanceof Blob ? data : data instanceof Uint8Array ? data.buffer : data;
|
|
155
|
+
if (typeof XMLHttpRequest !== "undefined" && typeof onProgress === "function") {
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
const xhr = new XMLHttpRequest();
|
|
159
158
|
xhr.upload.onprogress = (e) => {
|
|
160
|
-
if (e.lengthComputable)
|
|
161
|
-
onProgress(Math.round(e.loaded / e.total * 100));
|
|
162
|
-
}
|
|
159
|
+
if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
|
|
163
160
|
};
|
|
164
|
-
xhr.onload = () => {
|
|
165
|
-
|
|
166
|
-
resolve();
|
|
167
|
-
} else {
|
|
168
|
-
reject(new Error(`Upload failed: ${xhr.status}`));
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
xhr.onerror = () => reject(new Error("Upload network error"));
|
|
161
|
+
xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`GCS upload failed: ${xhr.status}`));
|
|
162
|
+
xhr.onerror = () => reject(new Error("GCS upload network error"));
|
|
172
163
|
xhr.open("PUT", signedUrl);
|
|
173
164
|
xhr.setRequestHeader("Content-Type", mimeType);
|
|
174
|
-
|
|
175
|
-
xhr.send(payload);
|
|
165
|
+
xhr.send(body);
|
|
176
166
|
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
} else {
|
|
168
|
+
const res = await fetch(signedUrl, {
|
|
169
|
+
method: "PUT",
|
|
170
|
+
headers: { "Content-Type": mimeType },
|
|
171
|
+
body
|
|
172
|
+
});
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
throw new HydrousError(`GCS upload failed: ${res.status}`, "GCS_UPLOAD_FAILED", res.status);
|
|
175
|
+
}
|
|
186
176
|
}
|
|
187
177
|
}
|
|
188
178
|
};
|
|
189
179
|
|
|
180
|
+
// src/routes.ts
|
|
181
|
+
var RECORDS = {
|
|
182
|
+
/** GET|POST|PATCH|DELETE|HEAD /api/:bucketKey */
|
|
183
|
+
bucket: (bucketKey) => `/api/${bucketKey}`,
|
|
184
|
+
/** POST /api/:bucketKey/batch/insert */
|
|
185
|
+
batchInsert: (bucketKey) => `/api/${bucketKey}/batch/insert`,
|
|
186
|
+
/** POST /api/:bucketKey/batch/update */
|
|
187
|
+
batchUpdate: (bucketKey) => `/api/${bucketKey}/batch/update`,
|
|
188
|
+
/** POST /api/:bucketKey/batch/delete */
|
|
189
|
+
batchDelete: (bucketKey) => `/api/${bucketKey}/batch/delete`
|
|
190
|
+
};
|
|
191
|
+
var ANALYTICS = {
|
|
192
|
+
/** POST /api/analytics/:bucketKey */
|
|
193
|
+
query: (bucketKey) => `/api/analytics/${bucketKey}`
|
|
194
|
+
};
|
|
195
|
+
var AUTH = {
|
|
196
|
+
/** POST /api/auth/signup body: { email, password, fullName?, ...extra } */
|
|
197
|
+
signup: "/api/auth/signup",
|
|
198
|
+
/** POST /api/auth/signin body: { email, password } */
|
|
199
|
+
signin: "/api/auth/signin",
|
|
200
|
+
/** POST /api/auth/signout body: { sessionId, allDevices? } */
|
|
201
|
+
signout: "/api/auth/signout",
|
|
202
|
+
/** POST /api/auth/session/validate body: { sessionId } */
|
|
203
|
+
sessionValidate: "/api/auth/session/validate",
|
|
204
|
+
/** POST /api/auth/session/refresh body: { refreshToken } */
|
|
205
|
+
sessionRefresh: "/api/auth/session/refresh",
|
|
206
|
+
/** GET /api/auth/user?userId=... */
|
|
207
|
+
getUser: "/api/auth/user",
|
|
208
|
+
/** GET /api/auth/users?limit=&cursor= */
|
|
209
|
+
listUsers: "/api/auth/users",
|
|
210
|
+
/** PATCH /api/auth/user body: { sessionId, userId, updates: {...} } */
|
|
211
|
+
updateUser: "/api/auth/user",
|
|
212
|
+
/** DELETE /api/auth/user?userId=... body: { sessionId } */
|
|
213
|
+
deleteUser: "/api/auth/user",
|
|
214
|
+
/** DELETE /api/auth/user/hard?userId=... body: { sessionId } */
|
|
215
|
+
hardDeleteUser: "/api/auth/user/hard",
|
|
216
|
+
/** DELETE /api/auth/users/bulk body: { userIds, hard?, sessionId } */
|
|
217
|
+
bulkDeleteUsers: "/api/auth/users/bulk",
|
|
218
|
+
/** POST /api/auth/password/change body: { sessionId, userId, oldPassword, newPassword } */
|
|
219
|
+
passwordChange: "/api/auth/password/change",
|
|
220
|
+
/** POST /api/auth/password/reset/request body: { email } */
|
|
221
|
+
passwordResetRequest: "/api/auth/password/reset/request",
|
|
222
|
+
/** POST /api/auth/password/reset/confirm body: { resetToken, newPassword } */
|
|
223
|
+
passwordResetConfirm: "/api/auth/password/reset/confirm",
|
|
224
|
+
/** POST /api/auth/email/verify/request body: { userId } */
|
|
225
|
+
emailVerifyRequest: "/api/auth/email/verify/request",
|
|
226
|
+
/** POST /api/auth/email/verify/confirm body: { verifyToken } */
|
|
227
|
+
emailVerifyConfirm: "/api/auth/email/verify/confirm",
|
|
228
|
+
/** POST /api/auth/account/lock body: { sessionId, userId, duration? } */
|
|
229
|
+
accountLock: "/api/auth/account/lock",
|
|
230
|
+
/** POST /api/auth/account/unlock body: { sessionId, userId } */
|
|
231
|
+
accountUnlock: "/api/auth/account/unlock"
|
|
232
|
+
};
|
|
233
|
+
var STORAGE = {
|
|
234
|
+
/** GET /storage/info — no auth required */
|
|
235
|
+
info: "/storage/info",
|
|
236
|
+
/** GET /storage/public/:fullScopedPath — no auth required */
|
|
237
|
+
publicFile: (fullScopedPath) => `/storage/public/${fullScopedPath}`,
|
|
238
|
+
/** POST /storage/upload-url body: { path, mimeType, size, isPublic?, overwrite?, expiresIn? } */
|
|
239
|
+
uploadUrl: "/storage/upload-url",
|
|
240
|
+
/** POST /storage/batch-upload-urls body: { files: [...], expiresIn? } */
|
|
241
|
+
batchUploadUrls: "/storage/batch-upload-urls",
|
|
242
|
+
/** POST /storage/confirm body: { path, mimeType, isPublic? } */
|
|
243
|
+
confirm: "/storage/confirm",
|
|
244
|
+
/** POST /storage/batch-confirm body: { files: [...] } */
|
|
245
|
+
batchConfirm: "/storage/batch-confirm",
|
|
246
|
+
/** POST /storage/upload multipart/form-data: file, path, mimeType, isPublic, overwrite */
|
|
247
|
+
upload: "/storage/upload",
|
|
248
|
+
/** POST /storage/upload-raw body: { path, content, mimeType?, isPublic?, overwrite? } */
|
|
249
|
+
uploadRaw: "/storage/upload-raw",
|
|
250
|
+
/** GET /storage/list?prefix=&limit=&cursor= */
|
|
251
|
+
list: "/storage/list",
|
|
252
|
+
/** GET /storage/download/:path — requires X-Storage-Key */
|
|
253
|
+
download: (filePath) => `/storage/download/${filePath}`,
|
|
254
|
+
/** POST /storage/batch-download body: { paths: [...], concurrency? } */
|
|
255
|
+
batchDownload: "/storage/batch-download",
|
|
256
|
+
/** GET /storage/metadata/:path */
|
|
257
|
+
metadata: (filePath) => `/storage/metadata/${filePath}`,
|
|
258
|
+
/** POST /storage/signed-url body: { path, expiresIn? } */
|
|
259
|
+
signedUrl: "/storage/signed-url",
|
|
260
|
+
/** PATCH /storage/visibility body: { path, isPublic } */
|
|
261
|
+
visibility: "/storage/visibility",
|
|
262
|
+
/** POST /storage/folder body: { path } */
|
|
263
|
+
folder: "/storage/folder",
|
|
264
|
+
/** DELETE /storage/file body: { path } */
|
|
265
|
+
file: "/storage/file",
|
|
266
|
+
/** DELETE /storage/folder body: { path } */
|
|
267
|
+
folderDelete: "/storage/folder",
|
|
268
|
+
/** POST /storage/move body: { from, to } */
|
|
269
|
+
move: "/storage/move",
|
|
270
|
+
/** POST /storage/copy body: { from, to } */
|
|
271
|
+
copy: "/storage/copy",
|
|
272
|
+
/** GET /storage/stats */
|
|
273
|
+
stats: "/storage/stats"
|
|
274
|
+
};
|
|
275
|
+
|
|
190
276
|
// src/auth/client.ts
|
|
191
277
|
var AuthClient = class {
|
|
192
|
-
constructor(http, authKey
|
|
278
|
+
constructor(http, authKey) {
|
|
193
279
|
this.http = http;
|
|
194
280
|
this.authKey = authKey;
|
|
195
|
-
this.basePath = `/auth/${bucketKey}`;
|
|
196
281
|
}
|
|
197
282
|
post(path, body) {
|
|
198
283
|
return this.http.post(path, this.authKey, body);
|
|
199
284
|
}
|
|
200
|
-
get(path) {
|
|
201
|
-
|
|
285
|
+
get(path, query, extraHeaders) {
|
|
286
|
+
const qs = query && Object.keys(query).length ? "?" + new URLSearchParams(query).toString() : "";
|
|
287
|
+
return this.http.get(`${path}${qs}`, this.authKey, extraHeaders);
|
|
202
288
|
}
|
|
203
289
|
patch(path, body) {
|
|
204
290
|
return this.http.patch(path, this.authKey, body);
|
|
205
291
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
292
|
+
// ─── Signup / Login / Logout ──────────────────────────────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Register a new user account.
|
|
295
|
+
*
|
|
296
|
+
* Server: POST /api/auth/signup
|
|
297
|
+
* Body: { email, password, fullName?, ...extraData }
|
|
298
|
+
*/
|
|
210
299
|
async signup(options) {
|
|
211
|
-
const result = await this.post(
|
|
212
|
-
|
|
300
|
+
const result = await this.post(AUTH.signup, options);
|
|
301
|
+
const user = result.data;
|
|
302
|
+
return this._buildAuthResult(user, result.session);
|
|
213
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Sign in with email + password.
|
|
306
|
+
*
|
|
307
|
+
* Server: POST /api/auth/signin (NOT /login)
|
|
308
|
+
* Body: { email, password }
|
|
309
|
+
*/
|
|
214
310
|
async login(options) {
|
|
215
|
-
const result = await this.post(
|
|
216
|
-
|
|
311
|
+
const result = await this.post(AUTH.signin, options);
|
|
312
|
+
const user = result.data;
|
|
313
|
+
return this._buildAuthResult(user, result.session);
|
|
217
314
|
}
|
|
218
|
-
|
|
219
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Sign out — revoke a session (or all sessions with `allDevices: true`).
|
|
317
|
+
*
|
|
318
|
+
* Server: POST /api/auth/signout
|
|
319
|
+
* Body: { sessionId, allDevices? }
|
|
320
|
+
*/
|
|
321
|
+
async logout(options) {
|
|
322
|
+
await this.post(AUTH.signout, options);
|
|
220
323
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
324
|
+
// ─── Session Management ───────────────────────────────────────────────────
|
|
325
|
+
/**
|
|
326
|
+
* Validate an existing session and retrieve the current user.
|
|
327
|
+
*
|
|
328
|
+
* Server: POST /api/auth/session/validate
|
|
329
|
+
* Body: { sessionId }
|
|
330
|
+
*/
|
|
331
|
+
async validateSession(sessionId) {
|
|
332
|
+
const result = await this.post(
|
|
333
|
+
AUTH.sessionValidate,
|
|
334
|
+
{ sessionId }
|
|
335
|
+
);
|
|
336
|
+
return { user: result.data, session: result.session };
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Rotate a refresh token to get a new session.
|
|
340
|
+
*
|
|
341
|
+
* Server: POST /api/auth/session/refresh
|
|
342
|
+
* Body: { refreshToken }
|
|
343
|
+
*/
|
|
344
|
+
async refreshSession(refreshToken) {
|
|
345
|
+
const result = await this.post(
|
|
346
|
+
AUTH.sessionRefresh,
|
|
347
|
+
{ refreshToken }
|
|
348
|
+
);
|
|
349
|
+
const s = result.session;
|
|
350
|
+
return {
|
|
351
|
+
sessionId: s.sessionId,
|
|
352
|
+
userId: result.data?.id ?? "",
|
|
353
|
+
bucketId: "",
|
|
354
|
+
createdAt: Date.now(),
|
|
355
|
+
expiresAt: s.expiresAt,
|
|
356
|
+
refreshToken: s.refreshToken,
|
|
357
|
+
refreshExpiresAt: s.expiresAt
|
|
358
|
+
};
|
|
224
359
|
}
|
|
225
360
|
// ─── User Profile ─────────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Fetch a user by ID.
|
|
363
|
+
*
|
|
364
|
+
* Server: GET /api/auth/user?userId=:userId
|
|
365
|
+
*/
|
|
366
|
+
async getUser(userId) {
|
|
367
|
+
const result = await this.get(AUTH.getUser, { userId });
|
|
368
|
+
return result.data;
|
|
229
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Update a user's profile fields.
|
|
372
|
+
* Regular users can only update themselves; admins can update any user.
|
|
373
|
+
*
|
|
374
|
+
* Server: PATCH /api/auth/user
|
|
375
|
+
* Body: { sessionId, userId, updates: { ...fields } }
|
|
376
|
+
*/
|
|
230
377
|
async updateUser(options) {
|
|
231
|
-
const { sessionId, userId,
|
|
232
|
-
const result = await this.patch(
|
|
233
|
-
|
|
378
|
+
const { sessionId, userId, updates } = options;
|
|
379
|
+
const result = await this.patch(
|
|
380
|
+
AUTH.updateUser,
|
|
381
|
+
{ sessionId, userId, updates }
|
|
382
|
+
);
|
|
383
|
+
return result.data;
|
|
234
384
|
}
|
|
235
|
-
|
|
236
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Soft-delete a user account.
|
|
387
|
+
* Regular users can only delete themselves; admins can delete any user.
|
|
388
|
+
*
|
|
389
|
+
* Server: DELETE /api/auth/user?userId=:userId
|
|
390
|
+
* Body: { sessionId }
|
|
391
|
+
*/
|
|
392
|
+
async deleteUser(sessionId, userId) {
|
|
393
|
+
await this.http.request(
|
|
394
|
+
`${AUTH.deleteUser}?userId=${encodeURIComponent(userId)}`,
|
|
395
|
+
{
|
|
396
|
+
method: "DELETE",
|
|
397
|
+
body: { sessionId },
|
|
398
|
+
headers: { "X-Api-Key": this.authKey }
|
|
399
|
+
}
|
|
400
|
+
);
|
|
237
401
|
}
|
|
238
402
|
// ─── Admin Operations ─────────────────────────────────────────────────────
|
|
403
|
+
/**
|
|
404
|
+
* List all users (paginated). Admin only.
|
|
405
|
+
*
|
|
406
|
+
* Server: GET /api/auth/users?limit=&cursor=
|
|
407
|
+
* The sessionId is passed via the X-Session-Id header (GET body is unreliable).
|
|
408
|
+
*/
|
|
239
409
|
async listUsers(options) {
|
|
240
|
-
const { sessionId, limit = 50,
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
410
|
+
const { sessionId, limit = 50, cursor } = options;
|
|
411
|
+
const params = { limit: String(limit) };
|
|
412
|
+
if (cursor) params["cursor"] = cursor;
|
|
413
|
+
const result = await this.http.request(
|
|
414
|
+
`${AUTH.listUsers}?${new URLSearchParams(params)}`,
|
|
415
|
+
{
|
|
416
|
+
method: "GET",
|
|
417
|
+
headers: { "X-Api-Key": this.authKey, "X-Session-Id": sessionId }
|
|
418
|
+
}
|
|
244
419
|
);
|
|
245
|
-
return {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
420
|
+
return {
|
|
421
|
+
users: result.data ?? [],
|
|
422
|
+
hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
|
|
423
|
+
nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? null
|
|
424
|
+
};
|
|
249
425
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Permanently delete a user (hard delete — cannot be undone).
|
|
428
|
+
* Admin only. Charged at 1 HC per deletion.
|
|
429
|
+
*
|
|
430
|
+
* Server: DELETE /api/auth/user/hard?userId=:userId
|
|
431
|
+
* Body: { sessionId }
|
|
432
|
+
*/
|
|
433
|
+
async hardDeleteUser(sessionId, userId) {
|
|
434
|
+
await this.http.request(
|
|
435
|
+
`${AUTH.hardDeleteUser}?userId=${encodeURIComponent(userId)}`,
|
|
436
|
+
{
|
|
437
|
+
method: "DELETE",
|
|
438
|
+
body: { sessionId },
|
|
439
|
+
headers: { "X-Api-Key": this.authKey }
|
|
440
|
+
}
|
|
254
441
|
);
|
|
255
|
-
return { deleted: result.deleted, failed: result.failed };
|
|
256
442
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Delete multiple users at once (soft or hard). Admin only.
|
|
445
|
+
*
|
|
446
|
+
* Server: DELETE /api/auth/users/bulk
|
|
447
|
+
* Body: { userIds, hard?, sessionId }
|
|
448
|
+
*/
|
|
449
|
+
async bulkDeleteUsers(options) {
|
|
450
|
+
const result = await this.http.request(
|
|
451
|
+
AUTH.bulkDeleteUsers,
|
|
452
|
+
{
|
|
453
|
+
method: "DELETE",
|
|
454
|
+
body: {
|
|
455
|
+
userIds: options.userIds,
|
|
456
|
+
hard: options.hard ?? false,
|
|
457
|
+
sessionId: options.sessionId
|
|
458
|
+
},
|
|
459
|
+
headers: { "X-Api-Key": this.authKey }
|
|
460
|
+
}
|
|
261
461
|
);
|
|
462
|
+
return { succeeded: result.meta.succeeded, failed: result.meta.failed };
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Lock a user account for a specified duration. Admin only.
|
|
466
|
+
*
|
|
467
|
+
* Server: POST /api/auth/account/lock
|
|
468
|
+
* Body: { sessionId, userId, duration? } — duration in ms, default 15 min
|
|
469
|
+
*/
|
|
470
|
+
async lockAccount(options) {
|
|
471
|
+
const result = await this.post(AUTH.accountLock, options);
|
|
262
472
|
return result.data;
|
|
263
473
|
}
|
|
264
|
-
|
|
265
|
-
|
|
474
|
+
/**
|
|
475
|
+
* Unlock a locked user account. Admin only.
|
|
476
|
+
*
|
|
477
|
+
* Server: POST /api/auth/account/unlock
|
|
478
|
+
* Body: { sessionId, userId }
|
|
479
|
+
*/
|
|
480
|
+
async unlockAccount(sessionId, userId) {
|
|
481
|
+
await this.post(AUTH.accountUnlock, { sessionId, userId });
|
|
266
482
|
}
|
|
267
483
|
// ─── Password Management ──────────────────────────────────────────────────
|
|
484
|
+
/**
|
|
485
|
+
* Change a user's password. Requires both a valid session AND the old password.
|
|
486
|
+
*
|
|
487
|
+
* Server: POST /api/auth/password/change
|
|
488
|
+
* Body: { sessionId, userId, oldPassword, newPassword }
|
|
489
|
+
*/
|
|
268
490
|
async changePassword(options) {
|
|
269
|
-
await this.post(
|
|
491
|
+
await this.post(AUTH.passwordChange, {
|
|
492
|
+
sessionId: options.sessionId,
|
|
493
|
+
userId: options.userId,
|
|
494
|
+
oldPassword: options.currentPassword,
|
|
495
|
+
// server field is `oldPassword`
|
|
496
|
+
newPassword: options.newPassword
|
|
497
|
+
});
|
|
270
498
|
}
|
|
271
|
-
|
|
272
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Request a password reset email.
|
|
501
|
+
* Always returns success regardless of whether the email exists (prevents enumeration).
|
|
502
|
+
*
|
|
503
|
+
* Server: POST /api/auth/password/reset/request
|
|
504
|
+
* Body: { email }
|
|
505
|
+
*/
|
|
506
|
+
async requestPasswordReset(email) {
|
|
507
|
+
await this.post(AUTH.passwordResetRequest, { email });
|
|
273
508
|
}
|
|
274
|
-
|
|
275
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Confirm a password reset using the token from the reset email.
|
|
511
|
+
*
|
|
512
|
+
* Server: POST /api/auth/password/reset/confirm
|
|
513
|
+
* Body: { resetToken, newPassword }
|
|
514
|
+
*/
|
|
515
|
+
async confirmPasswordReset(resetToken, newPassword) {
|
|
516
|
+
await this.post(AUTH.passwordResetConfirm, { resetToken, newPassword });
|
|
276
517
|
}
|
|
277
518
|
// ─── Email Verification ───────────────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Request a verification email be sent to a user.
|
|
521
|
+
*
|
|
522
|
+
* Server: POST /api/auth/email/verify/request
|
|
523
|
+
* Body: { userId }
|
|
524
|
+
*/
|
|
525
|
+
async requestEmailVerification(userId) {
|
|
526
|
+
await this.post(AUTH.emailVerifyRequest, { userId });
|
|
280
527
|
}
|
|
281
|
-
|
|
282
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Confirm email ownership using the token from the verification email.
|
|
530
|
+
*
|
|
531
|
+
* Server: POST /api/auth/email/verify/confirm
|
|
532
|
+
* Body: { verifyToken }
|
|
533
|
+
*/
|
|
534
|
+
async confirmEmailVerification(verifyToken) {
|
|
535
|
+
await this.post(AUTH.emailVerifyConfirm, { verifyToken });
|
|
536
|
+
}
|
|
537
|
+
// ─── Private helpers ──────────────────────────────────────────────────────
|
|
538
|
+
_buildAuthResult(user, session) {
|
|
539
|
+
return {
|
|
540
|
+
user,
|
|
541
|
+
session: {
|
|
542
|
+
sessionId: session.sessionId,
|
|
543
|
+
userId: user.id,
|
|
544
|
+
bucketId: "",
|
|
545
|
+
createdAt: Date.now(),
|
|
546
|
+
expiresAt: session.expiresAt,
|
|
547
|
+
refreshToken: session.refreshToken,
|
|
548
|
+
refreshExpiresAt: session.expiresAt
|
|
549
|
+
}
|
|
550
|
+
};
|
|
283
551
|
}
|
|
284
552
|
};
|
|
285
553
|
|
|
@@ -288,18 +556,20 @@ function buildQueryParams(options = {}) {
|
|
|
288
556
|
const params = new URLSearchParams();
|
|
289
557
|
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
290
558
|
if (options.offset !== void 0) params.set("offset", String(options.offset));
|
|
291
|
-
if (options.orderBy !== void 0) params.set("
|
|
292
|
-
if (options.order !== void 0) params.set("
|
|
559
|
+
if (options.orderBy !== void 0) params.set("sortBy", options.orderBy);
|
|
560
|
+
if (options.order !== void 0) params.set("sortOrder", options.order);
|
|
293
561
|
if (options.fields !== void 0) params.set("fields", options.fields);
|
|
294
|
-
if (options.startAfter !== void 0) params.set("
|
|
295
|
-
if (options.startAt !== void 0) params.set("
|
|
296
|
-
if (options.endAt !== void 0) params.set("endAt", options.endAt);
|
|
562
|
+
if (options.startAfter !== void 0) params.set("cursor", options.startAfter);
|
|
563
|
+
if (options.startAt !== void 0) params.set("cursor", options.startAt);
|
|
297
564
|
if (options.dateRange?.start !== void 0)
|
|
298
565
|
params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
|
|
299
566
|
if (options.dateRange?.end !== void 0)
|
|
300
567
|
params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
|
|
301
568
|
if (options.filters && options.filters.length > 0) {
|
|
302
|
-
|
|
569
|
+
for (const f of options.filters) {
|
|
570
|
+
const key = f.op === "==" ? f.field : `${f.field}[${f.op}]`;
|
|
571
|
+
params.set(key, String(f.value));
|
|
572
|
+
}
|
|
303
573
|
}
|
|
304
574
|
const str = params.toString();
|
|
305
575
|
return str ? `?${str}` : "";
|
|
@@ -322,10 +592,13 @@ function guessMimeType(filename) {
|
|
|
322
592
|
html: "text/html",
|
|
323
593
|
css: "text/css",
|
|
324
594
|
js: "application/javascript",
|
|
595
|
+
ts: "application/typescript",
|
|
325
596
|
json: "application/json",
|
|
326
597
|
xml: "application/xml",
|
|
327
598
|
zip: "application/zip",
|
|
328
|
-
csv: "text/csv"
|
|
599
|
+
csv: "text/csv",
|
|
600
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
601
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
329
602
|
};
|
|
330
603
|
return map[ext ?? ""] ?? "application/octet-stream";
|
|
331
604
|
}
|
|
@@ -343,90 +616,228 @@ var RecordsClient = class {
|
|
|
343
616
|
assertSafeName(bucketKey, "bucketKey");
|
|
344
617
|
this.http = http;
|
|
345
618
|
this.bucketKey = bucketKey;
|
|
346
|
-
this.
|
|
347
|
-
this.basePath = `/records/${bucketKey}`;
|
|
348
|
-
}
|
|
349
|
-
get key() {
|
|
350
|
-
return this.bucketKey_;
|
|
619
|
+
this.apiKey = bucketSecurityKey;
|
|
351
620
|
}
|
|
352
621
|
// ─── Single Record Operations ─────────────────────────────────────────────
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
622
|
+
/**
|
|
623
|
+
* Create a new record (auto-generated ID) or upsert by `customRecordId`.
|
|
624
|
+
*
|
|
625
|
+
* Server: POST /api/:bucketKey
|
|
626
|
+
* Body: { values, queryableFields?, userEmail?, customRecordId? }
|
|
627
|
+
* Returns 201 for new records, 200 for upserts.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* const post = await posts.create(
|
|
632
|
+
* { title: 'Hello', status: 'draft', authorId: 'u1' },
|
|
633
|
+
* { queryableFields: ['status', 'authorId'], userEmail: 'alice@example.com' },
|
|
634
|
+
* );
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
async create(data, options = {}) {
|
|
638
|
+
const { queryableFields, userEmail, customRecordId } = options;
|
|
639
|
+
const body = { values: data };
|
|
640
|
+
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
641
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
642
|
+
if (customRecordId) body["customRecordId"] = customRecordId;
|
|
643
|
+
const result = await this.http.post(
|
|
644
|
+
RECORDS.bucket(this.bucketKey),
|
|
645
|
+
this.apiKey,
|
|
646
|
+
body
|
|
647
|
+
);
|
|
648
|
+
return result.data;
|
|
356
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Get a single record by ID.
|
|
652
|
+
*
|
|
653
|
+
* Server: GET /api/:bucketKey?recordId=:id
|
|
654
|
+
*/
|
|
357
655
|
async get(id) {
|
|
358
|
-
const result = await this.http.get(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return result.record ?? result.data;
|
|
656
|
+
const result = await this.http.get(
|
|
657
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
|
|
658
|
+
this.apiKey
|
|
659
|
+
);
|
|
660
|
+
return result.data;
|
|
364
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Partially update an existing record (PATCH semantics).
|
|
664
|
+
* Supports write-filter sentinels: `{ __op: 'increment', delta: 1 }` etc.
|
|
665
|
+
*
|
|
666
|
+
* Server: PATCH /api/:bucketKey
|
|
667
|
+
* Body: { recordId, values, userEmail?, track_record_history? }
|
|
668
|
+
*/
|
|
365
669
|
async patch(id, data, options = {}) {
|
|
366
|
-
const {
|
|
670
|
+
const { userEmail, trackHistory } = options;
|
|
671
|
+
const body = { recordId: id, values: data };
|
|
672
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
673
|
+
if (trackHistory) body["track_record_history"] = true;
|
|
367
674
|
const result = await this.http.patch(
|
|
368
|
-
|
|
369
|
-
this.
|
|
370
|
-
|
|
675
|
+
RECORDS.bucket(this.bucketKey),
|
|
676
|
+
this.apiKey,
|
|
677
|
+
body
|
|
371
678
|
);
|
|
372
|
-
return result.
|
|
679
|
+
return { id: result.meta?.id ?? id, updatedAt: result.meta?.updatedAt };
|
|
373
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Delete a record permanently.
|
|
683
|
+
*
|
|
684
|
+
* Server: DELETE /api/:bucketKey?recordId=:id
|
|
685
|
+
*/
|
|
374
686
|
async delete(id) {
|
|
375
|
-
await this.http.delete(
|
|
687
|
+
await this.http.delete(
|
|
688
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
|
|
689
|
+
this.apiKey
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Check whether a record exists (HEAD request — very lightweight).
|
|
694
|
+
*
|
|
695
|
+
* Server: HEAD /api/:bucketKey?recordId=:id
|
|
696
|
+
* Returns true if the record exists, false if 404.
|
|
697
|
+
*/
|
|
698
|
+
async exists(id) {
|
|
699
|
+
try {
|
|
700
|
+
await this.http.request(
|
|
701
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
|
|
702
|
+
{ method: "HEAD", headers: { "X-Api-Key": this.apiKey } }
|
|
703
|
+
);
|
|
704
|
+
return true;
|
|
705
|
+
} catch (err) {
|
|
706
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) return false;
|
|
707
|
+
throw err;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Get a historical snapshot of a record at a specific generation.
|
|
712
|
+
*
|
|
713
|
+
* Server: GET /api/:bucketKey?recordId=:id&generation=:gen
|
|
714
|
+
*/
|
|
715
|
+
async getVersion(id, generation) {
|
|
716
|
+
const result = await this.http.get(
|
|
717
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}&generation=${encodeURIComponent(String(generation))}`,
|
|
718
|
+
this.apiKey
|
|
719
|
+
);
|
|
720
|
+
return result.data;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Get the version history of a record.
|
|
724
|
+
*
|
|
725
|
+
* Server: GET /api/:bucketKey?recordId=:id&showHistory=true
|
|
726
|
+
*/
|
|
727
|
+
async getHistory(id) {
|
|
728
|
+
const result = await this.http.get(
|
|
729
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}&showHistory=true`,
|
|
730
|
+
this.apiKey
|
|
731
|
+
);
|
|
732
|
+
return result.history ?? [];
|
|
376
733
|
}
|
|
377
734
|
// ─── Batch Operations ─────────────────────────────────────────────────────
|
|
378
|
-
|
|
735
|
+
/**
|
|
736
|
+
* Create up to 500 records in one request.
|
|
737
|
+
* Each record may include `_customRecordId` for upsert behaviour.
|
|
738
|
+
*
|
|
739
|
+
* Server: POST /api/:bucketKey/batch/insert
|
|
740
|
+
* Body: { records, queryableFields?, userEmail? }
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* const results = await posts.batchCreate(
|
|
745
|
+
* [{ title: 'A' }, { title: 'B' }],
|
|
746
|
+
* { queryableFields: ['title'] },
|
|
747
|
+
* );
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
async batchCreate(items, options = {}) {
|
|
751
|
+
const { queryableFields, userEmail } = options;
|
|
752
|
+
const body = { records: items };
|
|
753
|
+
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
754
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
379
755
|
const result = await this.http.post(
|
|
380
|
-
|
|
381
|
-
this.
|
|
382
|
-
|
|
756
|
+
RECORDS.batchInsert(this.bucketKey),
|
|
757
|
+
this.apiKey,
|
|
758
|
+
body
|
|
383
759
|
);
|
|
384
|
-
return
|
|
760
|
+
return {
|
|
761
|
+
results: result.data ?? [],
|
|
762
|
+
errors: result.errors ?? [],
|
|
763
|
+
successful: result.meta?.successful ?? (result.data?.length ?? 0),
|
|
764
|
+
failed: result.meta?.failed ?? 0
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Update up to 500 records in one request.
|
|
769
|
+
*
|
|
770
|
+
* Server: POST /api/:bucketKey/batch/update
|
|
771
|
+
* Body: { updates: [{ recordId, values }], userEmail? }
|
|
772
|
+
*/
|
|
773
|
+
async batchUpdate(updates, userEmail) {
|
|
774
|
+
const body = { updates };
|
|
775
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
776
|
+
const result = await this.http.post(
|
|
777
|
+
RECORDS.batchUpdate(this.bucketKey),
|
|
778
|
+
this.apiKey,
|
|
779
|
+
body
|
|
780
|
+
);
|
|
781
|
+
return {
|
|
782
|
+
successful: result.data?.successful ?? result.meta?.count ?? 0,
|
|
783
|
+
failed: result.data?.failed ?? []
|
|
784
|
+
};
|
|
385
785
|
}
|
|
386
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Delete up to 500 records in one request.
|
|
788
|
+
*
|
|
789
|
+
* Server: POST /api/:bucketKey/batch/delete
|
|
790
|
+
* Body: { recordIds, userEmail? }
|
|
791
|
+
*/
|
|
792
|
+
async batchDelete(ids, userEmail) {
|
|
793
|
+
const body = { recordIds: ids };
|
|
794
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
387
795
|
const result = await this.http.post(
|
|
388
|
-
|
|
389
|
-
this.
|
|
390
|
-
|
|
796
|
+
RECORDS.batchDelete(this.bucketKey),
|
|
797
|
+
this.apiKey,
|
|
798
|
+
body
|
|
391
799
|
);
|
|
392
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
successful: result.data?.successful ?? result.meta?.count ?? 0,
|
|
802
|
+
failed: result.data?.failed ?? []
|
|
803
|
+
};
|
|
393
804
|
}
|
|
394
805
|
// ─── Querying ─────────────────────────────────────────────────────────────
|
|
806
|
+
/**
|
|
807
|
+
* Query records with filters, sorting, and cursor-based pagination.
|
|
808
|
+
*
|
|
809
|
+
* Server: GET /api/:bucketKey?field=value&field[op]=value&limit=&sortBy=&sortOrder=&cursor=
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* const { records, hasMore, nextCursor } = await posts.query({
|
|
814
|
+
* filters: [{ field: 'status', op: '==', value: 'published' }],
|
|
815
|
+
* orderBy: 'createdAt',
|
|
816
|
+
* order: 'desc',
|
|
817
|
+
* limit: 20,
|
|
818
|
+
* });
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
395
821
|
async query(options = {}) {
|
|
396
822
|
const qs = buildQueryParams(options);
|
|
397
|
-
const result = await this.http.get(
|
|
823
|
+
const result = await this.http.get(
|
|
824
|
+
`${RECORDS.bucket(this.bucketKey)}${qs}`,
|
|
825
|
+
this.apiKey
|
|
826
|
+
);
|
|
398
827
|
return {
|
|
399
|
-
records: result.
|
|
400
|
-
total: result.total,
|
|
401
|
-
hasMore: result.hasMore,
|
|
402
|
-
nextCursor: result.nextCursor
|
|
828
|
+
records: result.data ?? [],
|
|
829
|
+
total: result.meta?.total ?? result.total,
|
|
830
|
+
hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
|
|
831
|
+
nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? void 0
|
|
403
832
|
};
|
|
404
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* Retrieve all records matching options (no filter support — use `query()` for filters).
|
|
836
|
+
*/
|
|
405
837
|
async getAll(options = {}) {
|
|
406
838
|
const { records } = await this.query(options);
|
|
407
839
|
return records;
|
|
408
840
|
}
|
|
409
|
-
async count(filters = []) {
|
|
410
|
-
const result = await this.http.post(
|
|
411
|
-
`${this.basePath}/count`,
|
|
412
|
-
this.key,
|
|
413
|
-
{ filters }
|
|
414
|
-
);
|
|
415
|
-
return result.count;
|
|
416
|
-
}
|
|
417
|
-
// ─── Version History ──────────────────────────────────────────────────────
|
|
418
|
-
async getHistory(id) {
|
|
419
|
-
const result = await this.http.get(`${this.basePath}/${id}/history`, this.key);
|
|
420
|
-
return result.history;
|
|
421
|
-
}
|
|
422
|
-
async restoreVersion(id, version) {
|
|
423
|
-
const result = await this.http.post(
|
|
424
|
-
`${this.basePath}/${id}/restore`,
|
|
425
|
-
this.key,
|
|
426
|
-
{ version }
|
|
427
|
-
);
|
|
428
|
-
return result.record ?? result.data;
|
|
429
|
-
}
|
|
430
841
|
};
|
|
431
842
|
|
|
432
843
|
// src/analytics/client.ts
|
|
@@ -435,49 +846,73 @@ var AnalyticsClient = class {
|
|
|
435
846
|
assertSafeName(bucketKey, "bucketKey");
|
|
436
847
|
this.http = http;
|
|
437
848
|
this.bucketSecurityKey = bucketSecurityKey;
|
|
438
|
-
this.
|
|
849
|
+
this.bucketKey = bucketKey;
|
|
439
850
|
}
|
|
440
851
|
async run(query) {
|
|
441
852
|
const result = await this.http.post(
|
|
442
|
-
this.
|
|
853
|
+
ANALYTICS.query(this.bucketKey),
|
|
443
854
|
this.bucketSecurityKey,
|
|
444
855
|
query
|
|
445
856
|
);
|
|
446
857
|
return result.data;
|
|
447
858
|
}
|
|
859
|
+
// ─── Query methods ────────────────────────────────────────────────────────
|
|
860
|
+
/** Count all records, optionally within a date range. */
|
|
448
861
|
async count(opts = {}) {
|
|
449
862
|
return this.run({ queryType: "count", ...opts });
|
|
450
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Get value distribution for a field (e.g. records per status).
|
|
866
|
+
* `order` maps to `sortBy` on the wire (the server param name).
|
|
867
|
+
*/
|
|
451
868
|
async distribution(opts) {
|
|
452
869
|
assertSafeName(opts.field, "field");
|
|
453
870
|
return this.run({ queryType: "distribution", ...opts });
|
|
454
871
|
}
|
|
872
|
+
/** Sum a numeric field, optionally grouped by another field. */
|
|
455
873
|
async sum(opts) {
|
|
456
874
|
assertSafeName(opts.field, "field");
|
|
457
875
|
if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
|
|
458
876
|
return this.run({ queryType: "sum", ...opts });
|
|
459
877
|
}
|
|
878
|
+
/** Count of records over time, bucketed by granularity. */
|
|
460
879
|
async timeSeries(opts = {}) {
|
|
461
880
|
return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
|
|
462
881
|
}
|
|
882
|
+
/** Aggregate a numeric field over time. */
|
|
463
883
|
async fieldTimeSeries(opts) {
|
|
464
884
|
assertSafeName(opts.field, "field");
|
|
465
|
-
return this.run({
|
|
885
|
+
return this.run({
|
|
886
|
+
queryType: "fieldTimeSeries",
|
|
887
|
+
aggregation: "sum",
|
|
888
|
+
granularity: "day",
|
|
889
|
+
...opts
|
|
890
|
+
});
|
|
466
891
|
}
|
|
892
|
+
/** Top N values for a field by count. */
|
|
467
893
|
async topN(opts) {
|
|
468
894
|
assertSafeName(opts.field, "field");
|
|
469
895
|
if (opts.labelField) assertSafeName(opts.labelField, "labelField");
|
|
470
896
|
return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
|
|
471
897
|
}
|
|
898
|
+
/** Statistical summary (min, max, avg, sum, count, stddev) for a numeric field. */
|
|
472
899
|
async stats(opts) {
|
|
473
900
|
assertSafeName(opts.field, "field");
|
|
474
901
|
return this.run({ queryType: "stats", ...opts });
|
|
475
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Fetch filtered records via the analytics engine (BigQuery).
|
|
905
|
+
* Bypasses Firestore pagination — useful for large result sets.
|
|
906
|
+
*/
|
|
476
907
|
async records(opts = {}) {
|
|
477
908
|
if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
|
|
478
909
|
if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
|
|
479
910
|
return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
|
|
480
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Compute multiple aggregations in a single request.
|
|
914
|
+
* `metrics` must have valid `field` and `name` (used as BigQuery column aliases).
|
|
915
|
+
*/
|
|
481
916
|
async multiMetric(opts) {
|
|
482
917
|
opts.metrics.forEach((m) => {
|
|
483
918
|
assertSafeName(m.field, "metric.field");
|
|
@@ -485,14 +920,23 @@ var AnalyticsClient = class {
|
|
|
485
920
|
});
|
|
486
921
|
return this.run({ queryType: "multiMetric", ...opts });
|
|
487
922
|
}
|
|
923
|
+
/** Storage usage stats for the bucket (record count, bytes, avg/min/max size). */
|
|
488
924
|
async storageStats(opts = {}) {
|
|
489
925
|
return this.run({ queryType: "storageStats", ...opts });
|
|
490
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Compare a metric across multiple buckets in one query.
|
|
929
|
+
* The caller's key must have read access to EVERY bucket in `bucketKeys`.
|
|
930
|
+
* System buckets (`users`, `_sys_*`) are blocked server-side.
|
|
931
|
+
*/
|
|
491
932
|
async crossBucket(opts) {
|
|
492
933
|
assertSafeName(opts.field, "field");
|
|
493
934
|
opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
|
|
494
935
|
return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
|
|
495
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Raw query — escape hatch when the typed helpers don't cover your case.
|
|
939
|
+
*/
|
|
496
940
|
async query(query) {
|
|
497
941
|
const data = await this.run(query);
|
|
498
942
|
return { queryType: query.queryType, data };
|
|
@@ -500,34 +944,36 @@ var AnalyticsClient = class {
|
|
|
500
944
|
};
|
|
501
945
|
|
|
502
946
|
// src/storage/manager.ts
|
|
947
|
+
function toUploadResult(r) {
|
|
948
|
+
return {
|
|
949
|
+
path: r.path,
|
|
950
|
+
mimeType: r.mimeType,
|
|
951
|
+
size: r.size,
|
|
952
|
+
isPublic: r.isPublic,
|
|
953
|
+
publicUrl: r.publicUrl,
|
|
954
|
+
downloadUrl: r.downloadUrl
|
|
955
|
+
};
|
|
956
|
+
}
|
|
503
957
|
var StorageManager = class {
|
|
504
958
|
constructor(http, storageKey) {
|
|
505
|
-
this.basePath = "/storage";
|
|
506
959
|
this.http = http;
|
|
507
960
|
this.storageKey = storageKey;
|
|
508
961
|
}
|
|
509
|
-
/** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
|
|
510
962
|
get authHeaders() {
|
|
511
963
|
return { "X-Storage-Key": this.storageKey };
|
|
512
964
|
}
|
|
513
|
-
// ─── Upload:
|
|
965
|
+
// ─── Upload: Server-buffered ──────────────────────────────────────────────
|
|
514
966
|
/**
|
|
515
|
-
* Upload a file
|
|
516
|
-
* For files >10 MB or when
|
|
967
|
+
* Upload a file in one step (server-buffered, up to 500 MB).
|
|
968
|
+
* For files >10 MB or when upload progress tracking is needed, use the
|
|
969
|
+
* signed URL flow: `getUploadUrl()` → `uploadToSignedUrl()` → `confirmUpload()`.
|
|
517
970
|
*
|
|
518
|
-
*
|
|
519
|
-
* @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
|
|
520
|
-
* @param options Upload options: isPublic, overwrite, mimeType.
|
|
971
|
+
* Server: POST /storage/upload (multipart/form-data)
|
|
521
972
|
*
|
|
522
973
|
* @example
|
|
523
974
|
* ```ts
|
|
524
|
-
* // Upload a public avatar
|
|
525
975
|
* const result = await storage.upload(file, 'avatars/alice.jpg', { isPublic: true });
|
|
526
|
-
* console.log(result.publicUrl);
|
|
527
|
-
*
|
|
528
|
-
* // Upload a private document
|
|
529
|
-
* const result = await storage.upload(pdfBuffer, 'docs/contract.pdf');
|
|
530
|
-
* console.log(result.downloadUrl); // → /storage/download/docs/contract.pdf
|
|
976
|
+
* console.log(result.publicUrl);
|
|
531
977
|
* ```
|
|
532
978
|
*/
|
|
533
979
|
async upload(data, path, options = {}) {
|
|
@@ -540,214 +986,178 @@ var StorageManager = class {
|
|
|
540
986
|
formData.append("mimeType", mime);
|
|
541
987
|
formData.append("isPublic", String(isPublic));
|
|
542
988
|
formData.append("overwrite", String(overwrite));
|
|
543
|
-
const result = await this.http.request(
|
|
989
|
+
const result = await this.http.request(STORAGE.upload, {
|
|
544
990
|
method: "POST",
|
|
545
991
|
rawBody: formData,
|
|
546
992
|
headers: this.authHeaders
|
|
547
993
|
});
|
|
548
|
-
return
|
|
549
|
-
path: result.path,
|
|
550
|
-
mimeType: result.mimeType,
|
|
551
|
-
size: result.size,
|
|
552
|
-
isPublic: result.isPublic,
|
|
553
|
-
publicUrl: result.publicUrl,
|
|
554
|
-
downloadUrl: result.downloadUrl
|
|
555
|
-
};
|
|
994
|
+
return toUploadResult(result);
|
|
556
995
|
}
|
|
557
996
|
/**
|
|
558
|
-
* Upload raw
|
|
997
|
+
* Upload raw string or JSON data directly as a file.
|
|
998
|
+
*
|
|
999
|
+
* Server: POST /storage/upload-raw
|
|
1000
|
+
* Body: { path, content, mimeType?, isPublic?, overwrite? }
|
|
559
1001
|
*
|
|
560
1002
|
* @example
|
|
561
1003
|
* ```ts
|
|
562
|
-
*
|
|
563
|
-
* { config: { theme: 'dark' } },
|
|
564
|
-
* 'settings/user-config.json',
|
|
565
|
-
* { isPublic: false },
|
|
566
|
-
* );
|
|
1004
|
+
* await storage.uploadRaw({ theme: 'dark' }, 'settings/config.json');
|
|
567
1005
|
* ```
|
|
568
1006
|
*/
|
|
569
1007
|
async uploadRaw(data, path, options = {}) {
|
|
570
1008
|
const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
|
|
571
|
-
const
|
|
572
|
-
const result = await this.http.request(
|
|
1009
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
1010
|
+
const result = await this.http.request(STORAGE.uploadRaw, {
|
|
573
1011
|
method: "POST",
|
|
574
|
-
body: { path,
|
|
1012
|
+
body: { path, content, mimeType, isPublic, overwrite },
|
|
575
1013
|
headers: this.authHeaders
|
|
576
1014
|
});
|
|
577
|
-
return
|
|
578
|
-
path: result.path,
|
|
579
|
-
mimeType: result.mimeType,
|
|
580
|
-
size: result.size,
|
|
581
|
-
isPublic: result.isPublic,
|
|
582
|
-
publicUrl: result.publicUrl,
|
|
583
|
-
downloadUrl: result.downloadUrl
|
|
584
|
-
};
|
|
1015
|
+
return toUploadResult(result);
|
|
585
1016
|
}
|
|
586
|
-
// ─── Upload: Direct-to-GCS (recommended for large files)
|
|
1017
|
+
// ─── Upload: Direct-to-GCS (recommended for large files / progress) ───────
|
|
587
1018
|
/**
|
|
588
|
-
* Step 1
|
|
589
|
-
*
|
|
1019
|
+
* Step 1 — get a signed GCS PUT URL for direct client-to-GCS upload.
|
|
1020
|
+
*
|
|
1021
|
+
* Server: POST /storage/upload-url
|
|
1022
|
+
* Body: { path, mimeType, size, isPublic?, overwrite?, expiresIn? }
|
|
590
1023
|
*
|
|
591
1024
|
* @example
|
|
592
1025
|
* ```ts
|
|
593
|
-
* const { uploadUrl, path:
|
|
594
|
-
* path: 'videos/intro.mp4',
|
|
595
|
-
* mimeType: 'video/mp4',
|
|
596
|
-
* size: file.size,
|
|
597
|
-
* isPublic: true,
|
|
598
|
-
* });
|
|
599
|
-
*
|
|
600
|
-
* // Upload using XHR for progress tracking
|
|
601
|
-
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
|
|
602
|
-
* console.log(`${pct}% uploaded`);
|
|
1026
|
+
* const { uploadUrl, path: p } = await storage.getUploadUrl({
|
|
1027
|
+
* path: 'videos/intro.mp4', mimeType: 'video/mp4', size: file.size,
|
|
603
1028
|
* });
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
|
|
1029
|
+
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => setProgress(pct));
|
|
1030
|
+
* const result = await storage.confirmUpload({ path: p, mimeType: 'video/mp4' });
|
|
607
1031
|
* ```
|
|
608
1032
|
*/
|
|
609
1033
|
async getUploadUrl(opts) {
|
|
610
|
-
const
|
|
1034
|
+
const { expiresInSeconds, ...rest } = opts;
|
|
1035
|
+
const result = await this.http.request(STORAGE.uploadUrl, {
|
|
611
1036
|
method: "POST",
|
|
612
|
-
body: { isPublic: false, overwrite: false,
|
|
1037
|
+
body: { isPublic: false, overwrite: false, expiresIn: expiresInSeconds ?? 900, ...rest },
|
|
613
1038
|
headers: this.authHeaders
|
|
614
1039
|
});
|
|
615
|
-
return
|
|
1040
|
+
return {
|
|
1041
|
+
uploadUrl: result.uploadUrl,
|
|
1042
|
+
path: result.path,
|
|
1043
|
+
mimeType: result.mimeType,
|
|
1044
|
+
expiresAt: result.expiresAt,
|
|
1045
|
+
expiresIn: result.expiresIn
|
|
1046
|
+
};
|
|
616
1047
|
}
|
|
617
1048
|
/**
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* @param signedUrl The URL returned by `getUploadUrl()`.
|
|
622
|
-
* @param data File data.
|
|
623
|
-
* @param mimeType Must match what was used in `getUploadUrl()`.
|
|
624
|
-
* @param onProgress Optional callback called with 0–100 progress percentage.
|
|
1049
|
+
* Step 2 — upload data directly to the signed GCS URL.
|
|
1050
|
+
* Supports progress tracking in browser environments via XHR.
|
|
625
1051
|
*/
|
|
626
1052
|
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
627
1053
|
await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
628
1054
|
}
|
|
629
1055
|
/**
|
|
630
|
-
* Step 3
|
|
631
|
-
* Confirm a direct upload and register metadata on the server.
|
|
1056
|
+
* Step 3 — confirm a direct upload and register metadata server-side.
|
|
632
1057
|
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
* const result = await storage.confirmUpload({
|
|
636
|
-
* path: 'videos/intro.mp4',
|
|
637
|
-
* mimeType: 'video/mp4',
|
|
638
|
-
* isPublic: true,
|
|
639
|
-
* });
|
|
640
|
-
* console.log(result.publicUrl);
|
|
641
|
-
* ```
|
|
1058
|
+
* Server: POST /storage/confirm
|
|
1059
|
+
* Body: { path, mimeType, isPublic? }
|
|
642
1060
|
*/
|
|
643
1061
|
async confirmUpload(opts) {
|
|
644
|
-
const result = await this.http.request(
|
|
1062
|
+
const result = await this.http.request(STORAGE.confirm, {
|
|
645
1063
|
method: "POST",
|
|
646
1064
|
body: { isPublic: false, ...opts },
|
|
647
1065
|
headers: this.authHeaders
|
|
648
1066
|
});
|
|
649
|
-
return
|
|
650
|
-
path: result.path,
|
|
651
|
-
mimeType: result.mimeType,
|
|
652
|
-
size: result.size,
|
|
653
|
-
isPublic: result.isPublic,
|
|
654
|
-
publicUrl: result.publicUrl,
|
|
655
|
-
downloadUrl: result.downloadUrl
|
|
656
|
-
};
|
|
1067
|
+
return toUploadResult(result);
|
|
657
1068
|
}
|
|
658
1069
|
// ─── Batch Upload ─────────────────────────────────────────────────────────
|
|
659
1070
|
/**
|
|
660
|
-
* Get signed upload URLs for
|
|
1071
|
+
* Get signed upload URLs for up to 50 files at once.
|
|
661
1072
|
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* const { files } = await storage.getBatchUploadUrls([
|
|
665
|
-
* { path: 'images/photo1.jpg', mimeType: 'image/jpeg', size: 204800 },
|
|
666
|
-
* { path: 'images/photo2.jpg', mimeType: 'image/jpeg', size: 153600 },
|
|
667
|
-
* ]);
|
|
668
|
-
*
|
|
669
|
-
* // Upload each file and confirm
|
|
670
|
-
* for (const f of files) {
|
|
671
|
-
* await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
672
|
-
* await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
|
|
673
|
-
* }
|
|
674
|
-
* ```
|
|
1073
|
+
* Server: POST /storage/batch-upload-urls
|
|
1074
|
+
* Body: { files: [...], expiresIn? }
|
|
675
1075
|
*/
|
|
676
1076
|
async getBatchUploadUrls(files) {
|
|
677
|
-
const result = await this.http.request(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1077
|
+
const result = await this.http.request(STORAGE.batchUploadUrls, {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
body: { files },
|
|
1080
|
+
headers: this.authHeaders
|
|
1081
|
+
});
|
|
1082
|
+
return {
|
|
1083
|
+
files: result.succeeded.map((f) => ({
|
|
1084
|
+
index: f.index,
|
|
1085
|
+
uploadUrl: f.uploadUrl,
|
|
1086
|
+
path: f.path,
|
|
1087
|
+
mimeType: f.mimeType,
|
|
1088
|
+
expiresAt: f.expiresAt,
|
|
1089
|
+
expiresIn: f.expiresIn
|
|
1090
|
+
}))
|
|
1091
|
+
};
|
|
682
1092
|
}
|
|
683
1093
|
/**
|
|
684
1094
|
* Confirm multiple direct uploads at once.
|
|
1095
|
+
*
|
|
1096
|
+
* Server: POST /storage/batch-confirm
|
|
1097
|
+
* Body: { files: [{ path, mimeType, isPublic? }] }
|
|
685
1098
|
*/
|
|
686
1099
|
async batchConfirmUploads(items) {
|
|
687
|
-
const result = await this.http.request(
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
publicUrl: r.publicUrl,
|
|
697
|
-
downloadUrl: r.downloadUrl
|
|
698
|
-
}));
|
|
1100
|
+
const result = await this.http.request(STORAGE.batchConfirm, {
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
body: { files: items },
|
|
1103
|
+
headers: this.authHeaders
|
|
1104
|
+
});
|
|
1105
|
+
return {
|
|
1106
|
+
succeeded: result.succeeded.map(toUploadResult),
|
|
1107
|
+
failed: result.failed
|
|
1108
|
+
};
|
|
699
1109
|
}
|
|
700
1110
|
// ─── Download ─────────────────────────────────────────────────────────────
|
|
701
1111
|
/**
|
|
702
1112
|
* Download a private file as an ArrayBuffer.
|
|
703
1113
|
* For public files, use the `publicUrl` directly — no SDK needed.
|
|
704
1114
|
*
|
|
705
|
-
*
|
|
706
|
-
* ```ts
|
|
707
|
-
* const buffer = await storage.download('docs/contract.pdf');
|
|
708
|
-
* const blob = new Blob([buffer], { type: 'application/pdf' });
|
|
709
|
-
* // Open in browser:
|
|
710
|
-
* window.open(URL.createObjectURL(blob));
|
|
711
|
-
* ```
|
|
1115
|
+
* Server: GET /storage/download/:path (requires X-Storage-Key)
|
|
712
1116
|
*/
|
|
713
1117
|
async download(path) {
|
|
714
1118
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
715
|
-
return this.http.request(
|
|
1119
|
+
return this.http.request(STORAGE.download(encoded), {
|
|
716
1120
|
method: "GET",
|
|
717
1121
|
headers: this.authHeaders
|
|
718
1122
|
});
|
|
719
1123
|
}
|
|
720
1124
|
/**
|
|
721
|
-
* Download
|
|
1125
|
+
* Download up to 20 files at once. Returns base64-encoded content.
|
|
722
1126
|
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* const files = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
|
|
726
|
-
* ```
|
|
1127
|
+
* Server: POST /storage/batch-download
|
|
1128
|
+
* Body: { paths, concurrency? }
|
|
727
1129
|
*/
|
|
728
|
-
async batchDownload(paths) {
|
|
729
|
-
const result = await this.http.request(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1130
|
+
async batchDownload(paths, concurrency = 5) {
|
|
1131
|
+
const result = await this.http.request(STORAGE.batchDownload, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
body: { paths, concurrency },
|
|
1134
|
+
headers: this.authHeaders
|
|
1135
|
+
});
|
|
1136
|
+
return {
|
|
1137
|
+
succeeded: result.succeeded.map((f) => ({
|
|
1138
|
+
index: f.index,
|
|
1139
|
+
path: f.path,
|
|
1140
|
+
mimeType: f.mimeType,
|
|
1141
|
+
size: f.size,
|
|
1142
|
+
content: f.content
|
|
1143
|
+
})),
|
|
1144
|
+
failed: result.failed.map((f) => ({
|
|
1145
|
+
index: f.index,
|
|
1146
|
+
path: f.path,
|
|
1147
|
+
error: f.error,
|
|
1148
|
+
code: f.code
|
|
1149
|
+
}))
|
|
1150
|
+
};
|
|
734
1151
|
}
|
|
735
1152
|
// ─── List ─────────────────────────────────────────────────────────────────
|
|
736
1153
|
/**
|
|
737
1154
|
* List files and folders at a given path prefix.
|
|
738
1155
|
*
|
|
1156
|
+
* Server: GET /storage/list?prefix=&limit=&cursor=
|
|
1157
|
+
*
|
|
739
1158
|
* @example
|
|
740
1159
|
* ```ts
|
|
741
|
-
*
|
|
742
|
-
* const { files, folders } = await storage.list();
|
|
743
|
-
*
|
|
744
|
-
* // List a specific folder
|
|
745
|
-
* const { files, folders, hasMore, nextCursor } = await storage.list({
|
|
746
|
-
* prefix: 'avatars/',
|
|
747
|
-
* limit: 20,
|
|
748
|
-
* });
|
|
749
|
-
*
|
|
750
|
-
* // Next page
|
|
1160
|
+
* const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/', limit: 20 });
|
|
751
1161
|
* const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
|
|
752
1162
|
* ```
|
|
753
1163
|
*/
|
|
@@ -756,35 +1166,44 @@ var StorageManager = class {
|
|
|
756
1166
|
if (opts.prefix) params.set("prefix", opts.prefix);
|
|
757
1167
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
758
1168
|
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
759
|
-
if (opts.recursive) params.set("recursive", "true");
|
|
760
1169
|
const qs = params.toString() ? `?${params}` : "";
|
|
761
|
-
const result = await this.http.request(`${
|
|
1170
|
+
const result = await this.http.request(`${STORAGE.list}${qs}`, {
|
|
762
1171
|
method: "GET",
|
|
763
1172
|
headers: this.authHeaders
|
|
764
1173
|
});
|
|
1174
|
+
if (result.items !== void 0) {
|
|
1175
|
+
const files = [];
|
|
1176
|
+
const folders = [];
|
|
1177
|
+
for (const item of result.items) {
|
|
1178
|
+
if (item.type === "folder") folders.push(item.path);
|
|
1179
|
+
else files.push(item);
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
files,
|
|
1183
|
+
folders,
|
|
1184
|
+
hasMore: result.pagination?.hasMore ?? false,
|
|
1185
|
+
nextCursor: result.pagination?.nextCursor ?? void 0
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
765
1188
|
return {
|
|
766
|
-
files: result.files,
|
|
767
|
-
folders: result.folders,
|
|
768
|
-
hasMore: result.hasMore,
|
|
769
|
-
nextCursor: result.nextCursor
|
|
1189
|
+
files: result.files ?? [],
|
|
1190
|
+
folders: result.folders ?? [],
|
|
1191
|
+
hasMore: result.hasMore ?? false,
|
|
1192
|
+
nextCursor: result.nextCursor ?? void 0
|
|
770
1193
|
};
|
|
771
1194
|
}
|
|
772
1195
|
// ─── Metadata ─────────────────────────────────────────────────────────────
|
|
773
1196
|
/**
|
|
774
|
-
* Get metadata
|
|
1197
|
+
* Get file metadata: size, MIME type, visibility, URLs.
|
|
775
1198
|
*
|
|
776
|
-
*
|
|
777
|
-
* ```ts
|
|
778
|
-
* const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
779
|
-
* console.log(meta.size, meta.isPublic, meta.publicUrl);
|
|
780
|
-
* ```
|
|
1199
|
+
* Server: GET /storage/metadata/:path
|
|
781
1200
|
*/
|
|
782
1201
|
async getMetadata(path) {
|
|
783
1202
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
784
|
-
const result = await this.http.request(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
);
|
|
1203
|
+
const result = await this.http.request(STORAGE.metadata(encoded), {
|
|
1204
|
+
method: "GET",
|
|
1205
|
+
headers: this.authHeaders
|
|
1206
|
+
});
|
|
788
1207
|
return {
|
|
789
1208
|
path: result.path,
|
|
790
1209
|
size: result.size,
|
|
@@ -796,25 +1215,21 @@ var StorageManager = class {
|
|
|
796
1215
|
updatedAt: result.updatedAt
|
|
797
1216
|
};
|
|
798
1217
|
}
|
|
799
|
-
// ─── Signed URL
|
|
1218
|
+
// ─── Signed URL ───────────────────────────────────────────────────────────
|
|
800
1219
|
/**
|
|
801
1220
|
* Generate a time-limited download URL for a private file.
|
|
802
|
-
*
|
|
1221
|
+
* Can be shared externally — no X-Storage-Key required to access.
|
|
803
1222
|
*
|
|
804
|
-
*
|
|
805
|
-
* > are NOT tracked. Use `downloadUrl` for tracked downloads.
|
|
1223
|
+
* Note: Downloads via signed URLs bypass the server — stats are NOT tracked.
|
|
806
1224
|
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
1225
|
+
* Server: POST /storage/signed-url
|
|
1226
|
+
* Body: { path, expiresIn? }
|
|
809
1227
|
*
|
|
810
|
-
* @
|
|
811
|
-
*
|
|
812
|
-
* const { signedUrl, expiresAt } = await storage.getSignedUrl('docs/invoice.pdf', 1800);
|
|
813
|
-
* // Share signedUrl with the recipient — it expires in 30 minutes.
|
|
814
|
-
* ```
|
|
1228
|
+
* @param path File path.
|
|
1229
|
+
* @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
|
|
815
1230
|
*/
|
|
816
1231
|
async getSignedUrl(path, expiresIn = 3600) {
|
|
817
|
-
const result = await this.http.request(
|
|
1232
|
+
const result = await this.http.request(STORAGE.signedUrl, {
|
|
818
1233
|
method: "POST",
|
|
819
1234
|
body: { path, expiresIn },
|
|
820
1235
|
headers: this.authHeaders
|
|
@@ -828,21 +1243,13 @@ var StorageManager = class {
|
|
|
828
1243
|
}
|
|
829
1244
|
// ─── Visibility ───────────────────────────────────────────────────────────
|
|
830
1245
|
/**
|
|
831
|
-
* Change a file's visibility between public and private
|
|
832
|
-
*
|
|
833
|
-
* @example
|
|
834
|
-
* ```ts
|
|
835
|
-
* // Make a file public
|
|
836
|
-
* const result = await storage.setVisibility('avatars/alice.jpg', true);
|
|
837
|
-
* console.log(result.publicUrl); // CDN URL
|
|
1246
|
+
* Change a file's visibility between public and private.
|
|
838
1247
|
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* console.log(result.downloadUrl); // Auth-required URL
|
|
842
|
-
* ```
|
|
1248
|
+
* Server: PATCH /storage/visibility
|
|
1249
|
+
* Body: { path, isPublic }
|
|
843
1250
|
*/
|
|
844
1251
|
async setVisibility(path, isPublic) {
|
|
845
|
-
const result = await this.http.request(
|
|
1252
|
+
const result = await this.http.request(STORAGE.visibility, {
|
|
846
1253
|
method: "PATCH",
|
|
847
1254
|
body: { path, isPublic },
|
|
848
1255
|
headers: this.authHeaders
|
|
@@ -856,113 +1263,96 @@ var StorageManager = class {
|
|
|
856
1263
|
}
|
|
857
1264
|
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
858
1265
|
/**
|
|
859
|
-
* Create a folder (
|
|
1266
|
+
* Create a folder (empty GCS object used as a prefix marker).
|
|
860
1267
|
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
* await storage.createFolder('uploads/2025/');
|
|
864
|
-
* ```
|
|
1268
|
+
* Server: POST /storage/folder
|
|
1269
|
+
* Body: { path }
|
|
865
1270
|
*/
|
|
866
1271
|
async createFolder(path) {
|
|
867
|
-
const result = await this.http.request(
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1272
|
+
const result = await this.http.request(STORAGE.folder, {
|
|
1273
|
+
method: "POST",
|
|
1274
|
+
body: { path },
|
|
1275
|
+
headers: this.authHeaders
|
|
1276
|
+
});
|
|
871
1277
|
return { path: result.path };
|
|
872
1278
|
}
|
|
873
1279
|
// ─── File Operations ──────────────────────────────────────────────────────
|
|
874
1280
|
/**
|
|
875
|
-
*
|
|
1281
|
+
* Permanently delete a file.
|
|
876
1282
|
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* await storage.deleteFile('avatars/old-avatar.jpg');
|
|
880
|
-
* ```
|
|
1283
|
+
* Server: DELETE /storage/file
|
|
1284
|
+
* Body: { path }
|
|
881
1285
|
*/
|
|
882
1286
|
async deleteFile(path) {
|
|
883
|
-
await this.http.request(
|
|
1287
|
+
await this.http.request(STORAGE.file, {
|
|
884
1288
|
method: "DELETE",
|
|
885
1289
|
body: { path },
|
|
886
1290
|
headers: this.authHeaders
|
|
887
1291
|
});
|
|
888
1292
|
}
|
|
889
1293
|
/**
|
|
890
|
-
*
|
|
1294
|
+
* Recursively delete a folder and all its contents.
|
|
891
1295
|
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* await storage.deleteFolder('temp/');
|
|
895
|
-
* ```
|
|
1296
|
+
* Server: DELETE /storage/folder
|
|
1297
|
+
* Body: { path }
|
|
896
1298
|
*/
|
|
897
1299
|
async deleteFolder(path) {
|
|
898
|
-
await this.http.request(
|
|
1300
|
+
await this.http.request(STORAGE.folderDelete, {
|
|
899
1301
|
method: "DELETE",
|
|
900
1302
|
body: { path },
|
|
901
1303
|
headers: this.authHeaders
|
|
902
1304
|
});
|
|
903
1305
|
}
|
|
904
1306
|
/**
|
|
905
|
-
* Move
|
|
1307
|
+
* Move (rename) a file.
|
|
906
1308
|
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* // Rename
|
|
910
|
-
* await storage.move('docs/draft.pdf', 'docs/final.pdf');
|
|
911
|
-
* // Move to a different folder
|
|
912
|
-
* await storage.move('inbox/report.xlsx', 'archive/2025/report.xlsx');
|
|
913
|
-
* ```
|
|
1309
|
+
* Server: POST /storage/move
|
|
1310
|
+
* Body: { from, to }
|
|
914
1311
|
*/
|
|
915
1312
|
async move(from, to) {
|
|
916
|
-
const result = await this.http.request(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1313
|
+
const result = await this.http.request(STORAGE.move, {
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
body: { from, to },
|
|
1316
|
+
headers: this.authHeaders
|
|
1317
|
+
});
|
|
920
1318
|
return { from: result.from, to: result.to };
|
|
921
1319
|
}
|
|
922
1320
|
/**
|
|
923
1321
|
* Copy a file to a new path.
|
|
924
1322
|
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
* await storage.copy('templates/base.html', 'sites/my-site/index.html');
|
|
928
|
-
* ```
|
|
1323
|
+
* Server: POST /storage/copy
|
|
1324
|
+
* Body: { from, to }
|
|
929
1325
|
*/
|
|
930
1326
|
async copy(from, to) {
|
|
931
|
-
const result = await this.http.request(
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1327
|
+
const result = await this.http.request(STORAGE.copy, {
|
|
1328
|
+
method: "POST",
|
|
1329
|
+
body: { from, to },
|
|
1330
|
+
headers: this.authHeaders
|
|
1331
|
+
});
|
|
935
1332
|
return { from: result.from, to: result.to };
|
|
936
1333
|
}
|
|
937
1334
|
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
938
1335
|
/**
|
|
939
|
-
* Get
|
|
1336
|
+
* Get usage statistics for this storage key.
|
|
940
1337
|
*
|
|
941
|
-
*
|
|
942
|
-
* ```ts
|
|
943
|
-
* const stats = await storage.getStats();
|
|
944
|
-
* console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
|
|
945
|
-
* ```
|
|
1338
|
+
* Server: GET /storage/stats
|
|
946
1339
|
*/
|
|
947
1340
|
async getStats() {
|
|
948
|
-
const result = await this.http.request(
|
|
1341
|
+
const result = await this.http.request(STORAGE.stats, {
|
|
949
1342
|
method: "GET",
|
|
950
1343
|
headers: this.authHeaders
|
|
951
1344
|
});
|
|
952
1345
|
return result.stats;
|
|
953
1346
|
}
|
|
954
|
-
// ─── Info (no auth) ───────────────────────────────────────────────────────
|
|
955
1347
|
/**
|
|
956
|
-
*
|
|
1348
|
+
* Get server info (no auth required).
|
|
957
1349
|
*
|
|
958
|
-
*
|
|
959
|
-
* ```ts
|
|
960
|
-
* const info = await storage.info();
|
|
961
|
-
* // → { ok: true, storageRoot: 'hydrous-storage' }
|
|
962
|
-
* ```
|
|
1350
|
+
* Server: GET /storage/info
|
|
963
1351
|
*/
|
|
964
1352
|
async info() {
|
|
965
|
-
return this.http.
|
|
1353
|
+
return this.http.request(STORAGE.info, {
|
|
1354
|
+
method: "GET"
|
|
1355
|
+
});
|
|
966
1356
|
}
|
|
967
1357
|
};
|
|
968
1358
|
|
|
@@ -970,85 +1360,106 @@ var StorageManager = class {
|
|
|
970
1360
|
var ScopedStorage = class _ScopedStorage {
|
|
971
1361
|
constructor(manager, prefix) {
|
|
972
1362
|
this.manager = manager;
|
|
973
|
-
this.prefix = prefix.
|
|
1363
|
+
this.prefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
974
1364
|
}
|
|
975
|
-
|
|
976
|
-
return `${this.prefix}${
|
|
1365
|
+
p(path) {
|
|
1366
|
+
return `${this.prefix}${path.replace(/^\/+/, "")}`;
|
|
977
1367
|
}
|
|
978
|
-
|
|
979
|
-
upload(data, path, options) {
|
|
980
|
-
return this.manager.upload(data, this.
|
|
1368
|
+
// ─── Upload ───────────────────────────────────────────────────────────────
|
|
1369
|
+
async upload(data, path, options = {}) {
|
|
1370
|
+
return this.manager.upload(data, this.p(path), options);
|
|
981
1371
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
return this.manager.uploadRaw(data, this.scopedPath(path), options);
|
|
1372
|
+
async uploadRaw(data, path, options = {}) {
|
|
1373
|
+
return this.manager.uploadRaw(data, this.p(path), options);
|
|
985
1374
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1375
|
+
async getUploadUrl(opts) {
|
|
1376
|
+
return this.manager.getUploadUrl({ ...opts, path: this.p(opts.path) });
|
|
1377
|
+
}
|
|
1378
|
+
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
1379
|
+
return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
1380
|
+
}
|
|
1381
|
+
async confirmUpload(opts) {
|
|
1382
|
+
return this.manager.confirmUpload({ ...opts, path: this.p(opts.path) });
|
|
1383
|
+
}
|
|
1384
|
+
async getBatchUploadUrls(files) {
|
|
1385
|
+
return this.manager.getBatchUploadUrls(
|
|
1386
|
+
files.map((f) => ({ ...f, path: this.p(f.path) }))
|
|
1387
|
+
);
|
|
989
1388
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1389
|
+
async batchConfirmUploads(items) {
|
|
1390
|
+
return this.manager.batchConfirmUploads(
|
|
1391
|
+
items.map((i) => ({ ...i, path: this.p(i.path) }))
|
|
1392
|
+
);
|
|
993
1393
|
}
|
|
994
|
-
|
|
995
|
-
download(path) {
|
|
996
|
-
return this.manager.download(this.
|
|
1394
|
+
// ─── Download ─────────────────────────────────────────────────────────────
|
|
1395
|
+
async download(path) {
|
|
1396
|
+
return this.manager.download(this.p(path));
|
|
997
1397
|
}
|
|
1398
|
+
async batchDownload(paths) {
|
|
1399
|
+
return this.manager.batchDownload(paths.map((p) => this.p(p)));
|
|
1400
|
+
}
|
|
1401
|
+
// ─── List ─────────────────────────────────────────────────────────────────
|
|
998
1402
|
/**
|
|
999
|
-
* List files within
|
|
1000
|
-
* `prefix`
|
|
1403
|
+
* List files within this scope.
|
|
1404
|
+
* The `prefix` option is relative to the scope root.
|
|
1405
|
+
*
|
|
1406
|
+
* @example
|
|
1407
|
+
* ```ts
|
|
1408
|
+
* const userDocs = storage.scope('users/alice/');
|
|
1409
|
+
* // Lists users/alice/docs/
|
|
1410
|
+
* const { files } = await userDocs.list({ prefix: 'docs/' });
|
|
1411
|
+
* ```
|
|
1001
1412
|
*/
|
|
1002
|
-
list(opts = {}) {
|
|
1003
|
-
|
|
1413
|
+
async list(opts = {}) {
|
|
1414
|
+
return this.manager.list({
|
|
1004
1415
|
...opts,
|
|
1005
|
-
prefix: this.
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1416
|
+
prefix: this.p(opts.prefix ?? "")
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
// ─── Metadata & URLs ──────────────────────────────────────────────────────
|
|
1420
|
+
async getMetadata(path) {
|
|
1421
|
+
return this.manager.getMetadata(this.p(path));
|
|
1008
1422
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return this.manager.getMetadata(this.scopedPath(path));
|
|
1423
|
+
async getSignedUrl(path, expiresIn = 3600) {
|
|
1424
|
+
return this.manager.getSignedUrl(this.p(path), expiresIn);
|
|
1012
1425
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
|
|
1426
|
+
async setVisibility(path, isPublic) {
|
|
1427
|
+
return this.manager.setVisibility(this.p(path), isPublic);
|
|
1016
1428
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return this.manager.
|
|
1429
|
+
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
1430
|
+
async createFolder(path) {
|
|
1431
|
+
return this.manager.createFolder(this.p(path));
|
|
1020
1432
|
}
|
|
1021
|
-
|
|
1022
|
-
deleteFile(path) {
|
|
1023
|
-
return this.manager.deleteFile(this.
|
|
1433
|
+
// ─── File Operations ──────────────────────────────────────────────────────
|
|
1434
|
+
async deleteFile(path) {
|
|
1435
|
+
return this.manager.deleteFile(this.p(path));
|
|
1024
1436
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
return this.manager.deleteFolder(this.scopedPath(path));
|
|
1437
|
+
async deleteFolder(path) {
|
|
1438
|
+
return this.manager.deleteFolder(this.p(path));
|
|
1028
1439
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
return this.manager.move(this.scopedPath(from), this.scopedPath(to));
|
|
1440
|
+
async move(from, to) {
|
|
1441
|
+
return this.manager.move(this.p(from), this.p(to));
|
|
1032
1442
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
|
|
1443
|
+
async copy(from, to) {
|
|
1444
|
+
return this.manager.copy(this.p(from), this.p(to));
|
|
1036
1445
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
return this.manager.
|
|
1446
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
1447
|
+
async getStats() {
|
|
1448
|
+
return this.manager.getStats();
|
|
1040
1449
|
}
|
|
1450
|
+
// ─── Nesting ──────────────────────────────────────────────────────────────
|
|
1041
1451
|
/**
|
|
1042
|
-
* Create a
|
|
1452
|
+
* Create a deeper scope within this one.
|
|
1043
1453
|
*
|
|
1044
1454
|
* @example
|
|
1045
1455
|
* ```ts
|
|
1046
|
-
* const
|
|
1047
|
-
* const
|
|
1456
|
+
* const user = storage.scope('users/alice/');
|
|
1457
|
+
* const userDocs = user.scope('docs/');
|
|
1458
|
+
* // Effective prefix: users/alice/docs/
|
|
1048
1459
|
* ```
|
|
1049
1460
|
*/
|
|
1050
1461
|
scope(subPrefix) {
|
|
1051
|
-
return new _ScopedStorage(this.manager, this.
|
|
1462
|
+
return new _ScopedStorage(this.manager, this.p(subPrefix));
|
|
1052
1463
|
}
|
|
1053
1464
|
};
|
|
1054
1465
|
|
|
@@ -1056,34 +1467,31 @@ var ScopedStorage = class _ScopedStorage {
|
|
|
1056
1467
|
var HydrousClient = class {
|
|
1057
1468
|
constructor(config) {
|
|
1058
1469
|
this._recordsCache = /* @__PURE__ */ new Map();
|
|
1059
|
-
this._authCache = /* @__PURE__ */ new Map();
|
|
1060
1470
|
this._analyticsCache = /* @__PURE__ */ new Map();
|
|
1061
1471
|
this._storageCache = /* @__PURE__ */ new Map();
|
|
1062
1472
|
if (!config.authKey) {
|
|
1063
|
-
throw new Error("[HydrousDB] authKey is required.
|
|
1473
|
+
throw new Error("[HydrousDB] authKey is required.");
|
|
1064
1474
|
}
|
|
1065
1475
|
if (!config.bucketSecurityKey) {
|
|
1066
|
-
throw new Error("[HydrousDB] bucketSecurityKey is required.
|
|
1476
|
+
throw new Error("[HydrousDB] bucketSecurityKey is required.");
|
|
1067
1477
|
}
|
|
1068
1478
|
if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
|
|
1069
|
-
throw new Error("[HydrousDB] storageKeys
|
|
1479
|
+
throw new Error("[HydrousDB] storageKeys must define at least one key.");
|
|
1070
1480
|
}
|
|
1071
|
-
|
|
1072
|
-
this.http = new HttpClient(baseUrl);
|
|
1481
|
+
this.http = new HttpClient(config.baseUrl ?? DEFAULT_BASE_URL);
|
|
1073
1482
|
this.authKey_ = config.authKey;
|
|
1074
1483
|
this.bucketSecurityKey_ = config.bucketSecurityKey;
|
|
1075
1484
|
this.storageKeys_ = config.storageKeys;
|
|
1076
1485
|
}
|
|
1077
|
-
// ─── Records
|
|
1486
|
+
// ─── Records ──────────────────────────────────────────────────────────────
|
|
1078
1487
|
/**
|
|
1079
|
-
* Get a typed
|
|
1080
|
-
* Uses your `bucketSecurityKey` automatically.
|
|
1488
|
+
* Get a typed RecordsClient for a bucket.
|
|
1081
1489
|
*
|
|
1082
1490
|
* @example
|
|
1083
1491
|
* ```ts
|
|
1084
1492
|
* interface Post { title: string; published: boolean }
|
|
1085
1493
|
* const posts = db.records<Post>('blog-posts');
|
|
1086
|
-
* const post
|
|
1494
|
+
* const post = await posts.create({ title: 'Hello', published: false });
|
|
1087
1495
|
* ```
|
|
1088
1496
|
*/
|
|
1089
1497
|
records(bucketKey) {
|
|
@@ -1097,30 +1505,28 @@ var HydrousClient = class {
|
|
|
1097
1505
|
}
|
|
1098
1506
|
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
1099
1507
|
/**
|
|
1100
|
-
* Get
|
|
1101
|
-
*
|
|
1508
|
+
* Get the AuthClient.
|
|
1509
|
+
* Auth routes are NOT bucket-scoped in the URL — the bucket is determined
|
|
1510
|
+
* server-side from the API key itself.
|
|
1102
1511
|
*
|
|
1103
1512
|
* @example
|
|
1104
1513
|
* ```ts
|
|
1105
|
-
* const
|
|
1106
|
-
* const { user, session } = await auth.login({ email: '…', password: '…' });
|
|
1514
|
+
* const { user, session } = await db.auth().signup({ email: 'alice@example.com', password: 'pw' });
|
|
1107
1515
|
* ```
|
|
1108
1516
|
*/
|
|
1109
|
-
auth(
|
|
1110
|
-
if (!this.
|
|
1111
|
-
this.
|
|
1517
|
+
auth() {
|
|
1518
|
+
if (!this._authClient) {
|
|
1519
|
+
this._authClient = new AuthClient(this.http, this.authKey_);
|
|
1112
1520
|
}
|
|
1113
|
-
return this.
|
|
1521
|
+
return this._authClient;
|
|
1114
1522
|
}
|
|
1115
1523
|
// ─── Analytics ────────────────────────────────────────────────────────────
|
|
1116
1524
|
/**
|
|
1117
|
-
* Get an
|
|
1118
|
-
* Uses your `bucketSecurityKey` automatically.
|
|
1525
|
+
* Get an AnalyticsClient for a bucket.
|
|
1119
1526
|
*
|
|
1120
1527
|
* @example
|
|
1121
1528
|
* ```ts
|
|
1122
|
-
* const
|
|
1123
|
-
* const { count } = await analytics.count();
|
|
1529
|
+
* const { count } = await db.analytics('orders').count();
|
|
1124
1530
|
* ```
|
|
1125
1531
|
*/
|
|
1126
1532
|
analytics(bucketKey) {
|
|
@@ -1134,23 +1540,19 @@ var HydrousClient = class {
|
|
|
1134
1540
|
}
|
|
1135
1541
|
// ─── Storage ──────────────────────────────────────────────────────────────
|
|
1136
1542
|
/**
|
|
1137
|
-
* Get a
|
|
1138
|
-
* The name must match a key
|
|
1139
|
-
* Uses the corresponding `ssk_…` key automatically via `X-Storage-Key` header.
|
|
1543
|
+
* Get a StorageManager for a named storage key.
|
|
1544
|
+
* The name must match a key in the `storageKeys` config object.
|
|
1140
1545
|
*
|
|
1141
|
-
*
|
|
1546
|
+
* Attach `.scope('prefix/')` to get a path-prefixed ScopedStorage.
|
|
1142
1547
|
*
|
|
1143
1548
|
* @example
|
|
1144
1549
|
* ```ts
|
|
1145
|
-
* const avatars
|
|
1146
|
-
* const
|
|
1147
|
-
*
|
|
1148
|
-
* // Upload to avatars bucket
|
|
1149
|
-
* await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
|
|
1550
|
+
* const avatars = db.storage('avatars');
|
|
1551
|
+
* const result = await avatars.upload(file, 'alice.jpg', { isPublic: true });
|
|
1150
1552
|
*
|
|
1151
|
-
* //
|
|
1152
|
-
* const
|
|
1153
|
-
* await
|
|
1553
|
+
* // Scoped:
|
|
1554
|
+
* const userFiles = db.storage('documents').scope(`users/${userId}/`);
|
|
1555
|
+
* await userFiles.upload(pdf, 'contract.pdf');
|
|
1154
1556
|
* ```
|
|
1155
1557
|
*/
|
|
1156
1558
|
storage(keyName) {
|
|
@@ -1176,15 +1578,21 @@ function createClient(config) {
|
|
|
1176
1578
|
return new HydrousClient(config);
|
|
1177
1579
|
}
|
|
1178
1580
|
|
|
1581
|
+
exports.ANALYTICS = ANALYTICS;
|
|
1582
|
+
exports.AUTH = AUTH;
|
|
1179
1583
|
exports.AnalyticsClient = AnalyticsClient;
|
|
1180
1584
|
exports.AnalyticsError = AnalyticsError;
|
|
1181
1585
|
exports.AuthClient = AuthClient;
|
|
1182
1586
|
exports.AuthError = AuthError;
|
|
1587
|
+
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
1588
|
+
exports.HttpClient = HttpClient;
|
|
1183
1589
|
exports.HydrousClient = HydrousClient;
|
|
1184
1590
|
exports.HydrousError = HydrousError;
|
|
1185
1591
|
exports.NetworkError = NetworkError;
|
|
1592
|
+
exports.RECORDS = RECORDS;
|
|
1186
1593
|
exports.RecordError = RecordError;
|
|
1187
1594
|
exports.RecordsClient = RecordsClient;
|
|
1595
|
+
exports.STORAGE = STORAGE;
|
|
1188
1596
|
exports.ScopedStorage = ScopedStorage;
|
|
1189
1597
|
exports.StorageError = StorageError;
|
|
1190
1598
|
exports.StorageManager = StorageManager;
|