@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.umd.js CHANGED
@@ -36,94 +36,27 @@
36
36
  }
37
37
  //#endregion
38
38
  //#region src/transport.ts
39
- var RebaseApiError = class extends Error {
40
- status;
41
- code;
42
- details;
43
- constructor(status, message, code, details) {
44
- super(message);
45
- this.name = "RebaseApiError";
46
- this.status = status;
47
- this.code = code;
48
- this.details = details;
49
- }
50
- };
51
- /**
52
- * Maps a short operator alias to the PostgREST-style short code.
53
- */
54
- var OP_MAP = {
55
- "==": "eq",
56
- "!=": "neq",
57
- ">": "gt",
58
- ">=": "gte",
59
- "<": "lt",
60
- "<=": "lte",
61
- "not-in": "nin",
62
- "array-contains": "cs",
63
- "array-contains-any": "csa"
64
- };
65
- /**
66
- * Normalise a single `WhereFieldValue` into the PostgREST query-string
67
- * representation the backend expects.
68
- *
69
- * Supports:
70
- * - `null` → `"eq.null"`
71
- * - `true`/`false` → `"eq.true"` / `"eq.false"`
72
- * - `42` → `"42"` (plain equality)
73
- * - `"active"` → `"active"` (plain equality, backward-compat)
74
- * - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
75
- * - `[">=", 18]` → `"gte.18"` (tuple syntax)
76
- * - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
77
- * - `["!=", null]` → `"neq.null"`
78
- */
79
- function normalizeWhereValue(value) {
80
- if (value === null) return "eq.null";
81
- if (typeof value === "boolean") return `eq.${value}`;
82
- if (typeof value === "number") return String(value);
83
- if (Array.isArray(value)) {
84
- const [rawOp, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
85
- if (rawOp) {
86
- const op = OP_MAP[rawOp] ?? rawOp;
87
- if (val === null) return `${op}.null`;
88
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
89
- return `${op}.${val}`;
90
- }
91
- }
92
- return String(value);
93
- }
94
- function serializeLogicalCondition(cond) {
95
- if ("type" in cond) {
96
- const sub = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
97
- return `${cond.type}(${sub})`;
98
- } else {
99
- const op = OP_MAP[cond.operator] ?? cond.operator;
100
- let formattedValue = cond.value;
101
- if (Array.isArray(cond.value)) formattedValue = `(${cond.value.join(",")})`;
102
- else if (cond.value === null) formattedValue = "null";
103
- return `${cond.column}.${op}.${formattedValue}`;
104
- }
105
- }
106
39
  function buildQueryString(params) {
107
40
  if (!params) return "";
108
41
  const parts = [];
109
42
  if (params.limit != null) parts.push(`limit=${params.limit}`);
110
43
  if (params.offset != null) parts.push(`offset=${params.offset}`);
111
44
  if (params.page != null) parts.push(`page=${params.page}`);
112
- if (params.orderBy) parts.push(`orderBy=${encodeURIComponent(params.orderBy)}`);
45
+ if (params.orderBy) {
46
+ const wire = (0, _rebasepro_common.serializeOrderBy)(params.orderBy);
47
+ if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
48
+ }
113
49
  if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
114
50
  if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
115
51
  if (params.logical) {
116
52
  const root = params.logical;
117
- const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
53
+ const serialized = (root.conditions ?? []).map(_rebasepro_common.serializeLogicalCondition).join(",");
118
54
  parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
119
55
  }
120
- 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) {
121
- const normalized = normalizeWhereValue(subVal);
122
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
123
- }
124
- else {
125
- const normalized = normalizeWhereValue(value);
126
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
56
+ if (params.where) {
57
+ const serialized = (0, _rebasepro_common.serializeFilter)(params.where);
58
+ for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
59
+ else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
127
60
  }
128
61
  return parts.length > 0 ? "?" + parts.join("&") : "";
129
62
  }
@@ -161,8 +94,7 @@
161
94
  } catch (e) {}
162
95
  const getErrorField = (obj, field) => {
163
96
  const err = obj?.error;
164
- if (err && typeof err === "object" && err !== null && field in err) return err[field];
165
- return obj?.[field];
97
+ if (err && typeof err === "object" && err !== null) return err[field];
166
98
  };
167
99
  if (res.status === 401 && onUnauthorizedHandler) {
168
100
  if (await onUnauthorizedHandler()) {
@@ -185,7 +117,11 @@
185
117
  if (!retryRes.ok) {
186
118
  let fallbackMessage = retryRes.statusText;
187
119
  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.`;
188
- throw new RebaseApiError(retryRes.status, String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), getErrorField(retryBody, "code"), getErrorField(retryBody, "details"));
120
+ throw new _rebasepro_types.RebaseApiError(String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), {
121
+ status: retryRes.status,
122
+ code: getErrorField(retryBody, "code"),
123
+ details: getErrorField(retryBody, "details")
124
+ });
189
125
  }
190
126
  return retryBody;
191
127
  }
@@ -193,7 +129,11 @@
193
129
  if (!res.ok) {
194
130
  let fallbackMessage = res.statusText;
195
131
  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.`;
196
- throw new RebaseApiError(res.status, String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), getErrorField(body, "code"), getErrorField(body, "details"));
132
+ throw new _rebasepro_types.RebaseApiError(String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), {
133
+ status: res.status,
134
+ code: getErrorField(body, "code"),
135
+ details: getErrorField(body, "details")
136
+ });
197
137
  }
198
138
  return body;
199
139
  }
@@ -229,6 +169,29 @@
229
169
  }
230
170
  //#endregion
231
171
  //#region src/auth.ts
172
+ /** Map a raw user object from an auth response (`/login`, `/refresh`, `/me`) to a `User`. */
173
+ function mapRawUser(raw) {
174
+ return {
175
+ uid: raw.uid,
176
+ email: raw.email ?? null,
177
+ displayName: raw.displayName ?? null,
178
+ photoURL: raw.photoURL ?? null,
179
+ providerId: raw.providerId ?? "password",
180
+ isAnonymous: raw.isAnonymous ?? false,
181
+ emailVerified: raw.emailVerified,
182
+ roles: raw.roles,
183
+ metadata: raw.metadata
184
+ };
185
+ }
186
+ /** Placeholder user, used only as a last resort when none can be resolved. */
187
+ var EMPTY_USER = {
188
+ uid: "",
189
+ email: null,
190
+ displayName: null,
191
+ photoURL: null,
192
+ providerId: "password",
193
+ isAnonymous: false
194
+ };
232
195
  function createMemoryStorage() {
233
196
  const store = {};
234
197
  return {
@@ -259,11 +222,20 @@
259
222
  const authPath = opts.authPath || "/auth";
260
223
  const autoRefresh = opts.autoRefresh !== false;
261
224
  const persistSession = opts.persistSession !== false;
225
+ const authFlowMode = opts.authFlowMode || "json";
262
226
  const STORAGE_KEY = "rebase_auth";
263
227
  const REFRESH_BUFFER_MS = 12e4;
228
+ const MAX_REFRESH_RETRIES = 5;
229
+ const REFRESH_RETRY_BASE_MS = 1e3;
230
+ const REFRESH_RETRY_MAX_MS = 3e4;
264
231
  let currentSession = null;
265
232
  const listeners = /* @__PURE__ */ new Set();
266
233
  let refreshTimeout = null;
234
+ let inFlightRefresh = null;
235
+ let resolveInitialized;
236
+ const isInitialized = new Promise((resolve) => {
237
+ resolveInitialized = resolve;
238
+ });
267
239
  function authUrl(endpoint) {
268
240
  return transport.baseUrl + transport.apiPath + authPath + endpoint;
269
241
  }
@@ -271,7 +243,11 @@
271
243
  return transport.fetchFn || globalThis.fetch;
272
244
  }
273
245
  function throwApiError(status, body, statusText) {
274
- throw new RebaseApiError(status, body?.error?.message || body?.message || statusText, body?.error?.code || body?.code, body?.error?.details || body?.details);
246
+ throw new _rebasepro_types.RebaseApiError(body?.error?.message || body?.message || statusText, {
247
+ status,
248
+ code: body?.error?.code || body?.code,
249
+ details: body?.error?.details || body?.details
250
+ });
275
251
  }
276
252
  function emit(event, session) {
277
253
  for (const fn of listeners) try {
@@ -279,7 +255,7 @@
279
255
  } catch (e) {}
280
256
  }
281
257
  function saveSession(session) {
282
- if (!persistSession) return;
258
+ if (!persistSession || authFlowMode === "cookie") return;
283
259
  try {
284
260
  storage.setItem(STORAGE_KEY, JSON.stringify(session));
285
261
  } catch (e) {}
@@ -296,28 +272,53 @@
296
272
  } catch (e) {}
297
273
  return null;
298
274
  }
