@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/LICENSE +21 -0
- package/dist/api-keys.d.ts +1 -0
- package/dist/collection.d.ts +3 -3
- package/dist/index.d.ts +20 -1
- package/dist/index.es.js +135 -168
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +138 -170
- package/dist/index.umd.js.map +1 -1
- package/dist/storage-registry.d.ts +42 -0
- package/dist/storage.d.ts +9 -1
- package/package.json +13 -12
- package/src/api-keys.ts +1 -0
- package/src/collection.test.ts +2 -2
- package/src/collection.ts +41 -119
- package/src/index.ts +77 -2
- package/src/storage-registry.ts +102 -0
- package/src/storage.ts +30 -20
- package/src/transport.ts +8 -82
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(
|
|
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
|
-
|
|
142
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
68
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
|
|
143
69
|
}
|
|
144
70
|
}
|
|
145
71
|
}
|