@rebasepro/client 0.3.0 → 0.5.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/README.md +164 -0
- package/dist/admin.d.ts +1 -31
- package/dist/auth.d.ts +8 -0
- package/dist/collection.d.ts +4 -3
- package/dist/index.es.js +315 -98
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +323 -97
- package/dist/index.umd.js.map +1 -1
- package/dist/query_builder.d.ts +1 -1
- package/dist/websocket.d.ts +6 -0
- package/package.json +4 -4
- package/src/admin.ts +1 -41
- package/src/auth.ts +64 -0
- package/src/collection.ts +112 -68
- package/src/index.ts +39 -9
- package/src/query_builder.ts +1 -1
- package/src/transport.ts +44 -10
- package/src/websocket.ts +135 -9
package/dist/query_builder.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { QueryBuilder } from "@rebasepro/common";
|
|
1
|
+
export { QueryBuilder, or, and, cond } from "@rebasepro/common";
|
package/dist/websocket.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface RebaseWebSocketConfig {
|
|
|
5
5
|
getAuthToken?: () => Promise<string>;
|
|
6
6
|
/** Optional WebSocket constructor to override globalThis.WebSocket (e.g. for Node environments) */
|
|
7
7
|
WebSocket?: typeof WebSocket;
|
|
8
|
+
/** Callback to handle unauthorized requests or token expiration (refreshes auth session) */
|
|
9
|
+
onUnauthorized?: () => Promise<boolean>;
|
|
8
10
|
}
|
|
9
11
|
export declare class ApiError extends Error {
|
|
10
12
|
code?: string;
|
|
@@ -32,6 +34,8 @@ export declare class RebaseWebSocketClient {
|
|
|
32
34
|
private isAuthenticated;
|
|
33
35
|
private authPromise;
|
|
34
36
|
private WebSocketConstructor;
|
|
37
|
+
onUnauthorized?: () => Promise<boolean>;
|
|
38
|
+
private refreshInProgress;
|
|
35
39
|
constructor(config: RebaseWebSocketConfig);
|
|
36
40
|
/**
|
|
37
41
|
* Authenticate the WebSocket connection
|
|
@@ -45,6 +49,8 @@ export declare class RebaseWebSocketClient {
|
|
|
45
49
|
private initWebSocket;
|
|
46
50
|
private processMessageQueue;
|
|
47
51
|
private attemptReconnect;
|
|
52
|
+
private isAuthError;
|
|
53
|
+
private handleAuthFailure;
|
|
48
54
|
private handleWebSocketMessage;
|
|
49
55
|
private ensureAuthenticated;
|
|
50
56
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "HTTP SDK client for the Rebase custom backend",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"./package.json": "./package.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@rebasepro/
|
|
34
|
-
"@rebasepro/
|
|
35
|
-
"@rebasepro/utils": "0.
|
|
33
|
+
"@rebasepro/types": "0.5.0",
|
|
34
|
+
"@rebasepro/common": "0.5.0",
|
|
35
|
+
"@rebasepro/utils": "0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@jest/globals": "^29.7.0",
|
package/src/admin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transport } from "./transport";
|
|
1
|
+
import type { Transport } from "./transport";
|
|
2
2
|
|
|
3
3
|
export interface AdminUser {
|
|
4
4
|
uid: string;
|
|
@@ -11,13 +11,6 @@ export interface AdminUser {
|
|
|
11
11
|
updatedAt: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface RebaseRole {
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
isAdmin: boolean;
|
|
18
|
-
defaultPermissions: Record<string, unknown> | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
export interface CreateAdminOptions {
|
|
22
15
|
adminPath?: string;
|
|
23
16
|
}
|
|
@@ -67,34 +60,6 @@ export function createAdmin(transport: Transport, options?: CreateAdminOptions)
|
|
|
67
60
|
});
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
async function listRoles() {
|
|
71
|
-
return transport.request<{ roles: RebaseRole[] }>(adminPath + "/roles", { method: "GET" });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function getRole(roleId: string) {
|
|
75
|
-
return transport.request<{ role: RebaseRole }>(adminPath + "/roles/" + encodeURIComponent(roleId), { method: "GET" });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function createRole(data: { id: string, name: string, isAdmin?: boolean, defaultPermissions?: Record<string, unknown> }) {
|
|
79
|
-
return transport.request<{ role: RebaseRole }>(adminPath + "/roles", {
|
|
80
|
-
method: "POST",
|
|
81
|
-
body: JSON.stringify(data)
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function updateRole(roleId: string, data: { name?: string, isAdmin?: boolean, defaultPermissions?: Record<string, unknown> }) {
|
|
86
|
-
return transport.request<{ role: RebaseRole }>(adminPath + "/roles/" + encodeURIComponent(roleId), {
|
|
87
|
-
method: "PUT",
|
|
88
|
-
body: JSON.stringify(data)
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function deleteRole(roleId: string) {
|
|
93
|
-
return transport.request<{ success: boolean }>(adminPath + "/roles/" + encodeURIComponent(roleId), {
|
|
94
|
-
method: "DELETE"
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
63
|
async function bootstrap() {
|
|
99
64
|
return transport.request<{ success: boolean; message: string; user: { uid: string; roles: string[] } }>(adminPath + "/bootstrap", {
|
|
100
65
|
method: "POST"
|
|
@@ -108,11 +73,6 @@ export function createAdmin(transport: Transport, options?: CreateAdminOptions)
|
|
|
108
73
|
createUser,
|
|
109
74
|
updateUser,
|
|
110
75
|
deleteUser,
|
|
111
|
-
listRoles,
|
|
112
|
-
getRole,
|
|
113
|
-
createRole,
|
|
114
|
-
updateRole,
|
|
115
|
-
deleteRole,
|
|
116
76
|
bootstrap
|
|
117
77
|
};
|
|
118
78
|
}
|
package/src/auth.ts
CHANGED
|
@@ -520,3 +520,67 @@ newPassword })
|
|
|
520
520
|
onAuthStateChange
|
|
521
521
|
};
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
export interface CookieStorageOptions {
|
|
525
|
+
path?: string;
|
|
526
|
+
domain?: string;
|
|
527
|
+
secure?: boolean;
|
|
528
|
+
sameSite?: "Lax" | "Strict" | "None";
|
|
529
|
+
maxAge?: number;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function createCookieStorage(options: CookieStorageOptions = {}): AuthStorage {
|
|
533
|
+
const defaultOptions = {
|
|
534
|
+
path: "/",
|
|
535
|
+
sameSite: "Lax" as const,
|
|
536
|
+
...options
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
getItem(key: string): string | null {
|
|
541
|
+
if (typeof document === "undefined") return null;
|
|
542
|
+
const nameEQ = encodeURIComponent(key) + "=";
|
|
543
|
+
const ca = document.cookie.split(";");
|
|
544
|
+
for (let i = 0; i < ca.length; i++) {
|
|
545
|
+
let c = ca[i];
|
|
546
|
+
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
|
547
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
548
|
+
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
},
|
|
553
|
+
setItem(key: string, value: string): void {
|
|
554
|
+
if (typeof document === "undefined") return;
|
|
555
|
+
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
556
|
+
|
|
557
|
+
if (defaultOptions.path) {
|
|
558
|
+
cookieStr += `; path=${defaultOptions.path}`;
|
|
559
|
+
}
|
|
560
|
+
if (defaultOptions.domain) {
|
|
561
|
+
cookieStr += `; domain=${defaultOptions.domain}`;
|
|
562
|
+
}
|
|
563
|
+
if (defaultOptions.maxAge !== undefined) {
|
|
564
|
+
cookieStr += `; max-age=${defaultOptions.maxAge}`;
|
|
565
|
+
} else {
|
|
566
|
+
cookieStr += `; max-age=${365 * 24 * 60 * 60}`;
|
|
567
|
+
}
|
|
568
|
+
if (defaultOptions.secure) {
|
|
569
|
+
cookieStr += "; secure";
|
|
570
|
+
}
|
|
571
|
+
if (defaultOptions.sameSite) {
|
|
572
|
+
cookieStr += `; samesite=${defaultOptions.sameSite}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
document.cookie = cookieStr;
|
|
576
|
+
},
|
|
577
|
+
removeItem(key: string): void {
|
|
578
|
+
if (typeof document === "undefined") return;
|
|
579
|
+
let cookieStr = `${encodeURIComponent(key)}=; path=${defaultOptions.path || "/"}; max-age=-1`;
|
|
580
|
+
if (defaultOptions.domain) {
|
|
581
|
+
cookieStr += `; domain=${defaultOptions.domain}`;
|
|
582
|
+
}
|
|
583
|
+
document.cookie = cookieStr;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
package/src/collection.ts
CHANGED
|
@@ -1,60 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildQueryString, FindParams, RebaseApiError, Transport } from "./transport";
|
|
2
2
|
import { RebaseWebSocketClient } from "./websocket";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CollectionAccessor,
|
|
5
|
+
Entity,
|
|
6
|
+
FilterOperator,
|
|
7
|
+
FilterValues,
|
|
8
|
+
FindResponse,
|
|
9
|
+
WhereFieldValue,
|
|
10
|
+
WhereFilterOp,
|
|
11
|
+
LogicalCondition,
|
|
12
|
+
WhereValue
|
|
13
|
+
} from "@rebasepro/types";
|
|
4
14
|
|
|
5
15
|
import { QueryBuilder } from "./query_builder";
|
|
6
16
|
|
|
7
17
|
function parseWhereFilter(where?: Record<string, WhereFieldValue>): FilterValues<string> | undefined {
|
|
8
18
|
if (!where) return undefined;
|
|
9
|
-
const filters: Record<string,
|
|
10
|
-
for (const [key, rawValue] of Object.entries(where)) {
|
|
11
|
-
// Handle null → equality
|
|
12
|
-
if (rawValue === null) {
|
|
13
|
-
filters[key] = ["==", null];
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
19
|
+
const filters: Record<string, any> = {};
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const OP_TO_FILTER: Record<string, WhereFilterOp> = {
|
|
22
|
+
"eq": "==",
|
|
23
|
+
"neq": "!=",
|
|
24
|
+
"gt": ">",
|
|
25
|
+
"gte": ">=",
|
|
26
|
+
"lt": "<",
|
|
27
|
+
"lte": "<=",
|
|
28
|
+
"==": "==",
|
|
29
|
+
"!=": "!=",
|
|
30
|
+
">": ">",
|
|
31
|
+
">=": ">=",
|
|
32
|
+
"<": "<",
|
|
33
|
+
"<=": "<=",
|
|
34
|
+
"in": "in",
|
|
35
|
+
"nin": "not-in",
|
|
36
|
+
"not-in": "not-in",
|
|
37
|
+
"cs": "array-contains",
|
|
38
|
+
"csa": "array-contains-any",
|
|
39
|
+
"array-contains": "array-contains",
|
|
40
|
+
"array-contains-any": "array-contains-any"
|
|
41
|
+
};
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
43
|
+
const parseSingle = (rawValue: any, fieldKey: string): [WhereFilterOp, unknown] => {
|
|
44
|
+
if (rawValue === null) return ["==", null];
|
|
45
|
+
if (typeof rawValue === "boolean") return ["==", rawValue];
|
|
46
|
+
if (typeof rawValue === "number") return ["==", rawValue];
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
if (Array.isArray(rawValue) && rawValue.length === 2) {
|
|
48
|
+
if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
|
|
31
49
|
const [rawOp, val] = rawValue;
|
|
32
|
-
|
|
33
|
-
"eq": "==",
|
|
34
|
-
"neq": "!=",
|
|
35
|
-
"gt": ">",
|
|
36
|
-
"gte": ">=",
|
|
37
|
-
"lt": "<",
|
|
38
|
-
"lte": "<=",
|
|
39
|
-
"==": "==",
|
|
40
|
-
"!=": "!=",
|
|
41
|
-
">": ">",
|
|
42
|
-
">=": ">=",
|
|
43
|
-
"<": "<",
|
|
44
|
-
"<=": "<=",
|
|
45
|
-
"in": "in",
|
|
46
|
-
"nin": "not-in",
|
|
47
|
-
"not-in": "not-in",
|
|
48
|
-
"cs": "array-contains",
|
|
49
|
-
"csa": "array-contains-any",
|
|
50
|
-
"array-contains": "array-contains",
|
|
51
|
-
"array-contains-any": "array-contains-any"
|
|
52
|
-
};
|
|
53
|
-
filters[key] = [OP_TO_FILTER[rawOp] ?? "==", val];
|
|
54
|
-
continue;
|
|
50
|
+
return [OP_TO_FILTER[rawOp] ?? "==", val];
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
// Handle string (original PostgREST format)
|
|
58
53
|
const value = String(rawValue);
|
|
59
54
|
const dotIndex = value.indexOf(".");
|
|
60
55
|
if (dotIndex > 0) {
|
|
@@ -64,12 +59,24 @@ function parseWhereFilter(where?: Record<string, WhereFieldValue>): FilterValues
|
|
|
64
59
|
let val: string | number | boolean | null | string[] = valStr;
|
|
65
60
|
|
|
66
61
|
switch (opStr) {
|
|
67
|
-
case "eq":
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
case "
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
case "eq":
|
|
63
|
+
op = "==";
|
|
64
|
+
break;
|
|
65
|
+
case "neq":
|
|
66
|
+
op = "!=";
|
|
67
|
+
break;
|
|
68
|
+
case "gt":
|
|
69
|
+
op = ">";
|
|
70
|
+
break;
|
|
71
|
+
case "gte":
|
|
72
|
+
op = ">=";
|
|
73
|
+
break;
|
|
74
|
+
case "lt":
|
|
75
|
+
op = "<";
|
|
76
|
+
break;
|
|
77
|
+
case "lte":
|
|
78
|
+
op = "<=";
|
|
79
|
+
break;
|
|
73
80
|
case "in":
|
|
74
81
|
op = "in";
|
|
75
82
|
val = valStr.startsWith("(") && valStr.endsWith(")")
|
|
@@ -82,24 +89,35 @@ function parseWhereFilter(where?: Record<string, WhereFieldValue>): FilterValues
|
|
|
82
89
|
? valStr.slice(1, -1).split(",").map(v => v.trim())
|
|
83
90
|
: valStr.split(",");
|
|
84
91
|
break;
|
|
85
|
-
case "cs":
|
|
92
|
+
case "cs":
|
|
93
|
+
op = "array-contains";
|
|
94
|
+
break;
|
|
86
95
|
case "csa":
|
|
87
96
|
op = "array-contains-any";
|
|
88
97
|
val = valStr.startsWith("(") && valStr.endsWith(")")
|
|
89
98
|
? valStr.slice(1, -1).split(",").map(v => v.trim())
|
|
90
99
|
: valStr.split(",");
|
|
91
100
|
break;
|
|
92
|
-
default:
|
|
101
|
+
default:
|
|
102
|
+
op = "==";
|
|
103
|
+
val = value;
|
|
93
104
|
}
|
|
94
|
-
// Simple type inference for parsing from URL-like strings
|
|
95
105
|
if (val === "true") val = true;
|
|
96
106
|
else if (val === "false") val = false;
|
|
97
107
|
else if (val === "null") val = null;
|
|
98
|
-
else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) &&
|
|
108
|
+
else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
return [op, val];
|
|
101
111
|
} else {
|
|
102
|
-
|
|
112
|
+
return ["==", value];
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const [key, rawValue] of Object.entries(where)) {
|
|
117
|
+
if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) {
|
|
118
|
+
filters[key] = rawValue.map(r => parseSingle(r, key));
|
|
119
|
+
} else {
|
|
120
|
+
filters[key] = parseSingle(rawValue, key);
|
|
103
121
|
}
|
|
104
122
|
}
|
|
105
123
|
return filters;
|
|
@@ -126,13 +144,19 @@ function rowToEntity<M extends Record<string, unknown>>(row: Record<string, unkn
|
|
|
126
144
|
* Additionally it exposes fluent query builder methods like `.where()`, `.orderBy()`.
|
|
127
145
|
*/
|
|
128
146
|
export interface CollectionClient<M extends Record<string, unknown> = Record<string, unknown>> extends CollectionAccessor<M> {
|
|
129
|
-
|
|
130
|
-
where(
|
|
147
|
+
where<K extends keyof M & string>(column: K, operator: FilterOperator, value: WhereValue<M[K]>): QueryBuilder<M>;
|
|
148
|
+
where(logicalCondition: LogicalCondition): QueryBuilder<M>;
|
|
149
|
+
|
|
131
150
|
orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilder<M>;
|
|
151
|
+
|
|
132
152
|
limit(count: number): QueryBuilder<M>;
|
|
153
|
+
|
|
133
154
|
offset(count: number): QueryBuilder<M>;
|
|
155
|
+
|
|
134
156
|
search(searchString: string): QueryBuilder<M>;
|
|
157
|
+
|
|
135
158
|
include(...relations: string[]): QueryBuilder<M>;
|
|
159
|
+
|
|
136
160
|
count(params?: FindParams): Promise<number>;
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -142,7 +166,10 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
142
166
|
const client: CollectionClient<M> = {
|
|
143
167
|
async find(params?: FindParams): Promise<FindResponse<M>> {
|
|
144
168
|
const qs = buildQueryString(params);
|
|
145
|
-
const raw = await transport.request<{
|
|
169
|
+
const raw = await transport.request<{
|
|
170
|
+
data: Record<string, unknown>[];
|
|
171
|
+
meta: FindResponse<M>["meta"]
|
|
172
|
+
}>(basePath + qs, { method: "GET" });
|
|
146
173
|
return {
|
|
147
174
|
data: (raw.data || []).map((row: Record<string, unknown>) => rowToEntity<M>(row, slug)),
|
|
148
175
|
meta: raw.meta
|
|
@@ -150,9 +177,16 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
150
177
|
},
|
|
151
178
|
|
|
152
179
|
async findById(id: string | number) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
try {
|
|
181
|
+
const raw = await transport.request<Record<string, unknown>>(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
|
|
182
|
+
if (!raw) return undefined;
|
|
183
|
+
return rowToEntity<M>(raw, slug);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err instanceof RebaseApiError && err.status === 404) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
156
190
|
},
|
|
157
191
|
|
|
158
192
|
async create(data: Partial<M>, id?: string | number) {
|
|
@@ -182,15 +216,23 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
182
216
|
},
|
|
183
217
|
|
|
184
218
|
async count(params?: FindParams): Promise<number> {
|
|
185
|
-
const countParams: FindParams = {
|
|
219
|
+
const countParams: FindParams = {
|
|
220
|
+
...params,
|
|
221
|
+
limit: undefined,
|
|
222
|
+
offset: undefined
|
|
223
|
+
};
|
|
186
224
|
const qs = buildQueryString(countParams);
|
|
187
225
|
const raw = await transport.request<{ count: number }>(basePath + "/count" + qs, { method: "GET" });
|
|
188
226
|
return raw.count ?? 0;
|
|
189
227
|
},
|
|
190
228
|
|
|
191
229
|
// Fluent builder instantiation
|
|
192
|
-
where(
|
|
193
|
-
|
|
230
|
+
where(columnOrCondition: string | LogicalCondition, operator?: FilterOperator, value?: unknown) {
|
|
231
|
+
const builder = new QueryBuilder<M>(client as unknown as CollectionAccessor<M>);
|
|
232
|
+
if (typeof columnOrCondition === "object") {
|
|
233
|
+
return builder.where(columnOrCondition);
|
|
234
|
+
}
|
|
235
|
+
return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
|
|
194
236
|
},
|
|
195
237
|
orderBy(column: keyof M & string, ascending?: "asc" | "desc") {
|
|
196
238
|
return new QueryBuilder<M>(client).orderBy(column, ascending);
|
|
@@ -239,8 +281,10 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
239
281
|
|
|
240
282
|
client.listenById = (id: string | number, onUpdate: (data: Entity<M> | undefined) => void, onError?: (error: Error) => void) => {
|
|
241
283
|
return ws.listenEntity(
|
|
242
|
-
{
|
|
243
|
-
|
|
284
|
+
{
|
|
285
|
+
path: slug,
|
|
286
|
+
entityId: String(id)
|
|
287
|
+
},
|
|
244
288
|
(entity: Entity | null) => {
|
|
245
289
|
if (entity) {
|
|
246
290
|
onUpdate(entity as Entity<M>);
|
package/src/index.ts
CHANGED
|
@@ -65,17 +65,32 @@ import { createStorage } from "./storage";
|
|
|
65
65
|
* `http://` → `ws://`, `https://` → `wss://`.
|
|
66
66
|
*/
|
|
67
67
|
function deriveWebSocketUrl(baseUrl?: string): string {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
if (typeof window !== "undefined") {
|
|
69
|
+
let absoluteUrl = "";
|
|
70
|
+
if (!baseUrl) {
|
|
71
|
+
absoluteUrl = window.location.origin;
|
|
72
|
+
} else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) {
|
|
73
|
+
absoluteUrl = baseUrl;
|
|
74
|
+
} else {
|
|
75
|
+
try {
|
|
76
|
+
absoluteUrl = new URL(baseUrl, window.location.href).origin;
|
|
77
|
+
} catch {
|
|
78
|
+
absoluteUrl = window.location.origin;
|
|
79
|
+
}
|
|
73
80
|
}
|
|
81
|
+
const protocol = absoluteUrl.startsWith("https:") || absoluteUrl.startsWith("wss:") ? "wss:" : "ws:";
|
|
82
|
+
return absoluteUrl
|
|
83
|
+
.replace(/^https?:\/\//i, `${protocol}//`)
|
|
84
|
+
.replace(/^wss?:\/\//i, `${protocol}//`)
|
|
85
|
+
.replace(/\/$/, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!baseUrl) return "";
|
|
89
|
+
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) {
|
|
74
90
|
return "";
|
|
75
91
|
}
|
|
76
92
|
return baseUrl
|
|
77
|
-
.replace(/^https
|
|
78
|
-
.replace(/^http:\/\//, "ws://")
|
|
93
|
+
.replace(/^https?:\/\//i, (match) => match.toLowerCase() === "https://" ? "wss://" : "ws://")
|
|
79
94
|
.replace(/\/$/, "");
|
|
80
95
|
}
|
|
81
96
|
|
|
@@ -91,12 +106,27 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
91
106
|
|
|
92
107
|
let ws: RebaseWebSocketClient | undefined;
|
|
93
108
|
if (resolvedWsUrl) {
|
|
109
|
+
const wsOnUnauthorized = options.onUnauthorized || (async () => {
|
|
110
|
+
try {
|
|
111
|
+
await auth.refreshSession();
|
|
112
|
+
return true;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
94
118
|
ws = new RebaseWebSocketClient({
|
|
95
119
|
websocketUrl: resolvedWsUrl,
|
|
96
120
|
getAuthToken: async () => {
|
|
97
|
-
|
|
121
|
+
let session = auth.getSession();
|
|
122
|
+
if (session && session.expiresAt <= Date.now() + 10000) {
|
|
123
|
+
try {
|
|
124
|
+
session = await auth.refreshSession();
|
|
125
|
+
} catch (e) { /* ignore */ }
|
|
126
|
+
}
|
|
98
127
|
return session?.accessToken || options.token || "";
|
|
99
|
-
}
|
|
128
|
+
},
|
|
129
|
+
onUnauthorized: wsOnUnauthorized
|
|
100
130
|
});
|
|
101
131
|
|
|
102
132
|
auth.onAuthStateChange((event, session) => {
|
package/src/query_builder.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { QueryBuilder } from "@rebasepro/common";
|
|
1
|
+
export { QueryBuilder, or, and, cond } from "@rebasepro/common";
|
package/src/transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FindParams as TypesFindParams, FindResponse as TypesFindResponse, WhereFieldValue } from "@rebasepro/types";
|
|
1
|
+
import { FindParams as TypesFindParams, FindResponse as TypesFindResponse, WhereFieldValue, WhereFilterOpShort } from "@rebasepro/types";
|
|
2
2
|
import { rebaseReviver } from "./reviver";
|
|
3
3
|
|
|
4
4
|
export interface RebaseClientConfig {
|
|
@@ -70,19 +70,40 @@ function normalizeWhereValue(value: WhereFieldValue): string {
|
|
|
70
70
|
if (typeof value === "number") return String(value);
|
|
71
71
|
|
|
72
72
|
// Tuple: [operator, val]
|
|
73
|
-
if (Array.isArray(value)
|
|
74
|
-
const [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
const conditions: [WhereFilterOpShort, any][] = Array.isArray(value[0])
|
|
75
|
+
? (value as [WhereFilterOpShort, any][])
|
|
76
|
+
: [value as [WhereFilterOpShort, any]];
|
|
77
|
+
|
|
78
|
+
const [rawOp, val] = conditions[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
|
+
}
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
// String — pass through (either plain equality value or PostgREST syntax)
|
|
83
88
|
return String(value);
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
function serializeLogicalCondition(cond: any): string {
|
|
92
|
+
if ("type" in cond) {
|
|
93
|
+
const sub = cond.conditions.map(serializeLogicalCondition).join(",");
|
|
94
|
+
return `${cond.type}(${sub})`;
|
|
95
|
+
} else {
|
|
96
|
+
const op = OP_MAP[cond.operator] ?? cond.operator;
|
|
97
|
+
let formattedValue = cond.value;
|
|
98
|
+
if (Array.isArray(cond.value)) {
|
|
99
|
+
formattedValue = `(${cond.value.join(",")})`;
|
|
100
|
+
} else if (cond.value === null) {
|
|
101
|
+
formattedValue = "null";
|
|
102
|
+
}
|
|
103
|
+
return `${cond.column}.${op}.${formattedValue}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
export function buildQueryString(params?: FindParams): string {
|
|
87
108
|
if (!params) return "";
|
|
88
109
|
const parts: string[] = [];
|
|
@@ -103,10 +124,23 @@ export function buildQueryString(params?: FindParams): string {
|
|
|
103
124
|
parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
if (params.logical) {
|
|
128
|
+
const root = params.logical;
|
|
129
|
+
const serialized = root.conditions.map(serializeLogicalCondition).join(",");
|
|
130
|
+
parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
106
133
|
if (params.where) {
|
|
107
134
|
for (const [field, value] of Object.entries(params.where)) {
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
|
|
136
|
+
for (const subVal of value) {
|
|
137
|
+
const normalized = normalizeWhereValue(subVal as WhereFieldValue);
|
|
138
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const normalized = normalizeWhereValue(value as WhereFieldValue);
|
|
142
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
143
|
+
}
|
|
110
144
|
}
|
|
111
145
|
}
|
|
112
146
|
|