275
+ /**
276
+ * A refresh failure is only fatal if the refresh token itself is rejected
277
+ * (expired / invalid / forbidden). Network blips, timeouts, and 5xx (e.g. a
278
+ * backend restart mid-session) are transient and must NOT log the user out.
279
+ */
280
+ function isFatalRefreshError(err) {
281
+ if (!(err instanceof _rebasepro_types.RebaseApiError)) return false;
282
+ if (err.code === "INVALID_TOKEN" || err.code === "TOKEN_EXPIRED") return true;
283
+ return err.status === 401 || err.status === 403;
284
+ }
285
+ async function attemptScheduledRefresh(attempt) {
286
+ try {
287
+ await refreshSession();
288
+ } catch (err) {
289
+ if (isFatalRefreshError(err)) {
290
+ signOut();
291
+ return;
292
+ }
293
+ if (attempt >= MAX_REFRESH_RETRIES) {
294
+ signOut();
295
+ return;
296
+ }
297
+ const backoff = Math.min(REFRESH_RETRY_BASE_MS * 2 ** attempt, REFRESH_RETRY_MAX_MS);
298
+ refreshTimeout = setTimeout(() => {
299
+ attemptScheduledRefresh(attempt + 1);
300
+ }, backoff);
301
+ }
302
+ }
299
303
  function scheduleRefresh(expiresAt) {
300
304
  if (refreshTimeout) clearTimeout(refreshTimeout);
301
305
  if (!autoRefresh) return;
302
306
  const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
303
307
  if (delay <= 0) {
304
- refreshSession().catch(() => signOut());
308
+ attemptScheduledRefresh(0);
305
309
  return;
306
310
  }
307
- refreshTimeout = setTimeout(async () => {
308
- try {
309
- await refreshSession();
310
- } catch (e) {
311
- signOut();
312
- }
311
+ refreshTimeout = setTimeout(() => {
312
+ attemptScheduledRefresh(0);
313
313
  }, delay);
314
314
  }
315
315
  function handleAuthResponse(data, event) {
316
+ const user = mapRawUser(data.user);
316
317
  const session = {
317
318
  accessToken: data.tokens.accessToken,
318
- refreshToken: data.tokens.refreshToken,
319
+ refreshToken: data.tokens.refreshToken || currentSession?.refreshToken || "",
319
320
  expiresAt: data.tokens.accessTokenExpiresAt,
320
- user: data.user
321
+ user
321
322
  };
322
323
  currentSession = session;
323
324
  saveSession(session);
@@ -333,7 +334,8 @@
333
334
  body: JSON.stringify({
334
335
  email,
335
336
  password
336
- })
337
+ }),
338
+ credentials: authFlowMode === "cookie" ? "include" : void 0
337
339
  });
338
340
  const body = await res.json().catch(() => ({}));
339
341
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -354,7 +356,8 @@
354
356
  const res = await fetchFn(authUrl("/register"), {
355
357
  method: "POST",
356
358
  headers: { "Content-Type": "application/json" },
357
- body: JSON.stringify(payload)
359
+ body: JSON.stringify(payload),
360
+ credentials: authFlowMode === "cookie" ? "include" : void 0
358
361
  });
359
362
  const body = await res.json().catch(() => ({}));
360
363
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -377,7 +380,8 @@
377
380
  const res = await getFetch()(authUrl("/google"), {
378
381
  method: "POST",
379
382
  headers: { "Content-Type": "application/json" },
380
- body: JSON.stringify(payload)
383
+ body: JSON.stringify(payload),
384
+ credentials: authFlowMode === "cookie" ? "include" : void 0
381
385
  });
382
386
  const responseBody = await res.json().catch(() => ({}));
383
387
  if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
@@ -395,7 +399,8 @@
395
399
  body: JSON.stringify({
396
400
  code,
397
401
  redirectUri
398
- })
402
+ }),
403
+ credentials: authFlowMode === "cookie" ? "include" : void 0
399
404
  });
400
405
  const body = await res.json().catch(() => ({}));
401
406
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -414,7 +419,8 @@
414
419
  const res = await getFetch()(authUrl(`/${providerId}`), {
415
420
  method: "POST",
416
421
  headers: { "Content-Type": "application/json" },
417
- body: JSON.stringify(payload)
422
+ body: JSON.stringify(payload),
423
+ credentials: authFlowMode === "cookie" ? "include" : void 0
418
424
  });
419
425
  const body = await res.json().catch(() => ({}));
420
426
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -490,10 +496,11 @@
490
496
  async function signOut() {
491
497
  const fetchFn = getFetch();
492
498
  try {
493
- if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
499
+ if (authFlowMode === "cookie" || currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
494
500
  method: "POST",
495
501
  headers: { "Content-Type": "application/json" },
496
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
502
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
503
+ credentials: authFlowMode === "cookie" ? "include" : void 0
497
504
  });
498
505
  } catch (e) {}
499
506
  currentSession = null;
@@ -505,20 +512,35 @@
505
512
  transport.setToken(null);
506
513
  emit("SIGNED_OUT", null);
507
514
  }
