Drop examples; popping into a separate repo

This commit is contained in:
Dannon Baker
2025-05-08 16:59:26 -04:00
parent 4027bf6294
commit 672e8ea942
9 changed files with 304 additions and 311 deletions
+42 -58
View File
@@ -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
- Backward compatibility with the original API
+1 -4
View File
@@ -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",
+22 -22
View File
@@ -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;
}
return "accessible" in entry && !entry.accessible;
}
+54 -54
View File
@@ -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');
});
});
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");
});
});
+4 -4
View File
@@ -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<GalaxyApiPaths>({
baseUrl: baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
});
return createClient<GalaxyApiPaths>({
baseUrl: baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl,
});
}
export type GalaxyApiClient = ReturnType<typeof createGalaxyApi>;
export type GalaxyApiClient = ReturnType<typeof createGalaxyApi>;
+44 -28
View File
@@ -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<HistorySummary | null> {
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<string, any[]> = {};
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 };
// 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 };
+8 -11
View File
@@ -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);
}
return errorMessageAsString(error);
}
+102 -103
View File
@@ -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' });
});
});
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" });
});
});
+27 -27
View File
@@ -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';
}
return "Unknown error";
}