@rebasepro/client 0.7.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/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
- import { EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
2
- import { QueryBuilder, and, cond, or } from "@rebasepro/common";
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,80 +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
- /**
46
- * Maps a short operator alias to the PostgREST-style short code.
47
- */
48
- var OP_MAP = {
49
- "==": "eq",
50
- "!=": "neq",
51
- ">": "gt",
52
- ">=": "gte",
53
- "<": "lt",
54
- "<=": "lte",
55
- "not-in": "nin",
56
- "array-contains": "cs",
57
- "array-contains-any": "csa"
58
- };
59
- /**
60
- * Normalise a single `WhereFieldValue` into the PostgREST query-string
61
- * representation the backend expects.
62
- *
63
- * Supports:
64
- * - `null` → `"eq.null"`
65
- * - `true`/`false` → `"eq.true"` / `"eq.false"`
66
- * - `42` → `"42"` (plain equality)
67
- * - `"active"` → `"active"` (plain equality, backward-compat)
68
- * - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
69
- * - `[">=", 18]` → `"gte.18"` (tuple syntax)
70
- * - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
71
- * - `["!=", null]` → `"neq.null"`
72
- */
73
- function normalizeWhereValue(value) {
74
- if (value === null) return "eq.null";
75
- if (typeof value === "boolean") return `eq.${value}`;
76
- if (typeof value === "number") return String(value);
77
- if (Array.isArray(value)) {
78
- const [rawOp, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
79
- if (rawOp) {
80
- const op = OP_MAP[rawOp] ?? rawOp;
81
- if (val === null) return `${op}.null`;
82
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
83
- return `${op}.${val}`;
84
- }
85
- }
86
- return String(value);
87
- }
88
- function serializeLogicalCondition(cond) {
89
- if ("type" in cond) {
90
- const sub = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
91
- return `${cond.type}(${sub})`;
92
- } else {
93
- const op = OP_MAP[cond.operator] ?? cond.operator;
94
- let formattedValue = cond.value;
95
- if (Array.isArray(cond.value)) formattedValue = `(${cond.value.join(",")})`;
96
- else if (cond.value === null) formattedValue = "null";
97
- return `${cond.column}.${op}.${formattedValue}`;
98
- }
99
- }
100
33
  function buildQueryString(params) {
101
34
  if (!params) return "";
102
35
  const parts = [];
103
36
  if (params.limit != null) parts.push(`limit=${params.limit}`);
104
37
  if (params.offset != null) parts.push(`offset=${params.offset}`);
105
38
  if (params.page != null) parts.push(`page=${params.page}`);
106
- if (params.orderBy) parts.push(`orderBy=${encodeURIComponent(params.orderBy)}`);
39
+ if (params.orderBy) {
40
+ const wire = serializeOrderBy(params.orderBy);
41
+ if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
42
+ }
107
43
  if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
108
44
  if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
109
45
  if (params.logical) {
@@ -111,13 +47,10 @@ function buildQueryString(params) {
111
47
  const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
112
48
  parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
113
49
  }
114
- if (params.where) for (const [field, value] of Object.entries(params.where)) if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) for (const subVal of value) {
115
- const normalized = normalizeWhereValue(subVal);
116
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
117
- }
118
- else {
119
- const normalized = normalizeWhereValue(value);
120
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
50
+ if (params.where) {
51
+ const serialized = serializeFilter(params.where);
52
+ for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
53
+ else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
121
54
  }
122
55
  return parts.length > 0 ? "?" + parts.join("&") : "";
123
56
  }
@@ -155,8 +88,7 @@ function createTransport(config) {
155
88
  } catch (e) {}
156
89
  const getErrorField = (obj, field) => {
157
90
  const err = obj?.error;
158
- if (err && typeof err === "object" && err !== null && field in err) return err[field];
159
- return obj?.[field];
91
+ if (err && typeof err === "object" && err !== null) return err[field];
160
92
  };
161
93
  if (res.status === 401 && onUnauthorizedHandler) {
162
94
  if (await onUnauthorizedHandler()) {
@@ -179,7 +111,11 @@ function createTransport(config) {
179
111
  if (!retryRes.ok) {
180
112
  let fallbackMessage = retryRes.statusText;
181
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.`;
182
- throw new RebaseApiError(retryRes.status, String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), getErrorField(retryBody, "code"), getErrorField(retryBody, "details"));
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
+ });
183
119
  }
184
120
  return retryBody;
185
121
  }
@@ -187,7 +123,11 @@ function createTransport(config) {
187
123
  if (!res.ok) {
188
124
  let fallbackMessage = res.statusText;
189
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.`;
190
- throw new RebaseApiError(res.status, String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), getErrorField(body, "code"), getErrorField(body, "details"));
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
+ });
191
131
  }
192
132
  return body;
193
133
  }
@@ -223,6 +163,29 @@ function createTransport(config) {
223
163
  }
224
164
  //#endregion
225
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
+ };
226
189
  function createMemoryStorage() {
227
190
  const store = {};
228
191
  return {
@@ -253,11 +216,20 @@ function createAuth(transport, options) {
253
216
  const authPath = opts.authPath || "/auth";
254
217
  const autoRefresh = opts.autoRefresh !== false;
255
218
  const persistSession = opts.persistSession !== false;
219
+ const authFlowMode = opts.authFlowMode || "json";
256
220
  const STORAGE_KEY = "rebase_auth";
257
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;
258
225
  let currentSession = null;
259
226
  const listeners = /* @__PURE__ */ new Set();
260
227
  let refreshTimeout = null;
228
+ let inFlightRefresh = null;
229
+ let resolveInitialized;
230
+ const isInitialized = new Promise((resolve) => {
231
+ resolveInitialized = resolve;
232
+ });
261
233
  function authUrl(endpoint) {
262
234
  return transport.baseUrl + transport.apiPath + authPath + endpoint;
263
235
  }
@@ -265,7 +237,11 @@ function createAuth(transport, options) {
265
237
  return transport.fetchFn || globalThis.fetch;
266
238
  }
267
239
  function throwApiError(status, body, statusText) {
268
- throw new RebaseApiError(status, body?.error?.message || body?.message || statusText, body?.error?.code || body?.code, body?.error?.details || body?.details);
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
+ });
269
245
  }
270
246
  function emit(event, session) {
271
247
  for (const fn of listeners) try {
@@ -273,7 +249,7 @@ function createAuth(transport, options) {
273
249
  } catch (e) {}
274
250
  }
275
251
  function saveSession(session) {
276
- if (!persistSession) return;
252
+ if (!persistSession || authFlowMode === "cookie") return;
277
253
  try {
278
254
  storage.setItem(STORAGE_KEY, JSON.stringify(session));
279
255
  } catch (e) {}
@@ -290,28 +266,53 @@ function createAuth(transport, options) {
290
266
  } catch (e) {}
291
267
  return null;
292
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
+ }
293
297
  function scheduleRefresh(expiresAt) {
294
298
  if (refreshTimeout) clearTimeout(refreshTimeout);
295
299
  if (!autoRefresh) return;
296
300
  const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
297
301
  if (delay <= 0) {
298
- refreshSession().catch(() => signOut());
302
+ attemptScheduledRefresh(0);
299
303
  return;
300
304
  }
301
- refreshTimeout = setTimeout(async () => {
302
- try {
303
- await refreshSession();
304
- } catch (e) {
305
- signOut();
306
- }
305
+ refreshTimeout = setTimeout(() => {
306
+ attemptScheduledRefresh(0);
307
307
  }, delay);
308
308
  }
309
309
  function handleAuthResponse(data, event) {
310
+ const user = mapRawUser(data.user);
310
311
  const session = {
311
312
  accessToken: data.tokens.accessToken,
312
- refreshToken: data.tokens.refreshToken,
313
+ refreshToken: data.tokens.refreshToken || currentSession?.refreshToken || "",
313
314
  expiresAt: data.tokens.accessTokenExpiresAt,
314
- user: data.user
315
+ user
315
316
  };
316
317
  currentSession = session;
317
318
  saveSession(session);
@@ -327,7 +328,8 @@ function createAuth(transport, options) {
327
328
  body: JSON.stringify({
328
329
  email,
329
330
  password
330
- })
331
+ }),
332
+ credentials: authFlowMode === "cookie" ? "include" : void 0
331
333
  });
332
334
  const body = await res.json().catch(() => ({}));
333
335
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -348,7 +350,8 @@ function createAuth(transport, options) {
348
350
  const res = await fetchFn(authUrl("/register"), {
349
351
  method: "POST",
350
352
  headers: { "Content-Type": "application/json" },
351
- body: JSON.stringify(payload)
353
+ body: JSON.stringify(payload),
354
+ credentials: authFlowMode === "cookie" ? "include" : void 0
352
355
  });
353
356
  const body = await res.json().catch(() => ({}));
354
357
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -371,7 +374,8 @@ function createAuth(transport, options) {
371
374
  const res = await getFetch()(authUrl("/google"), {
372
375
  method: "POST",
373
376
  headers: { "Content-Type": "application/json" },
374
- body: JSON.stringify(payload)
377
+ body: JSON.stringify(payload),
378
+ credentials: authFlowMode === "cookie" ? "include" : void 0
375
379
  });
376
380
  const responseBody = await res.json().catch(() => ({}));
377
381
  if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
@@ -389,7 +393,8 @@ function createAuth(transport, options) {
389
393
  body: JSON.stringify({
390
394
  code,
391
395
  redirectUri
392
- })
396
+ }),
397
+ credentials: authFlowMode === "cookie" ? "include" : void 0
393
398
  });
394
399
  const body = await res.json().catch(() => ({}));
395
400
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -408,7 +413,8 @@ function createAuth(transport, options) {
408
413
  const res = await getFetch()(authUrl(`/${providerId}`), {
409
414
  method: "POST",
410
415
  headers: { "Content-Type": "application/json" },
411
- body: JSON.stringify(payload)
416
+ body: JSON.stringify(payload),
417
+ credentials: authFlowMode === "cookie" ? "include" : void 0
412
418
  });
413
419
  const body = await res.json().catch(() => ({}));
414
420
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -484,10 +490,11 @@ function createAuth(transport, options) {
484
490
  async function signOut() {
485
491
  const fetchFn = getFetch();
486
492
  try {
487
- if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
493
+ if (authFlowMode === "cookie" || currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
488
494
  method: "POST",
489
495
  headers: { "Content-Type": "application/json" },
490
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
496
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
497
+ credentials: authFlowMode === "cookie" ? "include" : void 0
491
498
  });
492
499
  } catch (e) {}
493
500
  currentSession = null;
@@ -499,20 +506,35 @@ function createAuth(transport, options) {
499
506
  transport.setToken(null);
500
507
  emit("SIGNED_OUT", null);
501
508
  }
502
- async function refreshSession() {
503
- if (!currentSession?.refreshToken) throw new Error("No active session to refresh");
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");
504
518
  const res = await getFetch()(authUrl("/refresh"), {
505
519
  method: "POST",
506
520
  headers: { "Content-Type": "application/json" },
507
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
521
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
522
+ credentials: authFlowMode === "cookie" ? "include" : void 0
508
523
  });
509
524
  const body = await res.json().catch(() => ({}));
510
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 {}
511
533
  const session = {
512
- accessToken: body.tokens.accessToken,
513
- refreshToken: body.tokens.refreshToken,
534
+ accessToken,
535
+ refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
514
536
  expiresAt: body.tokens.accessTokenExpiresAt,
515
- user: currentSession.user
537
+ user: user ?? EMPTY_USER
516
538
  };
517
539
  currentSession = session;
518
540
  saveSession(session);
@@ -524,6 +546,18 @@ function createAuth(transport, options) {
524
546
  async function getUser() {
525
547
  return (await transport.request(authPath + "/me", { method: "GET" })).user;
526
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
+ }
527
561
  async function updateUser(updates) {
528
562
  const data = await transport.request(authPath + "/me", {
529
563
  method: "PATCH",
@@ -597,7 +631,8 @@ function createAuth(transport, options) {
597
631
  const res = await getFetch()(authUrl("/magic-link/verify"), {
598
632
  method: "POST",
599
633
  headers: { "Content-Type": "application/json" },
600
- body: JSON.stringify({ token })
634
+ body: JSON.stringify({ token }),
635
+ credentials: authFlowMode === "cookie" ? "include" : void 0
601
636
  });
602
637
  const body = await res.json().catch(() => ({}));
603
638
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -644,21 +679,29 @@ function createAuth(transport, options) {
644
679
  }
645
680
  if (persistSession) {
646
681
  const stored = loadStoredSession();
647
- if (stored && stored.accessToken && stored.refreshToken) {
648
- if (stored.expiresAt > Date.now()) {
649
- currentSession = stored;
650
- transport.setToken(stored.accessToken);
651
- scheduleRefresh(stored.expiresAt);
652
- } else if (stored.refreshToken) {
653
- currentSession = stored;
654
- refreshSession().catch(() => {
655
- currentSession = null;
656
- clearStoredSession();
657
- transport.setToken(null);
658
- });
659
- }
660
- }
661
- }
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();
662
705
  return {
663
706
  signInWithEmail,
664
707
  signUp,
@@ -678,6 +721,7 @@ function createAuth(transport, options) {
678
721
  signOut,
679
722
  refreshSession,
680
723
  getUser,
724
+ findUserByEmail,
681
725
  updateUser,
682
726
  resetPasswordForEmail,
683
727
  resetPassword,
@@ -691,7 +735,8 @@ function createAuth(transport, options) {
691
735
  revokeAllSessions,
692
736
  getAuthConfig,
693
737
  getSession,
694
- onAuthStateChange
738
+ onAuthStateChange,
739
+ isInitialized: () => isInitialized
695
740
  };
696
741
  }
697
742
  function createCookieStorage(options = {}) {
@@ -868,108 +913,108 @@ function createApiKeys(transport, options) {
868
913
  };
869
914
  }
870
915
  //#endregion
871
- //#region src/collection.ts
872
- function parseWhereFilter(where) {
873
- if (!where) return void 0;
874
- const filters = {};
875
- const OP_TO_FILTER = {
876
- "eq": "==",
877
- "neq": "!=",
878
- "gt": ">",
879
- "gte": ">=",
880
- "lt": "<",
881
- "lte": "<=",
882
- "==": "==",
883
- "!=": "!=",
884
- ">": ">",
885
- ">=": ">=",
886
- "<": "<",
887
- "<=": "<=",
888
- "in": "in",
889
- "nin": "not-in",
890
- "not-in": "not-in",
891
- "cs": "array-contains",
892
- "csa": "array-contains-any",
893
- "array-contains": "array-contains",
894
- "array-contains-any": "array-contains-any"
895
- };
896
- const parseSingle = (rawValue, fieldKey) => {
897
- if (rawValue === null) return ["==", null];
898
- if (typeof rawValue === "boolean") return ["==", rawValue];
899
- if (typeof rawValue === "number") return ["==", rawValue];
900
- if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
901
- const [rawOp, val] = rawValue;
902
- return [OP_TO_FILTER[rawOp] ?? "==", val];
903
- }
904
- const value = String(rawValue);
905
- const dotIndex = value.indexOf(".");
906
- if (dotIndex > 0) {
907
- const opStr = value.substring(0, dotIndex);
908
- const valStr = value.substring(dotIndex + 1);
909
- let op = "==";
910
- let val = valStr;
911
- switch (opStr) {
912
- case "eq":
913
- op = "==";
914
- break;
915
- case "neq":
916
- op = "!=";
917
- break;
918
- case "gt":
919
- op = ">";
920
- break;
921
- case "gte":
922
- op = ">=";
923
- break;
924
- case "lt":
925
- op = "<";
926
- break;
927
- case "lte":
928
- op = "<=";
929
- break;
930
- case "in":
931
- op = "in";
932
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
933
- break;
934
- case "nin":
935
- op = "not-in";
936
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
937
- break;
938
- case "cs":
939
- op = "array-contains";
940
- break;
941
- case "csa":
942
- op = "array-contains-any";
943
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
944
- break;
945
- default:
946
- op = "==";
947
- val = value;
948
- }
949
- if (val === "true") val = true;
950
- else if (val === "false") val = false;
951
- else if (val === "null") val = null;
952
- else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
953
- return [op, val];
954
- } else return ["==", value];
955
- };
956
- for (const [key, rawValue] of Object.entries(where)) if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) filters[key] = rawValue.map((r) => parseSingle(r, key));
957
- else filters[key] = parseSingle(rawValue, key);
958
- return filters;
959
- }
916
+ //#region src/sdk_query_builder.ts
960
917
  /**
961
- * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
962
- * a proper `Entity<M>` structure expected by the core framework.
963
- * The `id` is kept inside `values` as well, since collection properties
964
- * may define an `isId` field that the form binds to `formex.values`.
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
965
929
  */
966
- function rowToEntity(row, slug) {
967
- return {
968
- id: row.id,
969
- path: slug,
970
- values: row
971
- };
972
- }
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
973
1018
  function createCollectionClient(transport, slug, ws) {
974
1019
  const basePath = `/data/${slug}`;
975
1020
  const client = {
@@ -977,7 +1022,7 @@ function createCollectionClient(transport, slug, ws) {
977
1022
  const qs = buildQueryString(params);
978
1023
  const raw = await transport.request(basePath + qs, { method: "GET" });
979
1024
  return {
980
- data: (raw.data || []).map((row) => rowToEntity(row, slug)),
1025
+ data: raw.data || [],
981
1026
  meta: raw.meta
982
1027
  };
983
1028
  },
@@ -985,7 +1030,7 @@ function createCollectionClient(transport, slug, ws) {
985
1030
  try {
986
1031
  const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
987
1032
  if (!raw) return void 0;
988
- return rowToEntity(raw, slug);
1033
+ return raw;
989
1034
  } catch (err) {
990
1035
  if (err instanceof RebaseApiError && err.status === 404) return;
991
1036
  throw err;
@@ -994,19 +1039,19 @@ function createCollectionClient(transport, slug, ws) {
994
1039
  async create(data, id) {
995
1040
  const body = { ...data };
996
1041
  if (id !== void 0) body.id = id;
997
- return rowToEntity(await transport.request(basePath, {
1042
+ return await transport.request(basePath, {
998
1043
  method: "POST",
999
1044
  body: JSON.stringify(body)
1000
- }), slug);
1045
+ });
1001
1046
  },
1002
1047
  async update(id, data) {
1003
- return rowToEntity(await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1048
+ return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1004
1049
  method: "PUT",
1005
1050
  body: JSON.stringify(data)
1006
- }), slug);
1051
+ });
1007
1052
  },
1008
1053
  async delete(id) {
1009
- return transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
1054
+ await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
1010
1055
  },
1011
1056
  async count(params) {
1012
1057
  const qs = buildQueryString({
@@ -1017,55 +1062,87 @@ function createCollectionClient(transport, slug, ws) {
1017
1062
  return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
1018
1063
  },
1019
1064
  where(columnOrCondition, operator, value) {
1020
- const builder = new QueryBuilder(client);
1065
+ const builder = new SDKQueryBuilder(client);
1021
1066
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
1022
1067
  return builder.where(columnOrCondition, operator, value);
1023
1068
  },
1024
- orderBy(column, ascending) {
1025
- return new QueryBuilder(client).orderBy(column, ascending);
1069
+ orderBy(column, direction) {
1070
+ return new SDKQueryBuilder(client).orderBy(column, direction);
1026
1071
  },
1027
1072
  limit(count) {
1028
- return new QueryBuilder(client).limit(count);
1073
+ return new SDKQueryBuilder(client).limit(count);
1029
1074
  },
1030
1075
  offset(count) {
1031
- return new QueryBuilder(client).offset(count);
1076
+ return new SDKQueryBuilder(client).offset(count);
1032
1077
  },
1033
1078
  search(searchString) {
1034
- return new QueryBuilder(client).search(searchString);
1079
+ return new SDKQueryBuilder(client).search(searchString);
1035
1080
  },
1036
1081
  include(...relations) {
1037
- return new QueryBuilder(client).include(...relations);
1082
+ return new SDKQueryBuilder(client).include(...relations);
1038
1083
  }
1039
1084
  };
1040
1085
  if (ws) {
1041
1086
  client.listen = (params, onUpdate, onError) => {
1042
- return ws.listenCollection({
1087
+ let active = true;
1088
+ let lastUpdateId = 0;
1089
+ const unsub = ws.listenCollection({
1043
1090
  path: slug,
1044
- filter: parseWhereFilter(params?.where),
1091
+ filter: params?.where,
1045
1092
  limit: params?.limit,
1046
1093
  startAfter: params?.offset ? String(params.offset) : void 0,
1047
- orderBy: params?.orderBy?.split(":")[0],
1048
- order: params?.orderBy?.split(":")[1],
1094
+ orderBy: params?.orderBy?.[0],
1095
+ order: params?.orderBy?.[1],
1049
1096
  searchString: params?.searchString
1050
- }, (entities) => {
1097
+ }, (incomingRows) => {
1098
+ const currentUpdateId = ++lastUpdateId;
1051
1099
  const requestedLimit = params?.limit || 20;
1052
- onUpdate({
1053
- data: entities,
1100
+ const offset = params?.offset || 0;
1101
+ const rows = incomingRows;
1102
+ const heuristicTotal = rows.length;
1103
+ const heuristicHasMore = rows.length >= requestedLimit;
1104
+ if (client.count) client.count(params).then((total) => {
1105
+ if (active && currentUpdateId === lastUpdateId) onUpdate({
1106
+ data: rows,
1107
+ meta: {
1108
+ total,
1109
+ limit: requestedLimit,
1110
+ offset,
1111
+ hasMore: offset + rows.length < total
1112
+ }
1113
+ });
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,
1054
1127
  meta: {
1055
- total: entities.length,
1128
+ total: heuristicTotal,
1056
1129
  limit: requestedLimit,
1057
- offset: params?.offset || 0,
1058
- hasMore: entities.length >= requestedLimit
1130
+ offset,
1131
+ hasMore: heuristicHasMore
1059
1132
  }
1060
1133
  });
1061
1134
  }, onError);
1135
+ return () => {
1136
+ active = false;
1137
+ unsub();
1138
+ };
1062
1139
  };
1063
1140
  client.listenById = (id, onUpdate, onError) => {
1064
- return ws.listenEntity({
1141
+ return ws.listenOne({
1065
1142
  path: slug,
1066
- entityId: String(id)
1067
- }, (entity) => {
1068
- if (entity) onUpdate(entity);
1143
+ id: String(id)
1144
+ }, (row) => {
1145
+ if (row) onUpdate(row);
1069
1146
  else onUpdate(void 0);
1070
1147
  }, onError);
1071
1148
  };
@@ -1098,17 +1175,33 @@ function createFunctionsClient(transport) {
1098
1175
  }
1099
1176
  //#endregion
1100
1177
  //#region src/storage.ts
1101
- function createStorage(transport) {
1178
+ /**
1179
+ * Create a StorageSource that talks to the Rebase backend REST API.
1180
+ *
1181
+ * @param transport - HTTP transport instance
1182
+ * @param storageId - Optional storage-source key for multi-backend routing.
1183
+ * When set, it is forwarded to the server so the correct
1184
+ * `StorageController` is resolved from the registry.
1185
+ */
1186
+ function createStorage(transport, storageId) {
1102
1187
  const urlsCache = /* @__PURE__ */ new Map();
1103
- async function putObject({ file, key, metadata, bucket }) {
1188
+ /** Append ?storageId=... to a path when multi-backend routing is active. */
1189
+ const withStorageId = (path) => {
1190
+ if (!storageId) return path;
1191
+ return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
1192
+ };
1193
+ async function putObject({ file, key, metadata, bucket, public: isPublic }) {
1104
1194
  const formData = new FormData();
1105
1195
  formData.append("file", file);
1106
- if (key) formData.append("key", key);
1196
+ let effectiveKey = key;
1197
+ if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
1198
+ if (effectiveKey) formData.append("key", effectiveKey);
1107
1199
  if (bucket) formData.append("bucket", bucket);
1200
+ if (storageId) formData.append("storageId", storageId);
1108
1201
  if (metadata) {
1109
1202
  for (const [key, value] of Object.entries(metadata)) if (value !== void 0 && value !== null) formData.append(`metadata_${key}`, typeof value === "string" ? value : JSON.stringify(value));
1110
1203
  }
1111
- return (await transport.request("/storage/upload", {
1204
+ return (await transport.request(withStorageId("/storage/upload"), {
1112
1205
  method: "POST",
1113
1206
  body: formData,
1114
1207
  headers: {}
@@ -1116,24 +1209,44 @@ function createStorage(transport) {
1116
1209
  }
1117
1210
  async function getSignedUrl(keyOrUrl, bucket) {
1118
1211
  const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
1119
- const cached = urlsCache.get(cacheKey);
1120
- if (cached) return cached;
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
+ }
1121
1217
  let filePath = keyOrUrl;
1122
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1218
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1123
1219
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1124
1220
  if (!filePath || filePath.trim() === "" || filePath === "/") return {
1125
1221
  url: null,
1126
1222
  fileNotFound: true
1127
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
+ }
1128
1229
  try {
1129
- const result = await transport.request(`/storage/metadata/${filePath}`);
1130
- const activeToken = await transport.resolveToken();
1131
- const tokenQuery = activeToken ? `?token=${activeToken}` : "";
1230
+ const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
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}` : "";
1132
1241
  const downloadConfig = {
1133
- url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
1242
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
1134
1243
  metadata: result.data
1135
1244
  };
1136
- urlsCache.set(cacheKey, downloadConfig);
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
+ });
1137
1250
  return downloadConfig;
1138
1251
  } catch (e) {
1139
1252
  if (e instanceof Error && "status" in e && e.status === 404) return {
@@ -1144,25 +1257,22 @@ function createStorage(transport) {
1144
1257
  }
1145
1258
  }
1146
1259
  async function getObject(key, bucket) {
1147
- let filePath = key;
1148
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1149
- if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1150
- if (!filePath || filePath.trim() === "" || filePath === "/") return null;
1151
- const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
1152
- 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: {} });
1153
1263
  if (response.status === 404) return null;
1154
1264
  if (!response.ok) throw new Error("Failed to get file");
1155
1265
  const blob = await response.blob();
1156
- const fileName = filePath.split("/").pop() || "file";
1266
+ const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
1157
1267
  return new File([blob], fileName, { type: blob.type });
1158
1268
  }
1159
1269
  async function deleteObject(key, bucket) {
1160
1270
  let filePath = key;
1161
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1271
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1162
1272
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1163
1273
  if (!filePath || filePath.trim() === "" || filePath === "/") return;
1164
1274
  try {
1165
- await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
1275
+ await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
1166
1276
  } catch (e) {
1167
1277
  if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
1168
1278
  }
@@ -1174,6 +1284,7 @@ function createStorage(transport) {
1174
1284
  if (options?.bucket) params.set("bucket", options.bucket);
1175
1285
  if (options?.maxResults) params.set("maxResults", String(options.maxResults));
1176
1286
  if (options?.pageToken) params.set("pageToken", options.pageToken);
1287
+ if (storageId) params.set("storageId", storageId);
1177
1288
  return (await transport.request(`/storage/list?${params.toString()}`)).data;
1178
1289
  }
1179
1290
  return {
@@ -1185,6 +1296,62 @@ function createStorage(transport) {
1185
1296
  };
1186
1297
  }
1187
1298
  //#endregion
1299
+ //#region src/storage-registry.ts
1300
+ /**
1301
+ * Default implementation of the client-side `StorageSourceRegistry`.
1302
+ */
1303
+ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1304
+ sources = /* @__PURE__ */ new Map();
1305
+ /**
1306
+ * Register a storage source.
1307
+ * @param key - Unique key matching a `StorageSourceDefinition.key`
1308
+ * @param source - The `StorageSource` instance
1309
+ */
1310
+ register(key, source) {
1311
+ this.sources.set(key, source);
1312
+ }
1313
+ getDefault() {
1314
+ const source = this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
1315
+ if (!source) throw new Error(`[StorageSourceRegistry] No default storage source registered. Register one with key "${DEFAULT_STORAGE_SOURCE_KEY}".`);
1316
+ return source;
1317
+ }
1318
+ get(key) {
1319
+ if (key === void 0 || key === null) return this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
1320
+ return this.sources.get(key);
1321
+ }
1322
+ getOrDefault(key) {
1323
+ if (key === void 0 || key === null) return this.getDefault();
1324
+ const source = this.sources.get(key);
1325
+ if (source) return source;
1326
+ console.warn(`[StorageSourceRegistry] Storage source "${key}" not found, falling back to "${DEFAULT_STORAGE_SOURCE_KEY}".`);
1327
+ return this.getDefault();
1328
+ }
1329
+ has(key) {
1330
+ return this.sources.has(key);
1331
+ }
1332
+ list() {
1333
+ return Array.from(this.sources.keys());
1334
+ }
1335
+ /**
1336
+ * Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
1337
+ *
1338
+ * - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
1339
+ * - Sources with `transport: "direct"` are **not** auto-wired — they must
1340
+ * be registered manually after this call (e.g. via a Firebase hook).
1341
+ *
1342
+ * @param definitions - Array of storage source definitions
1343
+ * @param transport - HTTP transport for server-backed sources
1344
+ */
1345
+ static fromDefinitions(definitions, transport) {
1346
+ const registry = new ClientStorageSourceRegistry();
1347
+ for (const def of definitions) if (def.transport === "server") {
1348
+ const source = createStorage(transport, def.key === DEFAULT_STORAGE_SOURCE_KEY ? void 0 : def.key);
1349
+ registry.register(def.key, source);
1350
+ }
1351
+ return registry;
1352
+ }
1353
+ };
1354
+ //#endregion
1188
1355
  //#region src/websocket.ts
1189
1356
  /**
1190
1357
  * Extract error message and code from a WebSocket message payload.
@@ -1198,16 +1365,15 @@ function extractMessageError(message) {
1198
1365
  errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
1199
1366
  };
1200
1367
  }
1201
- var ApiError = class extends Error {
1202
- code;
1203
- error;
1204
- constructor(message, error, code) {
1205
- super(message);
1206
- this.name = "ApiError";
1207
- this.code = code;
1208
- this.error = error;
1209
- }
1210
- };
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
+ */
1211
1377
  var RebaseWebSocketClient = class {
1212
1378
  websocketUrl;
1213
1379
  ws = null;
@@ -1223,7 +1389,7 @@ var RebaseWebSocketClient = class {
1223
1389
  if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
1224
1390
  }
1225
1391
  collectionSubscriptions = /* @__PURE__ */ new Map();
1226
- entitySubscriptions = /* @__PURE__ */ new Map();
1392
+ singleSubscriptions = /* @__PURE__ */ new Map();
1227
1393
  backendToCollectionKey = /* @__PURE__ */ new Map();
1228
1394
  backendToEntityKey = /* @__PURE__ */ new Map();
1229
1395
  pendingRequests = /* @__PURE__ */ new Map();
@@ -1358,7 +1524,7 @@ var RebaseWebSocketClient = class {
1358
1524
  request.message._queuedResolve = request.resolve;
1359
1525
  request.message._queuedReject = request.reject;
1360
1526
  this.messageQueue.push(request.message);
1361
- } else request.reject(new ApiError("Connection closed", "Connection closed"));
1527
+ } else request.reject(new RebaseApiError$1("Connection closed"));
1362
1528
  this.pendingRequests.delete(reqId);
1363
1529
  }
1364
1530
  this.attemptReconnect();
@@ -1425,7 +1591,7 @@ var RebaseWebSocketClient = class {
1425
1591
  }
1426
1592
  }
1427
1593
  /**
1428
- * Shared logic for re-subscribing a collection or entity subscription
1594
+ * Shared logic for re-subscribing a collection or row subscription
1429
1595
  * after an auth error is resolved by refreshing credentials.
1430
1596
  */
1431
1597
  resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
@@ -1450,7 +1616,7 @@ var RebaseWebSocketClient = class {
1450
1616
  });
1451
1617
  } else {
1452
1618
  const { errorMessage, errorCode } = extractMessageError(message);
1453
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1619
+ const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1454
1620
  subscription.callbacks.forEach((callback) => {
1455
1621
  if (callback.onError) callback.onError(error);
1456
1622
  });
@@ -1471,7 +1637,7 @@ var RebaseWebSocketClient = class {
1471
1637
  if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
1472
1638
  else {
1473
1639
  const { errorMessage, errorCode } = extractMessageError(message);
1474
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1640
+ pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
1475
1641
  }
1476
1642
  }).catch((err) => {
1477
1643
  pendingReq.reject(err);
@@ -1479,7 +1645,7 @@ var RebaseWebSocketClient = class {
1479
1645
  } else {
1480
1646
  this.pendingRequests.delete(requestId);
1481
1647
  const { errorMessage, errorCode } = extractMessageError(message);
1482
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1648
+ pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
1483
1649
  }
1484
1650
  else {
1485
1651
  this.pendingRequests.delete(requestId);
@@ -1492,14 +1658,14 @@ var RebaseWebSocketClient = class {
1492
1658
  if (subscriptionKey) {
1493
1659
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1494
1660
  if (collectionSub) {
1495
- const incomingEntities = message.entities || [];
1496
- const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
1497
- collectionSub.latestData = entities;
1661
+ const incomingRows = message.rows || [];
1662
+ const rows = this.mergeRows(collectionSub.latestData, incomingRows);
1663
+ collectionSub.latestData = rows;
1498
1664
  collectionSub.lastUpdated = Date.now();
1499
1665
  collectionSub.isInitialDataReceived = true;
1500
1666
  collectionSub.callbacks.forEach((callback) => {
1501
1667
  try {
1502
- callback.onUpdate(entities);
1668
+ callback.onUpdate(rows);
1503
1669
  } catch (error) {
1504
1670
  console.error("Error in collection subscription callback:", error);
1505
1671
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
@@ -1509,21 +1675,22 @@ var RebaseWebSocketClient = class {
1509
1675
  }
1510
1676
  }
1511
1677
  }
1512
- if (subscriptionId && type === "collection_entity_patch") {
1678
+ if (subscriptionId && type === "collection_patch") {
1513
1679
  const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1514
1680
  if (subscriptionKey) {
1515
1681
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1516
1682
  if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
1517
- const patchEntity = message.entity ?? null;
1518
- const patchEntityId = message.entityId;
1683
+ const patchWireEntity = message.row ?? null;
1684
+ const patchEntityId = message.id;
1685
+ const patchRow = patchWireEntity ? patchWireEntity : null;
1519
1686
  let updated;
1520
- if (patchEntity === null || patchEntity === void 0) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1687
+ if (patchRow === null) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1521
1688
  else {
1522
- const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchEntity.id));
1689
+ const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchRow.id));
1523
1690
  if (idx >= 0) {
1524
1691
  updated = [...collectionSub.latestData];
1525
- updated[idx] = patchEntity;
1526
- } else updated = [patchEntity, ...collectionSub.latestData];
1692
+ updated[idx] = patchRow;
1693
+ } else updated = [patchRow, ...collectionSub.latestData];
1527
1694
  }
1528
1695
  collectionSub.latestData = updated;
1529
1696
  collectionSub.lastUpdated = Date.now();
@@ -1539,20 +1706,21 @@ var RebaseWebSocketClient = class {
1539
1706
  }
1540
1707
  }
1541
1708
  }
1542
- if (subscriptionId && type === "entity_update") {
1709
+ if (subscriptionId && type === "single_update") {
1543
1710
  const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
1544
1711
  if (subscriptionKey) {
1545
- const entitySub = this.entitySubscriptions.get(subscriptionKey);
1712
+ const entitySub = this.singleSubscriptions.get(subscriptionKey);
1546
1713
  if (entitySub) {
1547
- const entity = message.entity ?? null;
1548
- entitySub.latestData = entity;
1714
+ const wireEntity = message.row ?? null;
1715
+ const row = wireEntity ? wireEntity : null;
1716
+ entitySub.latestData = row;
1549
1717
  entitySub.lastUpdated = Date.now();
1550
1718
  entitySub.isInitialDataReceived = true;
1551
1719
  entitySub.callbacks.forEach((callback) => {
1552
1720
  try {
1553
- callback.onUpdate(entity);
1721
+ callback.onUpdate(row);
1554
1722
  } catch (error) {
1555
- console.error("Error in entity subscription callback:", error);
1723
+ console.error("Error in row subscription callback:", error);
1556
1724
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
1557
1725
  }
1558
1726
  });
@@ -1570,7 +1738,7 @@ var RebaseWebSocketClient = class {
1570
1738
  return;
1571
1739
  }
1572
1740
  const { errorMessage, errorCode } = extractMessageError(message);
1573
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1741
+ const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1574
1742
  collectionSub.callbacks.forEach((callback) => {
1575
1743
  if (callback.onError) callback.onError(error);
1576
1744
  });
@@ -1579,14 +1747,14 @@ var RebaseWebSocketClient = class {
1579
1747
  }
1580
1748
  const entityKey = this.backendToEntityKey.get(subscriptionId);
1581
1749
  if (entityKey) {
1582
- const entitySub = this.entitySubscriptions.get(entityKey);
1750
+ const entitySub = this.singleSubscriptions.get(entityKey);
1583
1751
  if (entitySub) {
1584
1752
  if (this.isAuthError(message)) {
1585
- this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "entity", this.backendToEntityKey, "subscribe_entity");
1753
+ this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
1586
1754
  return;
1587
1755
  }
1588
1756
  const { errorMessage, errorCode } = extractMessageError(message);
1589
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1757
+ const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1590
1758
  entitySub.callbacks.forEach((callback) => {
1591
1759
  if (callback.onError) callback.onError(error);
1592
1760
  });
@@ -1600,7 +1768,7 @@ var RebaseWebSocketClient = class {
1600
1768
  if (message.type === "ERROR" || message.error) {
1601
1769
  if (callback.onError) {
1602
1770
  const { errorMessage, errorCode } = extractMessageError(message);
1603
- callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
1771
+ callback.onError(new RebaseApiError$1(errorMessage, { code: errorCode }));
1604
1772
  }
1605
1773
  } else callback.onUpdate(message);
1606
1774
  }
@@ -1674,15 +1842,14 @@ var RebaseWebSocketClient = class {
1674
1842
  if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
1675
1843
  await this.ensureAuthenticated();
1676
1844
  } catch (error) {
1677
- const errorMessage = error instanceof Error ? error.message : "Authentication required";
1678
- reject(new ApiError(errorMessage, errorMessage));
1845
+ reject(new RebaseApiError$1(error instanceof Error ? error.message : "Authentication required"));
1679
1846
  return;
1680
1847
  }
1681
1848
  const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1682
1849
  message.requestId = requestId;
1683
1850
  const expectsResponse = ![
1684
1851
  "subscribe_collection",
1685
- "subscribe_entity",
1852
+ "subscribe_one",
1686
1853
  "unsubscribe",
1687
1854
  "join_channel",
1688
1855
  "leave_channel",
@@ -1695,7 +1862,7 @@ var RebaseWebSocketClient = class {
1695
1862
  const timeoutHandle = setTimeout(() => {
1696
1863
  if (this.pendingRequests.has(requestId)) {
1697
1864
  this.pendingRequests.delete(requestId);
1698
- reject(new ApiError("Request timed out", "Request timed out"));
1865
+ reject(new RebaseApiError$1("Request timed out"));
1699
1866
  }
1700
1867
  }, this.requestTimeoutMs);
1701
1868
  this.pendingRequests.set(requestId, {
@@ -1715,30 +1882,30 @@ var RebaseWebSocketClient = class {
1715
1882
  if (!expectsResponse) resolve(void 0);
1716
1883
  } catch (error) {
1717
1884
  if (expectsResponse) this.pendingRequests.delete(requestId);
1718
- reject(new ApiError("Failed to send message", error instanceof Error ? error.message : "Unknown error"));
1885
+ reject(new RebaseApiError$1("Failed to send message", { cause: error }));
1719
1886
  }
1720
1887
  }
1721
1888
  async fetchCollection(props) {
1722
1889
  return (await this.sendMessage({
1723
1890
  type: "FETCH_COLLECTION",
1724
1891
  payload: props
1725
- })).entities || [];
1892
+ })).rows || [];
1726
1893
  }
1727
- async fetchEntity(props) {
1894
+ async fetchOne(props) {
1728
1895
  return (await this.sendMessage({
1729
- type: "FETCH_ENTITY",
1896
+ type: "FETCH_ONE",
1730
1897
  payload: props
1731
- })).entity ?? void 0;
1898
+ })).row ?? void 0;
1732
1899
  }
1733
- async saveEntity(props) {
1900
+ async save(props) {
1734
1901
  return (await this.sendMessage({
1735
- type: "SAVE_ENTITY",
1902
+ type: "SAVE",
1736
1903
  payload: props
1737
- })).entity;
1904
+ })).row;
1738
1905
  }
1739
- async deleteEntity(props) {
1906
+ async delete(props) {
1740
1907
  await this.sendMessage({
1741
- type: "DELETE_ENTITY",
1908
+ type: "DELETE",
1742
1909
  payload: props
1743
1910
  });
1744
1911
  }
@@ -1763,21 +1930,21 @@ var RebaseWebSocketClient = class {
1763
1930
  async fetchCurrentDatabase() {
1764
1931
  return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
1765
1932
  }
1766
- async checkUniqueField(path, name, value, entityId, collection) {
1933
+ async checkUniqueField(path, name, value, id, collection) {
1767
1934
  return (await this.sendMessage({
1768
1935
  type: "CHECK_UNIQUE_FIELD",
1769
1936
  payload: {
1770
1937
  path,
1771
1938
  name,
1772
1939
  value,
1773
- entityId,
1940
+ id,
1774
1941
  collection
1775
1942
  }
1776
1943
  })).isUnique;
1777
1944
  }
1778
- async countEntities(props) {
1945
+ async count(props) {
1779
1946
  return (await this.sendMessage({
1780
- type: "COUNT_ENTITIES",
1947
+ type: "COUNT",
1781
1948
  payload: props
1782
1949
  })).count;
1783
1950
  }
@@ -1869,33 +2036,31 @@ var RebaseWebSocketClient = class {
1869
2036
  return val;
1870
2037
  }
1871
2038
  /**
1872
- * Merge incoming entities with cached data, preserving cached references
1873
- * for entities whose values haven't changed. This avoids unnecessary
1874
- * React re-renders when the server refetches all entities but most
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
1875
2042
  * haven't actually changed.
1876
2043
  */
1877
- mergeEntities(cached, incoming) {
2044
+ mergeRows(cached, incoming) {
1878
2045
  if (!cached || cached.length === 0) return incoming;
1879
2046
  const cachedById = /* @__PURE__ */ new Map();
1880
- for (const entity of cached) cachedById.set(entity.id, entity);
1881
- return incoming.map((incomingEntity) => {
1882
- const cachedEntity = cachedById.get(incomingEntity.id);
1883
- if (!cachedEntity) return incomingEntity;
1884
- if (cachedEntity.path === incomingEntity.path) {
1885
- const normCached = this.normalizeForComparison(cachedEntity.values);
1886
- const normIncoming = this.normalizeForComparison(incomingEntity.values);
1887
- if (this.deepEqual(normCached, normIncoming)) return cachedEntity;
1888
- else {
1889
- const mismatches = {};
1890
- const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
1891
- for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
1892
- cached: normCached[key],
1893
- incoming: normIncoming[key]
1894
- };
1895
- console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
1896
- }
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));
1897
2062
  }
1898
- return incomingEntity;
2063
+ return incomingRow;
1899
2064
  });
1900
2065
  }
1901
2066
  listenCollection(props, onUpdate, onError) {
@@ -1963,10 +2128,10 @@ var RebaseWebSocketClient = class {
1963
2128
  }
1964
2129
  };
1965
2130
  }
1966
- listenEntity(props, onUpdate, onError) {
1967
- const subscriptionKey = this.createEntitySubscriptionKey(props);
2131
+ listenOne(props, onUpdate, onError) {
2132
+ const subscriptionKey = this.createSingleSubscriptionKey(props);
1968
2133
  const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1969
- const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
2134
+ const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
1970
2135
  if (existingSubscription) {
1971
2136
  const callbackMap = existingSubscription.callbacks;
1972
2137
  callbackMap.set(callbackId, {
@@ -1976,13 +2141,13 @@ var RebaseWebSocketClient = class {
1976
2141
  if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
1977
2142
  onUpdate(existingSubscription.latestData);
1978
2143
  } catch (error) {
1979
- console.error("Error in entity subscription callback:", error);
2144
+ console.error("Error in row subscription callback:", error);
1980
2145
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
1981
2146
  }
1982
2147
  return () => {
1983
2148
  callbackMap.delete(callbackId);
1984
2149
  if (callbackMap.size === 0) {
1985
- this.entitySubscriptions.delete(subscriptionKey);
2150
+ this.singleSubscriptions.delete(subscriptionKey);
1986
2151
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
1987
2152
  if (this.isConnected && this.ws) this.sendMessage({
1988
2153
  type: "unsubscribe",
@@ -1997,14 +2162,14 @@ var RebaseWebSocketClient = class {
1997
2162
  onUpdate,
1998
2163
  onError
1999
2164
  });
2000
- this.entitySubscriptions.set(subscriptionKey, {
2165
+ this.singleSubscriptions.set(subscriptionKey, {
2001
2166
  backendSubscriptionId,
2002
2167
  callbacks: callbackMap,
2003
2168
  props
2004
2169
  });
2005
2170
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
2006
2171
  this.sendMessage({
2007
- type: "subscribe_entity",
2172
+ type: "subscribe_one",
2008
2173
  payload: {
2009
2174
  ...props,
2010
2175
  subscriptionId: backendSubscriptionId
@@ -2013,12 +2178,12 @@ var RebaseWebSocketClient = class {
2013
2178
  if (onError) onError(error);
2014
2179
  });
2015
2180
  return () => {
2016
- const subscription = this.entitySubscriptions.get(subscriptionKey);
2181
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
2017
2182
  if (subscription) {
2018
2183
  const callbacks = subscription.callbacks;
2019
2184
  callbacks.delete(callbackId);
2020
2185
  if (callbacks.size === 0) {
2021
- this.entitySubscriptions.delete(subscriptionKey);
2186
+ this.singleSubscriptions.delete(subscriptionKey);
2022
2187
  this.backendToEntityKey.delete(subscription.backendSubscriptionId);
2023
2188
  if (this.isConnected && this.ws) this.sendMessage({
2024
2189
  type: "unsubscribe",
@@ -2034,7 +2199,7 @@ var RebaseWebSocketClient = class {
2034
2199
  * we need to re-register everything to resume receiving updates.
2035
2200
  */
2036
2201
  resubscribeAll() {
2037
- console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
2202
+ console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
2038
2203
  for (const [key, sub] of this.collectionSubscriptions.entries()) {
2039
2204
  const oldBackendId = sub.backendSubscriptionId;
2040
2205
  const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -2051,20 +2216,20 @@ var RebaseWebSocketClient = class {
2051
2216
  console.error("[WS] Failed to re-subscribe collection:", key, error);
2052
2217
  });
2053
2218
  }
2054
- for (const [key, sub] of this.entitySubscriptions.entries()) {
2219
+ for (const [key, sub] of this.singleSubscriptions.entries()) {
2055
2220
  const oldBackendId = sub.backendSubscriptionId;
2056
2221
  const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2057
2222
  sub.backendSubscriptionId = newBackendId;
2058
2223
  this.backendToEntityKey.delete(oldBackendId);
2059
2224
  this.backendToEntityKey.set(newBackendId, key);
2060
2225
  this.sendMessage({
2061
- type: "subscribe_entity",
2226
+ type: "subscribe_one",
2062
2227
  payload: {
2063
2228
  ...sub.props,
2064
2229
  subscriptionId: newBackendId
2065
2230
  }
2066
2231
  }).catch((error) => {
2067
- console.error("[WS] Failed to re-subscribe entity:", key, error);
2232
+ console.error("[WS] Failed to re-subscribe row:", key, error);
2068
2233
  });
2069
2234
  }
2070
2235
  }
@@ -2087,8 +2252,8 @@ var RebaseWebSocketClient = class {
2087
2252
  return value;
2088
2253
  });
2089
2254
  }
2090
- createEntitySubscriptionKey(props) {
2091
- return `${props.path}|${props.entityId}`;
2255
+ createSingleSubscriptionKey(props) {
2256
+ return `${props.path}|${props.id}`;
2092
2257
  }
2093
2258
  };
2094
2259
  //#endregion
@@ -2122,6 +2287,23 @@ function createRebaseClient(options) {
2122
2287
  const apiKeys = createApiKeys(transport, options.apiKeys);
2123
2288
  const storage = createStorage(transport);
2124
2289
  const functions = createFunctionsClient(transport);
2290
+ const createStorageSource = (storageId) => storageId === DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
2291
+ const storageRegistry = new ClientStorageSourceRegistry();
2292
+ storageRegistry.register(DEFAULT_STORAGE_SOURCE_KEY, storage);
2293
+ for (const def of options.storageSources ?? []) if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY) storageRegistry.register(def.key, createStorageSource(def.key));
2294
+ let storageSourcesPromise;
2295
+ const fetchStorageSources = () => {
2296
+ if (storageSourcesPromise) return storageSourcesPromise;
2297
+ storageSourcesPromise = transport.request("/storage/sources").then((res) => {
2298
+ const defs = res.data ?? [];
2299
+ for (const def of defs) if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY && !storageRegistry.has(def.key)) storageRegistry.register(def.key, createStorageSource(def.key));
2300
+ return defs;
2301
+ }).catch((e) => {
2302
+ storageSourcesPromise = void 0;
2303
+ throw e;
2304
+ });
2305
+ return storageSourcesPromise;
2306
+ };
2125
2307
  const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2126
2308
  let ws;
2127
2309
  if (resolvedWsUrl) {
@@ -2159,7 +2341,45 @@ function createRebaseClient(options) {
2159
2341
  return false;
2160
2342
  }
2161
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
+ }
2162
2381
  const collectionClients = /* @__PURE__ */ new Map();
2382
+ let untypedWarned = false;
2163
2383
  function collection(slug) {
2164
2384
  if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
2165
2385
  return collectionClients.get(slug);
@@ -2167,7 +2387,22 @@ function createRebaseClient(options) {
2167
2387
  const dataProxy = new Proxy({ collection }, { get(_target, prop) {
2168
2388
  if (prop === "collection") return collection;
2169
2389
  if (typeof prop === "symbol") return void 0;
2170
- if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") return collection(toSnakeCase(prop));
2390
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
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
+ }
2404
+ return collection(toSnakeCase(prop));
2405
+ }
2171
2406
  } });
2172
2407
  return {
2173
2408
  auth,
@@ -2176,6 +2411,9 @@ function createRebaseClient(options) {
2176
2411
  apiKeys,
2177
2412
  functions,
2178
2413
  storage,
2414
+ storageRegistry,
2415
+ createStorageSource,
2416
+ fetchStorageSources,
2179
2417
  ws,
2180
2418
  setToken: transport.setToken,
2181
2419
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -2191,11 +2429,10 @@ function createRebaseClient(options) {
2191
2429
  });
2192
2430
  return res.data ?? res;
2193
2431
  },
2194
- data: dataProxy,
2195
- email: void 0
2432
+ data: dataProxy
2196
2433
  };
2197
2434
  }
2198
2435
  //#endregion
2199
- export { ApiError, QueryBuilder, RebaseApiError, RebaseWebSocketClient, and, buildQueryString, cond, createAdmin, createApiKeys, createAuth, createCollectionClient, createCookieStorage, createCron, createFunctionsClient, createMemoryStorage, createRebaseClient, createStorage, createTransport, or, rebaseReviver };
2436
+ export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createCookieStorage, createMemoryStorage, createRebaseClient, or };
2200
2437
 
2201
2438
  //# sourceMappingURL=index.es.js.map