@rebasepro/client 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -1,11 +1,11 @@
1
1
  (function(global, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@rebasepro/common"), require("@rebasepro/types"), require("@rebasepro/utils")) : typeof define === "function" && define.amd ? define([
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@rebasepro/types"), require("@rebasepro/common"), require("@rebasepro/utils")) : typeof define === "function" && define.amd ? define([
3
3
  "exports",
4
- "@rebasepro/common",
5
4
  "@rebasepro/types",
5
+ "@rebasepro/common",
6
6
  "@rebasepro/utils"
7
- ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Client"] = {}, global._rebasepro_common, global._rebasepro_types, global._rebasepro_utils));
8
- })(this, function(exports, _rebasepro_common, _rebasepro_types, _rebasepro_utils) {
7
+ ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Client"] = {}, global._rebasepro_types, global._rebasepro_common, global._rebasepro_utils));
8
+ })(this, function(exports, _rebasepro_types, _rebasepro_common, _rebasepro_utils) {
9
9
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
10
  //#region src/reviver.ts
11
11
  function rebaseReviver(_key, value) {
@@ -36,25 +36,16 @@
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
39
  function buildQueryString(params) {
52
40
  if (!params) return "";
53
41
  const parts = [];
54
42
  if (params.limit != null) parts.push(`limit=${params.limit}`);
55
43
  if (params.offset != null) parts.push(`offset=${params.offset}`);
56
44
  if (params.page != null) parts.push(`page=${params.page}`);
57
- 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
+ }
58
49
  if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
59
50
  if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
60
51
  if (params.logical) {
@@ -103,8 +94,7 @@
103
94
  } catch (e) {}
104
95
  const getErrorField = (obj, field) => {
105
96
  const err = obj?.error;
106
- if (err && typeof err === "object" && err !== null && field in err) return err[field];
107
- return obj?.[field];
97
+ if (err && typeof err === "object" && err !== null) return err[field];
108
98
  };
109
99
  if (res.status === 401 && onUnauthorizedHandler) {
110
100
  if (await onUnauthorizedHandler()) {
@@ -127,7 +117,11 @@
127
117
  if (!retryRes.ok) {
128
118
  let fallbackMessage = retryRes.statusText;
129
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.`;
130
- 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
+ });
131
125
  }
132
126
  return retryBody;
133
127
  }
@@ -135,7 +129,11 @@
135
129
  if (!res.ok) {
136
130
  let fallbackMessage = res.statusText;
137
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.`;
138
- 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
+ });
139
137
  }
140
138
  return body;
141
139
  }
@@ -171,6 +169,29 @@
171
169
  }
172
170
  //#endregion
173
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
+ };
174
195
  function createMemoryStorage() {
175
196
  const store = {};
176
197
  return {
@@ -201,11 +222,20 @@
201
222
  const authPath = opts.authPath || "/auth";
202
223
  const autoRefresh = opts.autoRefresh !== false;
203
224
  const persistSession = opts.persistSession !== false;
225
+ const authFlowMode = opts.authFlowMode || "json";
204
226
  const STORAGE_KEY = "rebase_auth";
205
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;
206
231
  let currentSession = null;
207
232
  const listeners = /* @__PURE__ */ new Set();
208
233
  let refreshTimeout = null;
234
+ let inFlightRefresh = null;
235
+ let resolveInitialized;
236
+ const isInitialized = new Promise((resolve) => {
237
+ resolveInitialized = resolve;
238
+ });
209
239
  function authUrl(endpoint) {
210
240
  return transport.baseUrl + transport.apiPath + authPath + endpoint;
211
241
  }
@@ -213,7 +243,11 @@
213
243
  return transport.fetchFn || globalThis.fetch;
214
244
  }
215
245
  function throwApiError(status, body, statusText) {
216
- 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
+ });
217
251
  }
218
252
  function emit(event, session) {
219
253
  for (const fn of listeners) try {
@@ -221,7 +255,7 @@
221
255
  } catch (e) {}
222
256
  }
223
257
  function saveSession(session) {
224
- if (!persistSession) return;
258
+ if (!persistSession || authFlowMode === "cookie") return;
225
259
  try {
226
260
  storage.setItem(STORAGE_KEY, JSON.stringify(session));
227
261
  } catch (e) {}
@@ -238,28 +272,53 @@
238
272
  } catch (e) {}
239
273
  return null;
240
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
+ }
241
303
  function scheduleRefresh(expiresAt) {
242
304
  if (refreshTimeout) clearTimeout(refreshTimeout);
243
305
  if (!autoRefresh) return;
244
306
  const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
245
307
  if (delay <= 0) {
246
- refreshSession().catch(() => signOut());
308
+ attemptScheduledRefresh(0);
247
309
  return;
248
310
  }
249
- refreshTimeout = setTimeout(async () => {
250
- try {
251
- await refreshSession();
252
- } catch (e) {
253
- signOut();
254
- }
311
+ refreshTimeout = setTimeout(() => {
312
+ attemptScheduledRefresh(0);
255
313
  }, delay);
256
314
  }
257
315
  function handleAuthResponse(data, event) {
316
+ const user = mapRawUser(data.user);
258
317
  const session = {
259
318
  accessToken: data.tokens.accessToken,
260
- refreshToken: data.tokens.refreshToken,
319
+ refreshToken: data.tokens.refreshToken || currentSession?.refreshToken || "",
261
320
  expiresAt: data.tokens.accessTokenExpiresAt,
262
- user: data.user
321
+ user
263
322
  };
264
323
  currentSession = session;
265
324
  saveSession(session);
@@ -275,7 +334,8 @@
275
334
  body: JSON.stringify({
276
335
  email,
277
336
  password
278
- })
337
+ }),
338
+ credentials: authFlowMode === "cookie" ? "include" : void 0
279
339
  });
