import { type components, GalaxyApi } from "@/api"; import { ERROR_STATES, type ShowFullJobResponse } from "@/api/jobs"; import type { Tool, ToolPanelItem, ToolSection, ToolSectionLabel } from "@/stores/toolStore"; import { errorMessageAsString } from "@/utils/simple-error"; export type HdcaUploadTarget = components["schemas"]["HdcaDataItemsTarget"]; export type HdasUploadTarget = components["schemas"]["DataElementsTarget"]; export type FetchDataPayload = components["schemas"]["FetchDataPayload"]; export type FileDataElement = components["schemas"]["FileDataElement"]; export type PastedDataElement = components["schemas"]["PastedDataElement"]; export type UrlDataElement = components["schemas"]["UrlDataElement"]; export type CompositeDataElement = components["schemas"]["CompositeDataElement"]; export type FetchDatasetHash = components["schemas"]["FetchDatasetHash"]; export type NestedElement = components["schemas"]["NestedElement"]; export type NestedElementItems = NestedElement["elements"]; export type NestedElementItem = NestedElementItems[number]; export type FetchTargets = FetchDataPayload["targets"]; export type AnyFetchTarget = FetchTargets[number]; export type ApiDataElement = FileDataElement | PastedDataElement | UrlDataElement; export interface ToolIdentifier { toolId: string; toolVersion: string; } export function getToolKey(toolId: string, toolVersion: string): string { return `${toolId}@${toolVersion}`; } export function urlDataElement(identifier: string, uri: string): UrlDataElement { const element: UrlDataElement = { src: "url", url: uri, name: identifier, // these shouldn't be required but the way our model -> ts stuff works it is... auto_decompress: false, dbkey: "?", ext: "auto", to_posix_lines: false, deferred: false, space_to_tab: false, }; return element; } export function nestedElement(identifier: string, elements: NestedElement["elements"]) { const nestedElement: NestedElement = { name: identifier, elements: elements, auto_decompress: false, dbkey: "?", ext: "auto", to_posix_lines: false, deferred: false, space_to_tab: false, }; return nestedElement; } export async function fetchDatasetsToJobId(payload: FetchDataPayload) { const { data } = await GalaxyApi().POST("/api/tools/fetch", { body: payload, }); return fetchResponseToJobId(data as FetchDataResponse); } /** * Fetches datasets into Galaxy via the /api/tools/fetch endpoint. * Uses callback-based result handling for upload trackers. * * @param payload - The fetch data payload defining datasets to import * @param callbacks - Optional callbacks for success/error/progress events */ export async function fetchDatasets(payload: FetchDataPayload, callbacks: FetchDatasetsCallbacks = {}): Promise { try { const { data, error } = await GalaxyApi().POST("/api/tools/fetch", { body: payload, }); if (error) { throw new Error(error.err_msg || "Upload request failed"); } callbacks.success?.(data as FetchDataResponse); } catch (error) { const errorMessage = errorMessageAsString(error); callbacks.error?.(errorMessage); } } // TODO: The response is not modeled yet in the FastAPI route for /api/tools/fetch // so we define a minimal interface here for our needs. // Once the route is properly modeled, we can replace this minimal placeholder interface. export interface FetchDataResponse { jobs: { id: string }[]; outputs?: Record; } /** * Callback functions for dataset fetch/upload lifecycle events. */ export interface FetchDatasetsCallbacks { /** Called when the request completes successfully */ success?: (response: FetchDataResponse) => void; /** Called when an error occurs */ error?: (error: string | Error) => void; /** Called with warning messages */ warning?: (message: string) => void; /** Called with progress percentage (0-100) for uploads */ progress?: (percentage: number) => void; } function fetchResponseToJobId(response: FetchDataResponse) { return response.jobs[0]!.id; } export function fetchJobErrorMessage(jobDetails: ShowFullJobResponse): string | undefined { const stderr = jobDetails.stderr; let errorMessage: string | undefined = undefined; if (stderr) { errorMessage = "An error was encountered while running your upload job. "; if (stderr.indexOf("binary file contains inappropriate content") > -1) { errorMessage += "The problem may be that the batch uploader will not automatically decompress your files the way the normal uploader does, please specify a correct extension or upload decompressed data."; } errorMessage += "Upload job completed with standard error: " + stderr; } else if (ERROR_STATES.indexOf(jobDetails.state) !== -1) { errorMessage = "Unknown error encountered while running your data import job, this could be a server issue or a problem with the upload definition."; } return errorMessage; } // TODO: Once the backend models are typed, make sure these type guards are correct. export function isTool(section: ToolPanelItem): section is Tool { return !isToolSection(section) && !isToolSectionLabel(section); } export function isToolSection(section: ToolPanelItem): section is ToolSection { return (section as ToolSection).tools !== undefined || (section as ToolSection).elems !== undefined; } export function isToolSectionLabel(section: ToolPanelItem): section is ToolSectionLabel { return ( (section as ToolSectionLabel).text !== undefined && (section as ToolSectionLabel).model_class === "ToolSectionLabel" ); }