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