hydrousdb 3.0.2 → 3.2.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 +877 -499
- package/dist/index.cjs +539 -427
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -325
- package/dist/index.d.ts +292 -325
- package/dist/{index.js → index.mjs} +541 -429
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -6
- 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 method ──────────────────────────────────────────────────
|
|
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,87 +83,100 @@ 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
101
|
throw new HydrousError(
|
|
122
|
-
|
|
102
|
+
`Failed to parse response JSON`,
|
|
123
103
|
"PARSE_ERROR",
|
|
124
104
|
response.status
|
|
125
105
|
);
|
|
126
106
|
}
|
|
127
107
|
if (!response.ok) {
|
|
128
|
-
const errData = responseData;
|
|
129
108
|
throw new HydrousError(
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
data["error"] || data["message"] || `Request failed with status ${response.status}`,
|
|
110
|
+
data["code"] || "REQUEST_FAILED",
|
|
132
111
|
response.status,
|
|
133
|
-
|
|
134
|
-
|
|
112
|
+
data["requestId"],
|
|
113
|
+
data["details"]
|
|
135
114
|
);
|
|
136
115
|
}
|
|
137
|
-
return
|
|
116
|
+
return data;
|
|
138
117
|
}
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
// ─── Convenience wrappers ─────────────────────────────────────────────────
|
|
119
|
+
get(path, apiKey, extraHeaders) {
|
|
120
|
+
return this.request(path, {
|
|
121
|
+
method: "GET",
|
|
122
|
+
headers: apiKey ? { "X-Api-Key": apiKey, ...extraHeaders } : { ...extraHeaders }
|
|
123
|
+
});
|
|
141
124
|
}
|
|
142
|
-
post(path, apiKey, body
|
|
143
|
-
return this.request(path,
|
|
125
|
+
post(path, apiKey, body) {
|
|
126
|
+
return this.request(path, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body,
|
|
129
|
+
headers: { "X-Api-Key": apiKey }
|
|
130
|
+
});
|
|
144
131
|
}
|
|
145
|
-
put(path, apiKey, body
|
|
146
|
-
return this.request(path,
|
|
132
|
+
put(path, apiKey, body) {
|
|
133
|
+
return this.request(path, {
|
|
134
|
+
method: "PUT",
|
|
135
|
+
body,
|
|
136
|
+
headers: { "X-Api-Key": apiKey }
|
|
137
|
+
});
|
|
147
138
|
}
|
|
148
|
-
patch(path, apiKey, body
|
|
149
|
-
return this.request(path,
|
|
139
|
+
patch(path, apiKey, body) {
|
|
140
|
+
return this.request(path, {
|
|
141
|
+
method: "PATCH",
|
|
142
|
+
body,
|
|
143
|
+
headers: { "X-Api-Key": apiKey }
|
|
144
|
+
});
|
|
150
145
|
}
|
|
151
|
-
delete(path, apiKey, body
|
|
152
|
-
return this.request(path,
|
|
146
|
+
delete(path, apiKey, body) {
|
|
147
|
+
return this.request(path, {
|
|
148
|
+
method: "DELETE",
|
|
149
|
+
body,
|
|
150
|
+
headers: { "X-Api-Key": apiKey }
|
|
151
|
+
});
|
|
153
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Upload directly to a signed GCS URL (no auth headers — signed URL is self-authenticating).
|
|
155
|
+
* Supports optional progress tracking via XHR in browser environments.
|
|
156
|
+
*/
|
|
154
157
|
async putToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
const xhr = new
|
|
158
|
+
const body = data instanceof Blob ? data : data instanceof Uint8Array ? data.buffer : data;
|
|
159
|
+
if (typeof XMLHttpRequest !== "undefined" && typeof onProgress === "function") {
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
const xhr = new XMLHttpRequest();
|
|
159
162
|
xhr.upload.onprogress = (e) => {
|
|
160
|
-
if (e.lengthComputable)
|
|
161
|
-
onProgress(Math.round(e.loaded / e.total * 100));
|
|
162
|
-
}
|
|
163
|
+
if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
|
|
163
164
|
};
|
|
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"));
|
|
165
|
+
xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`GCS upload failed: ${xhr.status}`));
|
|
166
|
+
xhr.onerror = () => reject(new Error("GCS upload network error"));
|
|
172
167
|
xhr.open("PUT", signedUrl);
|
|
173
168
|
xhr.setRequestHeader("Content-Type", mimeType);
|
|
174
|
-
|
|
175
|
-
xhr.send(payload);
|
|
169
|
+
xhr.send(body);
|
|
176
170
|
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
} else {
|
|
172
|
+
const res = await fetch(signedUrl, {
|
|
173
|
+
method: "PUT",
|
|
174
|
+
headers: { "Content-Type": mimeType },
|
|
175
|
+
body
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
throw new HydrousError(`GCS upload failed: ${res.status}`, "GCS_UPLOAD_FAILED", res.status);
|
|
179
|
+
}
|
|
186
180
|
}
|
|
187
181
|
}
|
|
188
182
|
};
|
|
@@ -197,8 +191,9 @@ var AuthClient = class {
|
|
|
197
191
|
post(path, body) {
|
|
198
192
|
return this.http.post(path, this.authKey, body);
|
|
199
193
|
}
|
|
200
|
-
get(path) {
|
|
201
|
-
|
|
194
|
+
get(path, query) {
|
|
195
|
+
const qs = query && Object.keys(query).length ? "?" + new URLSearchParams(query).toString() : "";
|
|
196
|
+
return this.http.get(`${path}${qs}`, this.authKey);
|
|
202
197
|
}
|
|
203
198
|
patch(path, body) {
|
|
204
199
|
return this.http.patch(path, this.authKey, body);
|
|
@@ -207,57 +202,136 @@ var AuthClient = class {
|
|
|
207
202
|
return this.http.delete(path, this.authKey, body);
|
|
208
203
|
}
|
|
209
204
|
// ─── Registration & Login ─────────────────────────────────────────────────
|
|
205
|
+
// Server: POST /auth/:bucket/signup → { user, session }
|
|
206
|
+
// Server: POST /auth/:bucket/signin → { user, session }
|
|
207
|
+
// Server: POST /auth/:bucket/signout → { success, message }
|
|
210
208
|
async signup(options) {
|
|
211
209
|
const result = await this.post(`${this.basePath}/signup`, options);
|
|
212
|
-
|
|
210
|
+
const user = result.user ?? result.data;
|
|
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
|
+
};
|
|
213
223
|
}
|
|
214
224
|
async login(options) {
|
|
215
|
-
const result = await this.post(`${this.basePath}/
|
|
216
|
-
|
|
225
|
+
const result = await this.post(`${this.basePath}/signin`, options);
|
|
226
|
+
const user = result.user ?? result.data;
|
|
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
|
+
};
|
|
217
239
|
}
|
|
218
|
-
async logout(
|
|
219
|
-
await this.post(`${this.basePath}/
|
|
240
|
+
async logout(options) {
|
|
241
|
+
await this.post(`${this.basePath}/signout`, options);
|
|
220
242
|
}
|
|
221
243
|
async refreshSession({ refreshToken }) {
|
|
222
|
-
const result = await this.post(
|
|
223
|
-
|
|
244
|
+
const result = await this.post(
|
|
245
|
+
`${this.basePath}/session/refresh`,
|
|
246
|
+
{ refreshToken }
|
|
247
|
+
);
|
|
248
|
+
const s = result.session;
|
|
249
|
+
return {
|
|
250
|
+
sessionId: s.sessionId,
|
|
251
|
+
userId: result.data?.id ?? "",
|
|
252
|
+
bucketId: this.basePath.split("/")[2],
|
|
253
|
+
createdAt: Date.now(),
|
|
254
|
+
expiresAt: s.expiresAt,
|
|
255
|
+
refreshToken: s.refreshToken,
|
|
256
|
+
refreshExpiresAt: s.expiresAt
|
|
257
|
+
};
|
|
258
|
+
}
|
|
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 };
|
|
224
265
|
}
|
|
225
266
|
// ─── User Profile ─────────────────────────────────────────────────────────
|
|
267
|
+
// Server: GET /auth/:bucket/user?userId=... → { success, data: UserRecord }
|
|
268
|
+
// Server: PATCH /auth/:bucket/user body: { sessionId, userId, updates: {...} }
|
|
269
|
+
// Server: DELETE /auth/:bucket/user?userId=... header/body: sessionId
|
|
226
270
|
async getUser({ userId }) {
|
|
227
|
-
const result = await this.get(`${this.basePath}/user
|
|
228
|
-
return result.
|
|
271
|
+
const result = await this.get(`${this.basePath}/user`, { userId });
|
|
272
|
+
return result.data;
|
|
229
273
|
}
|
|
230
274
|
async updateUser(options) {
|
|
231
|
-
const { sessionId, userId,
|
|
232
|
-
const result = await this.patch(
|
|
233
|
-
|
|
275
|
+
const { sessionId, userId, updates } = options;
|
|
276
|
+
const result = await this.patch(
|
|
277
|
+
`${this.basePath}/user`,
|
|
278
|
+
{ sessionId, userId, updates }
|
|
279
|
+
);
|
|
280
|
+
return result.data;
|
|
234
281
|
}
|
|
235
282
|
async deleteUser({ sessionId, userId }) {
|
|
236
|
-
await this.
|
|
283
|
+
await this.http.request(`${this.basePath}/user?userId=${encodeURIComponent(userId)}`, {
|
|
284
|
+
method: "DELETE",
|
|
285
|
+
body: { sessionId },
|
|
286
|
+
headers: { "X-Api-Key": this.authKey }
|
|
287
|
+
});
|
|
237
288
|
}
|
|
238
289
|
// ─── Admin Operations ─────────────────────────────────────────────────────
|
|
290
|
+
// Server: GET /auth/:bucket/users?limit=&cursor= → { data: UserRecord[], meta: { hasMore, nextCursor } }
|
|
291
|
+
// Server: DELETE /auth/:bucket/users/bulk body: { userIds, hard?, sessionId }
|
|
292
|
+
// Server: DELETE /auth/:bucket/user/hard?userId=... body: { sessionId }
|
|
239
293
|
async listUsers(options) {
|
|
240
|
-
const { sessionId, limit = 50,
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
294
|
+
const { sessionId, limit = 50, cursor } = options;
|
|
295
|
+
const params = { limit: String(limit) };
|
|
296
|
+
if (cursor) params["cursor"] = cursor;
|
|
297
|
+
const result = await this.http.request(
|
|
298
|
+
`${this.basePath}/users?${new URLSearchParams(params)}`,
|
|
299
|
+
{
|
|
300
|
+
method: "GET",
|
|
301
|
+
headers: { "X-Api-Key": this.authKey, "X-Session-Id": sessionId }
|
|
302
|
+
}
|
|
244
303
|
);
|
|
245
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
users: result.data ?? result.users ?? [],
|
|
306
|
+
hasMore: result.meta?.hasMore ?? result.hasMore ?? false,
|
|
307
|
+
nextCursor: result.meta?.nextCursor ?? result.nextCursor ?? null
|
|
308
|
+
};
|
|
246
309
|
}
|
|
247
310
|
async hardDeleteUser({ sessionId, userId }) {
|
|
248
|
-
await this.
|
|
311
|
+
await this.http.request(
|
|
312
|
+
`${this.basePath}/user/hard?userId=${encodeURIComponent(userId)}`,
|
|
313
|
+
{
|
|
314
|
+
method: "DELETE",
|
|
315
|
+
body: { sessionId },
|
|
316
|
+
headers: { "X-Api-Key": this.authKey }
|
|
317
|
+
}
|
|
318
|
+
);
|
|
249
319
|
}
|
|
250
|
-
async bulkDeleteUsers(
|
|
251
|
-
const result = await this.
|
|
252
|
-
`${this.basePath}/users/bulk
|
|
253
|
-
{
|
|
320
|
+
async bulkDeleteUsers(options) {
|
|
321
|
+
const result = await this.http.request(
|
|
322
|
+
`${this.basePath}/users/bulk`,
|
|
323
|
+
{
|
|
324
|
+
method: "DELETE",
|
|
325
|
+
body: { userIds: options.userIds, hard: options.hard ?? false, sessionId: options.sessionId },
|
|
326
|
+
headers: { "X-Api-Key": this.authKey }
|
|
327
|
+
}
|
|
254
328
|
);
|
|
255
|
-
return {
|
|
329
|
+
return { succeeded: result.meta.succeeded, failed: result.meta.failed };
|
|
256
330
|
}
|
|
257
|
-
async lockAccount(
|
|
331
|
+
async lockAccount(options) {
|
|
258
332
|
const result = await this.post(
|
|
259
333
|
`${this.basePath}/account/lock`,
|
|
260
|
-
|
|
334
|
+
options
|
|
261
335
|
);
|
|
262
336
|
return result.data;
|
|
263
337
|
}
|
|
@@ -265,8 +339,17 @@ var AuthClient = class {
|
|
|
265
339
|
await this.post(`${this.basePath}/account/unlock`, { sessionId, userId });
|
|
266
340
|
}
|
|
267
341
|
// ─── Password Management ──────────────────────────────────────────────────
|
|
342
|
+
// Server: POST /auth/:bucket/password/change body: { sessionId, userId, oldPassword, newPassword }
|
|
343
|
+
// Server: POST /auth/:bucket/password/reset/request body: { email }
|
|
344
|
+
// Server: POST /auth/:bucket/password/reset/confirm body: { resetToken, newPassword }
|
|
268
345
|
async changePassword(options) {
|
|
269
|
-
|
|
346
|
+
const { sessionId, userId, currentPassword, newPassword } = options;
|
|
347
|
+
await this.post(`${this.basePath}/password/change`, {
|
|
348
|
+
sessionId,
|
|
349
|
+
userId,
|
|
350
|
+
oldPassword: currentPassword,
|
|
351
|
+
newPassword
|
|
352
|
+
});
|
|
270
353
|
}
|
|
271
354
|
async requestPasswordReset({ email }) {
|
|
272
355
|
await this.post(`${this.basePath}/password/reset/request`, { email });
|
|
@@ -275,6 +358,8 @@ var AuthClient = class {
|
|
|
275
358
|
await this.post(`${this.basePath}/password/reset/confirm`, { resetToken, newPassword });
|
|
276
359
|
}
|
|
277
360
|
// ─── Email Verification ───────────────────────────────────────────────────
|
|
361
|
+
// Server: POST /auth/:bucket/email/verify/request body: { userId }
|
|
362
|
+
// Server: POST /auth/:bucket/email/verify/confirm body: { verifyToken }
|
|
278
363
|
async requestEmailVerification({ userId }) {
|
|
279
364
|
await this.post(`${this.basePath}/email/verify/request`, { userId });
|
|
280
365
|
}
|
|
@@ -298,9 +383,8 @@ function buildQueryParams(options = {}) {
|
|
|
298
383
|
params.set("startDate", new Date(options.dateRange.start).toISOString().split("T")[0]);
|
|
299
384
|
if (options.dateRange?.end !== void 0)
|
|
300
385
|
params.set("endDate", new Date(options.dateRange.end).toISOString().split("T")[0]);
|
|
301
|
-
if (options.filters && options.filters.length > 0)
|
|
386
|
+
if (options.filters && options.filters.length > 0)
|
|
302
387
|
params.set("filters", JSON.stringify(options.filters));
|
|
303
|
-
}
|
|
304
388
|
const str = params.toString();
|
|
305
389
|
return str ? `?${str}` : "";
|
|
306
390
|
}
|
|
@@ -343,89 +427,179 @@ var RecordsClient = class {
|
|
|
343
427
|
assertSafeName(bucketKey, "bucketKey");
|
|
344
428
|
this.http = http;
|
|
345
429
|
this.bucketKey = bucketKey;
|
|
346
|
-
this.
|
|
430
|
+
this.apiKey = bucketSecurityKey;
|
|
347
431
|
this.basePath = `/records/${bucketKey}`;
|
|
348
432
|
}
|
|
349
|
-
get key() {
|
|
350
|
-
return this.bucketKey_;
|
|
351
|
-
}
|
|
352
433
|
// ─── Single Record Operations ─────────────────────────────────────────────
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Create a new record.
|
|
436
|
+
*
|
|
437
|
+
* @param data The record fields to store.
|
|
438
|
+
* @param options Optional: queryableFields (for server-side filtering),
|
|
439
|
+
* userEmail (audit trail), customRecordId (upsert by ID).
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* const post = await posts.create(
|
|
444
|
+
* { title: 'Hello', status: 'draft', authorId: 'u1' },
|
|
445
|
+
* { queryableFields: ['status', 'authorId'], userEmail: 'alice@example.com' },
|
|
446
|
+
* );
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
async create(data, options = {}) {
|
|
450
|
+
const { queryableFields, userEmail, customRecordId } = options;
|
|
451
|
+
const body = { values: data };
|
|
452
|
+
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
453
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
454
|
+
if (customRecordId) body["customRecordId"] = customRecordId;
|
|
455
|
+
const result = await this.http.post(this.basePath, this.apiKey, body);
|
|
456
|
+
return result.data ?? result.record;
|
|
356
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Get a single record by ID.
|
|
460
|
+
*/
|
|
357
461
|
async get(id) {
|
|
358
|
-
const result = await this.http.get(
|
|
359
|
-
|
|
462
|
+
const result = await this.http.get(
|
|
463
|
+
`${this.basePath}/${encodeURIComponent(id)}`,
|
|
464
|
+
this.apiKey
|
|
465
|
+
);
|
|
466
|
+
return result.data ?? result.record;
|
|
360
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Fully replace a record (PUT semantics).
|
|
470
|
+
*/
|
|
361
471
|
async set(id, data) {
|
|
362
|
-
const result = await this.http.put(
|
|
363
|
-
|
|
472
|
+
const result = await this.http.put(
|
|
473
|
+
`${this.basePath}/${encodeURIComponent(id)}`,
|
|
474
|
+
this.apiKey,
|
|
475
|
+
data
|
|
476
|
+
);
|
|
477
|
+
return result.data ?? result.record;
|
|
364
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Partially update a record (PATCH semantics).
|
|
481
|
+
* By default merges with existing fields; set `merge: false` to replace.
|
|
482
|
+
* Supports write-filter sentinels: `{ __op: 'increment', delta: 1 }` etc.
|
|
483
|
+
*/
|
|
365
484
|
async patch(id, data, options = {}) {
|
|
366
485
|
const { merge = true } = options;
|
|
367
486
|
const result = await this.http.patch(
|
|
368
|
-
`${this.basePath}/${id}`,
|
|
369
|
-
this.
|
|
487
|
+
`${this.basePath}/${encodeURIComponent(id)}`,
|
|
488
|
+
this.apiKey,
|
|
370
489
|
{ ...data, _merge: merge }
|
|
371
490
|
);
|
|
372
|
-
return result.
|
|
491
|
+
return result.data ?? result.record;
|
|
373
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Delete a record permanently.
|
|
495
|
+
*/
|
|
374
496
|
async delete(id) {
|
|
375
|
-
await this.http.delete(
|
|
497
|
+
await this.http.delete(
|
|
498
|
+
`${this.basePath}/${encodeURIComponent(id)}`,
|
|
499
|
+
this.apiKey
|
|
500
|
+
);
|
|
376
501
|
}
|
|
377
502
|
// ─── Batch Operations ─────────────────────────────────────────────────────
|
|
378
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Create multiple records in one request (max 500).
|
|
505
|
+
* Each record can include a `_customRecordId` field for upsert behaviour.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* const created = await posts.batchCreate(
|
|
510
|
+
* [{ title: 'A' }, { title: 'B' }],
|
|
511
|
+
* { queryableFields: ['title'], userEmail: 'alice@example.com' },
|
|
512
|
+
* );
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
async batchCreate(items, options = {}) {
|
|
516
|
+
const { queryableFields, userEmail } = options;
|
|
517
|
+
const body = { records: items };
|
|
518
|
+
if (queryableFields?.length) body["queryableFields"] = queryableFields;
|
|
519
|
+
if (userEmail) body["userEmail"] = userEmail;
|
|
379
520
|
const result = await this.http.post(
|
|
380
|
-
`${this.basePath}/batch`,
|
|
381
|
-
this.
|
|
382
|
-
|
|
521
|
+
`${this.basePath}/batch/insert`,
|
|
522
|
+
this.apiKey,
|
|
523
|
+
body
|
|
383
524
|
);
|
|
384
|
-
return result.records;
|
|
525
|
+
return result.data ?? result.records ?? [];
|
|
385
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Delete multiple records in one request (max 500).
|
|
529
|
+
*/
|
|
386
530
|
async batchDelete(ids) {
|
|
387
531
|
const result = await this.http.post(
|
|
388
532
|
`${this.basePath}/batch/delete`,
|
|
389
|
-
this.
|
|
390
|
-
{ ids }
|
|
533
|
+
this.apiKey,
|
|
534
|
+
{ recordIds: ids }
|
|
391
535
|
);
|
|
392
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
deleted: result.data?.successful ?? result.deleted ?? 0,
|
|
538
|
+
failed: result.data?.failed ?? result.failed ?? []
|
|
539
|
+
};
|
|
393
540
|
}
|
|
394
541
|
// ─── Querying ─────────────────────────────────────────────────────────────
|
|
542
|
+
/**
|
|
543
|
+
* Query records with optional filters, sorting, and pagination.
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* ```ts
|
|
547
|
+
* const { records, hasMore, nextCursor } = await posts.query({
|
|
548
|
+
* filters: [{ field: 'status', op: '==', value: 'published' }],
|
|
549
|
+
* orderBy: 'createdAt',
|
|
550
|
+
* order: 'desc',
|
|
551
|
+
* limit: 20,
|
|
552
|
+
* });
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
395
555
|
async query(options = {}) {
|
|
396
556
|
const qs = buildQueryParams(options);
|
|
397
|
-
const result = await this.http.get(`${this.basePath}${qs}`, this.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
};
|
|
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 };
|
|
404
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Get all records matching the given options (no filter support — use `query()` for filters).
|
|
566
|
+
*/
|
|
405
567
|
async getAll(options = {}) {
|
|
406
568
|
const { records } = await this.query(options);
|
|
407
569
|
return records;
|
|
408
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Count records matching the given filters.
|
|
573
|
+
*/
|
|
409
574
|
async count(filters = []) {
|
|
410
575
|
const result = await this.http.post(
|
|
411
576
|
`${this.basePath}/count`,
|
|
412
|
-
this.
|
|
577
|
+
this.apiKey,
|
|
413
578
|
{ filters }
|
|
414
579
|
);
|
|
415
|
-
return result.count;
|
|
580
|
+
return result.data?.count ?? result.count ?? 0;
|
|
416
581
|
}
|
|
417
582
|
// ─── Version History ──────────────────────────────────────────────────────
|
|
583
|
+
/**
|
|
584
|
+
* Get the version history of a record.
|
|
585
|
+
*/
|
|
418
586
|
async getHistory(id) {
|
|
419
|
-
const result = await this.http.get(
|
|
587
|
+
const result = await this.http.get(
|
|
588
|
+
`${this.basePath}/${encodeURIComponent(id)}?showHistory=true`,
|
|
589
|
+
this.apiKey
|
|
590
|
+
);
|
|
420
591
|
return result.history;
|
|
421
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Restore a record to a previous version.
|
|
595
|
+
*/
|
|
422
596
|
async restoreVersion(id, version) {
|
|
423
597
|
const result = await this.http.post(
|
|
424
|
-
`${this.basePath}/${id}/restore`,
|
|
425
|
-
this.
|
|
598
|
+
`${this.basePath}/${encodeURIComponent(id)}/restore`,
|
|
599
|
+
this.apiKey,
|
|
426
600
|
{ version }
|
|
427
601
|
);
|
|
428
|
-
return result.
|
|
602
|
+
return result.data ?? result.record;
|
|
429
603
|
}
|
|
430
604
|
};
|
|
431
605
|
|
|
@@ -445,39 +619,54 @@ var AnalyticsClient = class {
|
|
|
445
619
|
);
|
|
446
620
|
return result.data;
|
|
447
621
|
}
|
|
622
|
+
// ─── Query methods ────────────────────────────────────────────────────────
|
|
623
|
+
/** Count all records, optionally within a date range. */
|
|
448
624
|
async count(opts = {}) {
|
|
449
625
|
return this.run({ queryType: "count", ...opts });
|
|
450
626
|
}
|
|
627
|
+
/** Get value distribution for a field (e.g. how many records per status). */
|
|
451
628
|
async distribution(opts) {
|
|
452
629
|
assertSafeName(opts.field, "field");
|
|
453
630
|
return this.run({ queryType: "distribution", ...opts });
|
|
454
631
|
}
|
|
632
|
+
/** Sum a numeric field, optionally grouped by another field. */
|
|
455
633
|
async sum(opts) {
|
|
456
634
|
assertSafeName(opts.field, "field");
|
|
457
635
|
if (opts.groupBy) assertSafeName(opts.groupBy, "groupBy");
|
|
458
636
|
return this.run({ queryType: "sum", ...opts });
|
|
459
637
|
}
|
|
638
|
+
/** Count of records over time, bucketed by granularity. */
|
|
460
639
|
async timeSeries(opts = {}) {
|
|
461
640
|
return this.run({ queryType: "timeSeries", granularity: "day", ...opts });
|
|
462
641
|
}
|
|
642
|
+
/** Aggregate a numeric field over time. */
|
|
463
643
|
async fieldTimeSeries(opts) {
|
|
464
644
|
assertSafeName(opts.field, "field");
|
|
465
|
-
return this.run({
|
|
645
|
+
return this.run({
|
|
646
|
+
queryType: "fieldTimeSeries",
|
|
647
|
+
aggregation: "sum",
|
|
648
|
+
granularity: "day",
|
|
649
|
+
...opts
|
|
650
|
+
});
|
|
466
651
|
}
|
|
652
|
+
/** Top N values for a field by count. */
|
|
467
653
|
async topN(opts) {
|
|
468
654
|
assertSafeName(opts.field, "field");
|
|
469
655
|
if (opts.labelField) assertSafeName(opts.labelField, "labelField");
|
|
470
656
|
return this.run({ queryType: "topN", n: 10, order: "desc", ...opts });
|
|
471
657
|
}
|
|
658
|
+
/** Statistical summary (min, max, avg, sum, count, stddev) for a numeric field. */
|
|
472
659
|
async stats(opts) {
|
|
473
660
|
assertSafeName(opts.field, "field");
|
|
474
661
|
return this.run({ queryType: "stats", ...opts });
|
|
475
662
|
}
|
|
663
|
+
/** Fetch filtered records via the analytics engine (bypasses Firestore pagination). */
|
|
476
664
|
async records(opts = {}) {
|
|
477
665
|
if (opts.orderBy) assertSafeName(opts.orderBy, "orderBy");
|
|
478
666
|
if (opts.selectFields) opts.selectFields.forEach((f) => assertSafeName(f, "selectField"));
|
|
479
667
|
return this.run({ queryType: "records", limit: 100, order: "desc", ...opts });
|
|
480
668
|
}
|
|
669
|
+
/** Compute multiple aggregations in a single request. */
|
|
481
670
|
async multiMetric(opts) {
|
|
482
671
|
opts.metrics.forEach((m) => {
|
|
483
672
|
assertSafeName(m.field, "metric.field");
|
|
@@ -485,14 +674,22 @@ var AnalyticsClient = class {
|
|
|
485
674
|
});
|
|
486
675
|
return this.run({ queryType: "multiMetric", ...opts });
|
|
487
676
|
}
|
|
677
|
+
/** Storage usage stats for the bucket (record count, bytes, avg/min/max size). */
|
|
488
678
|
async storageStats(opts = {}) {
|
|
489
679
|
return this.run({ queryType: "storageStats", ...opts });
|
|
490
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Compare a metric across multiple buckets in one query.
|
|
683
|
+
* The caller's key must have read access to every bucket in `bucketKeys`.
|
|
684
|
+
*/
|
|
491
685
|
async crossBucket(opts) {
|
|
492
686
|
assertSafeName(opts.field, "field");
|
|
493
687
|
opts.bucketKeys.forEach((k) => assertSafeName(k, "bucketKey"));
|
|
494
688
|
return this.run({ queryType: "crossBucket", aggregation: "sum", ...opts });
|
|
495
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Raw query — use this when none of the typed helpers cover your use case.
|
|
692
|
+
*/
|
|
496
693
|
async query(query) {
|
|
497
694
|
const data = await this.run(query);
|
|
498
695
|
return { queryType: query.queryType, data };
|
|
@@ -500,34 +697,34 @@ var AnalyticsClient = class {
|
|
|
500
697
|
};
|
|
501
698
|
|
|
502
699
|
// src/storage/manager.ts
|
|
700
|
+
function toUploadResult(r) {
|
|
701
|
+
return {
|
|
702
|
+
path: r.path,
|
|
703
|
+
mimeType: r.mimeType,
|
|
704
|
+
size: r.size,
|
|
705
|
+
isPublic: r.isPublic,
|
|
706
|
+
publicUrl: r.publicUrl,
|
|
707
|
+
downloadUrl: r.downloadUrl
|
|
708
|
+
};
|
|
709
|
+
}
|
|
503
710
|
var StorageManager = class {
|
|
504
711
|
constructor(http, storageKey) {
|
|
505
712
|
this.basePath = "/storage";
|
|
506
713
|
this.http = http;
|
|
507
714
|
this.storageKey = storageKey;
|
|
508
715
|
}
|
|
509
|
-
/** Headers for all storage requests — uses X-Storage-Key, not X-Api-Key. */
|
|
510
716
|
get authHeaders() {
|
|
511
717
|
return { "X-Storage-Key": this.storageKey };
|
|
512
718
|
}
|
|
513
719
|
// ─── Upload: Simple (server-buffered) ────────────────────────────────────
|
|
514
720
|
/**
|
|
515
721
|
* Upload a file to storage in one step (server-buffered, up to 500 MB).
|
|
516
|
-
* For files >10 MB or when
|
|
517
|
-
*
|
|
518
|
-
* @param data File data as a Blob, Buffer, Uint8Array, or ArrayBuffer.
|
|
519
|
-
* @param path Destination path in your storage (e.g. `"avatars/alice.jpg"`).
|
|
520
|
-
* @param options Upload options: isPublic, overwrite, mimeType.
|
|
722
|
+
* For files >10 MB or when upload progress is needed, use `getUploadUrl()`.
|
|
521
723
|
*
|
|
522
724
|
* @example
|
|
523
725
|
* ```ts
|
|
524
|
-
* // Upload a public avatar
|
|
525
726
|
* 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
|
|
727
|
+
* console.log(result.publicUrl);
|
|
531
728
|
* ```
|
|
532
729
|
*/
|
|
533
730
|
async upload(data, path, options = {}) {
|
|
@@ -545,100 +742,67 @@ var StorageManager = class {
|
|
|
545
742
|
rawBody: formData,
|
|
546
743
|
headers: this.authHeaders
|
|
547
744
|
});
|
|
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
|
-
};
|
|
745
|
+
return toUploadResult(result);
|
|
556
746
|
}
|
|
557
747
|
/**
|
|
558
|
-
* Upload raw JSON or plain
|
|
748
|
+
* Upload raw JSON or plain-text data as a file.
|
|
559
749
|
*
|
|
560
750
|
* @example
|
|
561
751
|
* ```ts
|
|
562
|
-
*
|
|
563
|
-
* { config: { theme: 'dark' } },
|
|
564
|
-
* 'settings/user-config.json',
|
|
565
|
-
* { isPublic: false },
|
|
566
|
-
* );
|
|
752
|
+
* await storage.uploadRaw({ theme: 'dark' }, 'settings/config.json');
|
|
567
753
|
* ```
|
|
568
754
|
*/
|
|
569
755
|
async uploadRaw(data, path, options = {}) {
|
|
570
756
|
const { isPublic = false, overwrite = false, mimeType = "application/json" } = options;
|
|
571
|
-
const
|
|
757
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
572
758
|
const result = await this.http.request(`${this.basePath}/upload-raw`, {
|
|
573
759
|
method: "POST",
|
|
574
|
-
body: { path,
|
|
760
|
+
body: { path, content, mimeType, isPublic, overwrite },
|
|
575
761
|
headers: this.authHeaders
|
|
576
762
|
});
|
|
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
|
-
};
|
|
763
|
+
return toUploadResult(result);
|
|
585
764
|
}
|
|
586
765
|
// ─── Upload: Direct-to-GCS (recommended for large files) ─────────────────
|
|
587
766
|
/**
|
|
588
|
-
* Step 1
|
|
589
|
-
*
|
|
767
|
+
* Step 1 — get a signed GCS URL to upload directly from the client.
|
|
768
|
+
* Supports upload progress via `uploadToSignedUrl()`.
|
|
590
769
|
*
|
|
591
770
|
* @example
|
|
592
771
|
* ```ts
|
|
593
772
|
* const { uploadUrl, path: confirmedPath } = await storage.getUploadUrl({
|
|
594
|
-
* path:
|
|
773
|
+
* path: 'videos/intro.mp4',
|
|
595
774
|
* mimeType: 'video/mp4',
|
|
596
|
-
* size:
|
|
775
|
+
* size: file.size,
|
|
597
776
|
* isPublic: true,
|
|
598
777
|
* });
|
|
599
|
-
*
|
|
600
|
-
* // Upload using XHR for progress tracking
|
|
601
|
-
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (pct) => {
|
|
602
|
-
* console.log(`${pct}% uploaded`);
|
|
603
|
-
* });
|
|
604
|
-
*
|
|
605
|
-
* // Step 3: confirm
|
|
778
|
+
* await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', pct => console.log(pct + '%'));
|
|
606
779
|
* const result = await storage.confirmUpload({ path: confirmedPath, mimeType: 'video/mp4', isPublic: true });
|
|
607
780
|
* ```
|
|
608
781
|
*/
|
|
609
782
|
async getUploadUrl(opts) {
|
|
783
|
+
const { expiresInSeconds, ...rest } = opts;
|
|
610
784
|
const result = await this.http.request(`${this.basePath}/upload-url`, {
|
|
611
785
|
method: "POST",
|
|
612
|
-
body: { isPublic: false, overwrite: false,
|
|
786
|
+
body: { isPublic: false, overwrite: false, expiresIn: expiresInSeconds ?? 900, ...rest },
|
|
613
787
|
headers: this.authHeaders
|
|
614
788
|
});
|
|
615
|
-
return
|
|
789
|
+
return {
|
|
790
|
+
uploadUrl: result.uploadUrl,
|
|
791
|
+
path: result.path,
|
|
792
|
+
mimeType: result.mimeType,
|
|
793
|
+
expiresAt: result.expiresAt,
|
|
794
|
+
expiresIn: result.expiresIn
|
|
795
|
+
};
|
|
616
796
|
}
|
|
617
797
|
/**
|
|
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.
|
|
798
|
+
* Step 2 — upload data directly to a signed GCS URL.
|
|
799
|
+
* Supports progress tracking in browser environments.
|
|
625
800
|
*/
|
|
626
801
|
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
627
802
|
await this.http.putToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
628
803
|
}
|
|
629
804
|
/**
|
|
630
|
-
* Step 3
|
|
631
|
-
* Confirm a direct upload and register metadata on the server.
|
|
632
|
-
*
|
|
633
|
-
* @example
|
|
634
|
-
* ```ts
|
|
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
|
-
* ```
|
|
805
|
+
* Step 3 — confirm a direct upload and register metadata on the server.
|
|
642
806
|
*/
|
|
643
807
|
async confirmUpload(opts) {
|
|
644
808
|
const result = await this.http.request(`${this.basePath}/confirm`, {
|
|
@@ -646,27 +810,19 @@ var StorageManager = class {
|
|
|
646
810
|
body: { isPublic: false, ...opts },
|
|
647
811
|
headers: this.authHeaders
|
|
648
812
|
});
|
|
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
|
-
};
|
|
813
|
+
return toUploadResult(result);
|
|
657
814
|
}
|
|
658
815
|
// ─── Batch Upload ─────────────────────────────────────────────────────────
|
|
659
816
|
/**
|
|
660
|
-
* Get signed upload URLs for multiple files at once.
|
|
817
|
+
* Get signed upload URLs for multiple files at once (max 50).
|
|
818
|
+
* Server returns `{ succeeded, failed }` — only succeeded items have URLs.
|
|
661
819
|
*
|
|
662
820
|
* @example
|
|
663
821
|
* ```ts
|
|
664
822
|
* const { files } = await storage.getBatchUploadUrls([
|
|
665
|
-
* { path: 'images/
|
|
666
|
-
* { path: 'images/
|
|
823
|
+
* { path: 'images/a.jpg', mimeType: 'image/jpeg', size: 204800 },
|
|
824
|
+
* { path: 'images/b.jpg', mimeType: 'image/jpeg', size: 153600 },
|
|
667
825
|
* ]);
|
|
668
|
-
*
|
|
669
|
-
* // Upload each file and confirm
|
|
670
826
|
* for (const f of files) {
|
|
671
827
|
* await storage.uploadToSignedUrl(f.uploadUrl, blobs[f.index], f.mimeType);
|
|
672
828
|
* await storage.confirmUpload({ path: f.path, mimeType: f.mimeType });
|
|
@@ -678,37 +834,35 @@ var StorageManager = class {
|
|
|
678
834
|
`${this.basePath}/batch-upload-urls`,
|
|
679
835
|
{ method: "POST", body: { files }, headers: this.authHeaders }
|
|
680
836
|
);
|
|
681
|
-
return {
|
|
837
|
+
return {
|
|
838
|
+
files: result.succeeded.map((f) => ({
|
|
839
|
+
index: f.index,
|
|
840
|
+
uploadUrl: f.uploadUrl,
|
|
841
|
+
path: f.path,
|
|
842
|
+
mimeType: f.mimeType,
|
|
843
|
+
expiresAt: f.expiresAt,
|
|
844
|
+
expiresIn: f.expiresIn
|
|
845
|
+
}))
|
|
846
|
+
};
|
|
682
847
|
}
|
|
683
848
|
/**
|
|
684
849
|
* Confirm multiple direct uploads at once.
|
|
850
|
+
* Returns both succeeded and failed results.
|
|
685
851
|
*/
|
|
686
852
|
async batchConfirmUploads(items) {
|
|
687
853
|
const result = await this.http.request(
|
|
688
854
|
`${this.basePath}/batch-confirm`,
|
|
689
855
|
{ method: "POST", body: { files: items }, headers: this.authHeaders }
|
|
690
856
|
);
|
|
691
|
-
return
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
isPublic: r.isPublic,
|
|
696
|
-
publicUrl: r.publicUrl,
|
|
697
|
-
downloadUrl: r.downloadUrl
|
|
698
|
-
}));
|
|
857
|
+
return {
|
|
858
|
+
succeeded: result.succeeded.map(toUploadResult),
|
|
859
|
+
failed: result.failed
|
|
860
|
+
};
|
|
699
861
|
}
|
|
700
862
|
// ─── Download ─────────────────────────────────────────────────────────────
|
|
701
863
|
/**
|
|
702
864
|
* Download a private file as an ArrayBuffer.
|
|
703
865
|
* For public files, use the `publicUrl` directly — no SDK needed.
|
|
704
|
-
*
|
|
705
|
-
* @example
|
|
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
|
-
* ```
|
|
712
866
|
*/
|
|
713
867
|
async download(path) {
|
|
714
868
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
@@ -718,11 +872,16 @@ var StorageManager = class {
|
|
|
718
872
|
});
|
|
719
873
|
}
|
|
720
874
|
/**
|
|
721
|
-
* Download multiple files at once
|
|
875
|
+
* Download multiple files at once (max 20).
|
|
876
|
+
* Returns a structured result with succeeded and failed items.
|
|
877
|
+
* Each succeeded item includes the file content as a base64 string.
|
|
722
878
|
*
|
|
723
879
|
* @example
|
|
724
880
|
* ```ts
|
|
725
|
-
* const
|
|
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
|
+
* }
|
|
726
885
|
* ```
|
|
727
886
|
*/
|
|
728
887
|
async batchDownload(paths) {
|
|
@@ -730,24 +889,30 @@ var StorageManager = class {
|
|
|
730
889
|
`${this.basePath}/batch-download`,
|
|
731
890
|
{ method: "POST", body: { paths }, headers: this.authHeaders }
|
|
732
891
|
);
|
|
733
|
-
return
|
|
892
|
+
return {
|
|
893
|
+
succeeded: result.succeeded.map((f) => ({
|
|
894
|
+
index: f.index,
|
|
895
|
+
path: f.path,
|
|
896
|
+
mimeType: f.mimeType,
|
|
897
|
+
size: f.size,
|
|
898
|
+
content: f.content
|
|
899
|
+
})),
|
|
900
|
+
failed: result.failed.map((f) => ({
|
|
901
|
+
index: f.index,
|
|
902
|
+
path: f.path,
|
|
903
|
+
error: f.error,
|
|
904
|
+
code: f.code
|
|
905
|
+
}))
|
|
906
|
+
};
|
|
734
907
|
}
|
|
735
908
|
// ─── List ─────────────────────────────────────────────────────────────────
|
|
736
909
|
/**
|
|
737
910
|
* List files and folders at a given path prefix.
|
|
911
|
+
* Returns separate `files` and `folders` arrays for easy consumption.
|
|
738
912
|
*
|
|
739
913
|
* @example
|
|
740
914
|
* ```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
|
|
915
|
+
* const { files, folders, hasMore, nextCursor } = await storage.list({ prefix: 'avatars/', limit: 20 });
|
|
751
916
|
* const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
|
|
752
917
|
* ```
|
|
753
918
|
*/
|
|
@@ -756,28 +921,38 @@ var StorageManager = class {
|
|
|
756
921
|
if (opts.prefix) params.set("prefix", opts.prefix);
|
|
757
922
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
758
923
|
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
759
|
-
if (opts.recursive) params.set("recursive", "true");
|
|
760
924
|
const qs = params.toString() ? `?${params}` : "";
|
|
761
925
|
const result = await this.http.request(`${this.basePath}/list${qs}`, {
|
|
762
926
|
method: "GET",
|
|
763
927
|
headers: this.authHeaders
|
|
764
928
|
});
|
|
929
|
+
if (result.items !== void 0) {
|
|
930
|
+
const files = [];
|
|
931
|
+
const folders = [];
|
|
932
|
+
for (const item of result.items) {
|
|
933
|
+
if (item.type === "folder") {
|
|
934
|
+
folders.push(item.path);
|
|
935
|
+
} else {
|
|
936
|
+
files.push(item);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
files,
|
|
941
|
+
folders,
|
|
942
|
+
hasMore: result.pagination?.hasMore ?? false,
|
|
943
|
+
nextCursor: result.pagination?.nextCursor ?? void 0
|
|
944
|
+
};
|
|
945
|
+
}
|
|
765
946
|
return {
|
|
766
|
-
files: result.files,
|
|
767
|
-
folders: result.folders,
|
|
768
|
-
hasMore: result.hasMore,
|
|
769
|
-
nextCursor: result.nextCursor
|
|
947
|
+
files: result.files ?? [],
|
|
948
|
+
folders: result.folders ?? [],
|
|
949
|
+
hasMore: result.hasMore ?? false,
|
|
950
|
+
nextCursor: result.nextCursor ?? void 0
|
|
770
951
|
};
|
|
771
952
|
}
|
|
772
953
|
// ─── Metadata ─────────────────────────────────────────────────────────────
|
|
773
954
|
/**
|
|
774
|
-
* Get metadata for a file
|
|
775
|
-
*
|
|
776
|
-
* @example
|
|
777
|
-
* ```ts
|
|
778
|
-
* const meta = await storage.getMetadata('avatars/alice.jpg');
|
|
779
|
-
* console.log(meta.size, meta.isPublic, meta.publicUrl);
|
|
780
|
-
* ```
|
|
955
|
+
* Get metadata for a file: size, MIME type, visibility, URLs.
|
|
781
956
|
*/
|
|
782
957
|
async getMetadata(path) {
|
|
783
958
|
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
@@ -796,22 +971,15 @@ var StorageManager = class {
|
|
|
796
971
|
updatedAt: result.updatedAt
|
|
797
972
|
};
|
|
798
973
|
}
|
|
799
|
-
// ─── Signed URL
|
|
974
|
+
// ─── Signed URL ───────────────────────────────────────────────────────────
|
|
800
975
|
/**
|
|
801
976
|
* Generate a time-limited download URL for a private file.
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
* > **Note:** Downloads via signed URLs bypass the server, so download stats
|
|
805
|
-
* > are NOT tracked. Use `downloadUrl` for tracked downloads.
|
|
977
|
+
* Can be shared externally without requiring an `X-Storage-Key`.
|
|
806
978
|
*
|
|
807
|
-
*
|
|
808
|
-
* @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
|
|
979
|
+
* > Note: Downloads via signed URLs bypass the server — download stats are NOT tracked.
|
|
809
980
|
*
|
|
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
|
-
* ```
|
|
981
|
+
* @param path File path.
|
|
982
|
+
* @param expiresIn URL lifetime in seconds (default 3600 = 1 hour).
|
|
815
983
|
*/
|
|
816
984
|
async getSignedUrl(path, expiresIn = 3600) {
|
|
817
985
|
const result = await this.http.request(`${this.basePath}/signed-url`, {
|
|
@@ -829,17 +997,6 @@ var StorageManager = class {
|
|
|
829
997
|
// ─── Visibility ───────────────────────────────────────────────────────────
|
|
830
998
|
/**
|
|
831
999
|
* Change a file's visibility between public and private after upload.
|
|
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
|
|
838
|
-
*
|
|
839
|
-
* // Make a file private
|
|
840
|
-
* const result = await storage.setVisibility('avatars/alice.jpg', false);
|
|
841
|
-
* console.log(result.downloadUrl); // Auth-required URL
|
|
842
|
-
* ```
|
|
843
1000
|
*/
|
|
844
1001
|
async setVisibility(path, isPublic) {
|
|
845
1002
|
const result = await this.http.request(`${this.basePath}/visibility`, {
|
|
@@ -855,14 +1012,6 @@ var StorageManager = class {
|
|
|
855
1012
|
};
|
|
856
1013
|
}
|
|
857
1014
|
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
858
|
-
/**
|
|
859
|
-
* Create a folder (a GCS prefix placeholder).
|
|
860
|
-
*
|
|
861
|
-
* @example
|
|
862
|
-
* ```ts
|
|
863
|
-
* await storage.createFolder('uploads/2025/');
|
|
864
|
-
* ```
|
|
865
|
-
*/
|
|
866
1015
|
async createFolder(path) {
|
|
867
1016
|
const result = await this.http.request(
|
|
868
1017
|
`${this.basePath}/folder`,
|
|
@@ -871,14 +1020,6 @@ var StorageManager = class {
|
|
|
871
1020
|
return { path: result.path };
|
|
872
1021
|
}
|
|
873
1022
|
// ─── File Operations ──────────────────────────────────────────────────────
|
|
874
|
-
/**
|
|
875
|
-
* Delete a single file.
|
|
876
|
-
*
|
|
877
|
-
* @example
|
|
878
|
-
* ```ts
|
|
879
|
-
* await storage.deleteFile('avatars/old-avatar.jpg');
|
|
880
|
-
* ```
|
|
881
|
-
*/
|
|
882
1023
|
async deleteFile(path) {
|
|
883
1024
|
await this.http.request(`${this.basePath}/file`, {
|
|
884
1025
|
method: "DELETE",
|
|
@@ -886,14 +1027,6 @@ var StorageManager = class {
|
|
|
886
1027
|
headers: this.authHeaders
|
|
887
1028
|
});
|
|
888
1029
|
}
|
|
889
|
-
/**
|
|
890
|
-
* Delete a folder and all its contents recursively.
|
|
891
|
-
*
|
|
892
|
-
* @example
|
|
893
|
-
* ```ts
|
|
894
|
-
* await storage.deleteFolder('temp/');
|
|
895
|
-
* ```
|
|
896
|
-
*/
|
|
897
1030
|
async deleteFolder(path) {
|
|
898
1031
|
await this.http.request(`${this.basePath}/folder`, {
|
|
899
1032
|
method: "DELETE",
|
|
@@ -901,17 +1034,6 @@ var StorageManager = class {
|
|
|
901
1034
|
headers: this.authHeaders
|
|
902
1035
|
});
|
|
903
1036
|
}
|
|
904
|
-
/**
|
|
905
|
-
* Move or rename a file.
|
|
906
|
-
*
|
|
907
|
-
* @example
|
|
908
|
-
* ```ts
|
|
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
|
-
* ```
|
|
914
|
-
*/
|
|
915
1037
|
async move(from, to) {
|
|
916
1038
|
const result = await this.http.request(
|
|
917
1039
|
`${this.basePath}/move`,
|
|
@@ -919,14 +1041,6 @@ var StorageManager = class {
|
|
|
919
1041
|
);
|
|
920
1042
|
return { from: result.from, to: result.to };
|
|
921
1043
|
}
|
|
922
|
-
/**
|
|
923
|
-
* Copy a file to a new path.
|
|
924
|
-
*
|
|
925
|
-
* @example
|
|
926
|
-
* ```ts
|
|
927
|
-
* await storage.copy('templates/base.html', 'sites/my-site/index.html');
|
|
928
|
-
* ```
|
|
929
|
-
*/
|
|
930
1044
|
async copy(from, to) {
|
|
931
1045
|
const result = await this.http.request(
|
|
932
1046
|
`${this.basePath}/copy`,
|
|
@@ -935,15 +1049,6 @@ var StorageManager = class {
|
|
|
935
1049
|
return { from: result.from, to: result.to };
|
|
936
1050
|
}
|
|
937
1051
|
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
938
|
-
/**
|
|
939
|
-
* Get storage statistics for your key: total files, bytes, operation counts.
|
|
940
|
-
*
|
|
941
|
-
* @example
|
|
942
|
-
* ```ts
|
|
943
|
-
* const stats = await storage.getStats();
|
|
944
|
-
* console.log(`${stats.totalFiles} files, ${(stats.totalBytes / 1e6).toFixed(1)} MB`);
|
|
945
|
-
* ```
|
|
946
|
-
*/
|
|
947
1052
|
async getStats() {
|
|
948
1053
|
const result = await this.http.request(`${this.basePath}/stats`, {
|
|
949
1054
|
method: "GET",
|
|
@@ -952,17 +1057,10 @@ var StorageManager = class {
|
|
|
952
1057
|
return result.stats;
|
|
953
1058
|
}
|
|
954
1059
|
// ─── Info (no auth) ───────────────────────────────────────────────────────
|
|
955
|
-
/**
|
|
956
|
-
* Ping the storage service. No authentication required.
|
|
957
|
-
*
|
|
958
|
-
* @example
|
|
959
|
-
* ```ts
|
|
960
|
-
* const info = await storage.info();
|
|
961
|
-
* // → { ok: true, storageRoot: 'hydrous-storage' }
|
|
962
|
-
* ```
|
|
963
|
-
*/
|
|
964
1060
|
async info() {
|
|
965
|
-
return this.http.
|
|
1061
|
+
return this.http.request(`${this.basePath}/info`, {
|
|
1062
|
+
method: "GET"
|
|
1063
|
+
});
|
|
966
1064
|
}
|
|
967
1065
|
};
|
|
968
1066
|
|
|
@@ -970,85 +1068,105 @@ var StorageManager = class {
|
|
|
970
1068
|
var ScopedStorage = class _ScopedStorage {
|
|
971
1069
|
constructor(manager, prefix) {
|
|
972
1070
|
this.manager = manager;
|
|
973
|
-
this.prefix = prefix.
|
|
1071
|
+
this.prefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
974
1072
|
}
|
|
975
|
-
|
|
976
|
-
return `${this.prefix}${
|
|
1073
|
+
p(path) {
|
|
1074
|
+
return `${this.prefix}${path.replace(/^\/+/, "")}`;
|
|
977
1075
|
}
|
|
978
|
-
|
|
979
|
-
upload(data, path, options) {
|
|
980
|
-
return this.manager.upload(data, this.
|
|
1076
|
+
// ─── Upload ───────────────────────────────────────────────────────────────
|
|
1077
|
+
async upload(data, path, options = {}) {
|
|
1078
|
+
return this.manager.upload(data, this.p(path), options);
|
|
981
1079
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
return this.manager.uploadRaw(data, this.scopedPath(path), options);
|
|
1080
|
+
async uploadRaw(data, path, options = {}) {
|
|
1081
|
+
return this.manager.uploadRaw(data, this.p(path), options);
|
|
985
1082
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
return this.manager.getUploadUrl({ ...opts, path: this.scopedPath(opts.path) });
|
|
1083
|
+
async getUploadUrl(opts) {
|
|
1084
|
+
return this.manager.getUploadUrl({ ...opts, path: this.p(opts.path) });
|
|
989
1085
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return this.manager.confirmUpload({ ...opts, path: this.scopedPath(opts.path) });
|
|
1086
|
+
async uploadToSignedUrl(signedUrl, data, mimeType, onProgress) {
|
|
1087
|
+
return this.manager.uploadToSignedUrl(signedUrl, data, mimeType, onProgress);
|
|
993
1088
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
return this.manager.download(this.scopedPath(path));
|
|
1089
|
+
async confirmUpload(opts) {
|
|
1090
|
+
return this.manager.confirmUpload({ ...opts, path: this.p(opts.path) });
|
|
997
1091
|
}
|
|
1092
|
+
async getBatchUploadUrls(files) {
|
|
1093
|
+
return this.manager.getBatchUploadUrls(
|
|
1094
|
+
files.map((f) => ({ ...f, path: this.p(f.path) }))
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
async batchConfirmUploads(items) {
|
|
1098
|
+
return this.manager.batchConfirmUploads(
|
|
1099
|
+
items.map((i) => ({ ...i, path: this.p(i.path) }))
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
// ─── Download ─────────────────────────────────────────────────────────────
|
|
1103
|
+
async download(path) {
|
|
1104
|
+
return this.manager.download(this.p(path));
|
|
1105
|
+
}
|
|
1106
|
+
async batchDownload(paths) {
|
|
1107
|
+
return this.manager.batchDownload(paths.map((p) => this.p(p)));
|
|
1108
|
+
}
|
|
1109
|
+
// ─── List ─────────────────────────────────────────────────────────────────
|
|
998
1110
|
/**
|
|
999
|
-
* List files within the
|
|
1000
|
-
*
|
|
1111
|
+
* List files within this scope. The `prefix` option is relative to the scope root.
|
|
1112
|
+
*
|
|
1113
|
+
* @example
|
|
1114
|
+
* ```ts
|
|
1115
|
+
* const userDocs = storage.scope('users/alice/');
|
|
1116
|
+
* // Lists users/alice/docs/
|
|
1117
|
+
* const { files } = await userDocs.list({ prefix: 'docs/' });
|
|
1118
|
+
* ```
|
|
1001
1119
|
*/
|
|
1002
|
-
list(opts = {}) {
|
|
1003
|
-
|
|
1120
|
+
async list(opts = {}) {
|
|
1121
|
+
return this.manager.list({
|
|
1004
1122
|
...opts,
|
|
1005
|
-
prefix: this.
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1123
|
+
prefix: this.p(opts.prefix ?? "")
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
// ─── Metadata & URLs ──────────────────────────────────────────────────────
|
|
1127
|
+
async getMetadata(path) {
|
|
1128
|
+
return this.manager.getMetadata(this.p(path));
|
|
1008
1129
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return this.manager.getMetadata(this.scopedPath(path));
|
|
1130
|
+
async getSignedUrl(path, expiresIn = 3600) {
|
|
1131
|
+
return this.manager.getSignedUrl(this.p(path), expiresIn);
|
|
1012
1132
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
return this.manager.getSignedUrl(this.scopedPath(path), expiresIn);
|
|
1133
|
+
async setVisibility(path, isPublic) {
|
|
1134
|
+
return this.manager.setVisibility(this.p(path), isPublic);
|
|
1016
1135
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return this.manager.
|
|
1136
|
+
// ─── Folder Operations ────────────────────────────────────────────────────
|
|
1137
|
+
async createFolder(path) {
|
|
1138
|
+
return this.manager.createFolder(this.p(path));
|
|
1020
1139
|
}
|
|
1021
|
-
|
|
1022
|
-
deleteFile(path) {
|
|
1023
|
-
return this.manager.deleteFile(this.
|
|
1140
|
+
// ─── File Operations ──────────────────────────────────────────────────────
|
|
1141
|
+
async deleteFile(path) {
|
|
1142
|
+
return this.manager.deleteFile(this.p(path));
|
|
1024
1143
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
return this.manager.deleteFolder(this.scopedPath(path));
|
|
1144
|
+
async deleteFolder(path) {
|
|
1145
|
+
return this.manager.deleteFolder(this.p(path));
|
|
1028
1146
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
return this.manager.move(this.scopedPath(from), this.scopedPath(to));
|
|
1147
|
+
async move(from, to) {
|
|
1148
|
+
return this.manager.move(this.p(from), this.p(to));
|
|
1032
1149
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return this.manager.copy(this.scopedPath(from), this.scopedPath(to));
|
|
1150
|
+
async copy(from, to) {
|
|
1151
|
+
return this.manager.copy(this.p(from), this.p(to));
|
|
1036
1152
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
return this.manager.
|
|
1153
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
1154
|
+
async getStats() {
|
|
1155
|
+
return this.manager.getStats();
|
|
1040
1156
|
}
|
|
1157
|
+
// ─── Nesting ──────────────────────────────────────────────────────────────
|
|
1041
1158
|
/**
|
|
1042
|
-
* Create a
|
|
1159
|
+
* Create a deeper scope within this one.
|
|
1043
1160
|
*
|
|
1044
1161
|
* @example
|
|
1045
1162
|
* ```ts
|
|
1046
|
-
* const
|
|
1047
|
-
* const
|
|
1163
|
+
* const user = storage.scope('users/alice/');
|
|
1164
|
+
* const userDocs = user.scope('docs/');
|
|
1165
|
+
* // Effective prefix: users/alice/docs/
|
|
1048
1166
|
* ```
|
|
1049
1167
|
*/
|
|
1050
1168
|
scope(subPrefix) {
|
|
1051
|
-
return new _ScopedStorage(this.manager, this.
|
|
1169
|
+
return new _ScopedStorage(this.manager, this.p(subPrefix));
|
|
1052
1170
|
}
|
|
1053
1171
|
};
|
|
1054
1172
|
|
|
@@ -1068,22 +1186,23 @@ var HydrousClient = class {
|
|
|
1068
1186
|
if (!config.storageKeys || Object.keys(config.storageKeys).length === 0) {
|
|
1069
1187
|
throw new Error("[HydrousDB] storageKeys is required. Define at least one storage key from https://hydrousdb.com/dashboard.");
|
|
1070
1188
|
}
|
|
1071
|
-
|
|
1072
|
-
this.http = new HttpClient(baseUrl);
|
|
1189
|
+
this.http = new HttpClient(config.baseUrl ?? DEFAULT_BASE_URL);
|
|
1073
1190
|
this.authKey_ = config.authKey;
|
|
1074
1191
|
this.bucketSecurityKey_ = config.bucketSecurityKey;
|
|
1075
1192
|
this.storageKeys_ = config.storageKeys;
|
|
1076
1193
|
}
|
|
1077
|
-
// ─── Records
|
|
1194
|
+
// ─── Records ──────────────────────────────────────────────────────────────
|
|
1078
1195
|
/**
|
|
1079
1196
|
* Get a typed records client for the named bucket.
|
|
1080
|
-
* Uses your `bucketSecurityKey` automatically.
|
|
1081
1197
|
*
|
|
1082
1198
|
* @example
|
|
1083
1199
|
* ```ts
|
|
1084
1200
|
* interface Post { title: string; published: boolean }
|
|
1085
1201
|
* const posts = db.records<Post>('blog-posts');
|
|
1086
|
-
* const post
|
|
1202
|
+
* const post = await posts.create(
|
|
1203
|
+
* { title: 'Hello', published: false },
|
|
1204
|
+
* { queryableFields: ['published'] },
|
|
1205
|
+
* );
|
|
1087
1206
|
* ```
|
|
1088
1207
|
*/
|
|
1089
1208
|
records(bucketKey) {
|
|
@@ -1098,7 +1217,6 @@ var HydrousClient = class {
|
|
|
1098
1217
|
// ─── Auth ─────────────────────────────────────────────────────────────────
|
|
1099
1218
|
/**
|
|
1100
1219
|
* Get an auth client for the named user bucket.
|
|
1101
|
-
* Uses your `authKey` automatically.
|
|
1102
1220
|
*
|
|
1103
1221
|
* @example
|
|
1104
1222
|
* ```ts
|
|
@@ -1115,7 +1233,6 @@ var HydrousClient = class {
|
|
|
1115
1233
|
// ─── Analytics ────────────────────────────────────────────────────────────
|
|
1116
1234
|
/**
|
|
1117
1235
|
* Get an analytics client for the named bucket.
|
|
1118
|
-
* Uses your `bucketSecurityKey` automatically.
|
|
1119
1236
|
*
|
|
1120
1237
|
* @example
|
|
1121
1238
|
* ```ts
|
|
@@ -1135,21 +1252,16 @@ var HydrousClient = class {
|
|
|
1135
1252
|
// ─── Storage ──────────────────────────────────────────────────────────────
|
|
1136
1253
|
/**
|
|
1137
1254
|
* Get a storage manager for the named storage key.
|
|
1138
|
-
* The name must match a key
|
|
1139
|
-
* Uses the corresponding `ssk_…` key automatically via `X-Storage-Key` header.
|
|
1255
|
+
* The name must match a key defined in `storageKeys` when calling `createClient`.
|
|
1140
1256
|
*
|
|
1141
|
-
*
|
|
1257
|
+
* Attach `.scope(prefix)` to namespace all operations under a path prefix.
|
|
1142
1258
|
*
|
|
1143
1259
|
* @example
|
|
1144
1260
|
* ```ts
|
|
1145
|
-
* const avatars
|
|
1146
|
-
* const
|
|
1261
|
+
* const avatars = db.storage('avatars');
|
|
1262
|
+
* const userDocs = db.storage('documents').scope(`users/${userId}/`);
|
|
1147
1263
|
*
|
|
1148
|
-
* // Upload to avatars bucket
|
|
1149
1264
|
* await avatars.upload(file, `${userId}.jpg`, { isPublic: true });
|
|
1150
|
-
*
|
|
1151
|
-
* // Scope to a sub-folder
|
|
1152
|
-
* const userDocs = db.storage('documents').scope(`users/${userId}`);
|
|
1153
1265
|
* await userDocs.upload(pdfBuffer, 'contract.pdf');
|
|
1154
1266
|
* ```
|
|
1155
1267
|
*/
|