@rebasepro/client 0.7.0 → 0.8.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/src/storage.ts CHANGED
@@ -1,13 +1,23 @@
1
1
  import { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult, DownloadMetadata } from "@rebasepro/types";
2
2
  import { Transport } from "./transport";
3
3
 
4
- export function createStorage(transport: Transport): StorageSource {
4
+ /**
5
+ * Create a StorageSource that talks to the Rebase backend REST API.
6
+ *
7
+ * @param transport - HTTP transport instance
8
+ * @param storageId - Optional storage-source key for multi-backend routing.
9
+ * When set, it is forwarded to the server so the correct
10
+ * `StorageController` is resolved from the registry.
11
+ */
12
+ export function createStorage(transport: Transport, storageId?: string): StorageSource {
5
13
  const urlsCache = new Map<string, DownloadConfig>();
6
14
 
7
- // We expect the transport to point to /api, and storage endpoints handle /api/storage internally if they are relative?
8
- // Wait, useBackendStorageSource uses `${apiUrl}/api/storage` directly.
9
- // Transport has `.request` which hits `${config.baseUrl}${config.apiPath}${path}`.
10
- // Assuming `config.apiPath` is "/api", we just request(`/storage/upload`, ...).
15
+ /** Append ?storageId=... to a path when multi-backend routing is active. */
16
+ const withStorageId = (path: string): string => {
17
+ if (!storageId) return path;
18
+ const sep = path.includes("?") ? "&" : "?";
19
+ return `${path}${sep}storageId=${encodeURIComponent(storageId)}`;
20
+ };
11
21
 
12
22
  async function putObject({
13
23
  file,
@@ -20,6 +30,7 @@ export function createStorage(transport: Transport): StorageSource {
20
30
 
21
31
  if (key) formData.append("key", key);
22
32
  if (bucket) formData.append("bucket", bucket);
33
+ if (storageId) formData.append("storageId", storageId);
23
34
 
24
35
  if (metadata) {
25
36
  for (const [key, value] of Object.entries(metadata)) {
@@ -32,16 +43,10 @@ export function createStorage(transport: Transport): StorageSource {
32
43
  }
33
44
  }
34
45
 
35
- // We use fetchFn directly if we need to do multipart boundary, but Transport.request might override Content-Type?
36
- // Wait, transport.request defaults to application/json. We must remove Content-Type header or allow it to be evaluated by fetch when body is FormData!
37
- const result = await transport.request<{ data: UploadFileResult }>("/storage/upload", {
46
+ const result = await transport.request<{ data: UploadFileResult }>(withStorageId("/storage/upload"), {
38
47
  method: "POST",
39
48
  body: formData,
40
- headers: {
41
- // transport.request merges headers, so to prevent it setting application/json we can delete it
42
- // in transport if body is FormData, or we can explicitly set it to an empty string.
43
- // Let's rely on standard behaviour for now and adjust transport if it fails.
44
- }
49
+ headers: {}
45
50
  });
46
51
 
47
52
  return result.data;
@@ -57,7 +62,7 @@ export function createStorage(transport: Transport): StorageSource {
57
62
 
58
63
  let filePath = keyOrUrl;
59
64
 
60
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
65
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
61
66
  filePath = filePath.substring(filePath.indexOf("://") + 3);
62
67
  }
63
68
 
@@ -71,13 +76,16 @@ fileNotFound: true };
71
76
  }
72
77
 
73
78
  try {
74
- const result = await transport.request<{ data: DownloadMetadata }>(`/storage/metadata/${filePath}`);
79
+ const result = await transport.request<{ data: DownloadMetadata }>(withStorageId(`/storage/metadata/${filePath}`));
75
80
 
76
81
  const activeToken = await transport.resolveToken();
77
82
  const tokenQuery = activeToken ? `?token=${activeToken}` : "";
78
83
 
79
84
  const downloadConfig: DownloadConfig = {
80
- url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
85
+ // `withStorageId` picks `?` or `&` based on whether the token
86
+ // query is already present, so the URL stays valid even when
87
+ // there is no auth token.
88
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
81
89
  metadata: result.data
82
90
  };
83
91
 
@@ -98,7 +106,7 @@ fileNotFound: true };
98
106
  ): Promise<File | null> {
99
107
  let filePath = key;
100
108
 
101
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
109
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
102
110
  filePath = filePath.substring(filePath.indexOf("://") + 3);
103
111
  }
104
112
 
@@ -111,7 +119,7 @@ fileNotFound: true };
111
119
  }
112
120
 
113
121
  // We must use plain fetch because transport.request expects JSON response, but here we want a Blob.
114
- const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
122
+ const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
115
123
 
116
124
  // This is a bit manual, but necessary for blob handling
117
125
  const response = await transport.fetchFn(url, {
@@ -132,7 +140,7 @@ fileNotFound: true };
132
140
  ): Promise<void> {
133
141
  let filePath = key;
134
142
 
135
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
143
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
136
144
  filePath = filePath.substring(filePath.indexOf("://") + 3);
137
145
  }
138
146
 
@@ -145,7 +153,7 @@ fileNotFound: true };
145
153
  }
146
154
 
147
155
  try {
148
- await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
156
+ await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
149
157
  } catch (e: unknown) {
150
158
  if (!(e instanceof Error && "status" in e && (e as { status: number }).status === 404)) throw e;
151
159
  }
@@ -167,6 +175,8 @@ fileNotFound: true };
167
175
  if (options?.maxResults) params.set("maxResults", String(options.maxResults));