280
340
  const body = await res.json().catch(() => ({}));
281
341
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -296,7 +356,8 @@
296
356
  const res = await fetchFn(authUrl("/register"), {
297
357
  method: "POST",
298
358
  headers: { "Content-Type": "application/json" },
299
- body: JSON.stringify(payload)
359
+ body: JSON.stringify(payload),
360
+ credentials: authFlowMode === "cookie" ? "include" : void 0
300
361
  });
301
362
  const body = await res.json().catch(() => ({}));
302
363
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -319,7 +380,8 @@
319
380
  const res = await getFetch()(authUrl("/google"), {
320
381
  method: "POST",
321
382
  headers: { "Content-Type": "application/json" },
322
- body: JSON.stringify(payload)
383
+ body: JSON.stringify(payload),
384
+ credentials: authFlowMode === "cookie" ? "include" : void 0
323
385
  });
324
386
  const responseBody = await res.json().catch(() => ({}));
325
387
  if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
@@ -337,7 +399,8 @@
337
399
  body: JSON.stringify({
338
400
  code,
339
401
  redirectUri
340
- })
402
+ }),
403
+ credentials: authFlowMode === "cookie" ? "include" : void 0
341
404
  });
342
405
  const body = await res.json().catch(() => ({}));
343
406
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -356,7 +419,8 @@
356
419
  const res = await getFetch()(authUrl(`/${providerId}`), {
357
420
  method: "POST",
358
421
  headers: { "Content-Type": "application/json" },
359
- body: JSON.stringify(payload)
422
+ body: JSON.stringify(payload),
423
+ credentials: authFlowMode === "cookie" ? "include" : void 0
360
424
  });
361
425
  const body = await res.json().catch(() => ({}));
362
426
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -432,10 +496,11 @@
432
496
  async function signOut() {
433
497
  const fetchFn = getFetch();
434
498
  try {
435
- if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
499
+ if (authFlowMode === "cookie" || currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
436
500
  method: "POST",
437
501
  headers: { "Content-Type": "application/json" },
438
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
502
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
503
+ credentials: authFlowMode === "cookie" ? "include" : void 0
439
504
  });
440
505
  } catch (e) {}
441
506
  currentSession = null;
@@ -447,20 +512,35 @@
447
512
  transport.setToken(null);
448
513
  emit("SIGNED_OUT", null);
449
514
  }
