@rebasepro/client 0.0.1-canary.4d4fb3e

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.
@@ -0,0 +1,198 @@
1
+ import { Transport, FindParams, FindResponse, buildQueryString } from "./transport";
2
+ import { RebaseWebSocketClient } from "./websocket";
3
+ import { Entity, FilterValues, WhereFilterOp, CollectionAccessor } from "@rebasepro/types";
4
+
5
+ import { FilterOperator, QueryBuilder } from "./query_builder";
6
+
7
+ function parseWhereFilter(where?: Record<string, string>): FilterValues<any> | undefined {
8
+ if (!where) return undefined;
9
+ const filters: Record<string, [WhereFilterOp, unknown]> = {};
10
+ for (const [key, value] of Object.entries(where)) {
11
+ const dotIndex = value.indexOf(".");
12
+ if (dotIndex > 0) {
13
+ const opStr = value.substring(0, dotIndex);
14
+ const valStr = value.substring(dotIndex + 1);
15
+ let op: WhereFilterOp = "==";
16
+ let val: any = valStr;
17
+
18
+ switch (opStr) {
19
+ case "eq": op = "=="; break;
20
+ case "neq": op = "!="; break;
21
+ case "gt": op = ">"; break;
22
+ case "gte": op = ">="; break;
23
+ case "lt": op = "<"; break;
24
+ case "lte": op = "<="; break;
25
+ case "in":
26
+ op = "in";
27
+ val = valStr.startsWith("(") && valStr.endsWith(")")
28
+ ? valStr.slice(1, -1).split(",").map(v => v.trim())
29
+ : valStr.split(",");
30
+ break;
31
+ case "nin":
32
+ op = "not-in";
33
+ val = valStr.startsWith("(") && valStr.endsWith(")")
34
+ ? valStr.slice(1, -1).split(",").map(v => v.trim())
35
+ : valStr.split(",");
36
+ break;
37
+ case "cs": op = "array-contains"; break;
38
+ case "csa":
39
+ op = "array-contains-any";
40
+ val = valStr.startsWith("(") && valStr.endsWith(")")
41
+ ? valStr.slice(1, -1).split(",").map(v => v.trim())
42
+ : valStr.split(",");
43
+ break;
44
+ default: op = "=="; val = value;
45
+ }
46
+ // Simple type inference for parsing from URL-like strings
47
+ if (val === "true") val = true;
48
+ else if (val === "false") val = false;
49
+ else if (val === "null") val = null;
50
+ else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val)) val = Number(val);
51
+
52
+ filters[key] = [op, val];
53
+ } else {
54
+ filters[key] = ["==", value];
55
+ }
56
+ }
57
+ return filters;
58
+ }
59
+
60
+ /**
61
+ * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
62
+ * a proper `Entity<M>` structure expected by the core framework.
63
+ * The `id` is kept inside `values` as well, since collection properties
64
+ * may define an `isId` field that the form binds to `formex.values`.
65
+ */
66
+ function rowToEntity<M extends Record<string, any>>(row: Record<string, any>, slug: string): Entity<M> {
67
+ return {
68
+ id: row.id,
69
+ path: slug,
70
+ values: row as M
71
+ };
72
+ }
73
+
74
+ /**
75
+ * CollectionClient extends `CollectionAccessor` from `@rebasepro/types` so that
76
+ * `client.data` can be passed directly to the core Rebase component.
77
+ *
78
+ * Additionally it exposes fluent query builder methods like `.where()`, `.orderBy()`.
79
+ */
80
+ export interface CollectionClient<M extends Record<string, any> = any> extends CollectionAccessor<M> {
81
+ // Fluent Query Builder
82
+ where(column: keyof M & string, operator: FilterOperator, value: any): QueryBuilder<M>;
83
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilder<M>;
84
+ limit(count: number): QueryBuilder<M>;
85
+ offset(count: number): QueryBuilder<M>;
86
+ search(searchString: string): QueryBuilder<M>;
87
+ include(...relations: string[]): QueryBuilder<M>;
88
+ }
89
+
90
+ export function createCollectionClient<M extends Record<string, any> = any>(transport: Transport, slug: string, ws?: RebaseWebSocketClient): CollectionClient<M> {
91
+ const basePath = `/data/${slug}`;
92
+
93
+ return {
94
+ async find(params?: FindParams) {
95
+ const qs = buildQueryString(params);
96
+ const raw = await transport.request<{ data: Record<string, any>[]; meta: any }>(basePath + qs, { method: "GET" });
97
+ return {
98
+ data: (raw.data || []).map((row: Record<string, any>) => rowToEntity<M>(row, slug)),
99
+ meta: raw.meta
100
+ };
101
+ },
102
+
103
+ async findById(id: string | number) {
104
+ const raw = await transport.request<Record<string, any>>(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
105
+ if (!raw) return undefined;
106
+ return rowToEntity<M>(raw, slug);
107
+ },
108
+
109
+ async create(data: Partial<M>, id?: string | number) {
110
+ const body: Record<string, unknown> = { ...data };
111
+ if (id !== undefined) {
112
+ body.id = id;
113
+ }
114
+ const raw = await transport.request<Record<string, any>>(basePath, {
115
+ method: "POST",
116
+ body: JSON.stringify(body),
117
+ });
118
+ return rowToEntity<M>(raw, slug);
119
+ },
120
+
121
+ async update(id: string | number, data: Partial<M>) {
122
+ const raw = await transport.request<Record<string, any>>(`${basePath}/${encodeURIComponent(String(id))}`, {
123
+ method: "PUT",
124
+ body: JSON.stringify(data),
125
+ });
126
+ return rowToEntity<M>(raw, slug);
127
+ },
128
+
129
+ async delete(id: string | number) {
130
+ return transport.request<void>(`${basePath}/${encodeURIComponent(String(id))}`, {
131
+ method: "DELETE",
132
+ });
133
+ },
134
+
135
+ ...(ws && {
136
+ listen(params: FindParams | undefined, onUpdate: (data: { data: Entity<M>[]; meta: any }) => void, onError?: (error: Error) => void) {
137
+ return ws.listenCollection(
138
+ {
139
+ path: slug,
140
+ filter: parseWhereFilter(params?.where),
141
+ limit: params?.limit,
142
+ startAfter: params?.offset ? String(params.offset) : undefined,
143
+ orderBy: params?.orderBy?.split(":")[0],
144
+ order: params?.orderBy?.split(":")[1] as "asc" | "desc",
145
+ searchString: params?.searchString
146
+ },
147
+ (entities: Entity[]) => {
148
+ const requestedLimit = params?.limit || 20;
149
+ onUpdate({
150
+ data: entities as Entity<M>[],
151
+ meta: {
152
+ total: entities.length,
153
+ limit: requestedLimit,
154
+ offset: params?.offset || 0,
155
+ hasMore: entities.length >= requestedLimit
156
+ }
157
+ });
158
+ },
159
+ onError
160
+ );
161
+ },
162
+
163
+ listenById(id: string | number, onUpdate: (data: Entity<M> | undefined) => void, onError?: (error: Error) => void) {
164
+ return ws.listenEntity(
165
+ { path: slug, entityId: String(id) },
166
+ (entity: Entity | null) => {
167
+ if (entity) {
168
+ onUpdate(entity as Entity<M>);
169
+ } else {
170
+ onUpdate(undefined);
171
+ }
172
+ },
173
+ onError
174
+ );
175
+ }
176
+ }),
177
+
178
+ // Fluent builder instantiation
179
+ where(column: keyof M & string, operator: FilterOperator, value: any) {
180
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).where(column, operator, value);
181
+ },
182
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc") {
183
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).orderBy(column, ascending);
184
+ },
185
+ limit(count: number) {
186
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).limit(count);
187
+ },
188
+ offset(count: number) {
189
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).offset(count);
190
+ },
191
+ search(searchString: string) {
192
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).search(searchString);
193
+ },
194
+ include(...relations: string[]) {
195
+ return new QueryBuilder<M>(this as unknown as CollectionClient<M>).include(...relations);
196
+ }
197
+ };
198
+ }
package/src/index.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { createTransport, RebaseClientConfig } from "./transport";
2
+ import { createAuth, CreateAuthOptions } from "./auth";
3
+ import { createAdmin, CreateAdminOptions } from "./admin";
4
+ import { createCollectionClient, CollectionClient } from "./collection";
5
+
6
+ export * from "./transport";
7
+ export * from "./auth";
8
+ export * from "./admin";
9
+ export * from "./collection";
10
+ export * from "./websocket";
11
+ export * from "./storage";
12
+
13
+ export interface CreateRebaseClientOptions extends RebaseClientConfig {
14
+ auth?: CreateAuthOptions;
15
+ admin?: CreateAdminOptions;
16
+ }
17
+
18
+ import { RebaseWebSocketClient } from "./websocket";
19
+ import { RebaseClient as BaseRebaseClient, RebaseData, CollectionAccessor, StorageSource } from "@rebasepro/types";
20
+
21
+ export type RebaseClient<DB = any> = BaseRebaseClient<DB> & {
22
+ setToken: (token: string | null) => void;
23
+ setAuthTokenGetter: (getter: () => Promise<string | null>) => void;
24
+ setOnUnauthorized: (handler: () => Promise<boolean>) => void;
25
+ resolveToken: () => Promise<string | null>;
26
+ auth: ReturnType<typeof createAuth>;
27
+ admin: ReturnType<typeof createAdmin>;
28
+ ws?: RebaseWebSocketClient;
29
+ storage?: StorageSource;
30
+ call: <T = any>(endpoint: string, payload?: any) => Promise<T>;
31
+ data: RebaseData & {
32
+ collection<K extends keyof DB>(slug: Extract<K, string>): CollectionClient<
33
+ DB[K] extends { Row: infer R extends Record<string, any> } ? R : any
34
+ >;
35
+ } & {
36
+ [K in keyof DB]: CollectionClient<
37
+ DB[K] extends { Row: infer R extends Record<string, any> } ? R : any
38
+ >;
39
+ };
40
+ };
41
+
42
+ import { createStorage } from "./storage";
43
+
44
+ /**
45
+ * Derive a WebSocket URL from an HTTP base URL.
46
+ * `http://` → `ws://`, `https://` → `wss://`.
47
+ */
48
+ function deriveWebSocketUrl(baseUrl: string): string {
49
+ return baseUrl
50
+ .replace(/^https:\/\//, "wss://")
51
+ .replace(/^http:\/\//, "ws://")
52
+ .replace(/\/$/, "");
53
+ }
54
+
55
+ export function createRebaseClient<DB = any>(options: CreateRebaseClientOptions): RebaseClient<DB> {
56
+ const transport = createTransport(options);
57
+ const auth = createAuth(transport, options.auth);
58
+ const admin = createAdmin(transport, options.admin);
59
+ const storage = createStorage(transport);
60
+
61
+ const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
62
+
63
+ let ws: RebaseWebSocketClient | undefined;
64
+ if (resolvedWsUrl) {
65
+ ws = new RebaseWebSocketClient({
66
+ websocketUrl: resolvedWsUrl,
67
+ getAuthToken: async () => {
68
+ const session = await auth.getSession();
69
+ return session?.accessToken || options.token || "";
70
+ }
71
+ });
72
+
73
+ auth.onAuthStateChange((event, session) => {
74
+ if (!ws) return;
75
+ if (event === "SIGNED_OUT") {
76
+ ws.disconnect();
77
+ } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
78
+ if (session?.accessToken) {
79
+ ws.authenticate(session.accessToken).catch(console.warn);
80
+ }
81
+ }
82
+ });
83
+ }
84
+
85
+ // Register transport callback for 401s after auth is instantiated
86
+ if (!options.onUnauthorized) {
87
+ options.onUnauthorized = async () => {
88
+ try {
89
+ await auth.refreshSession();
90
+ return true;
91
+ } catch (e) {
92
+ return false;
93
+ }
94
+ };
95
+ }
96
+
97
+ const collectionClients = new Map<string, CollectionClient<any>>();
98
+
99
+ function collection(slug: string): CollectionClient<any> {
100
+ if (!collectionClients.has(slug)) {
101
+ collectionClients.set(slug, createCollectionClient(transport, slug, ws));
102
+ }
103
+ return collectionClients.get(slug)!;
104
+ }
105
+
106
+ const dataTarget = { collection } as Record<string, any>;
107
+
108
+ const dataProxy = new Proxy(dataTarget, {
109
+ get(_target, prop: string | symbol) {
110
+ if (prop === "collection") {
111
+ return collection;
112
+ }
113
+ if (typeof prop === "symbol") return undefined;
114
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
115
+ return collection(prop);
116
+ }
117
+ return undefined;
118
+ }
119
+ });
120
+
121
+ const target = {
122
+ auth,
123
+ admin,
124
+ storage,
125
+ ws,
126
+ setToken: transport.setToken,
127
+ setAuthTokenGetter: transport.setAuthTokenGetter,
128
+ setOnUnauthorized: transport.setOnUnauthorized,
129
+ resolveToken: transport.resolveToken,
130
+ baseUrl: transport.baseUrl,
131
+ collection,
132
+ call: async <T = any>(endpoint: string, payload?: any): Promise<T> => {
133
+ const prefix = endpoint.startsWith("/") ? "" : "/";
134
+ const res = await transport.request<{ data: T }>(`${prefix}${endpoint}`, {
135
+ method: "POST",
136
+ body: payload ? JSON.stringify(payload) : undefined
137
+ });
138
+ return res.data ?? (res as unknown as T);
139
+ },
140
+ data: dataProxy
141
+ } as unknown as RebaseClient<DB>;
142
+
143
+ return new Proxy(target, {
144
+ get(obj, prop: string | symbol) {
145
+ if (prop in obj) {
146
+ return (obj as Record<string, unknown>)[prop as string];
147
+ }
148
+ if (typeof prop === "string" && prop !== "then") {
149
+ return collection(prop as Extract<keyof DB, string>);
150
+ }
151
+ return undefined;
152
+ }
153
+ }) as RebaseClient<DB>;
154
+ }
@@ -0,0 +1,126 @@
1
+ import { FindParams, Entity } from "@rebasepro/types";
2
+ import { FindResponse } from "./transport";
3
+ import { CollectionClient } from "./collection";
4
+
5
+ export type FilterOperator =
6
+ | "eq" | "neq" | "gt" | "gte" | "lt" | "lte"
7
+ | "in" | "nin" | "cs" | "csa"
8
+ | "==" | "!=" | ">" | ">=" | "<" | "<="
9
+ | "array-contains" | "array-contains-any"
10
+ | "not-in";
11
+
12
+ /**
13
+ * Maps standard operators to Rebase backend's string-based operators
14
+ */
15
+ function mapOperator(op: FilterOperator): string {
16
+ switch (op) {
17
+ case "==": return "eq";
18
+ case "!=": return "neq";
19
+ case ">": return "gt";
20
+ case ">=": return "gte";
21
+ case "<": return "lt";
22
+ case "<=": return "lte";
23
+ case "array-contains": return "cs";
24
+ case "array-contains-any": return "csa";
25
+ case "not-in": return "nin";
26
+ default: return op;
27
+ }
28
+ }
29
+
30
+ export class QueryBuilder<M extends Record<string, any> = any> {
31
+ private params: FindParams = { where: {} };
32
+
33
+ constructor(private collection: CollectionClient<M>) {}
34
+
35
+ /**
36
+ * Add a filter condition to your query.
37
+ * @example
38
+ * client.collection('users').where('age', '>=', 18).find()
39
+ */
40
+ where(column: keyof M & string, operator: FilterOperator, value: any): this {
41
+ if (!this.params.where) {
42
+ this.params.where = {};
43
+ }
44
+
45
+ const mappedOp = mapOperator(operator);
46
+ let formattedValue = value;
47
+
48
+ // Handle arrays for in, nin, cs, csa
49
+ if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
50
+ formattedValue = `(${value.join(",")})`;
51
+ } else if (value === null) {
52
+ formattedValue = "null";
53
+ }
54
+
55
+ this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Order the results by a specific column.
61
+ * @example
62
+ * client.collection('users').orderBy('createdAt', 'desc').find()
63
+ */
64
+ orderBy(column: keyof M & string, ascending: "asc" | "desc" = "asc"): this {
65
+ this.params.orderBy = `${column}:${ascending}`;
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Limit the number of results returned.
71
+ */
72
+ limit(count: number): this {
73
+ this.params.limit = count;
74
+ return this;
75
+ }
76
+
77
+ /**
78
+ * Skip the first N results.
79
+ */
80
+ offset(count: number): this {
81
+ this.params.offset = count;
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Set a free-text search string if supported by the backend.
87
+ */
88
+ search(searchString: string): this {
89
+ this.params.searchString = searchString;
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Include related entities in the response.
95
+ * Relations will be populated with full entity data instead of just IDs.
96
+ *
97
+ * @param relations - Relation names to include, or "*" for all.
98
+ * @example
99
+ * // Include specific relations
100
+ * client.data.posts.include("tags", "author").find()
101
+ *
102
+ * // Include all relations
103
+ * client.data.posts.include("*").find()
104
+ */
105
+ include(...relations: string[]): this {
106
+ this.params.include = relations;
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Execute the find query and return the results.
112
+ */
113
+ async find(): Promise<FindResponse<M>> {
114
+ return this.collection.find(this.params) as Promise<FindResponse<M>>;
115
+ }
116
+
117
+ /**
118
+ * Listen to realtime updates matching this query.
119
+ */
120
+ listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void {
121
+ if (!this.collection.listen) {
122
+ throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
123
+ }
124
+ return this.collection.listen(this.params, onUpdate as unknown as (data: { data: Entity<M>[]; meta: any }) => void, onError);
125
+ }
126
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,181 @@
1
+ import { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult } from "@rebasepro/types";
2
+ import { Transport } from "./transport";
3
+
4
+ export function createStorage(transport: Transport): StorageSource {
5
+ const urlsCache = new Map<string, DownloadConfig>();
6
+
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`, ...).
11
+
12
+ async function uploadFile({
13
+ file,
14
+ fileName,
15
+ path,
16
+ metadata,
17
+ bucket
18
+ }: UploadFileProps): Promise<UploadFileResult> {
19
+ const formData = new FormData();
20
+ formData.append("file", file);
21
+
22
+ if (fileName) formData.append("fileName", fileName);
23
+ if (path) formData.append("path", path);
24
+ if (bucket) formData.append("bucket", bucket);
25
+
26
+ if (metadata) {
27
+ for (const [key, value] of Object.entries(metadata)) {
28
+ if (value !== undefined && value !== null) {
29
+ formData.append(
30
+ `metadata_${key}`,
31
+ typeof value === "string" ? value : JSON.stringify(value)
32
+ );
33
+ }
34
+ }
35
+ }
36
+
37
+ // We use fetchFn directly if we need to do multipart boundary, but Transport.request might override Content-Type?
38
+ // 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!
39
+ const result = await transport.request<{ data: UploadFileResult }>("/storage/upload", {
40
+ method: "POST",
41
+ body: formData,
42
+ headers: {
43
+ // transport.request merges headers, so to prevent it setting application/json we can delete it
44
+ // in transport if body is FormData, or we can explicitly set it to an empty string.
45
+ // Let's rely on standard behaviour for now and adjust transport if it fails.
46
+ }
47
+ });
48
+
49
+ return result.data;
50
+ }
51
+
52
+ async function getDownloadURL(
53
+ pathOrUrl: string,
54
+ bucket?: string
55
+ ): Promise<DownloadConfig> {
56
+ const cacheKey = bucket ? `${bucket}/${pathOrUrl}` : pathOrUrl;
57
+ const cached = urlsCache.get(cacheKey);
58
+ if (cached) return cached;
59
+
60
+ let filePath = pathOrUrl;
61
+
62
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
63
+ filePath = filePath.substring(filePath.indexOf("://") + 3);
64
+ }
65
+
66
+ if (bucket && filePath && !filePath.startsWith(bucket)) {
67
+ filePath = `${bucket}/${filePath}`;
68
+ }
69
+
70
+ if (!filePath || filePath.trim() === '' || filePath === '/') {
71
+ return { url: null, fileNotFound: true };
72
+ }
73
+
74
+ try {
75
+ const result = await transport.request<{ data: any }>(`/storage/metadata/${filePath}`);
76
+
77
+ const activeToken = await transport.resolveToken();
78
+ const tokenQuery = activeToken ? `?token=${activeToken}` : '';
79
+
80
+ const downloadConfig: DownloadConfig = {
81
+ url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
82
+ metadata: result.data
83
+ };
84
+
85
+ urlsCache.set(cacheKey, downloadConfig);
86
+ return downloadConfig;
87
+ } catch (e: unknown) {
88
+ if (e instanceof Error && 'status' in e && (e as { status: number }).status === 404) {
89
+ return { url: null, fileNotFound: true };
90
+ }
91
+ throw e;
92
+ }
93
+ }
94
+
95
+ async function getFile(
96
+ path: string,
97
+ bucket?: string
98
+ ): Promise<File | null> {
99
+ let filePath = path;
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 === '/') {
110
+ return null;
111
+ }
112
+
113
+ // 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}`;
115
+
116
+ // This is a bit manual, but necessary for blob handling
117
+ const response = await transport.fetchFn(url, {
118
+ headers: transport.getHeaders ? transport.getHeaders() : {}
119
+ });
120
+
121
+ if (response.status === 404) return null;
122
+ if (!response.ok) throw new Error("Failed to get file");
123
+
124
+ const blob = await response.blob();
125
+ const fileName = filePath.split("/").pop() || "file";
126
+ return new File([blob], fileName, { type: blob.type });
127
+ }
128
+
129
+ async function deleteFile(
130
+ path: string,
131
+ bucket?: string
132
+ ): Promise<void> {
133
+ let filePath = path;
134
+
135
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
136
+ filePath = filePath.substring(filePath.indexOf("://") + 3);
137
+ }
138
+
139
+ if (bucket && filePath && !filePath.startsWith(bucket)) {
140
+ filePath = `${bucket}/${filePath}`;
141
+ }
142
+
143
+ if (!filePath || filePath.trim() === '' || filePath === '/') {
144
+ return;
145
+ }
146
+
147
+ try {
148
+ await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
149
+ } catch (e: unknown) {
150
+ if (!(e instanceof Error && 'status' in e && (e as { status: number }).status === 404)) throw e;
151
+ }
152
+
153
+ urlsCache.delete(bucket ? `${bucket}/${path}` : path);
154
+ }
155
+
156
+ async function list(
157
+ path: string,
158
+ options?: {
159
+ bucket?: string;
160
+ maxResults?: number;
161
+ pageToken?: string;
162
+ }
163
+ ): Promise<StorageListResult> {
164
+ const params = new URLSearchParams();
165
+ if (path) params.set("path", path);
166
+ if (options?.bucket) params.set("bucket", options.bucket);
167
+ if (options?.maxResults) params.set("maxResults", String(options.maxResults));
168
+ if (options?.pageToken) params.set("pageToken", options.pageToken);
169
+
170
+ const result = await transport.request<{ data: StorageListResult }>(`/storage/list?${params.toString()}`);
171
+ return result.data;
172
+ }
173
+
174
+ return {
175
+ uploadFile,
176
+ getDownloadURL,
177
+ getFile,
178
+ deleteFile,
179
+ list
180
+ };
181
+ }