Skip to main content
Glama
mocoApi.ts12.5 kB
/** * MoCo API service client * Handles all HTTP communication with the MoCo API including authentication, * pagination, and error handling */ import { getMocoConfig } from '../config/environment.js'; import { handleMocoApiError } from '../utils/errorHandler.js'; import type { Activity, Project, Task, UserHoliday, UserPresence } from '../types/mocoTypes.js'; /** * HTTP client for MoCo API with automatic pagination and error handling */ export class MocoApiService { private readonly config = getMocoConfig(); /** * Default request headers for MoCo API */ private get defaultHeaders(): Record<string, string> { return { 'Authorization': `Token token=${this.config.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }; } /** * Makes an HTTP request to the MoCo API with error handling * @param endpoint - API endpoint path (without base URL) * @param params - Query parameters * @returns Promise with parsed JSON response */ private async makeRequest<T>(endpoint: string, params: Record<string, string | number> = {}): Promise<T> { const url = new URL(`${this.config.baseUrl}${endpoint}`); // Add query parameters Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, String(value)); }); try { const response = await fetch(url.toString(), { method: 'GET', headers: this.defaultHeaders }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json() as T; } catch (error) { throw new Error(handleMocoApiError(error)); } } /** * Makes an HTTP request to the MoCo API with headers for pagination * @param endpoint - API endpoint path (without base URL) * @param params - Query parameters * @returns Promise with parsed JSON response and headers */ private async makeRequestWithHeaders<T>(endpoint: string, params: Record<string, string | number> = {}): Promise<{ data: T; headers: Headers }> { const url = new URL(`${this.config.baseUrl}${endpoint}`); // Add query parameters Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, String(value)); }); try { const response = await fetch(url.toString(), { method: 'GET', headers: this.defaultHeaders }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json() as T; return { data, headers: response.headers }; } catch (error) { throw new Error(handleMocoApiError(error)); } } /** * Fetches all pages of a paginated endpoint automatically using header-based pagination * @param endpoint - API endpoint path * @param params - Query parameters * @returns Promise with all items from all pages */ private async fetchAllPages<T>(endpoint: string, params: Record<string, string | number> = {}): Promise<T[]> { const allItems: T[] = []; let currentPage = 1; let hasMorePages = true; while (hasMorePages) { const { data, headers } = await this.makeRequestWithHeaders<T[]>(endpoint, { ...params, page: currentPage }); // MoCo API returns direct arrays, not nested in data property allItems.push(...data); // Check pagination info from headers const xPage = headers.get('X-Page'); const xTotal = headers.get('X-Total'); const xPerPage = headers.get('X-Per-Page'); if (xPage && xTotal && xPerPage) { const totalItems = parseInt(xTotal, 10); const itemsPerPage = parseInt(xPerPage, 10); const totalPages = Math.ceil(totalItems / itemsPerPage); hasMorePages = currentPage < totalPages; } else { // No pagination headers found, assume single page hasMorePages = false; } currentPage++; } return allItems; } /** * Retrieves activities for the current user within a date range * @param startDate - Start date in ISO 8601 format (YYYY-MM-DD) * @param endDate - End date in ISO 8601 format (YYYY-MM-DD) * @param projectId - Optional project ID to filter activities * @returns Promise with array of activities */ async getActivities(startDate: string, endDate: string, projectId?: number): Promise<Activity[]> { const params: Record<string, string | number> = { from: startDate, to: endDate }; if (projectId) { params.project_id = projectId; } return this.fetchAllPages<Activity>('/activities', params); } /** * Retrieves all projects assigned to the current user * @returns Promise with array of assigned projects */ async getProjects(): Promise<Project[]> { return this.fetchAllPages<Project>('/projects/assigned'); } /** * Searches for projects by name or description * @param query - Search query string * @returns Promise with array of matching projects */ async searchProjects(query: string): Promise<Project[]> { // Get all projects and filter client-side since MoCo API doesn't have text search const allProjects = await this.getProjects(); const lowerQuery = query.toLowerCase(); return allProjects.filter(project => project.name.toLowerCase().includes(lowerQuery) || (project.description && project.description.toLowerCase().includes(lowerQuery)) ); } /** * Retrieves all tasks for a specific assigned project * @param projectId - Project ID (must be assigned to current user) * @returns Promise with array of tasks */ async getProjectTasks(projectId: number): Promise<Task[]> { // Get all assigned projects const assignedProjects = await this.getProjects(); // Find the specific project const project = assignedProjects.find(p => p.id === projectId); if (!project) { throw new Error(`Project ${projectId} is not assigned to the current user or does not exist.`); } // Extract tasks from the project and convert to full Task interface return project.tasks.map(task => ({ id: task.id, name: task.name, active: task.active, billable: task.billable, project: { id: project.id, name: project.name }, created_at: project.created_at, updated_at: project.updated_at })); } /** * Retrieves user holidays for a specific year * @param year - Year (e.g., 2024) * @returns Promise with array of user holidays */ async getUserHolidays(year: number): Promise<UserHoliday[]> { try { return await this.makeRequest<UserHoliday[]>('/users/holidays', { year: year }); } catch (error) { // If 404 error (Resource not found), return empty array instead of throwing error // This happens when no holiday data exists for the year yet if (error instanceof Error && error.message.includes('Resource not found')) { return []; } // Re-throw other errors throw error; } } /** * Retrieves actual taken holidays (absences) for a specific year using schedules endpoint * @param year - Year (e.g., 2024) * @returns Promise with array of taken holiday schedules */ async getTakenHolidays(year: number): Promise<any[]> { // Calculate year date range const startDate = `${year}-01-01`; const endDate = `${year}-12-31`; console.error(`DEBUG API: Trying to fetch schedules for ${startDate} to ${endDate}`); try { // Schedules endpoint has different response structure, use direct request // Based on previous success with makeRequest showing 63 schedules const allSchedules = await this.makeRequest<any[]>('/schedules', { from: startDate, to: endDate }); console.error(`DEBUG API: Found ${allSchedules.length} total schedules for ${year}`); if (allSchedules.length > 0) { console.error('DEBUG API: First few schedules:', JSON.stringify(allSchedules.slice(0, 3), null, 2)); } // Filter for absences (schedules with assignment type "Absence") const absences = allSchedules.filter(schedule => schedule.assignment && schedule.assignment.type === 'Absence' ); console.error(`DEBUG API: Found ${absences.length} absences with assignment codes:`, absences.map(a => a.assignment?.code + ' (' + a.assignment?.name + ')')); // Look specifically for vacation/holiday codes (we need to figure out which code is for vacation) const vacationCodes = ['3', '4', '5']; // Common vacation codes to try const holidays = absences.filter(schedule => vacationCodes.includes(schedule.assignment?.code) ); console.error(`DEBUG API: Found ${holidays.length} potential holidays with codes:`, holidays.map(a => a.assignment?.code + ' (' + a.assignment?.name + ')')); // Filter for only vacation days (assignment code "4") const vacationDays = absences.filter(schedule => schedule.assignment?.code === '4' && schedule.assignment?.name === 'Urlaub' ); console.error(`DEBUG API: Found ${vacationDays.length} actual vacation days (code 4)`); return vacationDays; } catch (error) { console.error('DEBUG API: Error fetching schedules:', error); console.error('DEBUG API: Error details:', error instanceof Error ? error.message : 'Unknown error'); return []; } } /** * Retrieves actual taken sick days for a specific year using schedules endpoint * @param year - Year (e.g., 2024) * @returns Promise with array of taken sick day schedules */ async getTakenSickDays(year: number): Promise<any[]> { // Calculate year date range const startDate = `${year}-01-01`; const endDate = `${year}-12-31`; console.error(`DEBUG API: Trying to fetch sick days for ${startDate} to ${endDate}`); try { // Get ALL schedules using direct request (schedules has different response structure) const allSchedules = await this.makeRequest<any[]>('/schedules', { from: startDate, to: endDate }); console.error(`DEBUG API: Found ${allSchedules.length} total schedules for sick days query`); // Filter for sick days (assignment code "3" and name "Krankheit") const sickDays = allSchedules.filter(schedule => schedule.assignment && schedule.assignment.type === 'Absence' && schedule.assignment.code === '3' && schedule.assignment.name === 'Krankheit' ); console.error(`DEBUG API: Found ${sickDays.length} actual sick days (code 3)`); return sickDays; } catch (error) { console.error('DEBUG API: Error fetching sick days:', error); console.error('DEBUG API: Error details:', error instanceof Error ? error.message : 'Unknown error'); return []; } } /** * Retrieves public holidays for a specific year using schedules endpoint * @param year - Year (e.g., 2024) * @returns Promise with array of public holiday schedules */ async getPublicHolidays(year: number): Promise<any[]> { // Calculate year date range const startDate = `${year}-01-01`; const endDate = `${year}-12-31`; try { // Get ALL schedules using direct request const allSchedules = await this.makeRequest<any[]>('/schedules', { from: startDate, to: endDate }); // Filter for public holidays (assignment code "2" and type "Absence") const publicHolidays = allSchedules.filter(schedule => schedule.assignment && schedule.assignment.type === 'Absence' && schedule.assignment.code === '2' ); return publicHolidays; } catch (error) { console.error('DEBUG API: Error fetching public holidays:', error); return []; } } /** * Retrieves user presences within a date range * @param startDate - Start date in ISO 8601 format (YYYY-MM-DD) * @param endDate - End date in ISO 8601 format (YYYY-MM-DD) * @returns Promise with array of user presences */ async getUserPresences(startDate: string, endDate: string): Promise<UserPresence[]> { return this.fetchAllPages<UserPresence>('/users/presences', { from: startDate, to: endDate }); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/niondigital/moco-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server