@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/src/storage.ts
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
|
-
import { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult, DownloadMetadata } from "@rebasepro/types";
|
|
1
|
+
import { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult, DownloadMetadata, PUBLIC_STORAGE_PREFIX, isPublicStoragePath } from "@rebasepro/types";
|
|
2
2
|
import { Transport } from "./transport";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 {
|
|
13
|
+
const urlsCache = new Map<string, { config: DownloadConfig; expiresAt?: number }>();
|
|
14
|
+
|
|
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,
|
|
14
24
|
key,
|
|
15
25
|
metadata,
|
|
16
|
-
bucket
|
|
26
|
+
bucket,
|
|
27
|
+
public: isPublic
|
|
17
28
|
}: UploadFileProps): Promise<UploadFileResult> {
|
|
18
29
|
const formData = new FormData();
|
|
19
30
|
formData.append("file", file);
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
// Public objects live under the public prefix so they can be served
|
|
33
|
+
// token-less via a stable, permanent URL. Normalize the key here so the
|
|
34
|
+
// stored path is self-describing (no server round-trip needed to know
|
|
35
|
+
// it's public).
|
|
36
|
+
let effectiveKey = key;
|
|
37
|
+
if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) {
|
|
38
|
+
effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (effectiveKey) formData.append("key", effectiveKey);
|
|
22
42
|
if (bucket) formData.append("bucket", bucket);
|
|
43
|
+
if (storageId) formData.append("storageId", storageId);
|
|
23
44
|
|
|
24
45
|
if (metadata) {
|
|
25
46
|
for (const [key, value] of Object.entries(metadata)) {
|
|
@@ -32,16 +53,10 @@ export function createStorage(transport: Transport): StorageSource {
|
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
55
|
|
|
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", {
|
|
56
|
+
const result = await transport.request<{ data: UploadFileResult }>(withStorageId("/storage/upload"), {
|
|
38
57
|
method: "POST",
|
|
39
58
|
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
|
-
}
|
|
59
|
+
headers: {}
|
|
45
60
|
});
|
|
46
61
|
|
|
47
62
|
return result.data;
|
|
@@ -52,12 +67,17 @@ export function createStorage(transport: Transport): StorageSource {
|
|
|
52
67
|
bucket?: string
|
|
53
68
|
): Promise<DownloadConfig> {
|
|
54
69
|
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
70
|
+
const cachedEntry = urlsCache.get(cacheKey);
|
|
71
|
+
if (cachedEntry) {
|
|
72
|
+
if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) {
|
|
73
|
+
return cachedEntry.config;
|
|
74
|
+
}
|
|
75
|
+
urlsCache.delete(cacheKey);
|
|
76
|
+
}
|
|
57
77
|
|
|
58
78
|
let filePath = keyOrUrl;
|
|
59
79
|
|
|
60
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
80
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
|
|
61
81
|
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
62
82
|
}
|
|
63
83
|
|
|
@@ -66,27 +86,58 @@ export function createStorage(transport: Transport): StorageSource {
|
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
69
|
-
return { url: null,
|
|
70
|
-
|
|
89
|
+
return { url: null, fileNotFound: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Public objects ────────────────────────────────────────────────
|
|
93
|
+
// A public file (under the public prefix) is served token-less via a
|
|
94
|
+
// stable, permanent, CDN-cacheable URL. No metadata round-trip and no
|
|
95
|
+
// token are needed — build the URL directly and cache it forever.
|
|
96
|
+
if (isPublicStoragePath(filePath)) {
|
|
97
|
+
const publicConfig: DownloadConfig = {
|
|
98
|
+
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`)
|
|
99
|
+
};
|
|
100
|
+
urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
|
|
101
|
+
return publicConfig;
|
|
71
102
|
}
|
|
72
103
|
|
|
73
104
|
try {
|
|
74
|
-
const result = await transport.request<{ data: DownloadMetadata }>(`/storage/metadata/${filePath}`);
|
|
105
|
+
const result = await transport.request<{ data: DownloadMetadata }>(withStorageId(`/storage/metadata/${filePath}`));
|
|
106
|
+
|
|
107
|
+
// Public object (server-confirmed): token-less permanent URL.
|
|
108
|
+
if (result.data.public) {
|
|
109
|
+
const publicConfig: DownloadConfig = {
|
|
110
|
+
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`),
|
|
111
|
+
metadata: result.data
|
|
112
|
+
};
|
|
113
|
+
urlsCache.set(cacheKey, { config: publicConfig }); // no expiry
|
|
114
|
+
return publicConfig;
|
|
115
|
+
}
|
|
75
116
|
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
// Private object: use the short-lived, file-scoped download token
|
|
118
|
+
// minted by the server. We deliberately do NOT fall back to the
|
|
119
|
+
// caller's access token — a URL must never carry a full-privilege
|
|
120
|
+
// credential. If no scoped token is present the URL fails closed.
|
|
121
|
+
const scopedToken = result.data.token;
|
|
122
|
+
const tokenQuery = scopedToken ? `?token=${scopedToken}` : "";
|
|
78
123
|
|
|
79
124
|
const downloadConfig: DownloadConfig = {
|
|
80
|
-
|
|
125
|
+
// `withStorageId` picks `?` or `&` based on whether the token
|
|
126
|
+
// query is already present, so the URL stays valid even when
|
|
127
|
+
// there is no token.
|
|
128
|
+
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
|
|
81
129
|
metadata: result.data
|
|
82
130
|
};
|
|
83
131
|
|
|
84
|
-
|
|
132
|
+
const expiresAt = result.data.tokenExpiresIn
|
|
133
|
+
? Date.now() + (result.data.tokenExpiresIn - 10) * 1000 // subtract 10s buffer
|
|
134
|
+
: undefined;
|
|
135
|
+
|
|
136
|
+
urlsCache.set(cacheKey, { config: downloadConfig, expiresAt });
|
|
85
137
|
return downloadConfig;
|
|
86
138
|
} catch (e: unknown) {
|
|
87
139
|
if (e instanceof Error && "status" in e && (e as { status: number }).status === 404) {
|
|
88
|
-
return { url: null,
|
|
89
|
-
fileNotFound: true };
|
|
140
|
+
return { url: null, fileNotFound: true };
|
|
90
141
|
}
|
|
91
142
|
throw e;
|
|
92
143
|
}
|
|
@@ -96,33 +147,22 @@ fileNotFound: true };
|
|
|
96
147
|
key: string,
|
|
97
148
|
bucket?: string
|
|
98
149
|
): Promise<File | null> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
102
|
-
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
106
|
-
filePath = `${bucket}/${filePath}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
150
|
+
const downloadConfig = await getSignedUrl(key, bucket);
|
|
151
|
+
if (downloadConfig.fileNotFound || !downloadConfig.url) {
|
|
110
152
|
return null;
|
|
111
153
|
}
|
|
112
154
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const response = await transport.fetchFn(url, {
|
|
118
|
-
headers: transport.getHeaders ? transport.getHeaders() : {}
|
|
155
|
+
// Fetch using the signed URL directly. Since the scoped token is in the ?token= query param,
|
|
156
|
+
// we explicitly omit any Authorization headers to prevent passing full access tokens to file serving routes.
|
|
157
|
+
const response = await transport.fetchFn(downloadConfig.url, {
|
|
158
|
+
headers: {}
|
|
119
159
|
});
|
|
120
160
|
|
|
121
161
|
if (response.status === 404) return null;
|
|
122
162
|
if (!response.ok) throw new Error("Failed to get file");
|
|
123
163
|
|
|
124
164
|
const blob = await response.blob();
|
|
125
|
-
const fileName =
|
|
165
|
+
const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
|
|
126
166
|
return new File([blob], fileName, { type: blob.type });
|
|
127
167
|
}
|
|
128
168
|
|
|
@@ -132,7 +172,7 @@ fileNotFound: true };
|
|
|
132
172
|
): Promise<void> {
|
|
133
173
|
let filePath = key;
|
|
134
174
|
|
|
135
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
175
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) {
|
|
136
176
|
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
137
177
|
}
|
|
138
178
|
|
|
@@ -145,7 +185,7 @@ fileNotFound: true };
|
|
|
145
185
|
}
|
|
146
186
|
|
|
147
187
|
try {
|
|
148
|
-
await transport.request(`/storage/file/${filePath}
|
|
188
|
+
await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
|
|
149
189
|
} catch (e: unknown) {
|
|
150
190
|
if (!(e instanceof Error && "status" in e && (e as { status: number }).status === 404)) throw e;
|
|
151
191
|
}
|
|
@@ -167,6 +207,8 @@ fileNotFound: true };
|
|
|
167
207
|
if (options?.maxResults) params.set("maxResults", String(options.maxResults));
|
|
168
208
|
if (options?.pageToken) params.set("pageToken", options.pageToken);
|
|
169
209
|
|
|
210
|
+
if (storageId) params.set("storageId", storageId);
|
|
211
|
+
|
|
170
212
|
const result = await transport.request<{ data: StorageListResult }>(`/storage/list?${params.toString()}`);
|
|
171
213
|
return result.data;
|
|
172
214
|
}
|
package/src/transport.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { FindParams as TypesFindParams, FindResponse as TypesFindResponse,
|
|
1
|
+
import { FindParams as TypesFindParams, FindResponse as TypesFindResponse, RebaseApiError } from "@rebasepro/types";
|
|
2
|
+
import { serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
2
3
|
import { rebaseReviver } from "./reviver";
|
|
3
4
|
|
|
5
|
+
// The canonical client error now lives in `@rebasepro/types` so every package
|
|
6
|
+
// (client, auth, …) throws one type. Re-exported here to preserve the historical
|
|
7
|
+
// `import { RebaseApiError } from ".../transport"` path used across the SDK.
|
|
8
|
+
export { RebaseApiError } from "@rebasepro/types";
|
|
9
|
+
export type { RebaseErrorInit } from "@rebasepro/types";
|
|
10
|
+
|
|
4
11
|
export interface RebaseClientConfig {
|
|
5
12
|
baseUrl?: string;
|
|
6
13
|
token?: string;
|
|
@@ -16,94 +23,6 @@ export interface RebaseClientConfig {
|
|
|
16
23
|
export type FindParams = TypesFindParams;
|
|
17
24
|
export type FindResponse<T> = TypesFindResponse<T extends Record<string, unknown> ? T : Record<string, unknown>>;
|
|
18
25
|
|
|
19
|
-
export class RebaseApiError extends Error {
|
|
20
|
-
status: number;
|
|
21
|
-
code?: string;
|
|
22
|
-
details?: unknown;
|
|
23
|
-
|
|
24
|
-
constructor(status: number, message: string, code?: string, details?: unknown) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.name = "RebaseApiError";
|
|
27
|
-
this.status = status;
|
|
28
|
-
this.code = code;
|
|
29
|
-
this.details = details;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
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
26
|
export function buildQueryString(params?: FindParams): string {
|
|
108
27
|
if (!params) return "";
|
|
109
28
|
const parts: string[] = [];
|
|
@@ -113,7 +32,8 @@ export function buildQueryString(params?: FindParams): string {
|
|
|
113
32
|
if (params.page != null) parts.push(`page=${params.page}`);
|
|
114
33
|
|
|
115
34
|
if (params.orderBy) {
|
|
116
|
-
|
|
35
|
+
const wire = serializeOrderBy(params.orderBy);
|
|
36
|
+
if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
|
|
117
37
|
}
|
|
118
38
|
|
|
119
39
|
if (params.searchString) {
|
|
@@ -131,15 +51,14 @@ export function buildQueryString(params?: FindParams): string {
|
|
|
131
51
|
}
|
|
132
52
|
|
|
133
53
|
if (params.where) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(
|
|
54
|
+
const serialized = serializeFilter(params.where);
|
|
55
|
+
for (const [field, value] of Object.entries(serialized)) {
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
for (const v of value) {
|
|
58
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
|
|
139
59
|
}
|
|
140
60
|
} else {
|
|
141
|
-
|
|
142
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
61
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
|
|
143
62
|
}
|
|
144
63
|
}
|
|
145
64
|
}
|
|
@@ -212,12 +131,15 @@ headers });
|
|
|
212
131
|
}
|
|
213
132
|
}
|
|
214
133
|
|
|
134
|
+
// The server always emits the canonical `{ error: { message, code, details? } }`
|
|
135
|
+
// envelope (formatted by the central errorHandler), so we read strictly
|
|
136
|
+
// from `body.error.*`.
|
|
215
137
|
const getErrorField = (obj: Record<string, unknown>, field: string): unknown => {
|
|
216
138
|
const err = obj?.error;
|
|
217
|
-
if (err && typeof err === "object" && err !== null
|
|
139
|
+
if (err && typeof err === "object" && err !== null) {
|
|
218
140
|
return (err as Record<string, unknown>)[field];
|
|
219
141
|
}
|
|
220
|
-
return
|
|
142
|
+
return undefined;
|
|
221
143
|
};
|
|
222
144
|
|
|
223
145
|
if (res.status === 401 && onUnauthorizedHandler) {
|
|
@@ -250,10 +172,12 @@ headers: retryHeaders });
|
|
|
250
172
|
fallbackMessage = `Endpoint not found (${method} ${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.`;
|
|
251
173
|
}
|
|
252
174
|
throw new RebaseApiError(
|
|
253
|
-
retryRes.status,
|
|
254
175
|
String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`),
|
|
255
|
-
|
|
256
|
-
|
|
176
|
+
{
|
|
177
|
+
status: retryRes.status,
|
|
178
|
+
code: getErrorField(retryBody, "code") as string | undefined,
|
|
179
|
+
details: getErrorField(retryBody, "details")
|
|
180
|
+
}
|
|
257
181
|
);
|
|
258
182
|
}
|
|
259
183
|
return retryBody as T;
|
|
@@ -267,10 +191,12 @@ headers: retryHeaders });
|
|
|
267
191
|
fallbackMessage = `Endpoint not found (${method} ${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.`;
|
|
268
192
|
}
|
|
269
193
|
throw new RebaseApiError(
|
|
270
|
-
res.status,
|
|
271
194
|
String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`),
|
|
272
|
-
|
|
273
|
-
|
|
195
|
+
{
|
|
196
|
+
status: res.status,
|
|
197
|
+
code: getErrorField(body, "code") as string | undefined,
|
|
198
|
+
details: getErrorField(body, "details")
|
|
199
|
+
}
|
|
274
200
|
);
|
|
275
201
|
}
|
|
276
202
|
|