hydrousdb 3.2.0 → 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 +331 -1334
- package/dist/index.cjs +617 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +500 -222
- package/dist/index.d.ts +500 -222
- package/dist/index.mjs +612 -322
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,7 +59,7 @@ var HttpClient = class {
|
|
|
59
59
|
constructor(baseUrl) {
|
|
60
60
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
61
61
|
}
|
|
62
|
-
// ─── Core request
|
|
62
|
+
// ─── Core request ──────────────────────────────────────────────────────────
|
|
63
63
|
async request(path, options) {
|
|
64
64
|
const url = `${this.baseUrl}${path}`;
|
|
65
65
|
const headers = { ...options.headers };
|
|
@@ -98,11 +98,7 @@ var HttpClient = class {
|
|
|
98
98
|
try {
|
|
99
99
|
data = await response.json();
|
|
100
100
|
} catch {
|
|
101
|
-
throw new HydrousError(
|
|
102
|
-
`Failed to parse response JSON`,
|
|
103
|
-
"PARSE_ERROR",
|
|
104
|
-
response.status
|
|
105
|
-
);
|
|
101
|
+
throw new HydrousError("Failed to parse response JSON", "PARSE_ERROR", response.status);
|
|
106
102
|
}
|
|
107
103
|
if (!response.ok) {
|
|
108
104
|
throw new HydrousError(
|
|
@@ -151,8 +147,8 @@ var HttpClient = class {
|
|
|
151
147
|
});
|
|
152
148
|
}
|
|
153
149
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
150
|
+
* PUT directly to a signed GCS URL.
|
|
151
|
+
* Uses XHR in browsers for onprogress support; fetch in Node.
|
|
156
152
|
*/
|
|
157
153
|
async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
158
154
|
const body = data instanceof Blob ? data : data instanceof Uint8Array ? data.buffer : data;
|
|
@@ -181,135 +177,262 @@ var HttpClient = class {
|
|
|
181
177
|
}
|
|
182
178
|
};
|
|
183
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
|
+
|
|
184
276
|
// src/auth/client.ts
|
|
185
277
|
var AuthClient = class {
|
|
186
|
-
constructor(http, authKey
|
|
278
|
+
constructor(http, authKey) {
|
|
187
279
|
this.http = http;
|
|
188
280
|
this.authKey = authKey;
|
|
189
|
-
this.basePath = `/auth/${bucketKey}`;
|
|
190
281
|
}
|
|
191
282
|
post(path, body) {
|
|
192
283
|
return this.http.post(path, this.authKey, body);
|
|
193
284
|
}
|
|
194
|
-
get(path, query) {
|
|
285
|
+
get(path, query, extraHeaders) {
|
|
195
286
|
const qs = query && Object.keys(query).length ? "?" + new URLSearchParams(query).toString() : "";
|
|
196
|
-
return this.http.get(`${path}${qs}`, this.authKey);
|
|
287
|
+
return this.http.get(`${path}${qs}`, this.authKey, extraHeaders);
|
|
197
288
|
}
|
|
198
289
|
patch(path, body) {
|
|
199
290
|
return this.http.patch(path, this.authKey, body);
|
|
200
291
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
*/
|
|
208
299
|
async signup(options) {
|
|
209
|
-
const result = await this.post(
|
|
210
|
-
const user = result.
|
|
211
|
-
return
|
|
212
|
-
user,
|
|
213
|
-
session: {
|
|
214
|
-
sessionId: result.session.sessionId,
|
|
215
|
-
userId: user.id,
|
|
216
|
-
bucketId: this.basePath.split("/")[2],
|
|
217
|
-
createdAt: Date.now(),
|
|
218
|
-
expiresAt: result.session.expiresAt,
|
|
219
|
-
refreshToken: result.session.refreshToken,
|
|
220
|
-
refreshExpiresAt: result.session.expiresAt
|
|
221
|
-
}
|
|
222
|
-
};
|
|
300
|
+
const result = await this.post(AUTH.signup, options);
|
|
301
|
+
const user = result.data;
|
|
302
|
+
return this._buildAuthResult(user, result.session);
|
|
223
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Sign in with email + password.
|
|
306
|
+
*
|
|
307
|
+
* Server: POST /api/auth/signin (NOT /login)
|
|
308
|
+
* Body: { email, password }
|
|
309
|
+
*/
|
|
224
310
|
async login(options) {
|
|
225
|
-
const result = await this.post(
|
|
226
|
-
const user = result.
|
|
227
|
-
return
|
|
228
|
-
user,
|
|
229
|
-
session: {
|
|
230
|
-
sessionId: result.session.sessionId,
|
|
231
|
-
userId: user.id,
|
|
232
|
-
bucketId: this.basePath.split("/")[2],
|
|
233
|
-
createdAt: Date.now(),
|
|
234
|
-
expiresAt: result.session.expiresAt,
|
|
235
|
-
refreshToken: result.session.refreshToken,
|
|
236
|
-
refreshExpiresAt: result.session.expiresAt
|
|
237
|
-
}
|
|
238
|
-
};
|
|
311
|
+
const result = await this.post(AUTH.signin, options);
|
|
312
|
+
const user = result.data;
|
|
313
|
+
return this._buildAuthResult(user, result.session);
|
|
239
314
|
}
|
|
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
|
+
*/
|
|
240
321
|
async logout(options) {
|
|
241
|
-
await this.post(
|
|
322
|
+
await this.post(AUTH.signout, options);
|
|
242
323
|
}
|
|
243
|
-
|
|
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) {
|
|
244
332
|
const result = await this.post(
|
|
245
|
-
|
|
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,
|
|
246
347
|
{ refreshToken }
|
|
247
348
|
);
|
|
248
349
|
const s = result.session;
|
|
249
350
|
return {
|
|
250
351
|
sessionId: s.sessionId,
|
|
251
352
|
userId: result.data?.id ?? "",
|
|
252
|
-
bucketId:
|
|
353
|
+
bucketId: "",
|
|
253
354
|
createdAt: Date.now(),
|
|
254
355
|
expiresAt: s.expiresAt,
|
|
255
356
|
refreshToken: s.refreshToken,
|
|
256
357
|
refreshExpiresAt: s.expiresAt
|
|
257
358
|
};
|
|
258
359
|
}
|
|
259
|
-
async validateSession({ sessionId }) {
|
|
260
|
-
const result = await this.post(
|
|
261
|
-
`${this.basePath}/session/validate`,
|
|
262
|
-
{ sessionId }
|
|
263
|
-
);
|
|
264
|
-
return { user: result.data, session: result.session };
|
|
265
|
-
}
|
|
266
360
|
// ─── User Profile ─────────────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 });
|
|
272
368
|
return result.data;
|
|
273
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
|
+
*/
|
|
274
377
|
async updateUser(options) {
|
|
275
378
|
const { sessionId, userId, updates } = options;
|
|
276
379
|
const result = await this.patch(
|
|
277
|
-
|
|
380
|
+
AUTH.updateUser,
|
|
278
381
|
{ sessionId, userId, updates }
|
|
279
382
|
);
|
|
280
383
|
return result.data;
|
|
281
384
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
);
|
|
288
401
|
}
|
|
289
402
|
// ─── Admin Operations ─────────────────────────────────────────────────────
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
*/
|
|
293
409
|
async listUsers(options) {
|
|
294
410
|
const { sessionId, limit = 50, cursor } = options;
|
|
295
411
|
const params = { limit: String(limit) };
|
|
296
412
|
if (cursor) params["cursor"] = cursor;
|
|
297
413
|
const result = await this.http.request(
|
|
298
|
-
`${
|
|
414
|
+
`${AUTH.listUsers}?${new URLSearchParams(params)}`,
|
|
299
415
|
{
|
|
300
416
|
method: "GET",
|
|
301
417
|
headers: { "X-Api-Key": this.authKey, "X-Session-Id": sessionId }
|
|
302
418
|
}
|
|
303
419
|
);
|
|
304
420
|
return {
|
|
305
|
-
users: result.data ??
|
|
421
|
+
users: result.data ?? [],
|
|
306
422
|
hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
|
|
307
423
|
nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? null
|
|
308
424
|
};
|
|
309
425
|
}
|
|
310
|
-
|
|
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) {
|
|
311
434
|
await this.http.request(
|
|
312
|
-
`${
|
|
435
|
+
`${AUTH.hardDeleteUser}?userId=${encodeURIComponent(userId)}`,
|
|
313
436
|
{
|
|
314
437
|
method: "DELETE",
|
|
315
438
|
body: { sessionId },
|
|
@@ -317,54 +440,114 @@ var AuthClient = class {
|
|
|
317
440
|
}
|
|
318
441
|
);
|
|
319
442
|
}
|
|
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
|
+
*/
|
|
320
449
|
async bulkDeleteUsers(options) {
|
|
321
450
|
const result = await this.http.request(
|
|
322
|
-
|
|
451
|
+
AUTH.bulkDeleteUsers,
|
|
323
452
|
{
|
|
324
453
|
method: "DELETE",
|
|
325
|
-
body: {
|
|
454
|
+
body: {
|
|
455
|
+
userIds: options.userIds,
|
|
456
|
+
hard: options.hard ?? false,
|
|
457
|
+
sessionId: options.sessionId
|
|
458
|
+
},
|
|
326
459
|
headers: { "X-Api-Key": this.authKey }
|
|
327
460
|
}
|
|
328
461
|
);
|
|
329
462
|
return { succeeded: result.meta.succeeded, failed: result.meta.failed };
|
|
330
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
|
+
*/
|
|
331
470
|
async lockAccount(options) {
|
|
332
|
-
const result = await this.post(
|
|
333
|
-
`${this.basePath}/account/lock`,
|
|
334
|
-
options
|
|
335
|
-
);
|
|
471
|
+
const result = await this.post(AUTH.accountLock, options);
|
|
336
472
|
return result.data;
|
|
337
473
|
}
|
|
338
|
-
|
|
339
|
-
|
|
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 });
|
|
340
482
|
}
|
|
341
483
|
// ─── Password Management ──────────────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
*/
|
|
345
490
|
async changePassword(options) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
oldPassword
|
|
351
|
-
newPassword
|
|
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
|
|
352
497
|
});
|
|
353
498
|
}
|
|
354
|
-
|
|
355
|
-
|
|
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 });
|
|
356
508
|
}
|
|
357
|
-
|
|
358
|
-
|
|
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 });
|
|
359
517
|
}
|
|
360
518
|
// ─── Email Verification ───────────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 });
|
|
365
527
|
}
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
};
|
|
368
551
|
}
|
|
369
552
|
};
|
|
370
553
|
|
|
@@ -373,18 +556,21 @@ function buildQueryParams(options = {}) {
|
|
|
373
556
|
const params = new URLSearchParams();
|
|
374
557
|
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
375
558
|
if (options.offset !== void 0) params.set("offset", String(options.offset));
|
|
376
|
-
if (options.orderBy !== void 0) params.set("
|
|
377
|
-
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);
|
|
378
561
|
if (options.fields !== void 0) params.set("fields", options.fields);
|
|
379
|
-
if (options.startAfter !== void 0) params.set("
|
|
380
|
-
if (options.startAt !== void 0) params.set("
|
|
381
|
-
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);
|
|
382
564
|
if (options.dateRange?.start !== void 0)
|
|
383
565
|
params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
|
|
384
566
|
if (options.dateRange?.end !== void 0)
|
|
385
567
|
params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
|
|
386
|
-
if (options.filters && options.filters.length > 0)
|
|
387
|
-
|
|
568
|
+
if (options.filters && options.filters.length > 0) {
|
|
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
|
+
}
|
|
573
|
+
}
|
|
388
574
|
const str = params.toString();
|
|
389
575
|
return str ? `?${str}` : "";
|
|
390
576
|
}
|
|
@@ -406,10 +592,13 @@ function guessMimeType(filename) {
|
|
|
406
592
|
html: "text/html",
|
|
407
593
|
css: "text/css",
|
|
408
594
|
js: "application/javascript",
|
|
595
|
+
ts: "application/typescript",
|
|
409
596
|
json: "application/json",
|
|
410
597
|
xml: "application/xml",
|
|
411
598
|
zip: "application/zip",
|
|
412
|
-
csv: "text/csv"
|
|
599
|
+
csv: "text/csv",
|
|
600
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
601
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
413
602
|
};
|
|
414
603
|
return map[ext ?? ""] ?? "application/octet-stream";
|
|
415
604
|
}
|
|
@@ -428,15 +617,14 @@ var RecordsClient = class {
|
|
|
428
617
|
this.http = http;
|
|
429
618
|
this.bucketKey = bucketKey;
|
|
430
619
|
this.apiKey = bucketSecurityKey;
|
|
431
|
-
this.basePath = `/records/${bucketKey}`;
|
|
432
620
|
}
|
|
433
621
|
// ─── Single Record Operations ─────────────────────────────────────────────
|
|
434
622
|
/**
|
|
435
|
-
* Create a new record
|
|
623
|
+
* Create a new record (auto-generated ID) or upsert by `customRecordId`.
|
|
436
624
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
625
|
+
* Server: POST /api/:bucketKey
|
|
626
|
+
* Body: { values, queryableFields?, userEmail?, customRecordId? }
|
|
627
|
+
* Returns 201 for new records, 200 for upserts.
|
|
440
628
|
*
|
|
441
629
|
* @example
|
|
442
630
|
* ```ts
|
|
@@ -452,63 +640,110 @@ var RecordsClient = class {
|
|
|
452
640
|
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
453
641
|
if (userEmail) body["userEmail"] = userEmail;
|
|
454
642
|
if (customRecordId) body["customRecordId"] = customRecordId;
|
|
455
|
-
const result = await this.http.post(
|
|
456
|
-
|
|
643
|
+
const result = await this.http.post(
|
|
644
|
+
RECORDS.bucket(this.bucketKey),
|
|
645
|
+
this.apiKey,
|
|
646
|
+
body
|
|
647
|
+
);
|
|
648
|
+
return result.data;
|
|
457
649
|
}
|
|
458
650
|
/**
|
|
459
651
|
* Get a single record by ID.
|
|
652
|
+
*
|
|
653
|
+
* Server: GET /api/:bucketKey?recordId=:id
|
|
460
654
|
*/
|
|
461
655
|
async get(id) {
|
|
462
656
|
const result = await this.http.get(
|
|
463
|
-
`${this.
|
|
657
|
+
`${RECORDS.bucket(this.bucketKey)}?recordId=${encodeURIComponent(id)}`,
|
|
464
658
|
this.apiKey
|
|
465
659
|
);
|
|
466
|
-
return result.data
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Fully replace a record (PUT semantics).
|
|
470
|
-
*/
|
|
471
|
-
async set(id, data) {
|
|
472
|
-
const result = await this.http.put(
|
|
473
|
-
`${this.basePath}/${encodeURIComponent(id)}`,
|
|
474
|
-
this.apiKey,
|
|
475
|
-
data
|
|
476
|
-
);
|
|
477
|
-
return result.data ?? result.record;
|
|
660
|
+
return result.data;
|
|
478
661
|
}
|
|
479
662
|
/**
|
|
480
|
-
* Partially update
|
|
481
|
-
* By default merges with existing fields; set `merge: false` to replace.
|
|
663
|
+
* Partially update an existing record (PATCH semantics).
|
|
482
664
|
* Supports write-filter sentinels: `{ __op: 'increment', delta: 1 }` etc.
|
|
665
|
+
*
|
|
666
|
+
* Server: PATCH /api/:bucketKey
|
|
667
|
+
* Body: { recordId, values, userEmail?, track_record_history? }
|
|
483
668
|
*/
|
|
484
669
|
async patch(id, data, options = {}) {
|
|
485
|
-
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;
|
|
486
674
|
const result = await this.http.patch(
|
|
487
|
-
|
|
675
|
+
RECORDS.bucket(this.bucketKey),
|
|
488
676
|
this.apiKey,
|
|
489
|
-
|
|
677
|
+
body
|
|
490
678
|
);
|
|
491
|
-
return result.
|
|
679
|
+
return { id: result.meta?.id ?? id, updatedAt: result.meta?.updatedAt };
|
|
492
680
|
}
|
|
493
681
|
/**
|
|
494
682
|
* Delete a record permanently.
|
|
683
|
+
*
|
|
684
|
+
* Server: DELETE /api/:bucketKey?recordId=:id
|
|
495
685
|
*/
|
|
496
686
|
async delete(id) {
|
|
497
687
|
await this.http.delete(
|
|
498
|
-
`${this.
|
|
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`,
|
|
499
730
|
this.apiKey
|
|
500
731
|
);
|
|
732
|
+
return result.history ?? [];
|
|
501
733
|
}
|
|
502
734
|
// ─── Batch Operations ─────────────────────────────────────────────────────
|
|
503
735
|
/**
|
|
504
|
-
* Create
|
|
505
|
-
* Each record
|
|
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? }
|
|
506
741
|
*
|
|
507
742
|
* @example
|
|
508
743
|
* ```ts
|
|
509
|
-
* const
|
|
744
|
+
* const results = await posts.batchCreate(
|
|
510
745
|
* [{ title: 'A' }, { title: 'B' }],
|
|
511
|
-
* { queryableFields: ['title']
|
|
746
|
+
* { queryableFields: ['title'] },
|
|
512
747
|
* );
|
|
513
748
|
* ```
|
|
514
749
|
*/
|
|
@@ -518,29 +753,60 @@ var RecordsClient = class {
|
|
|
518
753
|
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
519
754
|
if (userEmail) body["userEmail"] = userEmail;
|
|
520
755
|
const result = await this.http.post(
|
|
521
|
-
|
|
756
|
+
RECORDS.batchInsert(this.bucketKey),
|
|
522
757
|
this.apiKey,
|
|
523
758
|
body
|
|
524
759
|
);
|
|
525
|
-
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
|
+
};
|
|
526
766
|
}
|
|
527
767
|
/**
|
|
528
|
-
*
|
|
768
|
+
* Update up to 500 records in one request.
|
|
769
|
+
*
|
|
770
|
+
* Server: POST /api/:bucketKey/batch/update
|
|
771
|
+
* Body: { updates: [{ recordId, values }], userEmail? }
|
|
529
772
|
*/
|
|
530
|
-
async
|
|
773
|
+
async batchUpdate(updates, userEmail) {
|
|
774
|
+
const body = { updates };
|
|
775
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
531
776
|
const result = await this.http.post(
|
|
532
|
-
|
|
777
|
+
RECORDS.batchUpdate(this.bucketKey),
|
|
533
778
|
this.apiKey,
|
|
534
|
-
|
|
779
|
+
body
|
|
535
780
|
);
|
|
536
781
|
return {
|
|
537
|
-
|
|
538
|
-
failed: result.data?.failed ??
|
|
782
|
+
successful: result.data?.successful ?? result.meta?.count ?? 0,
|
|
783
|
+
failed: result.data?.failed ?? []
|
|
784
|
+
};
|
|
785
|
+
}
|
|
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;
|
|
795
|
+
const result = await this.http.post(
|
|
796
|
+
RECORDS.batchDelete(this.bucketKey),
|
|
797
|
+
this.apiKey,
|
|
798
|
+
body
|
|
799
|
+
);
|
|
800
|
+
return {
|
|
801
|
+
successful: result.data?.successful ?? result.meta?.count ?? 0,
|
|
802
|
+
failed: result.data?.failed ?? []
|
|
539
803
|
};
|
|
540
804
|
}
|
|
541
805
|
// ─── Querying ─────────────────────────────────────────────────────────────
|
|
542
806
|
/**
|
|
543
|
-
* Query records with
|
|
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=
|
|
544
810
|
*
|
|
545
811
|
* @example
|
|
546
812
|
* ```ts
|
|
@@ -554,52 +820,23 @@ var RecordsClient = class {
|
|
|
554
820
|
*/
|
|
555
821
|
async query(options = {}) {
|
|
556
822
|
const qs = buildQueryParams(options);
|
|
557
|
-
const result = await this.http.get(`${this.basePath}${qs}`, this.apiKey);
|
|
558
|
-
const records = result.data ?? result.records ?? [];
|
|
559
|
-
const hasMore = result.meta?.hasMore ?? result.hasMore ?? false;
|
|
560
|
-
const total = result.meta?.total ?? result.total;
|
|
561
|
-
const nextCursor = result.meta?.nextCursor ?? result.nextCursor ?? void 0;
|
|
562
|
-
return { records, total, hasMore, nextCursor: nextCursor ?? void 0 };
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Get all records matching the given options (no filter support — use `query()` for filters).
|
|
566
|
-
*/
|
|
567
|
-
async getAll(options = {}) {
|
|
568
|
-
const { records } = await this.query(options);
|
|
569
|
-
return records;
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Count records matching the given filters.
|
|
573
|
-
*/
|
|
574
|
-
async count(filters = []) {
|
|
575
|
-
const result = await this.http.post(
|
|
576
|
-
`${this.basePath}/count`,
|
|
577
|
-
this.apiKey,
|
|
578
|
-
{ filters }
|
|
579
|
-
);
|
|
580
|
-
return result.data?.count ?? result.count ?? 0;
|
|
581
|
-
}
|
|
582
|
-
// ─── Version History ──────────────────────────────────────────────────────
|
|
583
|
-
/**
|
|
584
|
-
* Get the version history of a record.
|
|
585
|
-
*/
|
|
586
|
-
async getHistory(id) {
|
|
587
823
|
const result = await this.http.get(
|
|
588
|
-
`${this.
|
|
824
|
+
`${RECORDS.bucket(this.bucketKey)}${qs}`,
|
|
589
825
|
this.apiKey
|
|
590
826
|
);
|
|
591
|
-
return
|
|
827
|
+
return {
|
|
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
|
|
832
|
+
};
|
|
592
833
|
}
|
|
593
834
|
/**
|
|
594
|
-
*
|
|
835
|
+
* Retrieve all records matching options (no filter support — use `query()` for filters).
|
|
595
836
|
*/
|
|
596
|
-
async
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
this.apiKey,
|
|
600
|
-
{ version }
|
|
601
|
-
);
|
|
602
|
-
return result.data ?? result.record;
|
|
837
|
+
async getAll(options = {}) {
|
|
838
|
+
const { records } = await this.query(options);
|
|
839
|
+
return records;
|
|
603
840
|
}
|
|
604
841
|
};
|
|
605
842
|
|
|
@@ -609,11 +846,11 @@ var AnalyticsClient = class {
|
|
|
609
846
|
assertSafeName(bucketKey, "bucketKey");
|
|
610
847
|
this.http = http;
|
|
611
848
|
this.bucketSecurityKey = bucketSecurityKey;
|
|
612
|
-
this.
|
|
849
|
+
this.bucketKey = bucketKey;
|
|
613
850
|
}
|
|
614
851
|
async run(query) {
|
|
615
852
|
const result = await this.http.post(
|
|
616
|
-
this.
|
|
853
|
+
ANALYTICS.query(this.bucketKey),
|
|
617
854
|
this.bucketSecurityKey,
|
|
618
855
|
query
|
|
619
856
|
);
|
|
@@ -624,7 +861,10 @@ var AnalyticsClient = class {
|
|
|
624
861
|
async count(opts = {}) {
|
|
625
862
|
return this.run({ queryType: "count", ...opts });
|
|
626
863
|
}
|
|
627
|
-
/**
|
|
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
|
+
*/
|
|
628
868
|
async distribution(opts) {
|
|
629
869
|
assertSafeName(opts.field, "field");
|
|
630
870
|
return this.run({ queryType: "distribution", ...opts });
|
|
@@ -660,13 +900,19 @@ var AnalyticsClient = class {
|
|
|
660
900
|
assertSafeName(opts.field, "field");
|
|
661
901
|
return this.run({ queryType: "stats", ...opts });
|
|
662
902
|
}
|
|
663
|
-
/**
|
|
903
|
+
/**
|
|
904
|
+
* Fetch filtered records via the analytics engine (BigQuery).
|
|
905
|
+
* Bypasses Firestore pagination — useful for large result sets.
|
|
906
|
+
*/
|
|
664
907
|
async records(opts = {}) {
|
|
665
908
|
if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
|
|
666
909
|
if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
|
|
667
910
|
return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
|
|
668
911
|
}
|
|
669
|
-
/**
|
|
912
|
+
/**
|
|
913
|
+
* Compute multiple aggregations in a single request.
|
|
914
|
+
* `metrics` must have valid `field` and `name` (used as BigQuery column aliases).
|
|
915
|
+
*/
|
|
670
916
|
async multiMetric(opts) {
|
|
671
917
|
opts.metrics.forEach((m) => {
|
|
672
918
|
assertSafeName(m.field, "metric.field");
|
|
@@ -680,7 +926,8 @@ var AnalyticsClient = class {
|
|
|
680
926
|
}
|
|
681
927
|
/**
|
|
682
928
|
* Compare a metric across multiple buckets in one query.
|
|
683
|
-
* The caller's key must have read access to
|
|
929
|
+
* The caller's key must have read access to EVERY bucket in `bucketKeys`.
|
|
930
|
+
* System buckets (`users`, `_sys_*`) are blocked server-side.
|
|
684
931
|
*/
|
|
685
932
|
async crossBucket(opts) {
|
|
686
933
|
assertSafeName(opts.field, "field");
|
|
@@ -688,7 +935,7 @@ var AnalyticsClient = class {
|
|
|
688
935
|
return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
|
|
689
936
|
}
|
|
690
937
|
/**
|
|
691
|
-
* Raw query —
|
|
938
|
+
* Raw query — escape hatch when the typed helpers don't cover your case.
|
|
692
939
|
*/
|
|
693
940
|
async query(query) {
|
|
694
941
|
const data = await this.run(query);
|
|
@@ -709,17 +956,19 @@ function toUploadResult(r) {
|
|
|
709
956
|
}
|
|
710
957
|
var StorageManager = class {
|
|
711
958
|
constructor(http, storageKey) {
|
|
712
|
-
this.basePath = "/storage";
|
|
713
959
|
this.http = http;
|
|
714
960
|
this.storageKey = storageKey;
|
|
715
961
|
}
|
|
716
962
|
get authHeaders() {
|
|
717
963
|
return { "X-Storage-Key": this.storageKey };
|
|
718
964
|
}
|
|
719
|
-
// ─── Upload:
|
|
965
|
+
// ─── Upload: Server-buffered ──────────────────────────────────────────────
|
|
720
966
|
/**
|
|
721
|
-
* Upload a file
|
|
722
|
-
* For files >10 MB or when upload progress is needed, use
|
|
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()`.
|
|
970
|
+
*
|
|
971
|
+
* Server: POST /storage/upload (multipart/form-data)
|
|
723
972
|
*
|
|
724
973
|
* @example
|
|
725
974
|
* ```ts
|
|
@@ -737,7 +986,7 @@ var StorageManager = class {
|
|
|
737
986
|
formData.append("mimeType", mime);
|
|
738
987
|
formData.append("isPublic", String(isPublic));
|
|
739
988
|
formData.append("overwrite", String(overwrite));
|
|
740
|
-
const result = await this.http.request(
|
|
989
|
+
const result = await this.http.request(STORAGE.upload, {
|
|
741
990
|
method: "POST",
|
|
742
991
|
rawBody: formData,
|
|
743
992
|
headers: this.authHeaders
|
|
@@ -745,7 +994,10 @@ var StorageManager = class {
|
|
|
745
994
|
return toUploadResult(result);
|
|
746
995
|
}
|
|
747
996
|
/**
|
|
748
|
-
* 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? }
|
|
749
1001
|
*
|
|
750
1002
|
* @example
|
|
751
1003
|
* ```ts
|
|
@@ -755,33 +1007,32 @@ var StorageManager = class {
|
|
|
755
1007
|
async uploadRaw(data, path, options = {}) {
|
|
756
1008
|
const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
|
|
757
1009
|
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
758
|
-
const result = await this.http.request(
|
|
1010
|
+
const result = await this.http.request(STORAGE.uploadRaw, {
|
|
759
1011
|
method: "POST",
|
|
760
1012
|
body: { path, content, mimeType, isPublic, overwrite },
|
|
761
1013
|
headers: this.authHeaders
|
|
762
1014
|
});
|
|
763
1015
|
return toUploadResult(result);
|
|
764
1016
|
}
|
|
765
|
-
// ─── Upload: Direct-to-GCS (recommended for large files)
|
|
1017
|
+
// ─── Upload: Direct-to-GCS (recommended for large files / progress) ───────
|
|
766
1018
|
/**
|
|
767
|
-
* Step 1 — get a signed GCS URL to upload
|
|
768
|
-
*
|
|
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? }
|
|
769
1023
|
*
|
|
770
1024
|
* @example
|
|
771
1025
|
* ```ts
|
|
772
|
-
* const { uploadUrl, path:
|
|
773
|
-
* path:
|
|
774
|
-
* mimeType: 'video/mp4',
|
|
775
|
-
* size: file.size,
|
|
776
|
-
* isPublic: true,
|
|
1026
|
+
* const { uploadUrl, path: p } = await storage.getUploadUrl({
|
|
1027
|
+
* path: 'videos/intro.mp4', mimeType: 'video/mp4', size: file.size,
|
|
777
1028
|
* });
|
|
778
|
-
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct =>
|
|
779
|
-
* const result = await storage.confirmUpload({ path:
|
|
1029
|
+
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => setProgress(pct));
|
|
1030
|
+
* const result = await storage.confirmUpload({ path: p, mimeType: 'video/mp4' });
|
|
780
1031
|
* ```
|
|
781
1032
|
*/
|
|
782
1033
|
async getUploadUrl(opts) {
|
|
783
1034
|
const { expiresInSeconds, ...rest } = opts;
|
|
784
|
-
const result = await this.http.request(
|
|
1035
|
+
const result = await this.http.request(STORAGE.uploadUrl, {
|
|
785
1036
|
method: "POST",
|
|
786
1037
|
body: { isPublic: false, overwrite: false, expiresIn: expiresInSeconds ?? 900, ...rest },
|
|
787
1038
|
headers: this.authHeaders
|
|
@@ -795,17 +1046,20 @@ var StorageManager = class {
|
|
|
795
1046
|
};
|
|
796
1047
|
}
|
|
797
1048
|
/**
|
|
798
|
-
* Step 2 — upload data directly to
|
|
799
|
-
* Supports progress tracking in browser environments.
|
|
1049
|
+
* Step 2 — upload data directly to the signed GCS URL.
|
|
1050
|
+
* Supports progress tracking in browser environments via XHR.
|
|
800
1051
|
*/
|
|
801
1052
|
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
802
1053
|
await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
803
1054
|
}
|
|
804
1055
|
/**
|
|
805
|
-
* Step 3 — confirm a direct upload and register metadata
|
|
1056
|
+
* Step 3 — confirm a direct upload and register metadata server-side.
|
|
1057
|
+
*
|
|
1058
|
+
* Server: POST /storage/confirm
|
|
1059
|
+
* Body: { path, mimeType, isPublic? }
|
|
806
1060
|
*/
|
|
807
1061
|
async confirmUpload(opts) {
|
|
808
|
-
const result = await this.http.request(
|
|
1062
|
+
const result = await this.http.request(STORAGE.confirm, {
|
|
809
1063
|
method: "POST",
|
|
810
1064
|
body: { isPublic: false, ...opts },
|
|
811
1065
|
headers: this.authHeaders
|
|
@@ -814,26 +1068,17 @@ var StorageManager = class {
|
|
|
814
1068
|
}
|
|
815
1069
|
// ─── Batch Upload ─────────────────────────────────────────────────────────
|
|
816
1070
|
/**
|
|
817
|
-
* Get signed upload URLs for
|
|
818
|
-
* Server returns `{ succeeded, failed }` — only succeeded items have URLs.
|
|
1071
|
+
* Get signed upload URLs for up to 50 files at once.
|
|
819
1072
|
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
* const { files } = await storage.getBatchUploadUrls([
|
|
823
|
-
* { path: 'images/a.jpg', mimeType: 'image/jpeg', size: 204800 },
|
|
824
|
-
* { path: 'images/b.jpg', mimeType: 'image/jpeg', size: 153600 },
|
|
825
|
-
* ]);
|
|
826
|
-
* for (const f of files) {
|
|
827
|
-
* await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
828
|
-
* await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
|
|
829
|
-
* }
|
|
830
|
-
* ```
|
|
1073
|
+
* Server: POST /storage/batch-upload-urls
|
|
1074
|
+
* Body: { files: [...], expiresIn? }
|
|
831
1075
|
*/
|
|
832
1076
|
async getBatchUploadUrls(files) {
|
|
833
|
-
const result = await this.http.request(
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1077
|
+
const result = await this.http.request(STORAGE.batchUploadUrls, {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
body: { files },
|
|
1080
|
+
headers: this.authHeaders
|
|
1081
|
+
});
|
|
837
1082
|
return {
|
|
838
1083
|
files: result.succeeded.map((f) => ({
|
|
839
1084
|
index: f.index,
|
|
@@ -847,13 +1092,16 @@ var StorageManager = class {
|
|
|
847
1092
|
}
|
|
848
1093
|
/**
|
|
849
1094
|
* Confirm multiple direct uploads at once.
|
|
850
|
-
*
|
|
1095
|
+
*
|
|
1096
|
+
* Server: POST /storage/batch-confirm
|
|
1097
|
+
* Body: { files: [{ path, mimeType, isPublic? }] }
|
|
851
1098
|
*/
|
|
852
1099
|
async batchConfirmUploads(items) {
|
|
853
|
-
const result = await this.http.request(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1100
|
+
const result = await this.http.request(STORAGE.batchConfirm, {
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
body: { files: items },
|
|
1103
|
+
headers: this.authHeaders
|
|
1104
|
+
});
|
|
857
1105
|
return {
|
|
858
1106
|
succeeded: result.succeeded.map(toUploadResult),
|
|
859
1107
|
failed: result.failed
|
|
@@ -863,32 +1111,28 @@ var StorageManager = class {
|
|
|
863
1111
|
/**
|
|
864
1112
|
* Download a private file as an ArrayBuffer.
|
|
865
1113
|
* For public files, use the `publicUrl` directly — no SDK needed.
|
|
1114
|
+
*
|
|
1115
|
+
* Server: GET /storage/download/:path (requires X-Storage-Key)
|
|
866
1116
|
*/
|
|
867
1117
|
async download(path) {
|
|
868
1118
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
869
|
-
return this.http.request(
|
|
1119
|
+
return this.http.request(STORAGE.download(encoded), {
|
|
870
1120
|
method: "GET",
|
|
871
1121
|
headers: this.authHeaders
|
|
872
1122
|
});
|
|
873
1123
|
}
|
|
874
1124
|
/**
|
|
875
|
-
* Download
|
|
876
|
-
* Returns a structured result with succeeded and failed items.
|
|
877
|
-
* Each succeeded item includes the file content as a base64 string.
|
|
1125
|
+
* Download up to 20 files at once. Returns base64-encoded content.
|
|
878
1126
|
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* const { succeeded, failed } = await storage.batchDownload(['docs/a.pdf', 'docs/b.pdf']);
|
|
882
|
-
* for (const f of succeeded) {
|
|
883
|
-
* const bytes = Buffer.from(f.content, 'base64');
|
|
884
|
-
* }
|
|
885
|
-
* ```
|
|
1127
|
+
* Server: POST /storage/batch-download
|
|
1128
|
+
* Body: { paths, concurrency? }
|
|
886
1129
|
*/
|
|
887
|
-
async batchDownload(paths) {
|
|
888
|
-
const result = await this.http.request(
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
});
|
|
892
1136
|
return {
|
|
893
1137
|
succeeded: result.succeeded.map((f) => ({
|
|
894
1138
|
index: f.index,
|
|
@@ -908,7 +1152,8 @@ var StorageManager = class {
|
|
|
908
1152
|
// ─── List ─────────────────────────────────────────────────────────────────
|
|
909
1153
|
/**
|
|
910
1154
|
* List files and folders at a given path prefix.
|
|
911
|
-
*
|
|
1155
|
+
*
|
|
1156
|
+
* Server: GET /storage/list?prefix=&limit=&cursor=
|
|
912
1157
|
*
|
|
913
1158
|
* @example
|
|
914
1159
|
* ```ts
|
|
@@ -922,7 +1167,7 @@ var StorageManager = class {
|
|
|
922
1167
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
923
1168
|
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
924
1169
|
const qs = params.toString() ? `?${params}` : "";
|
|
925
|
-
const result = await this.http.request(`${
|
|
1170
|
+
const result = await this.http.request(`${STORAGE.list}${qs}`, {
|
|
926
1171
|
method: "GET",
|
|
927
1172
|
headers: this.authHeaders
|
|
928
1173
|
});
|
|
@@ -930,11 +1175,8 @@ var StorageManager = class {
|
|
|
930
1175
|
const files = [];
|
|
931
1176
|
const folders = [];
|
|
932
1177
|
for (const item of result.items) {
|
|
933
|
-
if (item.type === "folder")
|
|
934
|
-
|
|
935
|
-
} else {
|
|
936
|
-
files.push(item);
|
|
937
|
-
}
|
|
1178
|
+
if (item.type === "folder") folders.push(item.path);
|
|
1179
|
+
else files.push(item);
|
|
938
1180
|
}
|
|
939
1181
|
return {
|
|
940
1182
|
files,
|
|
@@ -952,14 +1194,16 @@ var StorageManager = class {
|
|
|
952
1194
|
}
|
|
953
1195
|
// ─── Metadata ─────────────────────────────────────────────────────────────
|
|
954
1196
|
/**
|
|
955
|
-
* Get metadata
|
|
1197
|
+
* Get file metadata: size, MIME type, visibility, URLs.
|
|
1198
|
+
*
|
|
1199
|
+
* Server: GET /storage/metadata/:path
|
|
956
1200
|
*/
|
|
957
1201
|
async getMetadata(path) {
|
|
958
1202
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
959
|
-
const result = await this.http.request(
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
);
|
|
1203
|
+
const result = await this.http.request(STORAGE.metadata(encoded), {
|
|
1204
|
+
method: "GET",
|
|
1205
|
+
headers: this.authHeaders
|
|
1206
|
+
});
|
|
963
1207
|
return {
|
|
964
1208
|
path: result.path,
|
|
965
1209
|
size: result.size,
|
|
@@ -974,15 +1218,18 @@ var StorageManager = class {
|
|
|
974
1218
|
// ─── Signed URL ───────────────────────────────────────────────────────────
|
|
975
1219
|
/**
|
|
976
1220
|
* Generate a time-limited download URL for a private file.
|
|
977
|
-
* Can be shared externally
|
|
1221
|
+
* Can be shared externally — no X-Storage-Key required to access.
|
|
978
1222
|
*
|
|
979
|
-
*
|
|
1223
|
+
* Note: Downloads via signed URLs bypass the server — stats are NOT tracked.
|
|
1224
|
+
*
|
|
1225
|
+
* Server: POST /storage/signed-url
|
|
1226
|
+
* Body: { path, expiresIn? }
|
|
980
1227
|
*
|
|
981
1228
|
* @param path File path.
|
|
982
1229
|
* @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
|
|
983
1230
|
*/
|
|
984
1231
|
async getSignedUrl(path, expiresIn = 3600) {
|
|
985
|
-
const result = await this.http.request(
|
|
1232
|
+
const result = await this.http.request(STORAGE.signedUrl, {
|
|
986
1233
|
method: "POST",
|
|
987
1234
|
body: { path, expiresIn },
|
|
988
1235
|
headers: this.authHeaders
|
|
@@ -996,10 +1243,13 @@ var StorageManager = class {
|
|
|
996
1243
|
}
|
|
997
1244
|
// ─── Visibility ───────────────────────────────────────────────────────────
|
|
998
1245
|
/**
|
|
999
|
-
* Change a file's visibility between public and private
|
|
1246
|
+
* Change a file's visibility between public and private.
|
|
1247
|
+
*
|
|
1248
|
+
* Server: PATCH /storage/visibility
|
|
1249
|
+
* Body: { path, isPublic }
|
|
1000
1250
|
*/
|
|
1001
1251
|
async setVisibility(path, isPublic) {
|
|
1002
|
-
const result = await this.http.request(
|
|
1252
|
+
const result = await this.http.request(STORAGE.visibility, {
|
|
1003
1253
|
method: "PATCH",
|
|
1004
1254
|
body: { path, isPublic },
|
|
1005
1255
|
headers: this.authHeaders
|
|
@@ -1012,53 +1262,95 @@ var StorageManager = class {
|
|
|
1012
1262
|
};
|
|
1013
1263
|
}
|
|
1014
1264
|
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
1265
|
+
/**
|
|
1266
|
+
* Create a folder (empty GCS object used as a prefix marker).
|
|
1267
|
+
*
|
|
1268
|
+
* Server: POST /storage/folder
|
|
1269
|
+
* Body: { path }
|
|
1270
|
+
*/
|
|
1015
1271
|
async createFolder(path) {
|
|
1016
|
-
const result = await this.http.request(
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1272
|
+
const result = await this.http.request(STORAGE.folder, {
|
|
1273
|
+
method: "POST",
|
|
1274
|
+
body: { path },
|
|
1275
|
+
headers: this.authHeaders
|
|
1276
|
+
});
|
|
1020
1277
|
return { path: result.path };
|
|
1021
1278
|
}
|
|
1022
1279
|
// ─── File Operations ──────────────────────────────────────────────────────
|
|
1280
|
+
/**
|
|
1281
|
+
* Permanently delete a file.
|
|
1282
|
+
*
|
|
1283
|
+
* Server: DELETE /storage/file
|
|
1284
|
+
* Body: { path }
|
|
1285
|
+
*/
|
|
1023
1286
|
async deleteFile(path) {
|
|
1024
|
-
await this.http.request(
|
|
1287
|
+
await this.http.request(STORAGE.file, {
|
|
1025
1288
|
method: "DELETE",
|
|
1026
1289
|
body: { path },
|
|
1027
1290
|
headers: this.authHeaders
|
|
1028
1291
|
});
|
|
1029
1292
|
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Recursively delete a folder and all its contents.
|
|
1295
|
+
*
|
|
1296
|
+
* Server: DELETE /storage/folder
|
|
1297
|
+
* Body: { path }
|
|
1298
|
+
*/
|
|
1030
1299
|
async deleteFolder(path) {
|
|
1031
|
-
await this.http.request(
|
|
1300
|
+
await this.http.request(STORAGE.folderDelete, {
|
|
1032
1301
|
method: "DELETE",
|
|
1033
1302
|
body: { path },
|
|
1034
1303
|
headers: this.authHeaders
|
|
1035
1304
|
});
|
|
1036
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Move (rename) a file.
|
|
1308
|
+
*
|
|
1309
|
+
* Server: POST /storage/move
|
|
1310
|
+
* Body: { from, to }
|
|
1311
|
+
*/
|
|
1037
1312
|
async move(from, to) {
|
|
1038
|
-
const result = await this.http.request(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1313
|
+
const result = await this.http.request(STORAGE.move, {
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
body: { from, to },
|
|
1316
|
+
headers: this.authHeaders
|
|
1317
|
+
});
|
|
1042
1318
|
return { from: result.from, to: result.to };
|
|
1043
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Copy a file to a new path.
|
|
1322
|
+
*
|
|
1323
|
+
* Server: POST /storage/copy
|
|
1324
|
+
* Body: { from, to }
|
|
1325
|
+
*/
|
|
1044
1326
|
async copy(from, to) {
|
|
1045
|
-
const result = await this.http.request(
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1327
|
+
const result = await this.http.request(STORAGE.copy, {
|
|
1328
|
+
method: "POST",
|
|
1329
|
+
body: { from, to },
|
|
1330
|
+
headers: this.authHeaders
|
|
1331
|
+
});
|
|
1049
1332
|
return { from: result.from, to: result.to };
|
|
1050
1333
|
}
|
|
1051
1334
|
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
1335
|
+
/**
|
|
1336
|
+
* Get usage statistics for this storage key.
|
|
1337
|
+
*
|
|
1338
|
+
* Server: GET /storage/stats
|
|
1339
|
+
*/
|
|
1052
1340
|
async getStats() {
|
|
1053
|
-
const result = await this.http.request(
|
|
1341
|
+
const result = await this.http.request(STORAGE.stats, {
|
|
1054
1342
|
method: "GET",
|
|
1055
1343
|
headers: this.authHeaders
|
|
1056
1344
|
});
|
|
1057
1345
|
return result.stats;
|
|
1058
1346
|
}
|
|
1059
|
-
|
|
1347
|
+
/**
|
|
1348
|
+
* Get server info (no auth required).
|
|
1349
|
+
*
|
|
1350
|
+
* Server: GET /storage/info
|
|
1351
|
+
*/
|
|
1060
1352
|
async info() {
|
|
1061
|
-
return this.http.request(
|
|
1353
|
+
return this.http.request(STORAGE.info, {
|
|
1062
1354
|
method: "GET"
|
|
1063
1355
|
});
|
|
1064
1356
|
}
|
|
@@ -1108,7 +1400,8 @@ var ScopedStorage = class _ScopedStorage {
|
|
|
1108
1400
|
}
|
|
1109
1401
|
// ─── List ─────────────────────────────────────────────────────────────────
|
|
1110
1402
|
/**
|
|
1111
|
-
* List files within this scope.
|
|
1403
|
+
* List files within this scope.
|
|
1404
|
+
* The `prefix` option is relative to the scope root.
|
|
1112
1405
|
*
|
|
1113
1406
|
* @example
|
|
1114
1407
|
* ```ts
|
|
@@ -1160,7 +1453,7 @@ var ScopedStorage = class _ScopedStorage {
|
|
|
1160
1453
|
*
|
|
1161
1454
|
* @example
|
|
1162
1455
|
* ```ts
|
|
1163
|
-
* const user
|
|
1456
|
+
* const user = storage.scope('users/alice/');
|
|
1164
1457
|
* const userDocs = user.scope('docs/');
|
|
1165
1458
|
* // Effective prefix: users/alice/docs/
|
|
1166
1459
|
* ```
|
|
@@ -1174,17 +1467,16 @@ var ScopedStorage = class _ScopedStorage {
|
|
|
1174
1467
|
var HydrousClient = class {
|
|
1175
1468
|
constructor(config) {
|
|
1176
1469
|
this._recordsCache = /* @__PURE__ */ new Map();
|
|
1177
|
-
this._authCache = /* @__PURE__ */ new Map();
|
|
1178
1470
|
this._analyticsCache = /* @__PURE__ */ new Map();
|
|
1179
1471
|
this._storageCache = /* @__PURE__ */ new Map();
|
|
1180
1472
|
if (!config.authKey) {
|
|
1181
|
-
throw new Error("[HydrousDB] authKey is required.
|
|
1473
|
+
throw new Error("[HydrousDB] authKey is required.");
|
|
1182
1474
|
}
|
|
1183
1475
|
if (!config.bucketSecurityKey) {
|
|
1184
|
-
throw new Error("[HydrousDB] bucketSecurityKey is required.
|
|
1476
|
+
throw new Error("[HydrousDB] bucketSecurityKey is required.");
|
|
1185
1477
|
}
|
|
1186
1478
|
if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
|
|
1187
|
-
throw new Error("[HydrousDB] storageKeys
|
|
1479
|
+
throw new Error("[HydrousDB] storageKeys must define at least one key.");
|
|
1188
1480
|
}
|
|
1189
1481
|
this.http = new HttpClient(config.baseUrl ?? DEFAULT_BASE_URL);
|
|
1190
1482
|
this.authKey_ = config.authKey;
|
|
@@ -1193,16 +1485,13 @@ var HydrousClient = class {
|
|
|
1193
1485
|
}
|
|
1194
1486
|
// ─── Records ──────────────────────────────────────────────────────────────
|
|
1195
1487
|
/**
|
|
1196
|
-
* Get a typed
|
|
1488
|
+
* Get a typed RecordsClient for a bucket.
|
|
1197
1489
|
*
|
|
1198
1490
|
* @example
|
|
1199
1491
|
* ```ts
|
|
1200
1492
|
* interface Post { title: string; published: boolean }
|
|
1201
1493
|
* const posts = db.records<Post>('blog-posts');
|
|
1202
|
-
* const post = await posts.create(
|
|
1203
|
-
* { title: 'Hello', published: false },
|
|
1204
|
-
* { queryableFields: ['published'] },
|
|
1205
|
-
* );
|
|
1494
|
+
* const post = await posts.create({ title: 'Hello', published: false });
|
|
1206
1495
|
* ```
|
|
1207
1496
|
*/
|
|
1208
1497
|
records(bucketKey) {
|
|
@@ -1216,28 +1505,28 @@ var HydrousClient = class {
|
|
|
1216
1505
|
}
|
|
1217
1506
|
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
1218
1507
|
/**
|
|
1219
|
-
* Get
|
|
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.
|
|
1220
1511
|
*
|
|
1221
1512
|
* @example
|
|
1222
1513
|
* ```ts
|
|
1223
|
-
* const
|
|
1224
|
-
* const { user, session } = await auth.login({ email: '…', password: '…' });
|
|
1514
|
+
* const { user, session } = await db.auth().signup({ email: 'alice@example.com', password: 'pw' });
|
|
1225
1515
|
* ```
|
|
1226
1516
|
*/
|
|
1227
|
-
auth(
|
|
1228
|
-
if (!this.
|
|
1229
|
-
this.
|
|
1517
|
+
auth() {
|
|
1518
|
+
if (!this._authClient) {
|
|
1519
|
+
this._authClient = new AuthClient(this.http, this.authKey_);
|
|
1230
1520
|
}
|
|
1231
|
-
return this.
|
|
1521
|
+
return this._authClient;
|
|
1232
1522
|
}
|
|
1233
1523
|
// ─── Analytics ────────────────────────────────────────────────────────────
|
|
1234
1524
|
/**
|
|
1235
|
-
* Get an
|
|
1525
|
+
* Get an AnalyticsClient for a bucket.
|
|
1236
1526
|
*
|
|
1237
1527
|
* @example
|
|
1238
1528
|
* ```ts
|
|
1239
|
-
* const
|
|
1240
|
-
* const { count } = await analytics.count();
|
|
1529
|
+
* const { count } = await db.analytics('orders').count();
|
|
1241
1530
|
* ```
|
|
1242
1531
|
*/
|
|
1243
1532
|
analytics(bucketKey) {
|
|
@@ -1251,18 +1540,19 @@ var HydrousClient = class {
|
|
|
1251
1540
|
}
|
|
1252
1541
|
// ─── Storage ──────────────────────────────────────────────────────────────
|
|
1253
1542
|
/**
|
|
1254
|
-
* Get a
|
|
1255
|
-
* The name must match a key
|
|
1543
|
+
* Get a StorageManager for a named storage key.
|
|
1544
|
+
* The name must match a key in the `storageKeys` config object.
|
|
1256
1545
|
*
|
|
1257
|
-
* Attach `.scope(prefix)` to
|
|
1546
|
+
* Attach `.scope('prefix/')` to get a path-prefixed ScopedStorage.
|
|
1258
1547
|
*
|
|
1259
1548
|
* @example
|
|
1260
1549
|
* ```ts
|
|
1261
|
-
* const avatars
|
|
1262
|
-
* const
|
|
1550
|
+
* const avatars = db.storage('avatars');
|
|
1551
|
+
* const result = await avatars.upload(file, 'alice.jpg', { isPublic: true });
|
|
1263
1552
|
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1553
|
+
* // Scoped:
|
|
1554
|
+
* const userFiles = db.storage('documents').scope(`users/${userId}/`);
|
|
1555
|
+
* await userFiles.upload(pdf, 'contract.pdf');
|
|
1266
1556
|
* ```
|
|
1267
1557
|
*/
|
|
1268
1558
|
storage(keyName) {
|
|
@@ -1288,15 +1578,21 @@ function createClient(config) {
|
|
|
1288
1578
|
return new HydrousClient(config);
|
|
1289
1579
|
}
|
|
1290
1580
|
|
|
1581
|
+
exports.ANALYTICS = ANALYTICS;
|
|
1582
|
+
exports.AUTH = AUTH;
|
|
1291
1583
|
exports.AnalyticsClient = AnalyticsClient;
|
|
1292
1584
|
exports.AnalyticsError = AnalyticsError;
|
|
1293
1585
|
exports.AuthClient = AuthClient;
|
|
1294
1586
|
exports.AuthError = AuthError;
|
|
1587
|
+
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
1588
|
+
exports.HttpClient = HttpClient;
|
|
1295
1589
|
exports.HydrousClient = HydrousClient;
|
|
1296
1590
|
exports.HydrousError = HydrousError;
|
|
1297
1591
|
exports.NetworkError = NetworkError;
|
|
1592
|
+
exports.RECORDS = RECORDS;
|
|
1298
1593
|
exports.RecordError = RecordError;
|
|
1299
1594
|
exports.RecordsClient = RecordsClient;
|
|
1595
|
+
exports.STORAGE = STORAGE;
|
|
1300
1596
|
exports.ScopedStorage = ScopedStorage;
|
|
1301
1597
|
exports.StorageError = StorageError;
|
|
1302
1598
|
exports.StorageManager = StorageManager;
|