refactor: api.ts
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import {
|
||||
apiRequest,
|
||||
ApiRequestError,
|
||||
createUuid,
|
||||
type ApiErrorEnvelope,
|
||||
} from "./_shared";
|
||||
|
||||
export type WorkspaceDirectory = {
|
||||
path: string;
|
||||
name: string;
|
||||
parent_path: string;
|
||||
owner_user_id: string;
|
||||
has_children?: boolean;
|
||||
};
|
||||
|
||||
export type ResourceItem = {
|
||||
resource_id: string;
|
||||
workspace_id: string;
|
||||
storage_object_id: string;
|
||||
owner_user_id: string;
|
||||
owner_display_name?: string | null;
|
||||
resource_name: string;
|
||||
description: string | null;
|
||||
visibility: "private" | "workspace" | "public";
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
file: {
|
||||
file_name: string;
|
||||
file_extension: string | null;
|
||||
mime_type: string | null;
|
||||
size_bytes: number;
|
||||
content_hash: string | null;
|
||||
object_status: string;
|
||||
};
|
||||
jupyter_accessible_path: string;
|
||||
absolute_path: string;
|
||||
};
|
||||
|
||||
export async function listResources(
|
||||
workspaceId: string,
|
||||
parentPath: string = "",
|
||||
opts?: { visibility?: string; keyword?: string; ownerUserId?: string },
|
||||
): Promise<ResourceItem[]> {
|
||||
// Default (no ownerUserId) scopes to the requester's own object_key
|
||||
// subtree; passing ownerUserId scopes to that owner's subtree so the tree
|
||||
// can lazily fetch another member's data resources on group expand.
|
||||
const parameters = new URLSearchParams();
|
||||
if (parentPath) parameters.set("parent_path", parentPath);
|
||||
if (opts?.visibility) parameters.set("visibility", opts.visibility);
|
||||
if (opts?.keyword) parameters.set("keyword", opts.keyword);
|
||||
if (opts?.ownerUserId) parameters.set("owner_user_id", opts.ownerUserId);
|
||||
const query = parameters.toString();
|
||||
// apiRequest<T> already unwraps the envelope's `data` field, so we
|
||||
// request `ResourceItem[]` directly here (matching listScripts).
|
||||
return apiRequest<ResourceItem[]>(
|
||||
`/api/v1/data-resources${query ? `?${query}` : ""}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteResource(
|
||||
workspaceId: string,
|
||||
resourceId: string,
|
||||
): Promise<{ resource_id: string; status: string }> {
|
||||
return apiRequest<{ resource_id: string; status: string }>(
|
||||
`/api/v1/data-resources/${encodeURIComponent(resourceId)}`,
|
||||
{ method: "DELETE" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/** 同源流式下载数据资源字节,供 Excel 等预览器使用。 */
|
||||
export async function fetchResourceContentFile(
|
||||
workspaceId: string,
|
||||
resourceId: string,
|
||||
fileName: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<File> {
|
||||
const response = await fetch(
|
||||
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/content?workspace_id=${encodeURIComponent(workspaceId)}`,
|
||||
{
|
||||
credentials: "same-origin",
|
||||
signal,
|
||||
headers: {
|
||||
"X-Request-ID": createUuid().replaceAll("-", ""),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 401 && typeof window !== "undefined") {
|
||||
const here = window.location.pathname;
|
||||
if (here !== "/login") {
|
||||
window.location.assign("/login");
|
||||
}
|
||||
throw new ApiRequestError("未登录或登录已过期", 401);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = `请求失败(HTTP ${response.status})`;
|
||||
try {
|
||||
const payload = (await response.json()) as ApiErrorEnvelope;
|
||||
const detailMessage =
|
||||
typeof payload.detail === "string"
|
||||
? payload.detail
|
||||
: payload.detail?.message;
|
||||
if (detailMessage) message = detailMessage;
|
||||
} catch {
|
||||
/* ignore non-JSON error bodies */
|
||||
}
|
||||
throw new ApiRequestError(message, response.status);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
return new File([blob], fileName, {
|
||||
type: blob.type || "application/octet-stream",
|
||||
});
|
||||
}
|
||||
|
||||
export type ResourcePreviewPayload = {
|
||||
kind: "table";
|
||||
columns: string[];
|
||||
rows: string[][];
|
||||
row_count: number;
|
||||
truncated: boolean;
|
||||
delimiter: string;
|
||||
};
|
||||
|
||||
/** 表格类数据资源抽样预览(csv / tsv)。 */
|
||||
export async function fetchResourcePreview(
|
||||
workspaceId: string,
|
||||
resourceId: string,
|
||||
input: { limit?: number } = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<ResourcePreviewPayload> {
|
||||
const parameters = new URLSearchParams();
|
||||
parameters.set("workspace_id", workspaceId);
|
||||
if (input.limit != null) parameters.set("limit", String(input.limit));
|
||||
const response = await fetch(
|
||||
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/preview?${parameters.toString()}`,
|
||||
{
|
||||
credentials: "same-origin",
|
||||
signal,
|
||||
headers: {
|
||||
"X-Request-ID": createUuid().replaceAll("-", ""),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 401 && typeof window !== "undefined") {
|
||||
const here = window.location.pathname;
|
||||
if (here !== "/login") {
|
||||
window.location.assign("/login");
|
||||
}
|
||||
throw new ApiRequestError("未登录或登录已过期", 401);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
const error = payload as ApiErrorEnvelope;
|
||||
const detailMessage =
|
||||
typeof error.detail === "string" ? error.detail : error.detail?.message;
|
||||
throw new ApiRequestError(
|
||||
detailMessage ?? `请求失败(HTTP ${response.status})`,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (payload as { data: ResourcePreviewPayload }).data;
|
||||
if (!data || data.kind !== "table" || !Array.isArray(data.columns)) {
|
||||
throw new ApiRequestError("响应数据格式错误", response.status);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createResourceUpload(
|
||||
workspaceId: string,
|
||||
body: {
|
||||
file_name: string;
|
||||
content_type: string;
|
||||
expected_size_bytes: number;
|
||||
expected_hash: string | null;
|
||||
target_path?: string;
|
||||
},
|
||||
): Promise<{ upload_id: string; upload_path: string }> {
|
||||
return apiRequest<{ upload_id: string; upload_path: string }>(
|
||||
"/api/v1/data-resources/uploads",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: { "Idempotency-Key": createUuid().replaceAll("-", "") },
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadResourceBytes(
|
||||
workspaceId: string,
|
||||
uploadId: string,
|
||||
fileBytes: ArrayBuffer | Blob,
|
||||
contentType: string,
|
||||
): Promise<{ storage_object_id: string }> {
|
||||
return apiRequest<{ storage_object_id: string }>(
|
||||
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": contentType },
|
||||
body: fileBytes,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function bindResourceUpload(
|
||||
workspaceId: string,
|
||||
uploadId: string,
|
||||
body: {
|
||||
resource_name: string;
|
||||
description: string;
|
||||
visibility: "private" | "workspace" | "public";
|
||||
},
|
||||
): Promise<ResourceItem> {
|
||||
return apiRequest<ResourceItem>(
|
||||
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}/bind`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listWorkspaceDirectories(
|
||||
workspaceId: string,
|
||||
parentPath: string = "",
|
||||
ownerUserId?: string,
|
||||
): Promise<WorkspaceDirectory[]> {
|
||||
// Default (no ownerUserId) scopes to the requester's own subtree; passing
|
||||
// ownerUserId scopes to that owner so the tree can lazily render their
|
||||
// directory structure on expand. Directories are structural rows; file
|
||||
// visibility is still enforced by the scripts/data-resources endpoints.
|
||||
const parameters = new URLSearchParams();
|
||||
if (parentPath) parameters.set("parent_path", parentPath);
|
||||
if (ownerUserId) parameters.set("owner_user_id", ownerUserId);
|
||||
const query = parameters.toString();
|
||||
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
|
||||
`/api/v1/workspace-directories${query ? `?${query}` : ""}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
return data.directories;
|
||||
}
|
||||
|
||||
export async function createWorkspaceDirectory(
|
||||
workspaceId: string,
|
||||
directoryName: string,
|
||||
parentPath = "",
|
||||
): Promise<WorkspaceDirectory> {
|
||||
return apiRequest<WorkspaceDirectory>(
|
||||
"/api/v1/workspace-directories",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
directory_name: directoryName,
|
||||
parent_path: parentPath,
|
||||
}),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteWorkspaceDirectory(
|
||||
workspaceId: string,
|
||||
path: string,
|
||||
): Promise<{
|
||||
path: string;
|
||||
status: string;
|
||||
deleted_scripts: number;
|
||||
versions_preserved: boolean;
|
||||
}> {
|
||||
const parameters = new URLSearchParams({ path });
|
||||
return apiRequest(
|
||||
`/api/v1/workspace-directories?${parameters.toString()}`,
|
||||
{ method: "DELETE" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user