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