450
- async function refreshSession() {
451
- 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");
452
524
  const res = await getFetch()(authUrl("/refresh"), {
453
525
  method: "POST",
454
526
  headers: { "Content-Type": "application/json" },
455
- body: JSON.stringify({ refreshToken: currentSession.refreshToken })
527
+ body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
528
+ credentials: authFlowMode === "cookie" ? "include" : void 0
456
529
  });
457
530
  const body = await res.json().catch(() => ({}));
458
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 {}
459
539
  const session = {
460
- accessToken: body.tokens.accessToken,
461
- refreshToken: body.tokens.refreshToken,
540
+ accessToken,
541
+ refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
462
542
  expiresAt: body.tokens.accessTokenExpiresAt,
463
- user: currentSession.user
543
+ user: user ?? EMPTY_USER
464
544
  };
465
545
  currentSession = session;
466
546
  saveSession(session);
@@ -472,6 +552,18 @@
472
552
  async function getUser() {
473
553
  return (await transport.request(authPath + "/me", { method: "GET" })).user;
474
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
+ }
475
567
  async function updateUser(updates) {
476
568
  const data = await transport.request(authPath + "/me", {
477
569
  method: "PATCH",
@@ -545,7 +637,8 @@
545
637
  const res = await getFetch()(authUrl("/magic-link/verify"), {
546
638
  method: "POST",
547
639
  headers: { "Content-Type": "application/json" },
548
- body: JSON.stringify({ token })
640
+ body: JSON.stringify({ token }),
641
+ credentials: authFlowMode === "cookie" ? "include" : void 0
549
642
  });
550
643
  const body = await res.json().catch(() => ({}));
551
644
  if (!res.ok) throwApiError(res.status, body, res.statusText);
@@ -592,21 +685,29 @@
592
685
  }
593
686
  if (persistSession) {
594
687
  const stored = loadStoredSession();
595
- if (stored && stored.accessToken && stored.refreshToken) {
596
- if (stored.expiresAt > Date.now()) {
597
- currentSession = stored;
598
- transport.setToken(stored.accessToken);
599
- scheduleRefresh(stored.expiresAt);
600
- } else if (stored.refreshToken) {
601
- currentSession = stored;
602
- refreshSession().catch(() => {
603
- currentSession = null;
604
- clearStoredSession();
605
- transport.setToken(null);
606
- });
607
- }
608
- }
609
- }
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();
610
711
  return {
611
712
  signInWithEmail,
612
713
  signUp,
@@ -626,6 +727,7 @@
626
727
  signOut,
627
728
  refreshSession,
628
729
  getUser,
730
+ findUserByEmail,
629
731
  updateUser,
630
732
  resetPasswordForEmail,
631
733
  resetPassword,
@@ -639,7 +741,8 @@
639
741
  revokeAllSessions,
640
742
  getAuthConfig,
641
743
  getSession,
642
- onAuthStateChange
744
+ onAuthStateChange,
745
+ isInitialized: () => isInitialized
643
746
  };
644
747
  }
645
748
  function createCookieStorage(options = {}) {
@@ -816,20 +919,108 @@
816
919
  };
817
920
  }
818
921
  //#endregion
819
- //#region src/collection.ts
922
+ //#region src/sdk_query_builder.ts
820
923
  /**
821
- * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
822
- * a proper `Entity<M>` structure expected by the core framework.
823
- * The `id` is kept inside `values` as well, since collection properties
824
- * 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
825
935
  */
826
- function rowToEntity(row, slug) {
827
- return {
828
- id: row.id,
829
- path: slug,
830
- values: row
831
- };
832
- }
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
833
1024
  function createCollectionClient(transport, slug, ws) {
834
1025
  const basePath = `/data/${slug}`;
835
1026
  const client = {
@@ -837,7 +1028,7 @@
837
1028
  const qs = buildQueryString(params);
838
1029
  const raw = await transport.request(basePath + qs, { method: "GET" });
839
1030
  return {
840
- data: (raw.data || []).map((row) => rowToEntity(row, slug)),
1031
+ data: raw.data || [],
841
1032
  meta: raw.meta
842
1033
  };
843
1034
  },
@@ -845,28 +1036,28 @@
845
1036
  try {
846
1037
  const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
847
1038
  if (!raw) return void 0;
848
- return rowToEntity(raw, slug);
1039
+ return raw;
849
1040
  } catch (err) {
850
- if (err instanceof RebaseApiError && err.status === 404) return;
1041
+ if (err instanceof _rebasepro_types.RebaseApiError && err.status === 404) return;
851
1042
  throw err;
852
1043
  }
853
1044
  },
854
1045
  async create(data, id) {
855
1046
  const body = { ...data };
856
1047
  if (id !== void 0) body.id = id;
857
- return rowToEntity(await transport.request(basePath, {
1048
+ return await transport.request(basePath, {
858
1049
  method: "POST",
859
1050
  body: JSON.stringify(body)
860
- }), slug);
1051
+ });
861
1052
  },
862
1053
  async update(id, data) {
863
- return rowToEntity(await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1054
+ return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
864
1055
  method: "PUT",
865
1056
  body: JSON.stringify(data)
866
- }), slug);
1057
+ });
867
1058
  },
868
1059
  async delete(id) {
869
- return transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
1060
+ await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
870
1061
  },
871
1062
  async count(params) {
872
1063
  const qs = buildQueryString({
@@ -877,24 +1068,24 @@
877
1068
  return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
878
1069
  },
879
1070
  where(columnOrCondition, operator, value) {
880
- const builder = new _rebasepro_common.QueryBuilder(client);
1071
+ const builder = new SDKQueryBuilder(client);
881
1072
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
882
1073
  return builder.where(columnOrCondition, operator, value);
883
1074
  },
884
1075
  orderBy(column, direction) {
885
- return new _rebasepro_common.QueryBuilder(client).orderBy(column, direction);
1076
+ return new SDKQueryBuilder(client).orderBy(column, direction);
886
1077
  },
887
1078
  limit(count) {
888
- return new _rebasepro_common.QueryBuilder(client).limit(count);
1079
+ return new SDKQueryBuilder(client).limit(count);
889
1080
  },
890
1081
  offset(count) {
891
- return new _rebasepro_common.QueryBuilder(client).offset(count);
1082
+ return new SDKQueryBuilder(client).offset(count);
892
1083
  },
893
1084
  search(searchString) {
894
- return new _rebasepro_common.QueryBuilder(client).search(searchString);
1085
+ return new SDKQueryBuilder(client).search(searchString);
895
1086
  },
896
1087
  include(...relations) {
897
- return new _rebasepro_common.QueryBuilder(client).include(...relations);
1088
+ return new SDKQueryBuilder(client).include(...relations);
898
1089
  }
899
1090
  };
900
1091
  if (ws) {
@@ -906,33 +1097,46 @@
906
1097
  filter: params?.where,
907
1098
  limit: params?.limit,
908
1099
  startAfter: params?.offset ? String(params.offset) : void 0,
909
- orderBy: params?.orderBy?.split(":")[0],
910
- order: params?.orderBy?.split(":")[1],
1100
+ orderBy: params?.orderBy?.[0],
1101
+ order: params?.orderBy?.[1],
911
1102
  searchString: params?.searchString
912
- }, (entities) => {
1103
+ }, (incomingRows) => {
913
1104
  const currentUpdateId = ++lastUpdateId;
914
1105
  const requestedLimit = params?.limit || 20;
915
1106
  const offset = params?.offset || 0;
916
- onUpdate({
917
- data: entities,
918
- meta: {
919
- total: entities.length,
920
- limit: requestedLimit,
921
- offset,
922
- hasMore: entities.length >= requestedLimit
923
- }
924
- });
1107
+ const rows = incomingRows;
1108
+ const heuristicTotal = rows.length;
1109
+ const heuristicHasMore = rows.length >= requestedLimit;
925
1110
  if (client.count) client.count(params).then((total) => {
926
1111
  if (active && currentUpdateId === lastUpdateId) onUpdate({
927
- data: entities,
1112
+ data: rows,
928
1113
  meta: {
929
1114
  total,
930
1115
  limit: requestedLimit,
931
1116
  offset,
932
- hasMore: offset + entities.length < total
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
933
1128
  }
934
1129
  });
935
- }).catch(() => {});
1130
+ });
1131
+ else onUpdate({
1132
+ data: rows,
1133
+ meta: {
1134
+ total: heuristicTotal,
1135
+ limit: requestedLimit,
1136
+ offset,
1137
+ hasMore: heuristicHasMore
1138
+ }
1139
+ });
936
1140
  }, onError);
