@rebasepro/client 0.8.0 → 0.9.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 +10 -10
- package/dist/auth.d.ts +35 -38
- package/dist/collection.d.ts +9 -13
- package/dist/data-proxy.test.d.ts +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +23 -22
- package/dist/index.es.js +485 -215
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +499 -230
- package/dist/index.umd.js.map +1 -1
- package/dist/sdk_query_builder.d.ts +63 -0
- package/dist/transport.d.ts +2 -6
- package/dist/websocket.d.ts +25 -21
- package/package.json +4 -4
- package/src/auth.ts +188 -64
- package/src/collection.test.ts +92 -3
- package/src/collection.ts +72 -75
- package/src/data-proxy.test.ts +167 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +146 -27
- package/src/reviver.ts +2 -2
- package/src/sdk_query_builder.ts +138 -0
- package/src/storage.ts +64 -32
- package/src/transport.ts +25 -25
- package/src/websocket.ts +133 -135
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, Vector, isPublicStoragePath } from "@rebasepro/types";
|
|
2
|
+
import { QueryBuilder, and, cond, or, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
@@ -30,25 +30,16 @@ function rebaseReviver(_key, value) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/transport.ts
|
|
33
|
-
var RebaseApiError = class extends Error {
|
|
34
|
-
status;
|
|
35
|
-
code;
|
|
36
|
-
details;
|
|
37
|
-
constructor(status, message, code, details) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "RebaseApiError";
|
|
40
|
-
this.status = status;
|
|
41
|
-
this.code = code;
|
|
42
|
-
this.details = details;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
33
|
function buildQueryString(params) {
|
|
46
34
|
if (!params) return "";
|
|
47
35
|
const parts = [];
|
|
48
36
|
if (params.limit != null) parts.push(`limit=${params.limit}`);
|
|
49
37
|
if (params.offset != null) parts.push(`offset=${params.offset}`);
|
|
50
38
|
if (params.page != null) parts.push(`page=${params.page}`);
|
|
51
|
-
if (params.orderBy)
|
|
39
|
+
if (params.orderBy) {
|
|
40
|
+
const wire = serializeOrderBy(params.orderBy);
|
|
41
|
+
if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
|
|
42
|
+
}
|
|
52
43
|
if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
|
|
53
44
|
if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
|
|
54
45
|
if (params.logical) {
|
|
@@ -97,8 +88,7 @@ function createTransport(config) {
|
|
|
97
88
|
} catch (e) {}
|
|
98
89
|
const getErrorField = (obj, field) => {
|
|
99
90
|
const err = obj?.error;
|
|
100
|
-
if (err && typeof err === "object" && err !== null
|
|
101
|
-
return obj?.[field];
|
|
91
|
+
if (err && typeof err === "object" && err !== null) return err[field];
|
|
102
92
|
};
|
|
103
93
|
if (res.status === 401 && onUnauthorizedHandler) {
|
|
104
94
|
if (await onUnauthorizedHandler()) {
|
|
@@ -121,7 +111,11 @@ function createTransport(config) {
|
|
|
121
111
|
if (!retryRes.ok) {
|
|
122
112
|
let fallbackMessage = retryRes.statusText;
|
|
123
113
|
if (retryRes.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
|
|
124
|
-
throw new RebaseApiError(
|
|
114
|
+
throw new RebaseApiError$1(String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), {
|
|
115
|
+
status: retryRes.status,
|
|
116
|
+
code: getErrorField(retryBody, "code"),
|
|
117
|
+
details: getErrorField(retryBody, "details")
|
|
118
|
+
});
|
|
125
119
|
}
|
|
126
120
|
return retryBody;
|
|
127
121
|
}
|
|
@@ -129,7 +123,11 @@ function createTransport(config) {
|
|
|
129
123
|
if (!res.ok) {
|
|
130
124
|
let fallbackMessage = res.statusText;
|
|
131
125
|
if (res.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
|
|
132
|
-
throw new RebaseApiError(
|
|
126
|
+
throw new RebaseApiError$1(String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), {
|
|
127
|
+
status: res.status,
|
|
128
|
+
code: getErrorField(body, "code"),
|
|
129
|
+
details: getErrorField(body, "details")
|
|
130
|
+
});
|
|
133
131
|
}
|
|
134
132
|
return body;
|
|
135
133
|
}
|
|
@@ -165,6 +163,29 @@ function createTransport(config) {
|
|
|
165
163
|
}
|
|
166
164
|
//#endregion
|
|
167
165
|
//#region src/auth.ts
|
|
166
|
+
/** Map a raw user object from an auth response (`/login`, `/refresh`, `/me`) to a `User`. */
|
|
167
|
+
function mapRawUser(raw) {
|
|
168
|
+
return {
|
|
169
|
+
uid: raw.uid,
|
|
170
|
+
email: raw.email ?? null,
|
|
171
|
+
displayName: raw.displayName ?? null,
|
|
172
|
+
photoURL: raw.photoURL ?? null,
|
|
173
|
+
providerId: raw.providerId ?? "password",
|
|
174
|
+
isAnonymous: raw.isAnonymous ?? false,
|
|
175
|
+
emailVerified: raw.emailVerified,
|
|
176
|
+
roles: raw.roles,
|
|
177
|
+
metadata: raw.metadata
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Placeholder user, used only as a last resort when none can be resolved. */
|
|
181
|
+
var EMPTY_USER = {
|
|
182
|
+
uid: "",
|
|
183
|
+
email: null,
|
|
184
|
+
displayName: null,
|
|
185
|
+
photoURL: null,
|
|
186
|
+
providerId: "password",
|
|
187
|
+
isAnonymous: false
|
|
188
|
+
};
|
|
168
189
|
function createMemoryStorage() {
|
|
169
190
|
const store = {};
|
|
170
191
|
return {
|
|
@@ -195,11 +216,20 @@ function createAuth(transport, options) {
|
|
|
195
216
|
const authPath = opts.authPath || "/auth";
|
|
196
217
|
const autoRefresh = opts.autoRefresh !== false;
|
|
197
218
|
const persistSession = opts.persistSession !== false;
|
|
219
|
+
const authFlowMode = opts.authFlowMode || "json";
|
|
198
220
|
const STORAGE_KEY = "rebase_auth";
|
|
199
221
|
const REFRESH_BUFFER_MS = 12e4;
|
|
222
|
+
const MAX_REFRESH_RETRIES = 5;
|
|
223
|
+
const REFRESH_RETRY_BASE_MS = 1e3;
|
|
224
|
+
const REFRESH_RETRY_MAX_MS = 3e4;
|
|
200
225
|
let currentSession = null;
|
|
201
226
|
const listeners = /* @__PURE__ */ new Set();
|
|
202
227
|
let refreshTimeout = null;
|
|
228
|
+
let inFlightRefresh = null;
|
|
229
|
+
let resolveInitialized;
|
|
230
|
+
const isInitialized = new Promise((resolve) => {
|
|
231
|
+
resolveInitialized = resolve;
|
|
232
|
+
});
|
|
203
233
|
function authUrl(endpoint) {
|
|
204
234
|
return transport.baseUrl + transport.apiPath + authPath + endpoint;
|
|
205
235
|
}
|
|
@@ -207,7 +237,11 @@ function createAuth(transport, options) {
|
|
|
207
237
|
return transport.fetchFn || globalThis.fetch;
|
|
208
238
|
}
|
|
209
239
|
function throwApiError(status, body, statusText) {
|
|
210
|
-
throw new RebaseApiError(
|
|
240
|
+
throw new RebaseApiError(body?.error?.message || body?.message || statusText, {
|
|
241
|
+
status,
|
|
242
|
+
code: body?.error?.code || body?.code,
|
|
243
|
+
details: body?.error?.details || body?.details
|
|
244
|
+
});
|
|
211
245
|
}
|
|
212
246
|
function emit(event, session) {
|
|
213
247
|
for (const fn of listeners) try {
|
|
@@ -215,7 +249,7 @@ function createAuth(transport, options) {
|
|
|
215
249
|
} catch (e) {}
|
|
216
250
|
}
|
|
217
251
|
function saveSession(session) {
|
|
218
|
-
if (!persistSession) return;
|
|
252
|
+
if (!persistSession || authFlowMode === "cookie") return;
|
|
219
253
|
try {
|
|
220
254
|
storage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
221
255
|
} catch (e) {}
|
|
@@ -232,28 +266,53 @@ function createAuth(transport, options) {
|
|
|
232
266
|
} catch (e) {}
|
|
233
267
|
return null;
|
|
234
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* A refresh failure is only fatal if the refresh token itself is rejected
|
|
271
|
+
* (expired / invalid / forbidden). Network blips, timeouts, and 5xx (e.g. a
|
|
272
|
+
* backend restart mid-session) are transient and must NOT log the user out.
|
|
273
|
+
*/
|
|
274
|
+
function isFatalRefreshError(err) {
|
|
275
|
+
if (!(err instanceof RebaseApiError)) return false;
|
|
276
|
+
if (err.code === "INVALID_TOKEN" || err.code === "TOKEN_EXPIRED") return true;
|
|
277
|
+
return err.status === 401 || err.status === 403;
|
|
278
|
+
}
|
|
279
|
+
async function attemptScheduledRefresh(attempt) {
|
|
280
|
+
try {
|
|
281
|
+
await refreshSession();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (isFatalRefreshError(err)) {
|
|
284
|
+
signOut();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (attempt >= MAX_REFRESH_RETRIES) {
|
|
288
|
+
signOut();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const backoff = Math.min(REFRESH_RETRY_BASE_MS * 2 ** attempt, REFRESH_RETRY_MAX_MS);
|
|
292
|
+
refreshTimeout = setTimeout(() => {
|
|
293
|
+
attemptScheduledRefresh(attempt + 1);
|
|
294
|
+
}, backoff);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
235
297
|
function scheduleRefresh(expiresAt) {
|
|
236
298
|
if (refreshTimeout) clearTimeout(refreshTimeout);
|
|
237
299
|
if (!autoRefresh) return;
|
|
238
300
|
const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
|
|
239
301
|
if (delay <= 0) {
|
|
240
|
-
|
|
302
|
+
attemptScheduledRefresh(0);
|
|
241
303
|
return;
|
|
242
304
|
}
|
|
243
|
-
refreshTimeout = setTimeout(
|
|
244
|
-
|
|
245
|
-
await refreshSession();
|
|
246
|
-
} catch (e) {
|
|
247
|
-
signOut();
|
|
248
|
-
}
|
|
305
|
+
refreshTimeout = setTimeout(() => {
|
|
306
|
+
attemptScheduledRefresh(0);
|
|
249
307
|
}, delay);
|
|
250
308
|
}
|
|
251
309
|
function handleAuthResponse(data, event) {
|
|
310
|
+
const user = mapRawUser(data.user);
|
|
252
311
|
const session = {
|
|
253
312
|
accessToken: data.tokens.accessToken,
|
|
254
|
-
refreshToken: data.tokens.refreshToken,
|
|
313
|
+
refreshToken: data.tokens.refreshToken || currentSession?.refreshToken || "",
|
|
255
314
|
expiresAt: data.tokens.accessTokenExpiresAt,
|
|
256
|
-
user
|
|
315
|
+
user
|
|
257
316
|
};
|
|
258
317
|
currentSession = session;
|
|
259
318
|
saveSession(session);
|
|
@@ -269,7 +328,8 @@ function createAuth(transport, options) {
|
|
|
269
328
|
body: JSON.stringify({
|
|
270
329
|
email,
|
|
271
330
|
password
|
|
272
|
-
})
|
|
331
|
+
}),
|
|
332
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
273
333
|
});
|
|
274
334
|
const body = await res.json().catch(() => ({}));
|
|
275
335
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -290,7 +350,8 @@ function createAuth(transport, options) {
|
|
|
290
350
|
const res = await fetchFn(authUrl("/register"), {
|
|
291
351
|
method: "POST",
|
|
292
352
|
headers: { "Content-Type": "application/json" },
|
|
293
|
-
body: JSON.stringify(payload)
|
|
353
|
+
body: JSON.stringify(payload),
|
|
354
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
294
355
|
});
|
|
295
356
|
const body = await res.json().catch(() => ({}));
|
|
296
357
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -313,7 +374,8 @@ function createAuth(transport, options) {
|
|
|
313
374
|
const res = await getFetch()(authUrl("/google"), {
|
|
314
375
|
method: "POST",
|
|
315
376
|
headers: { "Content-Type": "application/json" },
|
|
316
|
-
body: JSON.stringify(payload)
|
|
377
|
+
body: JSON.stringify(payload),
|
|
378
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
317
379
|
});
|
|
318
380
|
const responseBody = await res.json().catch(() => ({}));
|
|
319
381
|
if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
|
|
@@ -331,7 +393,8 @@ function createAuth(transport, options) {
|
|
|
331
393
|
body: JSON.stringify({
|
|
332
394
|
code,
|
|
333
395
|
redirectUri
|
|
334
|
-
})
|
|
396
|
+
}),
|
|
397
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
335
398
|
});
|
|
336
399
|
const body = await res.json().catch(() => ({}));
|
|
337
400
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -350,7 +413,8 @@ function createAuth(transport, options) {
|
|
|
350
413
|
const res = await getFetch()(authUrl(`/${providerId}`), {
|
|
351
414
|
method: "POST",
|
|
352
415
|
headers: { "Content-Type": "application/json" },
|
|
353
|
-
body: JSON.stringify(payload)
|
|
416
|
+
body: JSON.stringify(payload),
|
|
417
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
354
418
|
});
|
|
355
419
|
const body = await res.json().catch(() => ({}));
|
|
356
420
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -426,10 +490,11 @@ function createAuth(transport, options) {
|
|
|
426
490
|
async function signOut() {
|
|
427
491
|
const fetchFn = getFetch();
|
|
428
492
|
try {
|
|
429
|
-
if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
|
|
493
|
+
if (authFlowMode === "cookie" || currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
|
|
430
494
|
method: "POST",
|
|
431
495
|
headers: { "Content-Type": "application/json" },
|
|
432
|
-
body: JSON.stringify({ refreshToken: currentSession
|
|
496
|
+
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
497
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
433
498
|
});
|
|
434
499
|
} catch (e) {}
|
|
435
500
|
currentSession = null;
|
|
@@ -441,20 +506,35 @@ function createAuth(transport, options) {
|
|
|
441
506
|
transport.setToken(null);
|
|
442
507
|
emit("SIGNED_OUT", null);
|
|
443
508
|
}
|
|
444
|
-
|
|
445
|
-
if (
|
|
509
|
+
function refreshSession() {
|
|
510
|
+
if (inFlightRefresh) return inFlightRefresh;
|
|
511
|
+
inFlightRefresh = doRefreshSession().finally(() => {
|
|
512
|
+
inFlightRefresh = null;
|
|
513
|
+
});
|
|
514
|
+
return inFlightRefresh;
|
|
515
|
+
}
|
|
516
|
+
async function doRefreshSession() {
|
|
517
|
+
if (authFlowMode !== "cookie" && !currentSession?.refreshToken) throw new Error("No active session to refresh");
|
|
446
518
|
const res = await getFetch()(authUrl("/refresh"), {
|
|
447
519
|
method: "POST",
|
|
448
520
|
headers: { "Content-Type": "application/json" },
|
|
449
|
-
body: JSON.stringify({ refreshToken: currentSession
|
|
521
|
+
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
522
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
450
523
|
});
|
|
451
524
|
const body = await res.json().catch(() => ({}));
|
|
452
525
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
526
|
+
const accessToken = body.tokens.accessToken;
|
|
527
|
+
transport.setToken(accessToken);
|
|
528
|
+
let user = currentSession?.user;
|
|
529
|
+
if (body.user && typeof body.user.uid === "string") user = mapRawUser(body.user);
|
|
530
|
+
else if (!user || !user.uid) try {
|
|
531
|
+
user = await getUser();
|
|
532
|
+
} catch {}
|
|
453
533
|
const session = {
|
|
454
|
-
accessToken
|
|
455
|
-
refreshToken: body.tokens.refreshToken,
|
|
534
|
+
accessToken,
|
|
535
|
+
refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
|
|
456
536
|
expiresAt: body.tokens.accessTokenExpiresAt,
|
|
457
|
-
user:
|
|
537
|
+
user: user ?? EMPTY_USER
|
|
458
538
|
};
|
|
459
539
|
currentSession = session;
|
|
460
540
|
saveSession(session);
|
|
@@ -466,6 +546,18 @@ function createAuth(transport, options) {
|
|
|
466
546
|
async function getUser() {
|
|
467
547
|
return (await transport.request(authPath + "/me", { method: "GET" })).user;
|
|
468
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Resolve an email to a minimal public profile (`uid`, `displayName`,
|
|
551
|
+
* `photoURL`) for invite-by-email flows. Returns `null` when no account
|
|
552
|
+
* matches. Requires the backend to opt in via `auth.allowUserLookup`;
|
|
553
|
+
* otherwise the endpoint is absent and this rejects.
|
|
554
|
+
*/
|
|
555
|
+
async function findUserByEmail(email) {
|
|
556
|
+
return (await transport.request(authPath + "/find-user", {
|
|
557
|
+
method: "POST",
|
|
558
|
+
body: JSON.stringify({ email })
|
|
559
|
+
})).user;
|
|
560
|
+
}
|
|
469
561
|
async function updateUser(updates) {
|
|
470
562
|
const data = await transport.request(authPath + "/me", {
|
|
471
563
|
method: "PATCH",
|
|
@@ -539,7 +631,8 @@ function createAuth(transport, options) {
|
|
|
539
631
|
const res = await getFetch()(authUrl("/magic-link/verify"), {
|
|
540
632
|
method: "POST",
|
|
541
633
|
headers: { "Content-Type": "application/json" },
|
|
542
|
-
body: JSON.stringify({ token })
|
|
634
|
+
body: JSON.stringify({ token }),
|
|
635
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
543
636
|
});
|
|
544
637
|
const body = await res.json().catch(() => ({}));
|
|
545
638
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -586,21 +679,29 @@ function createAuth(transport, options) {
|
|
|
586
679
|
}
|
|
587
680
|
if (persistSession) {
|
|
588
681
|
const stored = loadStoredSession();
|
|
589
|
-
if (stored && stored.accessToken
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
682
|
+
if (stored && stored.accessToken) if (stored.expiresAt > Date.now()) {
|
|
683
|
+
currentSession = stored;
|
|
684
|
+
transport.setToken(stored.accessToken);
|
|
685
|
+
scheduleRefresh(stored.expiresAt);
|
|
686
|
+
resolveInitialized();
|
|
687
|
+
} else if (authFlowMode === "cookie" || stored.refreshToken) {
|
|
688
|
+
currentSession = stored;
|
|
689
|
+
refreshSession().then(() => {
|
|
690
|
+
resolveInitialized();
|
|
691
|
+
}).catch(() => {
|
|
692
|
+
currentSession = null;
|
|
693
|
+
clearStoredSession();
|
|
694
|
+
transport.setToken(null);
|
|
695
|
+
resolveInitialized();
|
|
696
|
+
});
|
|
697
|
+
} else resolveInitialized();
|
|
698
|
+
else if (authFlowMode === "cookie") refreshSession().then(() => {
|
|
699
|
+
resolveInitialized();
|
|
700
|
+
}).catch(() => {
|
|
701
|
+
resolveInitialized();
|
|
702
|
+
});
|
|
703
|
+
else resolveInitialized();
|
|
704
|
+
} else resolveInitialized();
|
|
604
705
|
return {
|
|
605
706
|
signInWithEmail,
|
|
606
707
|
signUp,
|
|
@@ -620,6 +721,7 @@ function createAuth(transport, options) {
|
|
|
620
721
|
signOut,
|
|
621
722
|
refreshSession,
|
|
622
723
|
getUser,
|
|
724
|
+
findUserByEmail,
|
|
623
725
|
updateUser,
|
|
624
726
|
resetPasswordForEmail,
|
|
625
727
|
resetPassword,
|
|
@@ -633,7 +735,8 @@ function createAuth(transport, options) {
|
|
|
633
735
|
revokeAllSessions,
|
|
634
736
|
getAuthConfig,
|
|
635
737
|
getSession,
|
|
636
|
-
onAuthStateChange
|
|
738
|
+
onAuthStateChange,
|
|
739
|
+
isInitialized: () => isInitialized
|
|
637
740
|
};
|
|
638
741
|
}
|
|
639
742
|
function createCookieStorage(options = {}) {
|
|
@@ -810,20 +913,108 @@ function createApiKeys(transport, options) {
|
|
|
810
913
|
};
|
|
811
914
|
}
|
|
812
915
|
//#endregion
|
|
813
|
-
//#region src/
|
|
916
|
+
//#region src/sdk_query_builder.ts
|
|
814
917
|
/**
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
918
|
+
* SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
|
|
919
|
+
* Entity-wrapped results (`FindResponse<M>`).
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* const { data } = await rebase.data.posts
|
|
923
|
+
* .where("status", "==", "published")
|
|
924
|
+
* .orderBy("created_at", "desc")
|
|
925
|
+
* .limit(10)
|
|
926
|
+
* .find();
|
|
927
|
+
*
|
|
928
|
+
* console.log(data[0].title); // flat access
|
|
819
929
|
*/
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
930
|
+
var SDKQueryBuilder = class {
|
|
931
|
+
collection;
|
|
932
|
+
params = { where: {} };
|
|
933
|
+
constructor(collection) {
|
|
934
|
+
this.collection = collection;
|
|
935
|
+
}
|
|
936
|
+
where(columnOrCondition, operator, value) {
|
|
937
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
938
|
+
this.params.logical = columnOrCondition;
|
|
939
|
+
return this;
|
|
940
|
+
}
|
|
941
|
+
if (!this.params.where) this.params.where = {};
|
|
942
|
+
const column = columnOrCondition;
|
|
943
|
+
const condition = [operator, value];
|
|
944
|
+
const existing = this.params.where[column];
|
|
945
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
946
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
947
|
+
else {
|
|
948
|
+
let firstCondition;
|
|
949
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
950
|
+
else firstCondition = ["==", existing];
|
|
951
|
+
this.params.where[column] = [firstCondition, condition];
|
|
952
|
+
}
|
|
953
|
+
return this;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Order the results by a specific column.
|
|
957
|
+
*/
|
|
958
|
+
orderBy(column, direction = "asc") {
|
|
959
|
+
this.params.orderBy = [column, direction];
|
|
960
|
+
return this;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Limit the number of results returned.
|
|
964
|
+
*/
|
|
965
|
+
limit(count) {
|
|
966
|
+
this.params.limit = count;
|
|
967
|
+
return this;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Skip the first N results.
|
|
971
|
+
*/
|
|
972
|
+
offset(count) {
|
|
973
|
+
this.params.offset = count;
|
|
974
|
+
return this;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Set a free-text search string if supported by the backend.
|
|
978
|
+
*/
|
|
979
|
+
search(searchString) {
|
|
980
|
+
this.params.searchString = searchString;
|
|
981
|
+
return this;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Include related entities in the response.
|
|
985
|
+
* Relations will be populated with full data instead of just IDs.
|
|
986
|
+
*
|
|
987
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
988
|
+
* @example
|
|
989
|
+
* client.data.posts.include("tags", "author").find()
|
|
990
|
+
*/
|
|
991
|
+
include(...relations) {
|
|
992
|
+
this.params.include = relations;
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Execute the find query and return the results as flat rows.
|
|
997
|
+
*/
|
|
998
|
+
async find() {
|
|
999
|
+
return this.collection.find(this.params);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Count the records matching this query.
|
|
1003
|
+
*/
|
|
1004
|
+
async count() {
|
|
1005
|
+
if (!this.collection.count) throw new Error("count() is not supported by this collection client.");
|
|
1006
|
+
return this.collection.count(this.params);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Listen to realtime updates matching this query.
|
|
1010
|
+
*/
|
|
1011
|
+
listen(onUpdate, onError) {
|
|
1012
|
+
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
1013
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/collection.ts
|
|
827
1018
|
function createCollectionClient(transport, slug, ws) {
|
|
828
1019
|
const basePath = `/data/${slug}`;
|
|
829
1020
|
const client = {
|
|
@@ -831,7 +1022,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
831
1022
|
const qs = buildQueryString(params);
|
|
832
1023
|
const raw = await transport.request(basePath + qs, { method: "GET" });
|
|
833
1024
|
return {
|
|
834
|
-
data:
|
|
1025
|
+
data: raw.data || [],
|
|
835
1026
|
meta: raw.meta
|
|
836
1027
|
};
|
|
837
1028
|
},
|
|
@@ -839,7 +1030,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
839
1030
|
try {
|
|
840
1031
|
const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
|
|
841
1032
|
if (!raw) return void 0;
|
|
842
|
-
return
|
|
1033
|
+
return raw;
|
|
843
1034
|
} catch (err) {
|
|
844
1035
|
if (err instanceof RebaseApiError && err.status === 404) return;
|
|
845
1036
|
throw err;
|
|
@@ -848,19 +1039,19 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
848
1039
|
async create(data, id) {
|
|
849
1040
|
const body = { ...data };
|
|
850
1041
|
if (id !== void 0) body.id = id;
|
|
851
|
-
return
|
|
1042
|
+
return await transport.request(basePath, {
|
|
852
1043
|
method: "POST",
|
|
853
1044
|
body: JSON.stringify(body)
|
|
854
|
-
})
|
|
1045
|
+
});
|
|
855
1046
|
},
|
|
856
1047
|
async update(id, data) {
|
|
857
|
-
return
|
|
1048
|
+
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
858
1049
|
method: "PUT",
|
|
859
1050
|
body: JSON.stringify(data)
|
|
860
|
-
})
|
|
1051
|
+
});
|
|
861
1052
|
},
|
|
862
1053
|
async delete(id) {
|
|
863
|
-
|
|
1054
|
+
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
|
|
864
1055
|
},
|
|
865
1056
|
async count(params) {
|
|
866
1057
|
const qs = buildQueryString({
|
|
@@ -871,24 +1062,24 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
871
1062
|
return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
|
|
872
1063
|
},
|
|
873
1064
|
where(columnOrCondition, operator, value) {
|
|
874
|
-
const builder = new
|
|
1065
|
+
const builder = new SDKQueryBuilder(client);
|
|
875
1066
|
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
876
1067
|
return builder.where(columnOrCondition, operator, value);
|
|
877
1068
|
},
|
|
878
1069
|
orderBy(column, direction) {
|
|
879
|
-
return new
|
|
1070
|
+
return new SDKQueryBuilder(client).orderBy(column, direction);
|
|
880
1071
|
},
|
|
881
1072
|
limit(count) {
|
|
882
|
-
return new
|
|
1073
|
+
return new SDKQueryBuilder(client).limit(count);
|
|
883
1074
|
},
|
|
884
1075
|
offset(count) {
|
|
885
|
-
return new
|
|
1076
|
+
return new SDKQueryBuilder(client).offset(count);
|
|
886
1077
|
},
|
|
887
1078
|
search(searchString) {
|
|
888
|
-
return new
|
|
1079
|
+
return new SDKQueryBuilder(client).search(searchString);
|
|
889
1080
|
},
|
|
890
1081
|
include(...relations) {
|
|
891
|
-
return new
|
|
1082
|
+
return new SDKQueryBuilder(client).include(...relations);
|
|
892
1083
|
}
|
|
893
1084
|
};
|
|
894
1085
|
if (ws) {
|
|
@@ -900,33 +1091,46 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
900
1091
|
filter: params?.where,
|
|
901
1092
|
limit: params?.limit,
|
|
902
1093
|
startAfter: params?.offset ? String(params.offset) : void 0,
|
|
903
|
-
orderBy: params?.orderBy?.
|
|
904
|
-
order: params?.orderBy?.
|
|
1094
|
+
orderBy: params?.orderBy?.[0],
|
|
1095
|
+
order: params?.orderBy?.[1],
|
|
905
1096
|
searchString: params?.searchString
|
|
906
|
-
}, (
|
|
1097
|
+
}, (incomingRows) => {
|
|
907
1098
|
const currentUpdateId = ++lastUpdateId;
|
|
908
1099
|
const requestedLimit = params?.limit || 20;
|
|
909
1100
|
const offset = params?.offset || 0;
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
total: entities.length,
|
|
914
|
-
limit: requestedLimit,
|
|
915
|
-
offset,
|
|
916
|
-
hasMore: entities.length >= requestedLimit
|
|
917
|
-
}
|
|
918
|
-
});
|
|
1101
|
+
const rows = incomingRows;
|
|
1102
|
+
const heuristicTotal = rows.length;
|
|
1103
|
+
const heuristicHasMore = rows.length >= requestedLimit;
|
|
919
1104
|
if (client.count) client.count(params).then((total) => {
|
|
920
1105
|
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
921
|
-
data:
|
|
1106
|
+
data: rows,
|
|
922
1107
|
meta: {
|
|
923
1108
|
total,
|
|
924
1109
|
limit: requestedLimit,
|
|
925
1110
|
offset,
|
|
926
|
-
hasMore: offset +
|
|
1111
|
+
hasMore: offset + rows.length < total
|
|
927
1112
|
}
|
|
928
1113
|
});
|
|
929
|
-
}).catch(() => {
|
|
1114
|
+
}).catch(() => {
|
|
1115
|
+
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
1116
|
+
data: rows,
|
|
1117
|
+
meta: {
|
|
1118
|
+
total: heuristicTotal,
|
|
1119
|
+
limit: requestedLimit,
|
|
1120
|
+
offset,
|
|
1121
|
+
hasMore: heuristicHasMore
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
else onUpdate({
|
|
1126
|
+
data: rows,
|
|
1127
|
+
meta: {
|
|
1128
|
+
total: heuristicTotal,
|
|
1129
|
+
limit: requestedLimit,
|
|
1130
|
+
offset,
|
|
1131
|
+
hasMore: heuristicHasMore
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
930
1134
|
}, onError);
|
|
931
1135
|
return () => {
|
|
932
1136
|
active = false;
|
|
@@ -934,11 +1138,11 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
934
1138
|
};
|
|
935
1139
|
};
|
|
936
1140
|
client.listenById = (id, onUpdate, onError) => {
|
|
937
|
-
return ws.
|
|
1141
|
+
return ws.listenOne({
|
|
938
1142
|
path: slug,
|
|
939
|
-
|
|
940
|
-
}, (
|
|
941
|
-
if (
|
|
1143
|
+
id: String(id)
|
|
1144
|
+
}, (row) => {
|
|
1145
|
+
if (row) onUpdate(row);
|
|
942
1146
|
else onUpdate(void 0);
|
|
943
1147
|
}, onError);
|
|
944
1148
|
};
|
|
@@ -986,10 +1190,12 @@ function createStorage(transport, storageId) {
|
|
|
986
1190
|
if (!storageId) return path;
|
|
987
1191
|
return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
|
|
988
1192
|
};
|
|
989
|
-
async function putObject({ file, key, metadata, bucket }) {
|
|
1193
|
+
async function putObject({ file, key, metadata, bucket, public: isPublic }) {
|
|
990
1194
|
const formData = new FormData();
|
|
991
1195
|
formData.append("file", file);
|
|
992
|
-
|
|
1196
|
+
let effectiveKey = key;
|
|
1197
|
+
if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
|
|
1198
|
+
if (effectiveKey) formData.append("key", effectiveKey);
|
|
993
1199
|
if (bucket) formData.append("bucket", bucket);
|
|
994
1200
|
if (storageId) formData.append("storageId", storageId);
|
|
995
1201
|
if (metadata) {
|
|
@@ -1003,8 +1209,11 @@ function createStorage(transport, storageId) {
|
|
|
1003
1209
|
}
|
|
1004
1210
|
async function getSignedUrl(keyOrUrl, bucket) {
|
|
1005
1211
|
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1212
|
+
const cachedEntry = urlsCache.get(cacheKey);
|
|
1213
|
+
if (cachedEntry) {
|
|
1214
|
+
if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) return cachedEntry.config;
|
|
1215
|
+
urlsCache.delete(cacheKey);
|
|
1216
|
+
}
|
|
1008
1217
|
let filePath = keyOrUrl;
|
|
1009
1218
|
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
1010
1219
|
if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
|
|
@@ -1012,15 +1221,32 @@ function createStorage(transport, storageId) {
|
|
|
1012
1221
|
url: null,
|
|
1013
1222
|
fileNotFound: true
|
|
1014
1223
|
};
|
|
1224
|
+
if (isPublicStoragePath(filePath)) {
|
|
1225
|
+
const publicConfig = { url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`) };
|
|
1226
|
+
urlsCache.set(cacheKey, { config: publicConfig });
|
|
1227
|
+
return publicConfig;
|
|
1228
|
+
}
|
|
1015
1229
|
try {
|
|
1016
1230
|
const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
|
|
1017
|
-
|
|
1018
|
-
|
|
1231
|
+
if (result.data.public) {
|
|
1232
|
+
const publicConfig = {
|
|
1233
|
+
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`),
|
|
1234
|
+
metadata: result.data
|
|
1235
|
+
};
|
|
1236
|
+
urlsCache.set(cacheKey, { config: publicConfig });
|
|
1237
|
+
return publicConfig;
|
|
1238
|
+
}
|
|
1239
|
+
const scopedToken = result.data.token;
|
|
1240
|
+
const tokenQuery = scopedToken ? `?token=${scopedToken}` : "";
|
|
1019
1241
|
const downloadConfig = {
|
|
1020
1242
|
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
|
|
1021
1243
|
metadata: result.data
|
|
1022
1244
|
};
|
|
1023
|
-
|
|
1245
|
+
const expiresAt = result.data.tokenExpiresIn ? Date.now() + (result.data.tokenExpiresIn - 10) * 1e3 : void 0;
|
|
1246
|
+
urlsCache.set(cacheKey, {
|
|
1247
|
+
config: downloadConfig,
|
|
1248
|
+
expiresAt
|
|
1249
|
+
});
|
|
1024
1250
|
return downloadConfig;
|
|
1025
1251
|
} catch (e) {
|
|
1026
1252
|
if (e instanceof Error && "status" in e && e.status === 404) return {
|
|
@@ -1031,16 +1257,13 @@ function createStorage(transport, storageId) {
|
|
|
1031
1257
|
}
|
|
1032
1258
|
}
|
|
1033
1259
|
async function getObject(key, bucket) {
|
|
1034
|
-
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") return null;
|
|
1038
|
-
const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
|
|
1039
|
-
const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
|
|
1260
|
+
const downloadConfig = await getSignedUrl(key, bucket);
|
|
1261
|
+
if (downloadConfig.fileNotFound || !downloadConfig.url) return null;
|
|
1262
|
+
const response = await transport.fetchFn(downloadConfig.url, { headers: {} });
|
|
1040
1263
|
if (response.status === 404) return null;
|
|
1041
1264
|
if (!response.ok) throw new Error("Failed to get file");
|
|
1042
1265
|
const blob = await response.blob();
|
|
1043
|
-
const fileName =
|
|
1266
|
+
const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
|
|
1044
1267
|
return new File([blob], fileName, { type: blob.type });
|
|
1045
1268
|
}
|
|
1046
1269
|
async function deleteObject(key, bucket) {
|
|
@@ -1142,16 +1365,15 @@ function extractMessageError(message) {
|
|
|
1142
1365
|
errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
|
|
1143
1366
|
};
|
|
1144
1367
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
};
|
|
1368
|
+
/**
|
|
1369
|
+
* Low-level realtime WebSocket client.
|
|
1370
|
+
*
|
|
1371
|
+
* @internal Not a stable app-facing API. `createRebaseClient()` constructs and
|
|
1372
|
+
* manages this internally (exposed as `client.ws`, typed by the minimal
|
|
1373
|
+
* `RebaseWebSocket` contract in `@rebasepro/types`). It is re-exported from the
|
|
1374
|
+
* package root only because the `@rebasepro/client-postgresql` driver
|
|
1375
|
+
* instantiates it directly; its surface may change without a major bump.
|
|
1376
|
+
*/
|
|
1155
1377
|
var RebaseWebSocketClient = class {
|
|
1156
1378
|
websocketUrl;
|
|
1157
1379
|
ws = null;
|
|
@@ -1167,7 +1389,7 @@ var RebaseWebSocketClient = class {
|
|
|
1167
1389
|
if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
|
|
1168
1390
|
}
|
|
1169
1391
|
collectionSubscriptions = /* @__PURE__ */ new Map();
|
|
1170
|
-
|
|
1392
|
+
singleSubscriptions = /* @__PURE__ */ new Map();
|
|
1171
1393
|
backendToCollectionKey = /* @__PURE__ */ new Map();
|
|
1172
1394
|
backendToEntityKey = /* @__PURE__ */ new Map();
|
|
1173
1395
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -1302,7 +1524,7 @@ var RebaseWebSocketClient = class {
|
|
|
1302
1524
|
request.message._queuedResolve = request.resolve;
|
|
1303
1525
|
request.message._queuedReject = request.reject;
|
|
1304
1526
|
this.messageQueue.push(request.message);
|
|
1305
|
-
} else request.reject(new
|
|
1527
|
+
} else request.reject(new RebaseApiError$1("Connection closed"));
|
|
1306
1528
|
this.pendingRequests.delete(reqId);
|
|
1307
1529
|
}
|
|
1308
1530
|
this.attemptReconnect();
|
|
@@ -1369,7 +1591,7 @@ var RebaseWebSocketClient = class {
|
|
|
1369
1591
|
}
|
|
1370
1592
|
}
|
|
1371
1593
|
/**
|
|
1372
|
-
* Shared logic for re-subscribing a collection or
|
|
1594
|
+
* Shared logic for re-subscribing a collection or row subscription
|
|
1373
1595
|
* after an auth error is resolved by refreshing credentials.
|
|
1374
1596
|
*/
|
|
1375
1597
|
resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
|
|
@@ -1394,7 +1616,7 @@ var RebaseWebSocketClient = class {
|
|
|
1394
1616
|
});
|
|
1395
1617
|
} else {
|
|
1396
1618
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1397
|
-
const error = new
|
|
1619
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1398
1620
|
subscription.callbacks.forEach((callback) => {
|
|
1399
1621
|
if (callback.onError) callback.onError(error);
|
|
1400
1622
|
});
|
|
@@ -1415,7 +1637,7 @@ var RebaseWebSocketClient = class {
|
|
|
1415
1637
|
if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
|
|
1416
1638
|
else {
|
|
1417
1639
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1418
|
-
pendingReq.reject(new
|
|
1640
|
+
pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1419
1641
|
}
|
|
1420
1642
|
}).catch((err) => {
|
|
1421
1643
|
pendingReq.reject(err);
|
|
@@ -1423,7 +1645,7 @@ var RebaseWebSocketClient = class {
|
|
|
1423
1645
|
} else {
|
|
1424
1646
|
this.pendingRequests.delete(requestId);
|
|
1425
1647
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1426
|
-
pendingReq.reject(new
|
|
1648
|
+
pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1427
1649
|
}
|
|
1428
1650
|
else {
|
|
1429
1651
|
this.pendingRequests.delete(requestId);
|
|
@@ -1436,14 +1658,14 @@ var RebaseWebSocketClient = class {
|
|
|
1436
1658
|
if (subscriptionKey) {
|
|
1437
1659
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1438
1660
|
if (collectionSub) {
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
collectionSub.latestData =
|
|
1661
|
+
const incomingRows = message.rows || [];
|
|
1662
|
+
const rows = this.mergeRows(collectionSub.latestData, incomingRows);
|
|
1663
|
+
collectionSub.latestData = rows;
|
|
1442
1664
|
collectionSub.lastUpdated = Date.now();
|
|
1443
1665
|
collectionSub.isInitialDataReceived = true;
|
|
1444
1666
|
collectionSub.callbacks.forEach((callback) => {
|
|
1445
1667
|
try {
|
|
1446
|
-
callback.onUpdate(
|
|
1668
|
+
callback.onUpdate(rows);
|
|
1447
1669
|
} catch (error) {
|
|
1448
1670
|
console.error("Error in collection subscription callback:", error);
|
|
1449
1671
|
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -1453,21 +1675,22 @@ var RebaseWebSocketClient = class {
|
|
|
1453
1675
|
}
|
|
1454
1676
|
}
|
|
1455
1677
|
}
|
|
1456
|
-
if (subscriptionId && type === "
|
|
1678
|
+
if (subscriptionId && type === "collection_patch") {
|
|
1457
1679
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1458
1680
|
if (subscriptionKey) {
|
|
1459
1681
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1460
1682
|
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
1461
|
-
const
|
|
1462
|
-
const patchEntityId = message.
|
|
1683
|
+
const patchWireEntity = message.row ?? null;
|
|
1684
|
+
const patchEntityId = message.id;
|
|
1685
|
+
const patchRow = patchWireEntity ? patchWireEntity : null;
|
|
1463
1686
|
let updated;
|
|
1464
|
-
if (
|
|
1687
|
+
if (patchRow === null) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
|
|
1465
1688
|
else {
|
|
1466
|
-
const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(
|
|
1689
|
+
const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchRow.id));
|
|
1467
1690
|
if (idx >= 0) {
|
|
1468
1691
|
updated = [...collectionSub.latestData];
|
|
1469
|
-
updated[idx] =
|
|
1470
|
-
} else updated = [
|
|
1692
|
+
updated[idx] = patchRow;
|
|
1693
|
+
} else updated = [patchRow, ...collectionSub.latestData];
|
|
1471
1694
|
}
|
|
1472
1695
|
collectionSub.latestData = updated;
|
|
1473
1696
|
collectionSub.lastUpdated = Date.now();
|
|
@@ -1483,20 +1706,21 @@ var RebaseWebSocketClient = class {
|
|
|
1483
1706
|
}
|
|
1484
1707
|
}
|
|
1485
1708
|
}
|
|
1486
|
-
if (subscriptionId && type === "
|
|
1709
|
+
if (subscriptionId && type === "single_update") {
|
|
1487
1710
|
const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
|
|
1488
1711
|
if (subscriptionKey) {
|
|
1489
|
-
const entitySub = this.
|
|
1712
|
+
const entitySub = this.singleSubscriptions.get(subscriptionKey);
|
|
1490
1713
|
if (entitySub) {
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1714
|
+
const wireEntity = message.row ?? null;
|
|
1715
|
+
const row = wireEntity ? wireEntity : null;
|
|
1716
|
+
entitySub.latestData = row;
|
|
1493
1717
|
entitySub.lastUpdated = Date.now();
|
|
1494
1718
|
entitySub.isInitialDataReceived = true;
|
|
1495
1719
|
entitySub.callbacks.forEach((callback) => {
|
|
1496
1720
|
try {
|
|
1497
|
-
callback.onUpdate(
|
|
1721
|
+
callback.onUpdate(row);
|
|
1498
1722
|
} catch (error) {
|
|
1499
|
-
console.error("Error in
|
|
1723
|
+
console.error("Error in row subscription callback:", error);
|
|
1500
1724
|
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1501
1725
|
}
|
|
1502
1726
|
});
|
|
@@ -1514,7 +1738,7 @@ var RebaseWebSocketClient = class {
|
|
|
1514
1738
|
return;
|
|
1515
1739
|
}
|
|
1516
1740
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1517
|
-
const error = new
|
|
1741
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1518
1742
|
collectionSub.callbacks.forEach((callback) => {
|
|
1519
1743
|
if (callback.onError) callback.onError(error);
|
|
1520
1744
|
});
|
|
@@ -1523,14 +1747,14 @@ var RebaseWebSocketClient = class {
|
|
|
1523
1747
|
}
|
|
1524
1748
|
const entityKey = this.backendToEntityKey.get(subscriptionId);
|
|
1525
1749
|
if (entityKey) {
|
|
1526
|
-
const entitySub = this.
|
|
1750
|
+
const entitySub = this.singleSubscriptions.get(entityKey);
|
|
1527
1751
|
if (entitySub) {
|
|
1528
1752
|
if (this.isAuthError(message)) {
|
|
1529
|
-
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "
|
|
1753
|
+
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
|
|
1530
1754
|
return;
|
|
1531
1755
|
}
|
|
1532
1756
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1533
|
-
const error = new
|
|
1757
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1534
1758
|
entitySub.callbacks.forEach((callback) => {
|
|
1535
1759
|
if (callback.onError) callback.onError(error);
|
|
1536
1760
|
});
|
|
@@ -1544,7 +1768,7 @@ var RebaseWebSocketClient = class {
|
|
|
1544
1768
|
if (message.type === "ERROR" || message.error) {
|
|
1545
1769
|
if (callback.onError) {
|
|
1546
1770
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1547
|
-
callback.onError(new
|
|
1771
|
+
callback.onError(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1548
1772
|
}
|
|
1549
1773
|
} else callback.onUpdate(message);
|
|
1550
1774
|
}
|
|
@@ -1618,15 +1842,14 @@ var RebaseWebSocketClient = class {
|
|
|
1618
1842
|
if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
|
|
1619
1843
|
await this.ensureAuthenticated();
|
|
1620
1844
|
} catch (error) {
|
|
1621
|
-
|
|
1622
|
-
reject(new ApiError(errorMessage, errorMessage));
|
|
1845
|
+
reject(new RebaseApiError$1(error instanceof Error ? error.message : "Authentication required"));
|
|
1623
1846
|
return;
|
|
1624
1847
|
}
|
|
1625
1848
|
const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1626
1849
|
message.requestId = requestId;
|
|
1627
1850
|
const expectsResponse = ![
|
|
1628
1851
|
"subscribe_collection",
|
|
1629
|
-
"
|
|
1852
|
+
"subscribe_one",
|
|
1630
1853
|
"unsubscribe",
|
|
1631
1854
|
"join_channel",
|
|
1632
1855
|
"leave_channel",
|
|
@@ -1639,7 +1862,7 @@ var RebaseWebSocketClient = class {
|
|
|
1639
1862
|
const timeoutHandle = setTimeout(() => {
|
|
1640
1863
|
if (this.pendingRequests.has(requestId)) {
|
|
1641
1864
|
this.pendingRequests.delete(requestId);
|
|
1642
|
-
reject(new
|
|
1865
|
+
reject(new RebaseApiError$1("Request timed out"));
|
|
1643
1866
|
}
|
|
1644
1867
|
}, this.requestTimeoutMs);
|
|
1645
1868
|
this.pendingRequests.set(requestId, {
|
|
@@ -1659,30 +1882,30 @@ var RebaseWebSocketClient = class {
|
|
|
1659
1882
|
if (!expectsResponse) resolve(void 0);
|
|
1660
1883
|
} catch (error) {
|
|
1661
1884
|
if (expectsResponse) this.pendingRequests.delete(requestId);
|
|
1662
|
-
reject(new
|
|
1885
|
+
reject(new RebaseApiError$1("Failed to send message", { cause: error }));
|
|
1663
1886
|
}
|
|
1664
1887
|
}
|
|
1665
1888
|
async fetchCollection(props) {
|
|
1666
1889
|
return (await this.sendMessage({
|
|
1667
1890
|
type: "FETCH_COLLECTION",
|
|
1668
1891
|
payload: props
|
|
1669
|
-
})).
|
|
1892
|
+
})).rows || [];
|
|
1670
1893
|
}
|
|
1671
|
-
async
|
|
1894
|
+
async fetchOne(props) {
|
|
1672
1895
|
return (await this.sendMessage({
|
|
1673
|
-
type: "
|
|
1896
|
+
type: "FETCH_ONE",
|
|
1674
1897
|
payload: props
|
|
1675
|
-
})).
|
|
1898
|
+
})).row ?? void 0;
|
|
1676
1899
|
}
|
|
1677
|
-
async
|
|
1900
|
+
async save(props) {
|
|
1678
1901
|
return (await this.sendMessage({
|
|
1679
|
-
type: "
|
|
1902
|
+
type: "SAVE",
|
|
1680
1903
|
payload: props
|
|
1681
|
-
})).
|
|
1904
|
+
})).row;
|
|
1682
1905
|
}
|
|
1683
|
-
async
|
|
1906
|
+
async delete(props) {
|
|
1684
1907
|
await this.sendMessage({
|
|
1685
|
-
type: "
|
|
1908
|
+
type: "DELETE",
|
|
1686
1909
|
payload: props
|
|
1687
1910
|
});
|
|
1688
1911
|
}
|
|
@@ -1707,21 +1930,21 @@ var RebaseWebSocketClient = class {
|
|
|
1707
1930
|
async fetchCurrentDatabase() {
|
|
1708
1931
|
return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
|
|
1709
1932
|
}
|
|
1710
|
-
async checkUniqueField(path, name, value,
|
|
1933
|
+
async checkUniqueField(path, name, value, id, collection) {
|
|
1711
1934
|
return (await this.sendMessage({
|
|
1712
1935
|
type: "CHECK_UNIQUE_FIELD",
|
|
1713
1936
|
payload: {
|
|
1714
1937
|
path,
|
|
1715
1938
|
name,
|
|
1716
1939
|
value,
|
|
1717
|
-
|
|
1940
|
+
id,
|
|
1718
1941
|
collection
|
|
1719
1942
|
}
|
|
1720
1943
|
})).isUnique;
|
|
1721
1944
|
}
|
|
1722
|
-
async
|
|
1945
|
+
async count(props) {
|
|
1723
1946
|
return (await this.sendMessage({
|
|
1724
|
-
type: "
|
|
1947
|
+
type: "COUNT",
|
|
1725
1948
|
payload: props
|
|
1726
1949
|
})).count;
|
|
1727
1950
|
}
|
|
@@ -1813,33 +2036,31 @@ var RebaseWebSocketClient = class {
|
|
|
1813
2036
|
return val;
|
|
1814
2037
|
}
|
|
1815
2038
|
/**
|
|
1816
|
-
* Merge incoming
|
|
1817
|
-
* for
|
|
1818
|
-
* React re-renders when the server refetches all
|
|
2039
|
+
* Merge incoming rows with cached data, preserving cached references
|
|
2040
|
+
* for rows whose values haven't changed. This avoids unnecessary
|
|
2041
|
+
* React re-renders when the server refetches all rows but most
|
|
1819
2042
|
* haven't actually changed.
|
|
1820
2043
|
*/
|
|
1821
|
-
|
|
2044
|
+
mergeRows(cached, incoming) {
|
|
1822
2045
|
if (!cached || cached.length === 0) return incoming;
|
|
1823
2046
|
const cachedById = /* @__PURE__ */ new Map();
|
|
1824
|
-
for (const
|
|
1825
|
-
return incoming.map((
|
|
1826
|
-
const
|
|
1827
|
-
if (!
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1840
|
-
}
|
|
2047
|
+
for (const row of cached) cachedById.set(row.id, row);
|
|
2048
|
+
return incoming.map((incomingRow) => {
|
|
2049
|
+
const cachedRow = cachedById.get(incomingRow.id);
|
|
2050
|
+
if (!cachedRow) return incomingRow;
|
|
2051
|
+
const normCached = this.normalizeForComparison(cachedRow);
|
|
2052
|
+
const normIncoming = this.normalizeForComparison(incomingRow);
|
|
2053
|
+
if (this.deepEqual(normCached, normIncoming)) return cachedRow;
|
|
2054
|
+
else {
|
|
2055
|
+
const mismatches = {};
|
|
2056
|
+
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2057
|
+
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
2058
|
+
cached: normCached[key],
|
|
2059
|
+
incoming: normIncoming[key]
|
|
2060
|
+
};
|
|
2061
|
+
console.debug(`[RebaseWS] Row ${incomingRow.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1841
2062
|
}
|
|
1842
|
-
return
|
|
2063
|
+
return incomingRow;
|
|
1843
2064
|
});
|
|
1844
2065
|
}
|
|
1845
2066
|
listenCollection(props, onUpdate, onError) {
|
|
@@ -1907,10 +2128,10 @@ var RebaseWebSocketClient = class {
|
|
|
1907
2128
|
}
|
|
1908
2129
|
};
|
|
1909
2130
|
}
|
|
1910
|
-
|
|
1911
|
-
const subscriptionKey = this.
|
|
2131
|
+
listenOne(props, onUpdate, onError) {
|
|
2132
|
+
const subscriptionKey = this.createSingleSubscriptionKey(props);
|
|
1912
2133
|
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1913
|
-
const existingSubscription = this.
|
|
2134
|
+
const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1914
2135
|
if (existingSubscription) {
|
|
1915
2136
|
const callbackMap = existingSubscription.callbacks;
|
|
1916
2137
|
callbackMap.set(callbackId, {
|
|
@@ -1920,13 +2141,13 @@ var RebaseWebSocketClient = class {
|
|
|
1920
2141
|
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
|
|
1921
2142
|
onUpdate(existingSubscription.latestData);
|
|
1922
2143
|
} catch (error) {
|
|
1923
|
-
console.error("Error in
|
|
2144
|
+
console.error("Error in row subscription callback:", error);
|
|
1924
2145
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
1925
2146
|
}
|
|
1926
2147
|
return () => {
|
|
1927
2148
|
callbackMap.delete(callbackId);
|
|
1928
2149
|
if (callbackMap.size === 0) {
|
|
1929
|
-
this.
|
|
2150
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1930
2151
|
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
1931
2152
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
1932
2153
|
type: "unsubscribe",
|
|
@@ -1941,14 +2162,14 @@ var RebaseWebSocketClient = class {
|
|
|
1941
2162
|
onUpdate,
|
|
1942
2163
|
onError
|
|
1943
2164
|
});
|
|
1944
|
-
this.
|
|
2165
|
+
this.singleSubscriptions.set(subscriptionKey, {
|
|
1945
2166
|
backendSubscriptionId,
|
|
1946
2167
|
callbacks: callbackMap,
|
|
1947
2168
|
props
|
|
1948
2169
|
});
|
|
1949
2170
|
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
1950
2171
|
this.sendMessage({
|
|
1951
|
-
type: "
|
|
2172
|
+
type: "subscribe_one",
|
|
1952
2173
|
payload: {
|
|
1953
2174
|
...props,
|
|
1954
2175
|
subscriptionId: backendSubscriptionId
|
|
@@ -1957,12 +2178,12 @@ var RebaseWebSocketClient = class {
|
|
|
1957
2178
|
if (onError) onError(error);
|
|
1958
2179
|
});
|
|
1959
2180
|
return () => {
|
|
1960
|
-
const subscription = this.
|
|
2181
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1961
2182
|
if (subscription) {
|
|
1962
2183
|
const callbacks = subscription.callbacks;
|
|
1963
2184
|
callbacks.delete(callbackId);
|
|
1964
2185
|
if (callbacks.size === 0) {
|
|
1965
|
-
this.
|
|
2186
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1966
2187
|
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
1967
2188
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
1968
2189
|
type: "unsubscribe",
|
|
@@ -1978,7 +2199,7 @@ var RebaseWebSocketClient = class {
|
|
|
1978
2199
|
* we need to re-register everything to resume receiving updates.
|
|
1979
2200
|
*/
|
|
1980
2201
|
resubscribeAll() {
|
|
1981
|
-
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.
|
|
2202
|
+
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
|
|
1982
2203
|
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
1983
2204
|
const oldBackendId = sub.backendSubscriptionId;
|
|
1984
2205
|
const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -1995,20 +2216,20 @@ var RebaseWebSocketClient = class {
|
|
|
1995
2216
|
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
1996
2217
|
});
|
|
1997
2218
|
}
|
|
1998
|
-
for (const [key, sub] of this.
|
|
2219
|
+
for (const [key, sub] of this.singleSubscriptions.entries()) {
|
|
1999
2220
|
const oldBackendId = sub.backendSubscriptionId;
|
|
2000
2221
|
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2001
2222
|
sub.backendSubscriptionId = newBackendId;
|
|
2002
2223
|
this.backendToEntityKey.delete(oldBackendId);
|
|
2003
2224
|
this.backendToEntityKey.set(newBackendId, key);
|
|
2004
2225
|
this.sendMessage({
|
|
2005
|
-
type: "
|
|
2226
|
+
type: "subscribe_one",
|
|
2006
2227
|
payload: {
|
|
2007
2228
|
...sub.props,
|
|
2008
2229
|
subscriptionId: newBackendId
|
|
2009
2230
|
}
|
|
2010
2231
|
}).catch((error) => {
|
|
2011
|
-
console.error("[WS] Failed to re-subscribe
|
|
2232
|
+
console.error("[WS] Failed to re-subscribe row:", key, error);
|
|
2012
2233
|
});
|
|
2013
2234
|
}
|
|
2014
2235
|
}
|
|
@@ -2031,8 +2252,8 @@ var RebaseWebSocketClient = class {
|
|
|
2031
2252
|
return value;
|
|
2032
2253
|
});
|
|
2033
2254
|
}
|
|
2034
|
-
|
|
2035
|
-
return `${props.path}|${props.
|
|
2255
|
+
createSingleSubscriptionKey(props) {
|
|
2256
|
+
return `${props.path}|${props.id}`;
|
|
2036
2257
|
}
|
|
2037
2258
|
};
|
|
2038
2259
|
//#endregion
|
|
@@ -2120,7 +2341,45 @@ function createRebaseClient(options) {
|
|
|
2120
2341
|
return false;
|
|
2121
2342
|
}
|
|
2122
2343
|
});
|
|
2344
|
+
/**
|
|
2345
|
+
* Suggest the closest known collection key for a mistyped accessor.
|
|
2346
|
+
* Uses edit-distance-1 and prefix matching — no external dependency.
|
|
2347
|
+
*/
|
|
2348
|
+
function suggestCollection(prop, knownKeys) {
|
|
2349
|
+
const prefixMatch = knownKeys.find((k) => k.startsWith(prop) || prop.startsWith(k));
|
|
2350
|
+
if (prefixMatch) return prefixMatch;
|
|
2351
|
+
for (const key of knownKeys) {
|
|
2352
|
+
if (Math.abs(key.length - prop.length) > 1) continue;
|
|
2353
|
+
let diffs = 0;
|
|
2354
|
+
const longer = key.length >= prop.length ? key : prop;
|
|
2355
|
+
const shorter = key.length >= prop.length ? prop : key;
|
|
2356
|
+
if (longer.length === shorter.length) for (let i = 0; i < longer.length; i++) {
|
|
2357
|
+
if (longer[i] !== shorter[i]) {
|
|
2358
|
+
if (i + 1 < longer.length && longer[i] === shorter[i + 1] && longer[i + 1] === shorter[i]) {
|
|
2359
|
+
diffs++;
|
|
2360
|
+
i++;
|
|
2361
|
+
if (diffs > 1) break;
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
diffs++;
|
|
2365
|
+
}
|
|
2366
|
+
if (diffs > 1) break;
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
let li = 0;
|
|
2370
|
+
let si = 0;
|
|
2371
|
+
while (li < longer.length) {
|
|
2372
|
+
if (si < shorter.length && longer[li] === shorter[si]) si++;
|
|
2373
|
+
else diffs++;
|
|
2374
|
+
li++;
|
|
2375
|
+
if (diffs > 1) break;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (diffs <= 1) return key;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2123
2381
|
const collectionClients = /* @__PURE__ */ new Map();
|
|
2382
|
+
let untypedWarned = false;
|
|
2124
2383
|
function collection(slug) {
|
|
2125
2384
|
if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
|
|
2126
2385
|
return collectionClients.get(slug);
|
|
@@ -2129,7 +2388,19 @@ function createRebaseClient(options) {
|
|
|
2129
2388
|
if (prop === "collection") return collection;
|
|
2130
2389
|
if (typeof prop === "symbol") return void 0;
|
|
2131
2390
|
if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
|
|
2132
|
-
if (options.collections
|
|
2391
|
+
if (options.collections) {
|
|
2392
|
+
if (prop in options.collections) return collection(options.collections[prop]);
|
|
2393
|
+
const knownKeys = Object.keys(options.collections);
|
|
2394
|
+
const suggestion = suggestCollection(prop, knownKeys);
|
|
2395
|
+
let msg = `Unknown collection accessor "${prop}". Known collections: ${knownKeys.join(", ")}.`;
|
|
2396
|
+
if (suggestion) msg += ` Did you mean "${suggestion}"?`;
|
|
2397
|
+
msg += ` Use data.collection("<slug>") for dynamic slugs.`;
|
|
2398
|
+
throw new RebaseClientError(msg);
|
|
2399
|
+
}
|
|
2400
|
+
if (!untypedWarned) {
|
|
2401
|
+
untypedWarned = true;
|
|
2402
|
+
console.warn(`[Rebase] Untyped data access detected (client.data.${prop}). Collection names are resolved via snake_case conversion, which may cause silent 404s at request time. Pass a \`collections\` dictionary to createRebaseClient() or use the generated SDK for type-safe access.`);
|
|
2403
|
+
}
|
|
2133
2404
|
return collection(toSnakeCase(prop));
|
|
2134
2405
|
}
|
|
2135
2406
|
} });
|
|
@@ -2158,11 +2429,10 @@ function createRebaseClient(options) {
|
|
|
2158
2429
|
});
|
|
2159
2430
|
return res.data ?? res;
|
|
2160
2431
|
},
|
|
2161
|
-
data: dataProxy
|
|
2162
|
-
email: void 0
|
|
2432
|
+
data: dataProxy
|
|
2163
2433
|
};
|
|
2164
2434
|
}
|
|
2165
2435
|
//#endregion
|
|
2166
|
-
export {
|
|
2436
|
+
export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createCookieStorage, createMemoryStorage, createRebaseClient, or };
|
|
2167
2437
|
|
|
2168
2438
|
//# sourceMappingURL=index.es.js.map
|