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