937
1141
  return () => {
938
1142
  active = false;
@@ -940,11 +1144,11 @@
940
1144
  };
941
1145
  };
942
1146
  client.listenById = (id, onUpdate, onError) => {
943
- return ws.listenEntity({
1147
+ return ws.listenOne({
944
1148
  path: slug,
945
- entityId: String(id)
946
- }, (entity) => {
947
- if (entity) onUpdate(entity);
1149
+ id: String(id)
1150
+ }, (row) => {
1151
+ if (row) onUpdate(row);
948
1152
  else onUpdate(void 0);
949
1153
  }, onError);
950
1154
  };
@@ -992,10 +1196,12 @@
992
1196
  if (!storageId) return path;
993
1197
  return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
994
1198
  };
995
- async function putObject({ file, key, metadata, bucket }) {
1199
+ async function putObject({ file, key, metadata, bucket, public: isPublic }) {
996
1200
  const formData = new FormData();
997
1201
  formData.append("file", file);
998
- 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);
999
1205
  if (bucket) formData.append("bucket", bucket);
1000
1206
  if (storageId) formData.append("storageId", storageId);
1001
1207
  if (metadata) {
@@ -1009,8 +1215,11 @@
1009
1215
  }
1010
1216
  async function getSignedUrl(keyOrUrl, bucket) {
1011
1217
  const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
1012
- const cached = urlsCache.get(cacheKey);
1013
- 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
+ }
1014
1223
  let filePath = keyOrUrl;
1015
1224
  if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1016
1225
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
@@ -1018,15 +1227,32 @@
1018
1227
  url: null,
1019
1228
  fileNotFound: true
1020
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
+ }
1021
1235
  try {
1022
1236
  const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
1023
- const activeToken = await transport.resolveToken();
1024
- const tokenQuery = activeToken ? `?token=${activeToken}` : "";
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}` : "";
1025
1247
  const downloadConfig = {
1026
1248
  url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
1027
1249
  metadata: result.data
1028
1250
  };
1029
- 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
+ });
1030
1256
  return downloadConfig;
1031
1257
  } catch (e) {
1032
1258
  if (e instanceof Error && "status" in e && e.status === 404) return {
@@ -1037,16 +1263,13 @@
1037
1263
  }
1038
1264
  }
1039
1265
  async function getObject(key, bucket) {
1040
- let filePath = key;
1041
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1042
- if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1043
- if (!filePath || filePath.trim() === "" || filePath === "/") return null;
1044
- const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
1045
- 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: {} });
1046
1269
  if (response.status === 404) return null;
1047
1270
  if (!response.ok) throw new Error("Failed to get file");
1048
1271
  const blob = await response.blob();
1049
- const fileName = filePath.split("/").pop() || "file";
1272
+ const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
1050
1273
  return new File([blob], fileName, { type: blob.type });
1051
1274
  }
1052
1275
  async function deleteObject(key, bucket) {
@@ -1148,16 +1371,15 @@
1148
1371
  errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
1149
1372
  };
1150
1373
  }
1151
- var ApiError = class extends Error {
1152
- code;
1153
- error;
1154
- constructor(message, error, code) {
1155
- super(message);
1156
- this.name = "ApiError";
1157
- this.code = code;
1158
- this.error = error;
1159
- }
1160
- };
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
+ */
1161
1383
  var RebaseWebSocketClient = class {
1162
1384
  websocketUrl;
1163
1385
  ws = null;
@@ -1173,7 +1395,7 @@
1173
1395
  if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
1174
1396
  }
1175
1397
  collectionSubscriptions = /* @__PURE__ */ new Map();
1176
- entitySubscriptions = /* @__PURE__ */ new Map();
1398
+ singleSubscriptions = /* @__PURE__ */ new Map();
1177
1399
  backendToCollectionKey = /* @__PURE__ */ new Map();
1178
1400
  backendToEntityKey = /* @__PURE__ */ new Map();
1179
1401
  pendingRequests = /* @__PURE__ */ new Map();
@@ -1308,7 +1530,7 @@
1308
1530
  request.message._queuedResolve = request.resolve;
1309
1531
  request.message._queuedReject = request.reject;
1310
1532
  this.messageQueue.push(request.message);
1311
- } else request.reject(new ApiError("Connection closed", "Connection closed"));
1533
+ } else request.reject(new _rebasepro_types.RebaseApiError("Connection closed"));
1312
1534
  this.pendingRequests.delete(reqId);
1313
1535
  }
1314
1536
  this.attemptReconnect();
@@ -1375,7 +1597,7 @@
1375
1597
  }
1376
1598
  }
1377
1599
  /**
1378
- * Shared logic for re-subscribing a collection or entity subscription
1600
+ * Shared logic for re-subscribing a collection or row subscription
1379
1601
  * after an auth error is resolved by refreshing credentials.
1380
1602
  */
1381
1603
  resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
@@ -1400,7 +1622,7 @@
1400
1622
  });
1401
1623
  } else {
1402
1624
  const { errorMessage, errorCode } = extractMessageError(message);
1403
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1625
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1404
1626
  subscription.callbacks.forEach((callback) => {
1405
1627
  if (callback.onError) callback.onError(error);
1406
1628
  });
@@ -1421,7 +1643,7 @@
1421
1643
  if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
1422
1644
  else {
1423
1645
  const { errorMessage, errorCode } = extractMessageError(message);
1424
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1646
+ pendingReq.reject(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1425
1647
  }
1426
1648
  }).catch((err) => {
1427
1649
  pendingReq.reject(err);
@@ -1429,7 +1651,7 @@
1429
1651
  } else {
1430
1652
  this.pendingRequests.delete(requestId);
1431
1653
  const { errorMessage, errorCode } = extractMessageError(message);
1432
- pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
1654
+ pendingReq.reject(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1433
1655
  }
1434
1656
  else {
1435
1657
  this.pendingRequests.delete(requestId);
@@ -1442,14 +1664,14 @@
1442
1664
  if (subscriptionKey) {
1443
1665
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1444
1666
  if (collectionSub) {
1445
- const incomingEntities = message.entities || [];
1446
- const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
1447
- collectionSub.latestData = entities;
1667
+ const incomingRows = message.rows || [];
1668
+ const rows = this.mergeRows(collectionSub.latestData, incomingRows);
1669
+ collectionSub.latestData = rows;
1448
1670
  collectionSub.lastUpdated = Date.now();
1449
1671
  collectionSub.isInitialDataReceived = true;
1450
1672
  collectionSub.callbacks.forEach((callback) => {
1451
1673
  try {
1452
- callback.onUpdate(entities);
1674
+ callback.onUpdate(rows);
1453
1675
  } catch (error) {
1454
1676
  console.error("Error in collection subscription callback:", error);
1455
1677
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
@@ -1459,21 +1681,22 @@
1459
1681
  }
1460
1682
  }
1461
1683
  }
1462
- if (subscriptionId && type === "collection_entity_patch") {
1684
+ if (subscriptionId && type === "collection_patch") {
1463
1685
  const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1464
1686
  if (subscriptionKey) {
1465
1687
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1466
1688
  if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
1467
- const patchEntity = message.entity ?? null;
1468
- const patchEntityId = message.entityId;
1689
+ const patchWireEntity = message.row ?? null;
1690
+ const patchEntityId = message.id;
1691
+ const patchRow = patchWireEntity ? patchWireEntity : null;
1469
1692
  let updated;
1470
- 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));
1471
1694
  else {
1472
- 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));
1473
1696
  if (idx >= 0) {
1474
1697
  updated = [...collectionSub.latestData];
1475
- updated[idx] = patchEntity;
1476
- } else updated = [patchEntity, ...collectionSub.latestData];
1698
+ updated[idx] = patchRow;
1699
+ } else updated = [patchRow, ...collectionSub.latestData];
1477
1700
  }
1478
1701
  collectionSub.latestData = updated;
1479
1702
  collectionSub.lastUpdated = Date.now();
@@ -1489,20 +1712,21 @@
1489
1712
  }
1490
1713
  }
1491
1714
  }
1492
- if (subscriptionId && type === "entity_update") {
1715
+ if (subscriptionId && type === "single_update") {
1493
1716
  const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
1494
1717
  if (subscriptionKey) {
1495
- const entitySub = this.entitySubscriptions.get(subscriptionKey);
1718
+ const entitySub = this.singleSubscriptions.get(subscriptionKey);
1496
1719
  if (entitySub) {
1497
- const entity = message.entity ?? null;
1498
- entitySub.latestData = entity;
1720
+ const wireEntity = message.row ?? null;
1721
+ const row = wireEntity ? wireEntity : null;
1722
+ entitySub.latestData = row;
1499
1723
  entitySub.lastUpdated = Date.now();
1500
1724
  entitySub.isInitialDataReceived = true;
1501
1725
  entitySub.callbacks.forEach((callback) => {
1502
1726
  try {
1503
- callback.onUpdate(entity);
1727
+ callback.onUpdate(row);
1504
1728
  } catch (error) {
1505
- console.error("Error in entity subscription callback:", error);
1729
+ console.error("Error in row subscription callback:", error);
1506
1730
  if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
1507
1731
  }
1508
1732
  });
@@ -1520,7 +1744,7 @@
1520
1744
  return;
1521
1745
  }
1522
1746
  const { errorMessage, errorCode } = extractMessageError(message);
1523
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1747
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1524
1748
  collectionSub.callbacks.forEach((callback) => {
1525
1749
  if (callback.onError) callback.onError(error);
1526
1750
  });
@@ -1529,14 +1753,14 @@
1529
1753
  }
1530
1754
  const entityKey = this.backendToEntityKey.get(subscriptionId);
1531
1755
  if (entityKey) {
1532
- const entitySub = this.entitySubscriptions.get(entityKey);
1756
+ const entitySub = this.singleSubscriptions.get(entityKey);
1533
1757
  if (entitySub) {
1534
1758
  if (this.isAuthError(message)) {
1535
- this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "entity", this.backendToEntityKey, "subscribe_entity");
1759
+ this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
1536
1760
  return;
1537
1761
  }
1538
1762
  const { errorMessage, errorCode } = extractMessageError(message);
1539
- const error = new ApiError(errorMessage, errorMessage, errorCode);
1763
+ const error = new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode });
1540
1764
  entitySub.callbacks.forEach((callback) => {
1541
1765
  if (callback.onError) callback.onError(error);
1542
1766
  });
@@ -1550,7 +1774,7 @@
1550
1774
  if (message.type === "ERROR" || message.error) {
1551
1775
  if (callback.onError) {
1552
1776
  const { errorMessage, errorCode } = extractMessageError(message);
1553
- callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
1777
+ callback.onError(new _rebasepro_types.RebaseApiError(errorMessage, { code: errorCode }));
1554
1778
  }
1555
1779
  } else callback.onUpdate(message);
1556
1780
  }
@@ -1624,15 +1848,14 @@
1624
1848
  if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
1625
1849
  await this.ensureAuthenticated();
1626
1850
  } catch (error) {
1627
- const errorMessage = error instanceof Error ? error.message : "Authentication required";
1628
- reject(new ApiError(errorMessage, errorMessage));
1851
+ reject(new _rebasepro_types.RebaseApiError(error instanceof Error ? error.message : "Authentication required"));
1629
1852
  return;
1630
1853
  }
1631
1854
  const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1632
1855
  message.requestId = requestId;
1633
1856
  const expectsResponse = ![
1634
1857
  "subscribe_collection",
1635
- "subscribe_entity",
1858
+ "subscribe_one",
1636
1859
  "unsubscribe",
1637
1860
  "join_channel",
1638
1861
  "leave_channel",
@@ -1645,7 +1868,7 @@
1645
1868
  const timeoutHandle = setTimeout(() => {
1646
1869
  if (this.pendingRequests.has(requestId)) {
1647
1870
  this.pendingRequests.delete(requestId);
1648
- reject(new ApiError("Request timed out", "Request timed out"));
1871
+ reject(new _rebasepro_types.RebaseApiError("Request timed out"));
1649
1872
  }
1650
1873
  }, this.requestTimeoutMs);
1651
1874
  this.pendingRequests.set(requestId, {
@@ -1665,30 +1888,30 @@
1665
1888
  if (!expectsResponse) resolve(void 0);
1666
1889
  } catch (error) {
1667
1890
  if (expectsResponse) this.pendingRequests.delete(requestId);
1668
- 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 }));
1669
1892
  }
1670
1893
  }
1671
1894
  async fetchCollection(props) {
1672
1895
  return (await this.sendMessage({
1673
1896
  type: "FETCH_COLLECTION",
1674
1897
  payload: props
1675
- })).entities || [];
1898
+ })).rows || [];
1676
1899
  }
1677
- async fetchEntity(props) {
1900
+ async fetchOne(props) {
1678
1901
  return (await this.sendMessage({
1679
- type: "FETCH_ENTITY",
1902
+ type: "FETCH_ONE",
1680
1903
  payload: props
1681
- })).entity ?? void 0;
1904
+ })).row ?? void 0;
1682
1905
  }
1683
- async saveEntity(props) {
1906
+ async save(props) {
1684
1907
  return (await this.sendMessage({
1685
- type: "SAVE_ENTITY",
1908
+ type: "SAVE",
1686
1909
  payload: props
1687
- })).entity;
1910
+ })).row;
1688
1911
  }
1689
- async deleteEntity(props) {
1912
+ async delete(props) {
1690
1913
  await this.sendMessage({
1691
- type: "DELETE_ENTITY",
1914
+ type: "DELETE",
1692
1915
  payload: props
1693
1916
  });
1694
1917
  }
@@ -1713,21 +1936,21 @@
1713
1936
  async fetchCurrentDatabase() {
1714
1937
  return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
1715
1938
  }
1716
- async checkUniqueField(path, name, value, entityId, collection) {
1939
+ async checkUniqueField(path, name, value, id, collection) {
1717
1940
  return (await this.sendMessage({
1718
1941
  type: "CHECK_UNIQUE_FIELD",
1719
1942
  payload: {
1720
1943
  path,
1721
1944
  name,
1722
1945
  value,
1723
- entityId,
1946
+ id,
1724
1947
  collection
1725
1948
  }
1726
1949
  })).isUnique;
1727
1950
  }
1728
- async countEntities(props) {
1951
+ async count(props) {
1729
1952
  return (await this.sendMessage({
1730
- type: "COUNT_ENTITIES",
1953
+ type: "COUNT",
1731
1954
  payload: props
1732
1955
  })).count;
1733
1956
  }
@@ -1819,33 +2042,31 @@
1819
2042
  return val;
1820
2043
  }
1821
2044
  /**
1822
- * Merge incoming entities with cached data, preserving cached references
1823
- * for entities whose values haven't changed. This avoids unnecessary
1824
- * 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
1825
2048
  * haven't actually changed.
1826
2049
  */
1827
- mergeEntities(cached, incoming) {
2050
+ mergeRows(cached, incoming) {
1828
2051
  if (!cached || cached.length === 0) return incoming;
1829
2052
  const cachedById = /* @__PURE__ */ new Map();
1830
- for (const entity of cached) cachedById.set(entity.id, entity);
1831
- return incoming.map((incomingEntity) => {
1832
- const cachedEntity = cachedById.get(incomingEntity.id);
1833
- if (!cachedEntity) return incomingEntity;
1834
- if (cachedEntity.path === incomingEntity.path) {
1835
- const normCached = this.normalizeForComparison(cachedEntity.values);
1836
- const normIncoming = this.normalizeForComparison(incomingEntity.values);
1837
- if (this.deepEqual(normCached, normIncoming)) return cachedEntity;
1838
- else {
1839
- const mismatches = {};
1840
- const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
1841
- for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
1842
- cached: normCached[key],
1843
- incoming: normIncoming[key]
1844
- };
1845
- console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
1846
- }
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));
1847
2068
  }
1848
- return incomingEntity;
2069
+ return incomingRow;
1849
2070
  });
1850
2071
  }
1851
2072
  listenCollection(props, onUpdate, onError) {
@@ -1913,10 +2134,10 @@
1913
2134
  }
1914
2135
  };
1915
2136
  }
1916
- listenEntity(props, onUpdate, onError) {
1917
- const subscriptionKey = this.createEntitySubscriptionKey(props);
2137
+ listenOne(props, onUpdate, onError) {
2138
+ const subscriptionKey = this.createSingleSubscriptionKey(props);
1918
2139
  const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1919
- const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
2140
+ const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
1920
2141
  if (existingSubscription) {
1921
2142
  const callbackMap = existingSubscription.callbacks;
1922
2143
  callbackMap.set(callbackId, {
@@ -1926,13 +2147,13 @@
1926
2147
  if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
1927
2148
  onUpdate(existingSubscription.latestData);
1928
2149
  } catch (error) {
1929
- console.error("Error in entity subscription callback:", error);
2150
+ console.error("Error in row subscription callback:", error);
1930
2151
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
1931
2152
  }
1932
2153
  return () => {
1933
2154
  callbackMap.delete(callbackId);
1934
2155
  if (callbackMap.size === 0) {
1935
- this.entitySubscriptions.delete(subscriptionKey);
2156
+ this.singleSubscriptions.delete(subscriptionKey);
1936
2157
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
1937
2158
  if (this.isConnected && this.ws) this.sendMessage({
1938
2159
  type: "unsubscribe",
@@ -1947,14 +2168,14 @@
1947
2168
  onUpdate,
1948
2169
  onError
1949
2170
  });
1950
- this.entitySubscriptions.set(subscriptionKey, {
2171
+ this.singleSubscriptions.set(subscriptionKey, {
1951
2172
  backendSubscriptionId,
1952
2173
  callbacks: callbackMap,
1953
2174
  props
1954
2175
  });
1955
2176
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
1956
2177
  this.sendMessage({
1957
- type: "subscribe_entity",
2178
+ type: "subscribe_one",
1958
2179
  payload: {
1959
2180
  ...props,
1960
2181
  subscriptionId: backendSubscriptionId
@@ -1963,12 +2184,12 @@
1963
2184
  if (onError) onError(error);
1964
2185
  });
1965
2186
  return () => {
1966
- const subscription = this.entitySubscriptions.get(subscriptionKey);
2187
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
1967
2188
  if (subscription) {
1968
2189
  const callbacks = subscription.callbacks;
1969
2190
  callbacks.delete(callbackId);
1970
2191
  if (callbacks.size === 0) {
1971
- this.entitySubscriptions.delete(subscriptionKey);
2192
+ this.singleSubscriptions.delete(subscriptionKey);
1972
2193
  this.backendToEntityKey.delete(subscription.backendSubscriptionId);
1973
2194
  if (this.isConnected && this.ws) this.sendMessage({
1974
2195
  type: "unsubscribe",
@@ -1984,7 +2205,7 @@
1984
2205
  * we need to re-register everything to resume receiving updates.
1985
2206
  */
1986
2207
  resubscribeAll() {
1987
- 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)`);
1988
2209
  for (const [key, sub] of this.collectionSubscriptions.entries()) {
1989
2210
  const oldBackendId = sub.backendSubscriptionId;
1990
2211
  const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -2001,20 +2222,20 @@
2001
2222
  console.error("[WS] Failed to re-subscribe collection:", key, error);
2002
2223
  });