508
- async function refreshSession() {
509
- if (!currentSession?.refreshToken) throw new Error("No active session to refresh");
515
+ function refreshSession() {
516
+ if (inFlightRefresh) return inFlightRefresh;
517
+ inFlightRefresh = doRefreshSession().finally(() => {
518
+ inFlightRefresh = null;
519
+ });
520
+ return inFlightRefresh;
521
+ }
522
+ async function doRefreshSession() {
523
+ if (authFlowMode !== "cookie" && !currentSession?.refreshToken) throw new Error("No active session to refresh");
510
524
  const res = await getFetch()(authUrl("/refresh"), {
511
525
  method: "POST",
512
526
  headers: { "Content-Type": "application/json" },
513
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
527
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
528
+ credentials: authFlowMode === "cookie" ? "include" : void 0
514
529
  });
515
530
  const body = await res.json().catch(() => ({}));
516
531
  if (!res.ok) throwApiError(res.status, body, res.statusText);
532
+ const accessToken = body.tokens.accessToken;
533
+ transport.setToken(accessToken);
534
+ let user = currentSession?.user;
535
+ if (body.user && typeof body.user.uid === "string") user = mapRawUser(body.user);
536
+ else if (!user || !user.uid) try {
537
+ user = await getUser();
538
+ } catch {}
517
539
  const session = {
518
- accessToken: body.tokens.accessToken,
519
- refreshToken: body.tokens.refreshToken,
540
+ accessToken,
541
+ refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
520
542
  expiresAt: body.tokens.accessTokenExpiresAt,
521
- user: currentSession.user
543
+ user: user ?? EMPTY_USER
522
544
  };
523
545
  currentSession = session;
524
546
  saveSession(session);
@@ -530,6 +552,18 @@
530
552
  async function getUser() {
531
553
  return (await transport.request(authPath + "/me", { method: "GET" })).user;
532
554
  }
555
+ /**
556
+ * Resolve an email to a minimal public profile (`uid`, `displayName`,
557
+ * `photoURL`) for invite-by-email flows. Returns `null` when no account
558
+ * matches. Requires the backend to opt in via `auth.allowUserLookup`;
559
+ * otherwise the endpoint is absent and this rejects.
560
+ */
561
+ async function findUserByEmail(email) {
562
+ return (await transport.request(authPath + "/find-user", {
563
+ method: "POST",
564
+ body: JSON.stringify({ email })
565
+ })).user;
566
+ }
533
567
  async function updateUser(updates) {
534
568
  const data = await transport.request(authPath + "/me", {
535
569
  method: "PATCH",
@@ -603,7 +637,8 @@
603
637
  const res = await getFetch()(authUrl("/magic-link/verify"), {
604
638
  method: "POST",
605
639
  headers: { "Content-Type": "application/json" },
606
- body: JSON.stringify({ token })
640
+ body: JSON.stringify({ token }),
641
+ credentials: authFlowMode === "cookie" ? "include" : void 0
607
642
  });
608
643
  const body = await res.json().catch(() => ({}));
609
644
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -650,21 +685,29 @@
650
685
  }
651
686
  if (persistSession) {
652
687
  const stored = loadStoredSession();
653
- if (stored && stored.accessToken && stored.refreshToken) {
654
- if (stored.expiresAt > Date.now()) {
655
- currentSession = stored;
656
- transport.setToken(stored.accessToken);
657
- scheduleRefresh(stored.expiresAt);
658
- } else if (stored.refreshToken) {
659
- currentSession = stored;
660
- refreshSession().catch(() => {
661
- currentSession = null;
662
- clearStoredSession();
663
- transport.setToken(null);
664
- });
665
- }
666
- }
667
- }
688
+ if (stored && stored.accessToken) if (stored.expiresAt > Date.now()) {
689
+ currentSession = stored;
690
+ transport.setToken(stored.accessToken);
691
+ scheduleRefresh(stored.expiresAt);
692
+ resolveInitialized();
693
+ } else if (authFlowMode === "cookie" || stored.refreshToken) {
694
+ currentSession = stored;
695
+ refreshSession().then(() => {
696
+ resolveInitialized();
697
+ }).catch(() => {
698
+ currentSession = null;
699
+ clearStoredSession();
700
+ transport.setToken(null);
701
+ resolveInitialized();
702
+ });
703
+ } else resolveInitialized();
704
+ else if (authFlowMode === "cookie") refreshSession().then(() => {
705
+ resolveInitialized();
706
+ }).catch(() => {
707
+ resolveInitialized();
708
+ });
709
+ else resolveInitialized();
710
+ } else resolveInitialized();
668
711
  return {
669
712
  signInWithEmail,
670
713
  signUp,
@@ -684,6 +727,7 @@
684
727
  signOut,
685
728
  refreshSession,
686
729
  getUser,
730
+ findUserByEmail,
687
731
  updateUser,
688
732
  resetPasswordForEmail,
689
733
  resetPassword,
@@ -697,7 +741,8 @@
697
741
  revokeAllSessions,
698
742
  getAuthConfig,
699
743
  getSession,
700
- onAuthStateChange
744
+ onAuthStateChange,
745
+ isInitialized: () => isInitialized
701
746
  };
702
747
  }
703
748
  function createCookieStorage(options = {}) {
@@ -874,108 +919,108 @@
874
919
  };
875
920
  }
876
921
  //#endregion
877
- //#region src/collection.ts
878
- function parseWhereFilter(where) {
879
- if (!where) return void 0;
880
- const filters = {};
881
- const OP_TO_FILTER = {
882
- "eq": "==",
883
- "neq": "!=",
884
- "gt": ">",
885
- "gte": ">=",
886
- "lt": "<",
887
- "lte": "<=",
888
- "==": "==",
889
- "!=": "!=",
890
- ">": ">",
891
- ">=": ">=",
892
- "<": "<",
893
- "<=": "<=",
894
- "in": "in",
895
- "nin": "not-in",
896
- "not-in": "not-in",
897
- "cs": "array-contains",
898
- "csa": "array-contains-any",
899
- "array-contains": "array-contains",
900
- "array-contains-any": "array-contains-any"
901
- };
902
- const parseSingle = (rawValue, fieldKey) => {
903
- if (rawValue === null) return ["==", null];
904
- if (typeof rawValue === "boolean") return ["==", rawValue];
905
- if (typeof rawValue === "number") return ["==", rawValue];
906
- if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
907
- const [rawOp, val] = rawValue;
908
- return [OP_TO_FILTER[rawOp] ?? "==", val];
909
- }
910
- const value = String(rawValue);
911
- const dotIndex = value.indexOf(".");
912
- if (dotIndex > 0) {
913
- const opStr = value.substring(0, dotIndex);
914
- const valStr = value.substring(dotIndex + 1);
915
- let op = "==";
916
- let val = valStr;
917
- switch (opStr) {
918
- case "eq":
919
- op = "==";
920
- break;
921
- case "neq":
922
- op = "!=";
923
- break;
924
- case "gt":
925
- op = ">";
926
- break;
927
- case "gte":
928
- op = ">=";
929
- break;
930
- case "lt":
931
- op = "<";
932
- break;
933
- case "lte":
934
- op = "<=";
935
- break;
936
- case "in":
937
- op = "in";
938
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
939
- break;
940
- case "nin":
941
- op = "not-in";
942
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
943
- break;
944
- case "cs":
945
- op = "array-contains";
946
- break;
947
- case "csa":
948
- op = "array-contains-any";
949
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
950
- break;
951
- default:
952
- op = "==";
953
- val = value;
954
- }
955
- if (val === "true") val = true;
956
- else if (val === "false") val = false;
957
- else if (val === "null") val = null;
958
- else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
959
- return [op, val];
960
- } else return ["==", value];
961
- };
962
- 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));
963
- else filters[key] = parseSingle(rawValue, key);
964
- return filters;
965
- }
922
+ //#region src/sdk_query_builder.ts
966
923
  /**
967
- * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
968
- * a proper `Entity<M>` structure expected by the core framework.
969
- * The `id` is kept inside `values` as well, since collection properties
970
- * may define an `isId` field that the form binds to `formex.values`.
924
+ * SDK Query Builder returns flat rows (`FindResult<M>`) instead of
925
+ * Entity-wrapped results (`FindResponse<M>`).
926
+ *
927
+ * @example
928
+ * const { data } = await rebase.data.posts
929
+ * .where("status", "==", "published")
930
+ * .orderBy("created_at", "desc")
931
+ * .limit(10)
932
+ * .find();
933
+ *
934
+ * console.log(data[0].title); // flat access
971
935
  */
972
- function rowToEntity(row, slug) {
973
- return {
974
- id: row.id,
975
- path: slug,
976
- values: row
977
- };
978
- }
936
+ var SDKQueryBuilder = class {
937
+ collection;
938
+ params = { where: {} };
939
+ constructor(collection) {
940
+ this.collection = collection;
941
+ }
942
+ where(columnOrCondition, operator, value) {
943
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
944
+ this.params.logical = columnOrCondition;
945
+ return this;
946
+ }
947
+ if (!this.params.where) this.params.where = {};
948
+ const column = columnOrCondition;
949
+ const condition = [operator, value];
950
+ const existing = this.params.where[column];
951
+ if (existing === void 0) this.params.where[column] = condition;
952
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
953
+ else {
954
+ let firstCondition;
955
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
956
+ else firstCondition = ["==", existing];
957
+ this.params.where[column] = [firstCondition, condition];
958
+ }
959
+ return this;
960
+ }
961
+ /**
962
+ * Order the results by a specific column.
963
+ */
964
+ orderBy(column, direction = "asc") {
965
+ this.params.orderBy = [column, direction];
966
+ return this;
967
+ }
968
+ /**
969
+ * Limit the number of results returned.
970
+ */
971
+ limit(count) {
972
+ this.params.limit = count;
973
+ return this;
974
+ }
975
+ /**
976
+ * Skip the first N results.
977
+ */
978
+ offset(count) {
979
+ this.params.offset = count;
980
+ return this;
981
+ }
982
+ /**
983
+ * Set a free-text search string if supported by the backend.
984
+ */
985
+ search(searchString) {
986
+ this.params.searchString = searchString;
987
+ return this;
988
+ }
989
+ /**
990
+ * Include related entities in the response.
991
+ * Relations will be populated with full data instead of just IDs.
992
+ *
993
+ * @param relations - Relation names to include, or "*" for all.
994
+ * @example
995
+ * client.data.posts.include("tags", "author").find()
996
+ */
997
+ include(...relations) {
998
+ this.params.include = relations;
999
+ return this;
1000
+ }
1001
+ /**
1002
+ * Execute the find query and return the results as flat rows.
1003
+ */
1004
+ async find() {
1005
+ return this.collection.find(this.params);
1006
+ }
1007
+ /**
1008
+ * Count the records matching this query.
1009
+ */
1010
+ async count() {
1011
+ if (!this.collection.count) throw new Error("count() is not supported by this collection client.");
1012
+ return this.collection.count(this.params);
1013
+ }
1014
+ /**
1015
+ * Listen to realtime updates matching this query.
1016
+ */
1017
+ listen(onUpdate, onError) {
1018
+ if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
1019
+ return this.collection.listen(this.params, onUpdate, onError);
1020
+ }
1021
+ };
1022
+ //#endregion
1023
+ //#region src/collection.ts
979
1024
  function createCollectionClient(transport, slug, ws) {
980
1025
  const basePath = `/data/${slug}`;
981
1026
  const client = {
@@ -983,7 +1028,7 @@
983
1028
  const qs = buildQueryString(params);
984
1029
  const raw = await transport.request(basePath + qs, { method: "GET" });
985
1030
  return {
986
- data: (raw.data || []).map((row) => rowToEntity(row, slug)),
1031
+ data: raw.data || [],
987
1032
  meta: raw.meta
988
1033
  };
989
1034
  },
@@ -991,28 +1036,28 @@
991
1036
  try {
992
1037
  const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
993
1038
  if (!raw) return void 0;
994
- return rowToEntity(raw, slug);
1039
+ return raw;
995
1040
  } catch (err) {
996
- if (err instanceof RebaseApiError && err.status === 404) return;
1041
+ if (err instanceof _rebasepro_types.RebaseApiError && err.status === 404) return;
997
1042
  throw err;
998
1043
  }
999
1044
  },
