@rebasepro/client 0.3.0 → 0.4.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.
@@ -1 +1 @@
1
- export { QueryBuilder } from "@rebasepro/common";
1
+ export { QueryBuilder, or, and, cond } from "@rebasepro/common";
@@ -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.3.0",
4
+ "version": "0.4.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/common": "0.3.0",
34
- "@rebasepro/types": "0.3.0",
35
- "@rebasepro/utils": "0.3.0"
33
+ "@rebasepro/common": "0.4.0",
34
+ "@rebasepro/types": "0.4.0",
35
+ "@rebasepro/utils": "0.4.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 { Transport, FindParams, buildQueryString } from "./transport";
1
+ import { buildQueryString, FindParams, RebaseApiError, Transport } from "./transport";
2
2
  import { RebaseWebSocketClient } from "./websocket";
3
- import { Entity, FilterValues, WhereFilterOp, CollectionAccessor, WhereFieldValue, FindResponse, FilterOperator } from "@rebasepro/types";
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, [WhereFilterOp, unknown]> = {};
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
- // Handle boolean equality
18
- if (typeof rawValue === "boolean") {
19
- filters[key] = ["==", rawValue];
20
- continue;
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
- // Handle number equality
24
- if (typeof rawValue === "number") {
25
- filters[key] = ["==", rawValue];
26
- continue;
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
- // Handle tuple: [operator, value]
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
- const OP_TO_FILTER: Record<string, WhereFilterOp> = {
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": op = "=="; break;
68
- case "neq": op = "!="; break;
69
- case "gt": op = ">"; break;
70
- case "gte": op = ">="; break;
71
- case "lt": op = "<"; break;
72
- case "lte": op = "<="; break;
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": op = "array-contains"; break;
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: op = "=="; val = value;
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) && key !== "id" && !key.endsWith("_id")) val = Number(val);
108
+ else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
99
109
 
100
- filters[key] = [op, val];
110
+ return [op, val];
101
111
  } else {
102
- filters[key] = ["==", value];
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
- // Fluent Query Builder
130
- where(column: keyof M & string, operator: FilterOperator, value: unknown): QueryBuilder<M>;
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<{ data: Record<string, unknown>[]; meta: FindResponse<M>["meta"] }>(basePath + qs, { method: "GET" });
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
- const raw = await transport.request<Record<string, unknown>>(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
154
- if (!raw) return undefined;
155
- return rowToEntity<M>(raw, slug);
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 = { ...params, limit: undefined, offset: undefined };
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(column: keyof M & string, operator: FilterOperator, value: unknown) {
193
- return new QueryBuilder<M>(client).where(column, operator, value);
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
- { path: slug,
243
- entityId: String(id) },
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 (!baseUrl) {
69
- // If no baseUrl is provided, we can try to derive it from the window object if in browser
70
- if (typeof window !== "undefined") {
71
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
72
- return `${protocol}//${window.location.host}`;
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:\/\//, "wss://")
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
- const session = await auth.getSession();
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) => {
@@ -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) && value.length === 2) {
74
- const [rawOp, val] = value;
75
- const op = OP_MAP[rawOp] ?? rawOp;
76
-
77
- if (val === null) return `${op}.null`;
78
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
79
- return `${op}.${val}`;
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
- const normalized = normalizeWhereValue(value as WhereFieldValue);
109
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
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