2003
2224
  }
2004
- for (const [key, sub] of this.entitySubscriptions.entries()) {
2225
+ for (const [key, sub] of this.singleSubscriptions.entries()) {
2005
2226
  const oldBackendId = sub.backendSubscriptionId;
2006
2227
  const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2007
2228
  sub.backendSubscriptionId = newBackendId;
2008
2229
  this.backendToEntityKey.delete(oldBackendId);
2009
2230
  this.backendToEntityKey.set(newBackendId, key);
2010
2231
  this.sendMessage({
2011
- type: "subscribe_entity",
2232
+ type: "subscribe_one",
2012
2233
  payload: {
2013
2234
  ...sub.props,
2014
2235
  subscriptionId: newBackendId
2015
2236
  }
2016
2237
  }).catch((error) => {
2017
- console.error("[WS] Failed to re-subscribe entity:", key, error);
2238
+ console.error("[WS] Failed to re-subscribe row:", key, error);
2018
2239
  });
2019
2240
  }
2020
2241
  }
@@ -2037,8 +2258,8 @@
2037
2258
  return value;
2038
2259
  });
2039
2260
  }
2040
- createEntitySubscriptionKey(props) {
2041
- return `${props.path}|${props.entityId}`;
2261
+ createSingleSubscriptionKey(props) {
2262
+ return `${props.path}|${props.id}`;
2042
2263
  }
2043
2264
  };