1000
1045
  async create(data, id) {
1001
1046
  const body = { ...data };
1002
1047
  if (id !== void 0) body.id = id;
1003
- return rowToEntity(await transport.request(basePath, {
1048
+ return await transport.request(basePath, {
1004
1049
  method: "POST",
1005
1050
  body: JSON.stringify(body)
1006
- }), slug);
1051
+ });
1007
1052
  },
1008
1053
  async update(id, data) {
1009
- return rowToEntity(await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1054
+ return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1010
1055
  method: "PUT",
1011
1056
  body: JSON.stringify(data)
1012
- }), slug);
1057
+ });
1013
1058
  },
1014
1059
  async delete(id) {
1015
- return transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
1060
+ await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
1016
1061
  },
1017
1062
  async count(params) {
1018
1063
  const qs = buildQueryString({
@@ -1023,55 +1068,87 @@
1023
1068
  return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
1024
1069
  },
1025
1070
  where(columnOrCondition, operator, value) {
1026
- const builder = new _rebasepro_common.QueryBuilder(client);
1071
+ const builder = new SDKQueryBuilder(client);
1027
1072
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
1028
1073
  return builder.where(columnOrCondition, operator, value);
1029
1074
  },
1030
- orderBy(column, ascending) {
1031
- return new _rebasepro_common.QueryBuilder(client).orderBy(column, ascending);
1075
+ orderBy(column, direction) {
1076
+ return new SDKQueryBuilder(client).orderBy(column, direction);
1032
1077
  },
1033
1078
  limit(count) {
1034
- return new _rebasepro_common.QueryBuilder(client).limit(count);
1079
+ return new SDKQueryBuilder(client).limit(count);
1035
1080
  },
1036
1081
  offset(count) {
1037
- return new _rebasepro_common.QueryBuilder(client).offset(count);
1082
+ return new SDKQueryBuilder(client).offset(count);
1038
1083
  },
1039
1084
  search(searchString) {
1040
- return new _rebasepro_common.QueryBuilder(client).search(searchString);
1085
+ return new SDKQueryBuilder(client).search(searchString);
1041
1086
  },
1042
1087
  include(...relations) {
1043
- return new _rebasepro_common.QueryBuilder(client).include(...relations);
1088
+ return new SDKQueryBuilder(client).include(...relations);
1044
1089
  }
1045
1090
  };
1046
1091
  if (ws) {
1047
1092
  client.listen = (params, onUpdate, onError) => {
1048
- return ws.listenCollection({
1093
+ let active = true;
1094
+ let lastUpdateId = 0;
1095
+ const unsub = ws.listenCollection({
1049
1096
  path: slug,
1050
- filter: parseWhereFilter(params?.where),
1097
+ filter: params?.where,
1051
1098
  limit: params?.limit,
1052
1099
  startAfter: params?.offset ? String(params.offset) : void 0,
1053
- orderBy: params?.orderBy?.split(":")[0],
1054
- order: params?.orderBy?.split(":")[1],
1100
+ orderBy: params?.orderBy?.[0],
1101
+ order: params?.orderBy?.[1],
1055
1102
  searchString: params?.searchString
1056
- }, (entities) => {
1103
+ }, (incomingRows) => {
1104
+ const currentUpdateId = ++lastUpdateId;
1057
1105
  const requestedLimit = params?.limit || 20;
1058
- onUpdate({
1059
- data: entities,
1106
+ const offset = params?.offset || 0;
1107
+ const rows = incomingRows;
1108
+ const heuristicTotal = rows.length;
1109
+ const heuristicHasMore = rows.length >= requestedLimit;
1110
+ if (client.count) client.count(params).then((total) => {
1111
+ if (active && currentUpdateId === lastUpdateId) onUpdate({
1112
+ data: rows,
1113
+ meta: {
1114
+ total,
1115
+ limit: requestedLimit,
1116
+ offset,
1117
+ hasMore: offset + rows.length < total
1118
+ }
1119
+ });
1120
+ }).catch(() => {
1121
+ if (active && currentUpdateId === lastUpdateId) onUpdate({
1122
+ data: rows,
1123
+ meta: {
1124
+ total: heuristicTotal,
1125
+ limit: requestedLimit,
1126
+ offset,
1127
+ hasMore: heuristicHasMore
1128
+ }
1129
+ });
1130
+ });
1131
+ else onUpdate({
1132
+ data: rows,
1060
1133
  meta: {
1061
- total: entities.length,
1134
+ total: heuristicTotal,
1062
1135
  limit: requestedLimit,
1063
- offset: params?.offset || 0,
1064
- hasMore: entities.length >= requestedLimit
1136
+ offset,
1137
+ hasMore: heuristicHasMore
1065
1138
  }
1066
1139
  });
1067
1140
  }, onError);
1141
+ return () => {
1142
+ active = false;
1143
+ unsub();
1144
+ };
1068
1145
  };
1069
1146
  client.listenById = (id, onUpdate, onError) => {
1070
- return ws.listenEntity({
1147
+ return ws.listenOne({
1071
1148
  path: slug,
1072
- entityId: String(id)
1073
- }, (entity) => {
1074
- if (entity) onUpdate(entity);
1149
+ id: String(id)
1150
+ }, (row) => {
1151
+ if (row) onUpdate(row);
1075
1152
  else onUpdate(void 0);
1076
1153
  }, onError);
1077
1154
  };
@@ -1104,17 +1181,33 @@
1104
1181
  }
1105
1182
  //#endregion
1106
1183
  //#region src/storage.ts
1107
- function createStorage(transport) {
1184
+ /**
1185
+ * Create a StorageSource that talks to the Rebase backend REST API.
1186
+ *
1187
+ * @param transport - HTTP transport instance
1188
+ * @param storageId - Optional storage-source key for multi-backend routing.
1189
+ * When set, it is forwarded to the server so the correct
1190
+ * `StorageController` is resolved from the registry.
1191
+ */
1192
+ function createStorage(transport, storageId) {
1108
1193
  const urlsCache = /* @__PURE__ */ new Map();
1109
- async function putObject({ file, key, metadata, bucket }) {
1194
+ /** Append ?storageId=... to a path when multi-backend routing is active. */
1195
+ const withStorageId = (path) => {
1196
+ if (!storageId) return path;
1197
+ return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
1198
+ };
1199
+ async function putObject({ file, key, metadata, bucket, public: isPublic }) {
1110
1200
  const formData = new FormData();
1111
1201
  formData.append("file", file);
1112
- if (key) formData.append("key", key);
1202
+ let effectiveKey = key;
1203
+ if (isPublic && effectiveKey && !(0, _rebasepro_types.isPublicStoragePath)(effectiveKey)) effectiveKey = `${_rebasepro_types.PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
1204
+ if (effectiveKey) formData.append("key", effectiveKey);
1113
1205
  if (bucket) formData.append("bucket", bucket);
1206
+ if (storageId) formData.append("storageId", storageId);
1114
1207
  if (metadata) {
1115
1208
  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));
1116
1209
  }
1117
- return (await transport.request("/storage/upload", {
1210
+ return (await transport.request(withStorageId("/storage/upload"), {
1118
1211
  method: "POST",
1119
1212
  body: formData,
1120
1213
  headers: {}
@@ -1122,24 +1215,44 @@
1122
1215
  }
1123
1216
  async function getSignedUrl(keyOrUrl, bucket) {
1124
1217
  const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
1125
- const cached = urlsCache.get(cacheKey);
1126
- if (cached) return cached;
1218
+ const cachedEntry = urlsCache.get(cacheKey);
1219
+ if (cachedEntry) {
1220
+ if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) return cachedEntry.config;
1221
+ urlsCache.delete(cacheKey);
1222
+ }
1127
1223
  let filePath = keyOrUrl;
1128
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1224
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1129
1225
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1130
1226
  if (!filePath || filePath.trim() === "" || filePath === "/") return {
1131
1227
  url: null,
1132
1228
  fileNotFound: true
1133
1229
  };
1230
+ if ((0, _rebasepro_types.isPublicStoragePath)(filePath)) {
1231
+ const publicConfig = { url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`) };
1232
+ urlsCache.set(cacheKey, { config: publicConfig });
1233
+ return publicConfig;
1234
+ }
1134
1235
  try {
1135
- const result = await transport.request(`/storage/metadata/${filePath}`);
1136
- const activeToken = await transport.resolveToken();
1137
- const tokenQuery = activeToken ? `?token=${activeToken}` : "";
1236
+ const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
1237
+ if (result.data.public) {
1238
+ const publicConfig = {
1239
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`),
1240
+ metadata: result.data
1241
+ };
1242
+ urlsCache.set(cacheKey, { config: publicConfig });
1243
+ return publicConfig;
1244
+ }
1245
+ const scopedToken = result.data.token;
1246
+ const tokenQuery = scopedToken ? `?token=${scopedToken}` : "";
1138
1247
  const downloadConfig = {
1139
- url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
1248
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
1140
1249
  metadata: result.data
1141
1250
  };
1142
- urlsCache.set(cacheKey, downloadConfig);
1251
+ const expiresAt = result.data.tokenExpiresIn ? Date.now() + (result.data.tokenExpiresIn - 10) * 1e3 : void 0;
1252
+ urlsCache.set(cacheKey, {
1253
+ config: downloadConfig,
1254
+ expiresAt
1255
+ });
1143
1256
  return downloadConfig;
1144
1257
  } catch (e) {
1145
1258
  if (e instanceof Error && "status" in e && e.status === 404) return {
@@ -1150,25 +1263,22 @@
1150
1263
  }
1151
1264
  }
1152
1265
  async function getObject(key, bucket) {
1153
- let filePath = key;
1154
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1155
- if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1156
- if (!filePath || filePath.trim() === "" || filePath === "/") return null;
1157
- const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
1158
- const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
1266
+ const downloadConfig = await getSignedUrl(key, bucket);
1267
+ if (downloadConfig.fileNotFound || !downloadConfig.url) return null;
1268
+ const response = await transport.fetchFn(downloadConfig.url, { headers: {} });
1159
1269
  if (response.status === 404) return null;
1160
1270
  if (!response.ok) throw new Error("Failed to get file");
1161
1271
  const blob = await response.blob();
1162
- const fileName = filePath.split("/").pop() || "file";
1272
+ const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
1163
1273
  return new File([blob], fileName, { type: blob.type });
1164
1274
  }
1165
1275
  async function deleteObject(key, bucket) {
1166
1276
  let filePath = key;
1167
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1277
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1168
1278
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1169
1279
  if (!filePath || filePath.trim() === "" || filePath === "/") return;
1170
1280
  try {
1171
- await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
1281
+ await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
1172
1282
  } catch (e) {
1173
1283
  if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
1174
1284
  }
@@ -1180,6 +1290,7 @@
1180
1290
  if (options?.bucket) params.set("bucket", options.bucket);
1181
1291
  if (options?.maxResults) params.set("maxResults", String(options.maxResults));
1182
1292
  if (options?.pageToken) params.set("pageToken", options.pageToken);
1293
+ if (storageId) params.set("storageId", storageId);
1183
1294
  return (await transport.request(`/storage/list?${params.toString()}`)).data;
1184
1295
  }
1185
1296
  return {
@@ -1191,6 +1302,62 @@
1191
1302
  };
1192
1303
  }
1193
1304
  //#endregion
1305
+ //#region src/storage-registry.ts
1306
+ /**
1307
+ * Default implementation of the client-side `StorageSourceRegistry`.
1308
+ */
1309
+ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1310
+ sources = /* @__PURE__ */ new Map();
1311
+ /**
1312
+ * Register a storage source.
1313
+ * @param key - Unique key matching a `StorageSourceDefinition.key`
1314
+ * @param source - The `StorageSource` instance
1315
+ */
1316
+ register(key, source) {
1317
+ this.sources.set(key, source);
1318
+ }
1319
+ getDefault() {
1320
+ const source = this.sources.get(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY);
1321
+ if (!source) throw new Error(`[StorageSourceRegistry] No default storage source registered. Register one with key "${_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY}".`);
1322
+ return source;
1323
+ }
1324
+ get(key) {
1325
+ if (key === void 0 || key === null) return this.sources.get(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY);
1326
+ return this.sources.get(key);
1327
+ }
1328
+ getOrDefault(key) {
1329
+ if (key === void 0 || key === null) return this.getDefault();
1330
+ const source = this.sources.get(key);
1331
+ if (source) return source;
1332
+ console.warn(`[StorageSourceRegistry] Storage source "${key}" not found, falling back to "${_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY}".`);
1333
+ return this.getDefault();
1334
+ }
1335
+ has(key) {
1336
+ return this.sources.has(key);
1337
+ }
1338
+ list() {
1339
+ return Array.from(this.sources.keys());
1340
+ }
1341
+ /**
1342
+ * Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
1343
+ *
1344
+ * - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
1345
+ * - Sources with `transport: "direct"` are **not** auto-wired — they must
1346
+ * be registered manually after this call (e.g. via a Firebase hook).
1347
+ *
1348
+ * @param definitions - Array of storage source definitions
1349
+ * @param transport - HTTP transport for server-backed sources
1350
+ */
1351
+ static fromDefinitions(definitions, transport) {
1352
+ const registry = new ClientStorageSourceRegistry();
1353
+ for (const def of definitions) if (def.transport === "server") {
1354
+ const source = createStorage(transport, def.key === _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY ? void 0 : def.key);
1355
+ registry.register(def.key, source);
1356
+ }
1357
+ return registry;
1358
+ }
1359
+ };
1360
+ //#endregion
1194
1361
  //#region src/websocket.ts
1195
1362
  /**
1196
1363
  * Extract error message and code from a WebSocket message payload.
@@ -1204,16 +1371,15 @@
1204
1371
  errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
1205
1372
  };
1206
1373
  }
1207
- var ApiError = class extends Error {
1208
- code;
1209
- error;
1210
- constructor(message, error, code) {
1211
- super(message);
1212
- this.name = "ApiError";
1213
- this.code = code;
1214
- this.error = error;
1215
- }
1216
- };
1374
+ /**
1375
+ * Low-level realtime WebSocket client.
1376
+ *
1377
+ * @internal Not a stable app-facing API. `createRebaseClient()` constructs and
1378
+ * manages this internally (exposed as `client.ws`, typed by the minimal
1379
+ * `RebaseWebSocket` contract in `@rebasepro/types`). It is re-exported from the
1380
+ * package root only because the `@rebasepro/client-postgresql` driver
1381
+ * instantiates it directly; its surface may change without a major bump.
1382
+ */
1217
1383
  var RebaseWebSocketClient = class {
1218
1384
  websocketUrl;
1219
1385
  ws = null;
@@ -1229,7 +1395,7 @@
1229
1395
  if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
1230
1396
  }
1231
1397
  collectionSubscriptions = /* @__PURE__ */ new Map();
1232
- entitySubscriptions = /* @__PURE__ */ new Map();
1398
+ singleSubscriptions = /* @__PURE__ */ new Map();
1233
1399
  backendToCollectionKey = /* @__PURE__ */ new Map();
1234
1400
  backendToEntityKey = /* @__PURE__ */ new Map();
1235
1401
  pendingRequests = /* @__PURE__ */ new Map();
@@ -1364,7 +1530,7 @@
1364
1530
  request.message._queuedResolve = request.resolve;
1365
1531
  request.message._queuedReject = request.reject;
1366
1532
  this.messageQueue.push(request.message);
1367
- } else request.reject(new ApiError("Connection closed", "Connection closed"));
1533
+ } else request.reject(new _rebasepro_types.RebaseApiError("Connection closed"));
1368
1534
  this.pendingRequests.delete(reqId);
1369
1535
  }
1370
1536
  this.attemptReconnect();
@@ -1431,7 +1597,7 @@
1431
1597
  }
1432
1598
  }
1433
1599
  /**
1434
- * Shared logic for re-subscribing a collection or entity subscription
1600
+ * Shared logic for re-subscribing a collection or row subscription
1435
1601
  * after an auth error is resolved by refreshing credentials.
1436
1602
  */
1437
1603
  resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
@@ -1456,7 +1622,7 @@
1456
1622
  });
1457
1623
  } else {
1458
1624
  const { errorMessage, errorCode } = extractMessageError(message);
1459
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1625
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1460
1626
  subscription.callbacks.forEach((callback) => {
1461
1627
  if (callback.onError) callback.onError(error);
1462
1628
  });
@@ -1477,7 +1643,7 @@
1477
1643
  if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
1478
1644
  else {
1479
1645
  const { errorMessage, errorCode } = extractMessageError(message);
1480
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1646
+ pendingReq.reject(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1481
1647
  }
1482
1648
  }).catch((err) => {
1483
1649
  pendingReq.reject(err);
@@ -1485,7 +1651,7 @@
1485
1651
  } else {
1486
1652
  this.pendingRequests.delete(requestId);
1487
1653
  const { errorMessage, errorCode } = extractMessageError(message);
1488
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1654
+ pendingReq.reject(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1489
1655
  }
1490
1656
  else {
1491
1657
  this.pendingRequests.delete(requestId);
@@ -1498,14 +1664,14 @@
1498
1664
  if (subscriptionKey) {
1499
1665
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1500
1666
  if (collectionSub) {
1501
- const incomingEntities = message.entities || [];
1502
- const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
1503
- collectionSub.latestData = entities;
1667
+ const incomingRows = message.rows || [];
1668
+ const rows = this.mergeRows(collectionSub.latestData, incomingRows);
1669
+ collectionSub.latestData = rows;
1504
1670
  collectionSub.lastUpdated = Date.now();
1505
1671
  collectionSub.isInitialDataReceived = true;
1506
1672
  collectionSub.callbacks.forEach((callback) => {
1507
1673
  try {
1508
- callback.onUpdate(entities);
1674
+ callback.onUpdate(rows);
1509
1675
  } catch (error) {
1510
1676
  console.error("Error in collection subscription callback:", error);
1511
1677
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
@@ -1515,21 +1681,22 @@
1515
1681
  }
1516
1682
  }
1517
1683
  }
1518
- if (subscriptionId && type === "collection_entity_patch") {
1684
+ if (subscriptionId && type === "collection_patch") {
1519
1685
  const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1520
1686
  if (subscriptionKey) {
1521
1687
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1522
1688
  if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
1523
- const patchEntity = message.entity ?? null;
1524
- const patchEntityId = message.entityId;
1689
+ const patchWireEntity = message.row ?? null;
1690
+ const patchEntityId = message.id;
1691
+ const patchRow = patchWireEntity ? patchWireEntity : null;
1525
1692
  let updated;
1526
- if (patchEntity === null || patchEntity === void 0) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1693
+ if (patchRow === null) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1527
1694
  else {
1528
- const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchEntity.id));
1695
+ const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchRow.id));
1529
1696
  if (idx >= 0) {
1530
1697
  updated = [...collectionSub.latestData];
1531
- updated[idx] = patchEntity;
1532
- } else updated = [patchEntity, ...collectionSub.latestData];
1698
+ updated[idx] = patchRow;
1699
+ } else updated = [patchRow, ...collectionSub.latestData];
1533
1700
  }
1534
1701
  collectionSub.latestData = updated;
1535
1702
  collectionSub.lastUpdated = Date.now();
@@ -1545,20 +1712,21 @@
1545
1712
  }
1546
1713
  }
1547
1714
  }
1548
- if (subscriptionId && type === "entity_update") {
1715
+ if (subscriptionId && type === "single_update") {
1549
1716
  const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
1550
1717
  if (subscriptionKey) {
1551
- const entitySub = this.entitySubscriptions.get(subscriptionKey);
1718
+ const entitySub = this.singleSubscriptions.get(subscriptionKey);
1552
1719
  if (entitySub) {
1553
- const entity = message.entity ?? null;
1554
- entitySub.latestData = entity;
1720
+ const wireEntity = message.row ?? null;
1721
+ const row = wireEntity ? wireEntity : null;
1722
+ entitySub.latestData = row;
1555
1723
  entitySub.lastUpdated = Date.now();
1556
1724
  entitySub.isInitialDataReceived = true;
1557
1725
  entitySub.callbacks.forEach((callback) => {
1558
1726
  try {
1559
- callback.onUpdate(entity);
1727
+ callback.onUpdate(row);
1560
1728
  } catch (error) {
1561
- console.error("Error in entity subscription callback:", error);
1729
+ console.error("Error in row subscription callback:", error);
1562
1730
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
1563
1731
  }
1564
1732
  });
@@ -1576,7 +1744,7 @@
1576
1744
  return;
1577
1745
  }
1578
1746
  const { errorMessage, errorCode } = extractMessageError(message);
1579
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1747
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1580
1748
  collectionSub.callbacks.forEach((callback) => {
1581
1749
  if (callback.onError) callback.onError(error);
1582
1750
  });
@@ -1585,14 +1753,14 @@
1585
1753
  }
1586
1754
  const entityKey = this.backendToEntityKey.get(subscriptionId);
1587
1755
  if (entityKey) {
1588
- const entitySub = this.entitySubscriptions.get(entityKey);
1756
+ const entitySub = this.singleSubscriptions.get(entityKey);
1589
1757
  if (entitySub) {
1590
1758
  if (this.isAuthError(message)) {
1591
- this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "entity", this.backendToEntityKey, "subscribe_entity");
1759
+ this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
1592
1760
  return;
1593
1761
  }
1594
1762
  const { errorMessage, errorCode } = extractMessageError(message);
1595
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1763
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1596
1764
  entitySub.callbacks.forEach((callback) => {
1597
1765
  if (callback.onError) callback.onError(error);
1598
1766
  });
@@ -1606,7 +1774,7 @@
1606
1774
  if (message.type === "ERROR" || message.error) {
1607
1775
  if (callback.onError) {
1608
1776
  const { errorMessage, errorCode } = extractMessageError(message);
1609
- callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
1777
+ callback.onError(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1610
1778
  }
1611
1779
  } else callback.onUpdate(message);
1612
1780
  }
@@ -1680,15 +1848,14 @@
1680
1848
  if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
1681
1849
  await this.ensureAuthenticated();
1682
1850
  } catch (error) {
1683
- const errorMessage = error instanceof Error ? error.message : "Authentication required";
1684
- reject(new ApiError(errorMessage, errorMessage));
1851
+ reject(new _rebasepro_types.RebaseApiError(error instanceof Error ? error.message : "Authentication required"));
1685
1852
  return;
1686
1853
  }
1687
1854
  const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1688
1855
  message.requestId = requestId;
1689
1856
  const expectsResponse = ![
1690
1857
  "subscribe_collection",
1691
- "subscribe_entity",
1858
+ "subscribe_one",
1692
1859
  "unsubscribe",
1693
1860
  "join_channel",
1694
1861
  "leave_channel",
@@ -1701,7 +1868,7 @@
1701
1868
  const timeoutHandle = setTimeout(() => {
1702
1869
  if (this.pendingRequests.has(requestId)) {
1703
1870
  this.pendingRequests.delete(requestId);
1704
- reject(new ApiError("Request timed out", "Request timed out"));
1871
+ reject(new _rebasepro_types.RebaseApiError("Request timed out"));
1705
1872
  }
1706
1873
  }, this.requestTimeoutMs);
1707
1874
  this.pendingRequests.set(requestId, {
@@ -1721,30 +1888,30 @@
1721
1888
  if (!expectsResponse) resolve(void 0);
1722
1889
  } catch (error) {
1723
1890
  if (expectsResponse) this.pendingRequests.delete(requestId);
1724
- reject(new ApiError("Failed to send message", error instanceof Error ? error.message : "Unknown error"));
1891
+ reject(new _rebasepro_types.RebaseApiError("Failed to send message", { cause: error }));
1725
1892
  }
1726
1893
  }
1727
1894
  async fetchCollection(props) {
1728
1895
  return (await this.sendMessage({
1729
1896
  type: "FETCH_COLLECTION",
1730
1897
  payload: props
1731
- })).entities || [];
1898
+ })).rows || [];
1732
1899
  }
1733
- async fetchEntity(props) {
1900
+ async fetchOne(props) {
1734
1901
  return (await this.sendMessage({
1735
- type: "FETCH_ENTITY",
1902
+ type: "FETCH_ONE",
1736
1903
  payload: props
1737
- })).entity ?? void 0;
1904
+ })).row ?? void 0;
1738
1905
  }
1739
- async saveEntity(props) {
1906
+ async save(props) {
1740
1907
  return (await this.sendMessage({
1741
- type: "SAVE_ENTITY",
1908
+ type: "SAVE",
1742
1909
  payload: props
1743
- })).entity;
1910
+ })).row;
1744
1911
  }
1745
- async deleteEntity(props) {
1912
+ async delete(props) {
1746
1913
  await this.sendMessage({
1747
- type: "DELETE_ENTITY",
1914
+ type: "DELETE",
1748
1915
  payload: props
1749
1916
  });
1750
1917
  }
@@ -1769,21 +1936,21 @@
1769
1936
  async fetchCurrentDatabase() {
1770
1937
  return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
1771
1938
  }
1772
- async checkUniqueField(path, name, value, entityId, collection) {
1939
+ async checkUniqueField(path, name, value, id, collection) {
1773
1940
  return (await this.sendMessage({
1774
1941
  type: "CHECK_UNIQUE_FIELD",
1775
1942
  payload: {
1776
1943
  path,
1777
1944
  name,
1778
1945
  value,
1779
- entityId,
1946
+ id,
1780
1947
  collection
1781
1948
  }
1782
1949
  })).isUnique;
1783
1950
  }
1784
- async countEntities(props) {
1951
+ async count(props) {
1785
1952
  return (await this.sendMessage({
1786
- type: "COUNT_ENTITIES",
1953
+ type: "COUNT",
1787
1954
  payload: props
1788
1955
  })).count;
1789
1956
  }
@@ -1875,33 +2042,31 @@
1875
2042
  return val;
1876
2043
  }
1877
2044
  /**
1878
- * Merge incoming entities with cached data, preserving cached references
1879
- * for entities whose values haven't changed. This avoids unnecessary
1880
- * React re-renders when the server refetches all entities but most
2045
+ * Merge incoming rows with cached data, preserving cached references
2046
+ * for rows whose values haven't changed. This avoids unnecessary
2047
+ * React re-renders when the server refetches all rows but most
1881
2048
  * haven't actually changed.
1882
2049
  */
1883
- mergeEntities(cached, incoming) {
2050
+ mergeRows(cached, incoming) {
1884
2051
  if (!cached || cached.length === 0) return incoming;
1885
2052
  const cachedById = /* @__PURE__ */ new Map();
1886
- for (const entity of cached) cachedById.set(entity.id, entity);
1887
- return incoming.map((incomingEntity) => {
1888
- const cachedEntity = cachedById.get(incomingEntity.id);
1889
- if (!cachedEntity) return incomingEntity;
1890
- if (cachedEntity.path === incomingEntity.path) {
1891
- const normCached = this.normalizeForComparison(cachedEntity.values);
1892
- const normIncoming = this.normalizeForComparison(incomingEntity.values);
1893
- if (this.deepEqual(normCached, normIncoming)) return cachedEntity;
1894
- else {
1895
- const mismatches = {};
1896
- const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
1897
- for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
1898
- cached: normCached[key],
1899
- incoming: normIncoming[key]
1900
- };
1901
- console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
1902
- }
2053
+ for (const row of cached) cachedById.set(row.id, row);
2054
+ return incoming.map((incomingRow) => {
2055
+ const cachedRow = cachedById.get(incomingRow.id);
2056
+ if (!cachedRow) return incomingRow;
2057
+ const normCached = this.normalizeForComparison(cachedRow);
2058
+ const normIncoming = this.normalizeForComparison(incomingRow);
2059
+ if (this.deepEqual(normCached, normIncoming)) return cachedRow;
2060
+ else {
2061
+ const mismatches = {};
2062
+ const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
2063
+ for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
2064
+ cached: normCached[key],
2065
+ incoming: normIncoming[key]
2066
+ };
2067
+ console.debug(`[RebaseWS] Row ${incomingRow.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
1903
2068
  }
1904
- return incomingEntity;
2069
+ return incomingRow;
1905
2070
  });
1906
2071
  }
1907
2072
  listenCollection(props, onUpdate, onError) {
@@ -1969,10 +2134,10 @@
1969
2134
  }
1970
2135
  };
1971
2136
  }
1972
- listenEntity(props, onUpdate, onError) {
1973
- const subscriptionKey = this.createEntitySubscriptionKey(props);
2137
+ listenOne(props, onUpdate, onError) {
2138
+ const subscriptionKey = this.createSingleSubscriptionKey(props);
1974
2139
  const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1975
- const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
2140
+ const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
1976
2141
  if (existingSubscription) {
1977
2142
  const callbackMap = existingSubscription.callbacks;
1978
2143
  callbackMap.set(callbackId, {
@@ -1982,13 +2147,13 @@
1982
2147
  if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
1983
2148
  onUpdate(existingSubscription.latestData);
1984
2149
  } catch (error) {
1985
- console.error("Error in entity subscription callback:", error);
2150
+ console.error("Error in row subscription callback:", error);
1986
2151
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
1987
2152
  }
1988
2153
  return () => {
1989
2154
  callbackMap.delete(callbackId);
1990
2155
  if (callbackMap.size === 0) {
1991
- this.entitySubscriptions.delete(subscriptionKey);
2156
+ this.singleSubscriptions.delete(subscriptionKey);
1992
2157
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
1993
2158
  if (this.isConnected && this.ws) this.sendMessage({
1994
2159
  type: "unsubscribe",
@@ -2003,14 +2168,14 @@
2003
2168
  onUpdate,
2004
2169
  onError
2005
2170
  });
2006
- this.entitySubscriptions.set(subscriptionKey, {
2171
+ this.singleSubscriptions.set(subscriptionKey, {
2007
2172
  backendSubscriptionId,
2008
2173
  callbacks: callbackMap,
2009
2174
  props
2010
2175
  });
2011
2176
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
2012
2177
  this.sendMessage({
2013
- type: "subscribe_entity",
2178
+ type: "subscribe_one",
2014
2179
  payload: {
2015
2180
  ...props,
2016
2181
  subscriptionId: backendSubscriptionId
@@ -2019,12 +2184,12 @@
2019
2184
  if (onError) onError(error);
2020
2185
  });
2021
2186
  return () => {
2022
- const subscription = this.entitySubscriptions.get(subscriptionKey);
2187
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
2023
2188
  if (subscription) {
2024
2189
  const callbacks = subscription.callbacks;
2025
2190
  callbacks.delete(callbackId);
2026
2191
  if (callbacks.size === 0) {
2027
- this.entitySubscriptions.delete(subscriptionKey);
2192
+ this.singleSubscriptions.delete(subscriptionKey);
2028
2193
  this.backendToEntityKey.delete(subscription.backendSubscriptionId);
2029
2194
  if (this.isConnected && this.ws) this.sendMessage({
2030
2195
  type: "unsubscribe",
@@ -2040,7 +2205,7 @@
2040
2205
  * we need to re-register everything to resume receiving updates.
2041
2206
  */
2042
2207
  resubscribeAll() {
2043
- console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
2208
+ console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
2044
2209
  for (const [key, sub] of this.collectionSubscriptions.entries()) {
2045
2210
  const oldBackendId = sub.backendSubscriptionId;
2046
2211
  const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -2057,20 +2222,20 @@
2057
2222
  console.error("[WS] Failed to re-subscribe collection:", key, error);
2058
2223
  });
2059
2224
  }
2060
- for (const [key, sub] of this.entitySubscriptions.entries()) {
2225
+ for (const [key, sub] of this.singleSubscriptions.entries()) {
2061
2226
  const oldBackendId = sub.backendSubscriptionId;
2062
2227
  const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2063
2228
  sub.backendSubscriptionId = newBackendId;
2064
2229
  this.backendToEntityKey.delete(oldBackendId);
2065
2230
  this.backendToEntityKey.set(newBackendId, key);
2066
2231
  this.sendMessage({
2067
- type: "subscribe_entity",
2232
+ type: "subscribe_one",
2068
2233
  payload: {
2069
2234
  ...sub.props,
2070
2235
  subscriptionId: newBackendId
2071
2236
  }
2072
2237
  }).catch((error) => {
2073
- console.error("[WS] Failed to re-subscribe entity:", key, error);
2238
+ console.error("[WS] Failed to re-subscribe row:", key, error);
2074
2239
  });
2075
2240
  }
2076
2241
  }
@@ -2093,8 +2258,8 @@
2093
2258
  return value;
2094
2259
  });
2095
2260
  }
2096
- createEntitySubscriptionKey(props) {
2097
- return `${props.path}|${props.entityId}`;
2261
+ createSingleSubscriptionKey(props) {
2262
+ return `${props.path}|${props.id}`;
2098
2263
  }
2099
2264
  };
2100
2265
  //#endregion
@@ -2128,6 +2293,23 @@
2128
2293
  const apiKeys = createApiKeys(transport, options.apiKeys);
2129
2294
  const storage = createStorage(transport);
2130
2295
  const functions = createFunctionsClient(transport);
2296
+ const createStorageSource = (storageId) => storageId === _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
2297
+ const storageRegistry = new ClientStorageSourceRegistry();
2298
+ storageRegistry.register(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY, storage);
2299
+ for (const def of options.storageSources ?? []) if (def.transport === "server" && def.key !== _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY) storageRegistry.register(def.key, createStorageSource(def.key));
2300
+ let storageSourcesPromise;
2301
+ const fetchStorageSources = () => {
2302
+ if (storageSourcesPromise) return storageSourcesPromise;
2303
+ storageSourcesPromise = transport.request("/storage/sources").then((res) => {
2304
+ const defs = res.data ?? [];
2305
+ for (const def of defs) if (def.transport === "server" && def.key !== _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY && !storageRegistry.has(def.key)) storageRegistry.register(def.key, createStorageSource(def.key));
2306
+ return defs;
2307
+ }).catch((e) => {
2308
+ storageSourcesPromise = void 0;
2309
+ throw e;
2310
+ });
2311
+ return storageSourcesPromise;
2312
+ };
2131
2313
  const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2132
2314
  let ws;
2133
2315
  if (resolvedWsUrl) {
@@ -2165,7 +2347,45 @@
2165
2347
  return false;
2166
2348
  }
2167
2349
  });
2350
+ /**
2351
+ * Suggest the closest known collection key for a mistyped accessor.
2352
+ * Uses edit-distance-1 and prefix matching — no external dependency.
2353
+ */
2354
+ function suggestCollection(prop, knownKeys) {
2355
+ const prefixMatch = knownKeys.find((k) => k.startsWith(prop) || prop.startsWith(k));
2356
+ if (prefixMatch) return prefixMatch;
2357
+ for (const key of knownKeys) {
2358
+ if (Math.abs(key.length - prop.length) > 1) continue;
2359
+ let diffs = 0;
2360
+ const longer = key.length >= prop.length ? key : prop;
2361
+ const shorter = key.length >= prop.length ? prop : key;
2362
+ if (longer.length === shorter.length) for (let i = 0; i < longer.length; i++) {
2363
+ if (longer[i] !== shorter[i]) {
2364
+ if (i + 1 < longer.length && longer[i] === shorter[i + 1] && longer[i + 1] === shorter[i]) {
2365
+ diffs++;
2366
+ i++;
2367
+ if (diffs > 1) break;
2368
+ continue;
2369
+ }
2370
+ diffs++;
2371
+ }
2372
+ if (diffs > 1) break;
2373
+ }
2374
+ else {
2375
+ let li = 0;
2376
+ let si = 0;
2377
+ while (li < longer.length) {
2378
+ if (si < shorter.length && longer[li] === shorter[si]) si++;
2379
+ else diffs++;
2380
+ li++;
2381
+ if (diffs > 1) break;
2382
+ }
2383
+ }
2384
+ if (diffs <= 1) return key;
2385
+ }
2386
+ }
2168
2387
  const collectionClients = /* @__PURE__ */ new Map();
2388
+ let untypedWarned = false;
2169
2389
  function collection(slug) {
2170
2390
  if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
2171
2391
  return collectionClients.get(slug);
@@ -2173,7 +2393,22 @@
2173
2393
  const dataProxy = new Proxy({ collection }, { get(_target, prop) {
2174
2394
  if (prop === "collection") return collection;
2175
2395
  if (typeof prop === "symbol") return void 0;
2176
- if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") return collection((0, _rebasepro_utils.toSnakeCase)(prop));
2396
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
2397
+ if (options.collections) {
2398
+ if (prop in options.collections) return collection(options.collections[prop]);
2399
+ const knownKeys = Object.keys(options.collections);
2400
+ const suggestion = suggestCollection(prop, knownKeys);
2401
+ let msg = `Unknown collection accessor "${prop}". Known collections: ${knownKeys.join(", ")}.`;
2402
+ if (suggestion) msg += ` Did you mean "${suggestion}"?`;
2403
+ msg += ` Use data.collection("<slug>") for dynamic slugs.`;
2404
+ throw new _rebasepro_types.RebaseClientError(msg);
2405
+ }
2406
+ if (!untypedWarned) {
2407
+ untypedWarned = true;
2408
+ 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.`);
2409
+ }
2410
+ return collection((0, _rebasepro_utils.toSnakeCase)(prop));
2411
+ }
2177
2412
  } });
2178
2413
  return {
2179
2414
  auth,
@@ -2182,6 +2417,9 @@
2182
2417
  apiKeys,
2183
2418
  functions,
2184
2419
  storage,
2420
+ storageRegistry,
2421
+ createStorageSource,
2422
+ fetchStorageSources,
2185
2423
  ws,
2186
2424
  setToken: transport.setToken,
2187
2425
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -2197,19 +2435,28 @@
2197
2435
  });
2198
2436
  return res.data ?? res;
2199
2437
  },
2200
- data: dataProxy,
2201
- email: void 0
2438
+ data: dataProxy
2202
2439
  };
2203
2440
  }
2204
2441
  //#endregion
2205
- exports.ApiError = ApiError;
2206
2442
  Object.defineProperty(exports, "QueryBuilder", {
2207
2443
  enumerable: true,
2208
2444
  get: function() {
2209
2445
  return _rebasepro_common.QueryBuilder;
2210
2446
  }
2211
2447
  });
2212
- exports.RebaseApiError = RebaseApiError;
2448
+ Object.defineProperty(exports, "RebaseApiError", {
2449
+ enumerable: true,
2450
+ get: function() {
2451
+ return _rebasepro_types.RebaseApiError;
2452
+ }
2453
+ });
2454
+ Object.defineProperty(exports, "RebaseClientError", {
2455
+ enumerable: true,
2456
+ get: function() {
2457
+ return _rebasepro_types.RebaseClientError;
2458
+ }
2459
+ });
2213
2460
  exports.RebaseWebSocketClient = RebaseWebSocketClient;
2214
2461
  Object.defineProperty(exports, "and", {
2215
2462
  enumerable: true,
@@ -2217,31 +2464,21 @@
2217
2464
  return _rebasepro_common.and;
2218
2465
  }
2219
2466
  });
2220
- exports.buildQueryString = buildQueryString;
2221
2467
  Object.defineProperty(exports, "cond", {
2222
2468
  enumerable: true,
2223
2469
  get: function() {
2224
2470
  return _rebasepro_common.cond;
2225
2471
  }
2226
2472
  });
2227
- exports.createAdmin = createAdmin;
2228
- exports.createApiKeys = createApiKeys;
2229
- exports.createAuth = createAuth;
2230
- exports.createCollectionClient = createCollectionClient;
2231
2473
  exports.createCookieStorage = createCookieStorage;
2232
- exports.createCron = createCron;
2233
- exports.createFunctionsClient = createFunctionsClient;
2234
2474
  exports.createMemoryStorage = createMemoryStorage;
2235
2475
  exports.createRebaseClient = createRebaseClient;
2236
- exports.createStorage = createStorage;
2237
- exports.createTransport = createTransport;
2238
2476
  Object.defineProperty(exports, "or", {
2239
2477
  enumerable: true,
2240
2478
  get: function() {
2241
2479
  return _rebasepro_common.or;
2242
2480
  }
2243
2481
  });
2244
- exports.rebaseReviver = rebaseReviver;
2245
2482
  });
2246
2483
 
2247
2484
  //# sourceMappingURL=index.umd.js.map