From 672e8ea9426a9b8a404a2d23feaf879c9595c45f Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 7 May 2025 18:40:38 -0400 Subject: [PATCH] Drop examples; popping into a separate repo --- client-api/README.md | 100 ++++++-------- client-api/package.json | 5 +- client-api/src/api-types.ts | 44 +++---- client-api/src/client.test.ts | 108 +++++++-------- client-api/src/client.ts | 8 +- client-api/src/example.ts | 72 ++++++---- client-api/src/index.ts | 19 ++- client-api/src/integration.test.ts | 205 ++++++++++++++--------------- client-api/src/utils/error.ts | 54 ++++---- 9 files changed, 304 insertions(+), 311 deletions(-) diff --git a/client-api/README.md b/client-api/README.md index 84a1a6499d6..0d279c4914a 100644 --- a/client-api/README.md +++ b/client-api/README.md @@ -15,25 +15,25 @@ pnpm add @galaxyproject/client-api ## Usage ```typescript -import { createGalaxyApi } from '@galaxyproject/client-api'; +import { createGalaxyApi } from "@galaxyproject/client-api"; // Create an instance of the Galaxy API client with a specific base URL -const api = createGalaxyApi('https://usegalaxy.org'); +const api = createGalaxyApi("https://usegalaxy.org"); // Alternatively, use the default (current origin) // const api = createGalaxyApi(); // Example: Get a list of histories -const { data, error } = await api.GET('/api/histories'); +const { data, error } = await api.GET("/api/histories"); if (error) { - console.error('Error fetching histories:', error); + console.error("Error fetching histories:", error); } else { - console.log('Histories:', data); + console.log("Histories:", data); } // For backward compatibility -import { GalaxyApi } from '@galaxyproject/client-api'; +import { GalaxyApi } from "@galaxyproject/client-api"; const legacyApi = GalaxyApi(); // Uses current origin ``` @@ -42,67 +42,48 @@ const legacyApi = GalaxyApi(); // Uses current origin This package provides TypeScript types for all Galaxy API endpoints and models: ```typescript -import { - createGalaxyApi, - type HistorySummary, - type DatasetEntry -} from '@galaxyproject/client-api'; +import { createGalaxyApi, type HistorySummary, type DatasetEntry } from "@galaxyproject/client-api"; const api = createGalaxyApi(); // Type-safe API calls -const { data: histories } = await api.GET('/api/histories'); +const { data: histories } = await api.GET("/api/histories"); // histories is typed as HistorySummary[] | undefined // You can use the types for your variables const myHistory: HistorySummary = { - id: '123', - name: 'My History', - // ...other required properties + id: "123", + name: "My History", + // ...other required properties }; ``` -## Comprehensive Examples +## Example -The package includes several comprehensive examples in the `src/examples` directory that demonstrate common use cases: - -### List Tools Example +The package includes a simple example in the `src/example.ts` file that demonstrates how to use the client: ```typescript -import { createGalaxyApi } from '@galaxyproject/client-api'; +import { createGalaxyApi } from "@galaxyproject/client-api"; -// Default to localhost:8080 if no URL is provided -const api = createGalaxyApi('http://localhost:8080'); +// Create a client with a specific Galaxy instance +const api = createGalaxyApi("https://usegalaxy.org"); -// Fetch all available tools -const { data, error } = await api.GET('/api/tools'); +// Example: Get a list of tools +async function getTools() { + const { data, error } = await api.GET("/api/tools"); -if (error) { - console.error('Error fetching tools:', error); -} else { - console.log(`Found ${data.length} tools`); - - // Group tools by section - const sections = {}; - for (const tool of data) { - const section = tool.panel_section_name || 'Ungrouped'; - if (!sections[section]) sections[section] = []; - sections[section].push(tool); - } - - // Print summary - Object.keys(sections).forEach(section => { - console.log(`${section}: ${sections[section].length} tools`); - }); + if (error) { + console.error("Error fetching tools:", error); + return []; + } + + // Log tool count + console.log(`Found ${data.length} tools`); + + return data; } ``` -See all examples in the `src/examples` directory for: -- Working with histories and datasets -- Authentication with API keys -- Comprehensive error handling -- And more! - ## Design Notes This package uses symlinks to reference API type definitions from the main Galaxy client while providing a standalone client implementation. This approach was chosen to: @@ -112,8 +93,9 @@ This package uses symlinks to reference API type definitions from the main Galax 3. Allow the client to work independently of Galaxy's internal utilities Key symlinks: + - `src/api` → `../../client/src/api` (for type definitions) -- `src/utils/simple-error.ts` → `../../../client/src/utils/simple-error.ts` +- `src/utils/error.ts` - Custom error handling utilities ## Development @@ -122,16 +104,17 @@ To work on this package: 1. Make changes to the API type definitions in the main Galaxy client 2. The type changes will automatically be available in this package via symlinks 3. Build the library for distribution: - ```bash - # Install dependencies - npm install - # Build the library - npm run build - - # Watch mode for development - npm run dev - ``` + ```bash + # Install dependencies + npm install + + # Build the library + npm run build + + # Watch mode for development + npm run dev + ``` ### Testing @@ -152,7 +135,8 @@ npm run test:coverage ``` The tests verify: + - Client creation with default and custom base URLs - Basic API interaction with proper typing - Error handling -- Backward compatibility with the original API \ No newline at end of file +- Backward compatibility with the original API diff --git a/client-api/package.json b/client-api/package.json index 9c16047bf88..c243f77112a 100644 --- a/client-api/package.json +++ b/client-api/package.json @@ -24,10 +24,7 @@ "prepublishOnly": "npm run clean && npm run build", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "examples:tools": "node src/examples/listTools.mjs", - "examples:histories": "node src/examples/historyOperations.mjs", - "examples:auth": "node src/examples/authentication.mjs" + "test:coverage": "vitest run --coverage" }, "keywords": [ "galaxy", diff --git a/client-api/src/api-types.ts b/client-api/src/api-types.ts index 273d91fd95b..07544c43510 100644 --- a/client-api/src/api-types.ts +++ b/client-api/src/api-types.ts @@ -40,35 +40,35 @@ export type AsyncTaskResultSummary = components["schemas"]["AsyncTaskResultSumma export type HistorySortByLiteral = "create_time" | "name" | "update_time" | "username" | undefined; export interface HistoryContentsStats { - id: string; - update_time: string; - size: number; - contents_active: components["schemas"]["HistoryActiveContentCounts"]; + id: string; + update_time: string; + size: number; + contents_active: components["schemas"]["HistoryActiveContentCounts"]; } export interface HistorySummaryExtended extends HistorySummary, HistoryContentsStats { - user_id: string | null; + user_id: string | null; } export interface SelectableObjectStore extends ConcreteObjectStoreModel { - object_store_id: string; + object_store_id: string; } export type DatasetEntry = HDASummary | HDADetailed | HDAInaccessible; export interface DCECollection extends DCESummary { - element_type: "dataset_collection"; - object: DCObject; + element_type: "dataset_collection"; + object: DCObject; } export interface DCEDataset extends DCESummary { - element_type: "hda"; - object: HDAObject; + element_type: "hda"; + object: HDAObject; } export interface SubCollection extends DCObject { - name: string; - hdca_id: string; + name: string; + hdca_id: string; } export type CollectionEntry = HDCASummary | SubCollection; @@ -76,31 +76,31 @@ export type HistoryItemSummary = HDASummary | HDCASummary; // Utility functions export function isHDA(entry?: HistoryItemSummary): entry is HDASummary { - return entry !== undefined && "history_content_type" in entry && entry.history_content_type === "dataset"; + return entry !== undefined && "history_content_type" in entry && entry.history_content_type === "dataset"; } export function isHDCA(entry?: HistoryItemSummary | CollectionEntry): entry is HDCASummary { - return ( - entry !== undefined && "history_content_type" in entry && entry.history_content_type === "dataset_collection" - ); + return ( + entry !== undefined && "history_content_type" in entry && entry.history_content_type === "dataset_collection" + ); } export function isDCE(item: object): item is DCESummary { - return item && "element_type" in item; + return item && "element_type" in item; } export function isCollectionElement(element: DCESummary): element is DCECollection { - return element.element_type === "dataset_collection"; + return element.element_type === "dataset_collection"; } export function isDatasetElement(element: DCESummary): element is DCEDataset { - return element.element_type === "hda"; + return element.element_type === "hda"; } export function hasDetails(entry: DatasetEntry): entry is HDADetailed { - return "peek" in entry; + return "peek" in entry; } export function isInaccessible(entry: DatasetEntry): entry is HDAInaccessible { - return "accessible" in entry && !entry.accessible; -} \ No newline at end of file + return "accessible" in entry && !entry.accessible; +} diff --git a/client-api/src/client.test.ts b/client-api/src/client.test.ts index 588bc336296..b83fec20c3c 100644 --- a/client-api/src/client.test.ts +++ b/client-api/src/client.test.ts @@ -1,68 +1,68 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { createGalaxyApi } from './client'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createGalaxyApi } from "./client"; // Create a mock API client for testing const mockApiClient = { - GET: vi.fn(), - POST: vi.fn(), - PUT: vi.fn(), - DELETE: vi.fn(), - PATCH: vi.fn(), + GET: vi.fn(), + POST: vi.fn(), + PUT: vi.fn(), + DELETE: vi.fn(), + PATCH: vi.fn(), }; // Mock createClient before importing -vi.mock('openapi-fetch', () => { - return { - default: vi.fn().mockImplementation(() => mockApiClient) - }; +vi.mock("openapi-fetch", () => { + return { + default: vi.fn().mockImplementation(() => mockApiClient), + }; }); -import createClient from 'openapi-fetch'; +import createClient from "openapi-fetch"; -describe('Galaxy API Client', () => { - beforeEach(() => { - // Reset mock function calls before each test - vi.clearAllMocks(); - }); +describe("Galaxy API Client", () => { + beforeEach(() => { + // Reset mock function calls before each test + vi.clearAllMocks(); + }); - it('creates a client with the default base URL', () => { - // Set up window.location.origin for testing - const originalLocation = window.location; - delete (window as any).location; - window.location = { ...originalLocation, origin: 'https://test-galaxy.org' } as any; + it("creates a client with the default base URL", () => { + // Set up window.location.origin for testing + const originalLocation = window.location; + delete (window as any).location; + window.location = { ...originalLocation, origin: "https://test-galaxy.org" } as any; - const api = createGalaxyApi(); - - expect(createClient).toHaveBeenCalledWith({ baseUrl: 'https://test-galaxy.org' }); - - // Restore window.location - window.location = originalLocation; - }); + const api = createGalaxyApi(); - it('creates a client with a custom base URL', () => { - const customUrl = 'https://usegalaxy.org'; - const api = createGalaxyApi(customUrl); - - expect(createClient).toHaveBeenCalledWith({ baseUrl: customUrl }); - }); + expect(createClient).toHaveBeenCalledWith({ baseUrl: "https://test-galaxy.org" }); - it('strips trailing slash from base URL', () => { - const customUrl = 'https://usegalaxy.org/'; - const expectedUrl = 'https://usegalaxy.org'; - const api = createGalaxyApi(customUrl); - - expect(createClient).toHaveBeenCalledWith({ baseUrl: expectedUrl }); - }); + // Restore window.location + window.location = originalLocation; + }); - it('returns the configured client', () => { - const api = createGalaxyApi(); - - // Should have all the HTTP methods - expect(api).toEqual(mockApiClient); - expect(api).toHaveProperty('GET'); - expect(api).toHaveProperty('POST'); - expect(api).toHaveProperty('PUT'); - expect(api).toHaveProperty('DELETE'); - expect(api).toHaveProperty('PATCH'); - }); -}); \ No newline at end of file + it("creates a client with a custom base URL", () => { + const customUrl = "https://usegalaxy.org"; + const api = createGalaxyApi(customUrl); + + expect(createClient).toHaveBeenCalledWith({ baseUrl: customUrl }); + }); + + it("strips trailing slash from base URL", () => { + const customUrl = "https://usegalaxy.org/"; + const expectedUrl = "https://usegalaxy.org"; + const api = createGalaxyApi(customUrl); + + expect(createClient).toHaveBeenCalledWith({ baseUrl: expectedUrl }); + }); + + it("returns the configured client", () => { + const api = createGalaxyApi(); + + // Should have all the HTTP methods + expect(api).toEqual(mockApiClient); + expect(api).toHaveProperty("GET"); + expect(api).toHaveProperty("POST"); + expect(api).toHaveProperty("PUT"); + expect(api).toHaveProperty("DELETE"); + expect(api).toHaveProperty("PATCH"); + }); +}); diff --git a/client-api/src/client.ts b/client-api/src/client.ts index eab55f85a97..fc9359ebf86 100644 --- a/client-api/src/client.ts +++ b/client-api/src/client.ts @@ -7,9 +7,9 @@ import { type GalaxyApiPaths } from "./api-types"; * @returns The Galaxy API client */ export function createGalaxyApi(baseUrl: string = window.location.origin) { - return createClient({ - baseUrl: baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl - }); + return createClient({ + baseUrl: baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl, + }); } -export type GalaxyApiClient = ReturnType; \ No newline at end of file +export type GalaxyApiClient = ReturnType; diff --git a/client-api/src/example.ts b/client-api/src/example.ts index 64179d90ee5..342c91f4b94 100644 --- a/client-api/src/example.ts +++ b/client-api/src/example.ts @@ -1,37 +1,53 @@ // Example usage of the Galaxy API client -import { createGalaxyApi, type HistorySummary } from "./index"; +import { createGalaxyApi } from "./index"; // Create a client const api = createGalaxyApi("https://usegalaxy.org"); -// Example function to get histories -async function getHistories() { - const { data, error } = await api.GET("/api/histories"); - - if (error) { - console.error("Error fetching histories:", error); - return []; - } - - return data; -} +// Example function to get a list of tools +async function getTools() { + const { data, error } = await api.GET("/api/tools"); -// Example function with type safety -async function getHistoryById(id: string): Promise { - const { data, error } = await api.GET("/api/histories/{history_id}", { - params: { - path: { - history_id: id - } + if (error) { + console.error("Error fetching tools:", error); + return []; } - }); - - if (error || !data) { - console.error("Error fetching history:", error); - return null; - } - - return data; + + // Log tool count + console.log(`Found ${data.length} tools`); + + // Group tools by section + const sections: Record = {}; + for (const tool of data) { + const section = tool.panel_section_name || "Ungrouped"; + if (!sections[section]) sections[section] = []; + sections[section].push(tool); + } + + // Print summary + Object.keys(sections).forEach((section) => { + console.log(`${section}: ${sections[section].length} tools`); + }); + + return data; } -export { getHistories, getHistoryById }; \ No newline at end of file +// Example function to get a specific tool by ID +async function getToolById(id: string) { + const { data, error } = await api.GET("/api/tools/{tool_id}", { + params: { + path: { + tool_id: id, + }, + }, + }); + + if (error || !data) { + console.error("Error fetching tool:", error); + return null; + } + + return data; +} + +export { getTools, getToolById }; diff --git a/client-api/src/index.ts b/client-api/src/index.ts index 6c999dde945..393d62f6b7b 100644 --- a/client-api/src/index.ts +++ b/client-api/src/index.ts @@ -11,24 +11,21 @@ export * from "./api-types"; // SimpleError class for error handling export class SimpleError extends Error { - constructor(message: string) { - super(message); - this.name = "GalaxyApiError"; - } + constructor(message: string) { + super(message); + this.name = "GalaxyApiError"; + } } // Re-export the client functions -export { - createGalaxyApi, - type GalaxyApiClient -}; +export { createGalaxyApi, type GalaxyApiClient }; // For backward compatibility - creates a client with default settings export function GalaxyApi() { - return createGalaxyApi(); + return createGalaxyApi(); } // Utility function to format error messages export function formatErrorMessage(error: any): string { - return errorMessageAsString(error); -} \ No newline at end of file + return errorMessageAsString(error); +} diff --git a/client-api/src/integration.test.ts b/client-api/src/integration.test.ts index 86a5ed140ef..3861a6f244a 100644 --- a/client-api/src/integration.test.ts +++ b/client-api/src/integration.test.ts @@ -1,119 +1,118 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { type HistorySummary } from './api-types'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { type HistorySummary } from "./api-types"; // Set up mock responses const mockHistories: HistorySummary[] = [ - { - id: '1', - name: 'Test History 1', - deleted: false, - purged: false, - archived: false, - tags: [], - model_class: 'History', - url: '/api/histories/1', - }, - { - id: '2', - name: 'Test History 2', - deleted: false, - purged: false, - archived: false, - tags: ['test'], - model_class: 'History', - url: '/api/histories/2', - }, + { + id: "1", + name: "Test History 1", + deleted: false, + purged: false, + archived: false, + tags: [], + model_class: "History", + url: "/api/histories/1", + }, + { + id: "2", + name: "Test History 2", + deleted: false, + purged: false, + archived: false, + tags: ["test"], + model_class: "History", + url: "/api/histories/2", + }, ]; // Create a mock API client with typed responses const mockApiClient = { - GET: vi.fn().mockImplementation((path, options) => { - if (path === '/api/histories') { - return Promise.resolve({ - data: mockHistories, - error: null, - response: new Response(), - }); - } - if (path === '/api/histories/{history_id}' && - options?.params?.path?.history_id === '1') { - return Promise.resolve({ - data: mockHistories[0], - error: null, - response: new Response(), - }); - } - return Promise.resolve({ - data: null, - error: { status: 404, message: 'Not found' }, - response: new Response(), - }); - }), - POST: vi.fn(), - PUT: vi.fn(), - DELETE: vi.fn(), - PATCH: vi.fn(), + GET: vi.fn().mockImplementation((path, options) => { + if (path === "/api/histories") { + return Promise.resolve({ + data: mockHistories, + error: null, + response: new Response(), + }); + } + if (path === "/api/histories/{history_id}" && options?.params?.path?.history_id === "1") { + return Promise.resolve({ + data: mockHistories[0], + error: null, + response: new Response(), + }); + } + return Promise.resolve({ + data: null, + error: { status: 404, message: "Not found" }, + response: new Response(), + }); + }), + POST: vi.fn(), + PUT: vi.fn(), + DELETE: vi.fn(), + PATCH: vi.fn(), }; // Setup mocks before importing the modules that use them -vi.mock('openapi-fetch', () => { - return { - default: vi.fn().mockImplementation(() => mockApiClient) - }; +vi.mock("openapi-fetch", () => { + return { + default: vi.fn().mockImplementation(() => mockApiClient), + }; }); // Import the modules after setting up mocks -import { createGalaxyApi, GalaxyApi } from './index'; +import { createGalaxyApi, GalaxyApi } from "./index"; -describe('Galaxy API Integration', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('exports the createGalaxyApi function', () => { - expect(typeof createGalaxyApi).toBe('function'); - }); - - it('exports the backward-compatible GalaxyApi function', () => { - expect(typeof GalaxyApi).toBe('function'); - expect(GalaxyApi()).toEqual(mockApiClient); - }); - - it('retrieves histories with proper typing', async () => { - const api = createGalaxyApi(); - const { data, error } = await api.GET('/api/histories'); - - expect(error).toBeNull(); - expect(data).toEqual(mockHistories); - - if (data) { - // This is a type check - if TypeScript can compile this, - // it means the types are working correctly - const firstHistory = data[0]; - expect(firstHistory.id).toBe('1'); - expect(firstHistory.name).toBe('Test History 1'); - } - }); - - it('retrieves a single history by ID', async () => { - const api = createGalaxyApi(); - const { data, error } = await api.GET('/api/histories/{history_id}', { - params: { - path: { - history_id: '1' - } - } +describe("Galaxy API Integration", () => { + beforeEach(() => { + vi.clearAllMocks(); }); - - expect(error).toBeNull(); - expect(data).toEqual(mockHistories[0]); - }); - it('handles errors properly', async () => { - const api = createGalaxyApi(); - const { data, error } = await api.GET('/api/nonexistent'); - - expect(data).toBeNull(); - expect(error).toEqual({ status: 404, message: 'Not found' }); - }); -}); \ No newline at end of file + it("exports the createGalaxyApi function", () => { + expect(typeof createGalaxyApi).toBe("function"); + }); + + it("exports the backward-compatible GalaxyApi function", () => { + expect(typeof GalaxyApi).toBe("function"); + expect(GalaxyApi()).toEqual(mockApiClient); + }); + + it("retrieves histories with proper typing", async () => { + const api = createGalaxyApi(); + const { data, error } = await api.GET("/api/histories"); + + expect(error).toBeNull(); + expect(data).toEqual(mockHistories); + + if (data) { + // This is a type check - if TypeScript can compile this, + // it means the types are working correctly + const firstHistory = data[0]; + expect(firstHistory.id).toBe("1"); + expect(firstHistory.name).toBe("Test History 1"); + } + }); + + it("retrieves a single history by ID", async () => { + const api = createGalaxyApi(); + const { data, error } = await api.GET("/api/histories/{history_id}", { + params: { + path: { + history_id: "1", + }, + }, + }); + + expect(error).toBeNull(); + expect(data).toEqual(mockHistories[0]); + }); + + it("handles errors properly", async () => { + const api = createGalaxyApi(); + const { data, error } = await api.GET("/api/nonexistent"); + + expect(data).toBeNull(); + expect(error).toEqual({ status: 404, message: "Not found" }); + }); +}); diff --git a/client-api/src/utils/error.ts b/client-api/src/utils/error.ts index f4205f30ca4..8be44f9ee9a 100644 --- a/client-api/src/utils/error.ts +++ b/client-api/src/utils/error.ts @@ -6,33 +6,33 @@ * Convert various error formats to a readable string message */ export function errorMessageAsString(error: any): string { - if (typeof error === 'string') { - return error; - } - - if (error instanceof Error) { - return error.message; - } - - if (error && typeof error === 'object') { - // Handle API response errors - if (error.status && error.error) { - return `API Error (${error.status}): ${error.error}`; + if (typeof error === "string") { + return error; } - - // Handle response objects with error message - if (error.message) { - return error.message; + + if (error instanceof Error) { + return error.message; } - - // Try to stringify the error object - try { - return JSON.stringify(error); - } catch (e) { - // Fall back to object inspection - return String(error); + + if (error && typeof error === "object") { + // Handle API response errors + if (error.status && error.error) { + return `API Error (${error.status}): ${error.error}`; + } + + // Handle response objects with error message + if (error.message) { + return error.message; + } + + // Try to stringify the error object + try { + return JSON.stringify(error); + } catch (e) { + // Fall back to object inspection + return String(error); + } } - } - - return 'Unknown error'; -} \ No newline at end of file + + return "Unknown error"; +}