2044
2265
  //#endregion
@@ -2126,7 +2347,45 @@
2126
2347
  return false;
2127
2348
  }
2128
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
+ }
2129
2387
  const collectionClients = /* @__PURE__ */ new Map();
2388
+ let untypedWarned = false;
2130
2389
  function collection(slug) {
2131
2390
  if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
2132
2391
  return collectionClients.get(slug);
@@ -2135,7 +2394,19 @@
2135
2394
  if (prop === "collection") return collection;
2136
2395
  if (typeof prop === "symbol") return void 0;
2137
2396
  if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
2138
- if (options.collections && prop in options.collections) return collection(options.collections[prop]);
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
+ }
2139
2410
  return collection((0, _rebasepro_utils.toSnakeCase)(prop));
2140
2411
  }
2141
2412
  } });
@@ -2164,20 +2435,28 @@
2164
2435
  });
2165
2436
  return res.data ?? res;
2166
2437
  },
2167
- data: dataProxy,
2168
- email: void 0
2438
+ data: dataProxy
2169
2439
  };
2170
2440
  }
2171
2441
  //#endregion
2172
- exports.ApiError = ApiError;
2173
- exports.ClientStorageSourceRegistry = ClientStorageSourceRegistry;
2174
2442
  Object.defineProperty(exports, "QueryBuilder", {
2175
2443
  enumerable: true,
2176
2444
  get: function() {
2177
2445
  return _rebasepro_common.QueryBuilder;
2178
2446
  }
2179
2447
  });
