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