168
176
  if (options?.pageToken) params.set("pageToken", options.pageToken);
169
177
 
178
+ if (storageId) params.set("storageId", storageId);
179
+
170
180
  const result = await transport.request<{ data: StorageListResult }>(`/storage/list?${params.toString()}`);
171
181
  return result.data;
172
182
  }
package/src/transport.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { FindParams as TypesFindParams, FindResponse as TypesFindResponse, WhereFieldValue, WhereFilterOpShort } from "@rebasepro/types";
1
+ import { FindParams as TypesFindParams, FindResponse as TypesFindResponse } from "@rebasepro/types";
2
+ import { serializeFilter, serializeLogicalCondition } from "@rebasepro/common";
2
3
  import { rebaseReviver } from "./reviver";
3
4
 
4
5
  export interface RebaseClientConfig {
@@ -30,80 +31,6 @@ export class RebaseApiError extends Error {
30
31
  }
31
32
  }
32
33
 
33
- /**
34
- * Maps a short operator alias to the PostgREST-style short code.
35
- */
36
- const OP_MAP: Record<string, string> = {
37
- "==": "eq",
38
- "!=": "neq",
39
- ">": "gt",
40
- ">=": "gte",
41
- "<": "lt",
42
- "<=": "lte",
43
- "not-in": "nin",
44
- "array-contains": "cs",
45
- "array-contains-any": "csa"
46
- };
47
-
48
- /**
49
- * Normalise a single `WhereFieldValue` into the PostgREST query-string
50
- * representation the backend expects.
51
- *
52
- * Supports:
53
- * - `null` → `"eq.null"`
54
- * - `true`/`false` → `"eq.true"` / `"eq.false"`
55
- * - `42` → `"42"` (plain equality)
56
- * - `"active"` → `"active"` (plain equality, backward-compat)
57
- * - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
58
- * - `[">=", 18]` → `"gte.18"` (tuple syntax)
59
- * - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
60
- * - `["!=", null]` → `"neq.null"`
61
- */
62
- function normalizeWhereValue(value: WhereFieldValue): string {
63
- // Null → eq.null
64
- if (value === null) return "eq.null";
65
-
66
- // Boolean → eq.true / eq.false
67
- if (typeof value === "boolean") return `eq.${value}`;
68
-
69
- // Number → plain equality
70
- if (typeof value === "number") return String(value);
71
-
72
- // Tuple: [operator, 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
- }
85
- }
86
-
87
- // String — pass through (either plain equality value or PostgREST syntax)
88
- return String(value);
89
- }
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
-
107
34
  export function buildQueryString(params?: FindParams): string {
108
35
  if (!params) return "";
109
36
  const parts: string[] = [];
@@ -131,15 +58,14 @@ export function buildQueryString(params?: FindParams): string {
131
58
  }
132
59
 
133
60
  if (params.where) {
134
- for (const [field, value] of Object.entries(params.where)) {
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)}`);
61
+ const serialized = serializeFilter(params.where);
62
+ for (const [field, value] of Object.entries(serialized)) {
63
+ if (Array.isArray(value)) {
64
+ for (const v of value) {
65
+ parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
139
66
  }
140
67
  } else {
141
- const normalized = normalizeWhereValue(value as WhereFieldValue);
142
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
68
+ parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
143
69
  }
144
70
  }
145
71
  }