2180
- 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
+ });
2181
2460
  exports.RebaseWebSocketClient = RebaseWebSocketClient;
2182
2461
  Object.defineProperty(exports, "and", {
2183
2462
  enumerable: true,
@@ -2185,31 +2464,21 @@
2185
2464
  return _rebasepro_common.and;
2186
2465
  }
2187
2466
  });
2188
- exports.buildQueryString = buildQueryString;
2189
2467
  Object.defineProperty(exports, "cond", {
2190
2468
  enumerable: true,
2191
2469
  get: function() {
2192
2470
  return _rebasepro_common.cond;
2193
2471
  }
2194
2472
  });
2195
- exports.createAdmin = createAdmin;
2196
- exports.createApiKeys = createApiKeys;
2197
- exports.createAuth = createAuth;
2198
- exports.createCollectionClient = createCollectionClient;
2199
2473
  exports.createCookieStorage = createCookieStorage;
2200
- exports.createCron = createCron;
2201
- exports.createFunctionsClient = createFunctionsClient;
2202
2474
  exports.createMemoryStorage = createMemoryStorage;
2203
2475
  exports.createRebaseClient = createRebaseClient;
2204
- exports.createStorage = createStorage;
2205
- exports.createTransport = createTransport;
2206
2476
  Object.defineProperty(exports, "or", {
2207
2477
  enumerable: true,
2208
2478
  get: function() {
2209
2479
  return _rebasepro_common.or;
2210
2480
  }
2211
2481
  });
2212
- exports.rebaseReviver = rebaseReviver;
2213
2482
  });
2214
2483
 
2215
2484
  //# sourceMappingURL=index.umd.js.map