Skip to main content
Glama
by cristip73
asana-client-wrapper.ts55.2 kB
import Asana from 'asana'; import { ensureArray } from './utils/array-utils.js'; import { handlePagination, PaginationOptions, PaginatedResponse } from './utils/pagination.js'; export class AsanaClientWrapper { private workspaces: any; private projects: any; private tasks: any; private stories: any; private projectStatuses: any; private tags: any; private attachments: any; private sections: any; private users: any; private teams: any; private defaultWorkspaceId?: string; constructor(token: string, defaultWorkspaceId?: string) { const client = Asana.ApiClient.instance; client.authentications['token'].accessToken = token; // Initialize API instances this.workspaces = new Asana.WorkspacesApi(); this.projects = new Asana.ProjectsApi(); this.tasks = new Asana.TasksApi(); this.stories = new Asana.StoriesApi(); this.projectStatuses = new Asana.ProjectStatusesApi(); this.tags = new Asana.TagsApi(); this.attachments = new Asana.AttachmentsApi(); this.sections = new Asana.SectionsApi(); this.users = new Asana.UsersApi(); this.teams = new Asana.TeamsApi(); this.defaultWorkspaceId = defaultWorkspaceId; } /** * Helper method to ensure input is always an array * @param input The input to normalize * @returns An array representation of the input * @private */ private ensureArray(input: any): any[] { return ensureArray(input); } /** * Helper method to handle paginated API calls * @param initialFetch Function to fetch the initial page * @param nextPageFetch Function to fetch subsequent pages * @param options Pagination options * @returns Combined results from all pages */ private async handlePaginatedResults<T>( initialFetch: () => Promise<PaginatedResponse<T>>, nextPageFetch: (offset: string) => Promise<PaginatedResponse<T>>, options: PaginationOptions = {} ): Promise<T[]> { try { // Fetch the initial page const initialResponse = await initialFetch(); // Use the pagination utility to handle additional pages if needed return await handlePagination(initialResponse, nextPageFetch, options); } catch (error: any) { console.error(`Error in paginated request: ${error.message}`); throw error; } } async listWorkspaces(opts: any = {}) { try { // If DEFAULT_WORKSPACE_ID is set, only return that workspace if (this.defaultWorkspaceId) { console.error(`Using default workspace ID: ${this.defaultWorkspaceId}`); const response = await this.workspaces.getWorkspace(this.defaultWorkspaceId, opts); return [response.data]; // Return as an array with a single workspace } // Extract pagination parameters const { auto_paginate = false, max_pages = 10, limit, offset, ...otherOpts } = opts; // Build search parameters const searchParams: any = { ...otherOpts }; // Add pagination parameters if (limit !== undefined) { // Ensure limit is between 1 and 100 searchParams.limit = Math.min(Math.max(1, Number(limit)), 100); } if (offset) searchParams.offset = offset; // Use the paginated results handler for more reliable pagination return await this.handlePaginatedResults( // Initial fetch function () => this.workspaces.getWorkspaces(searchParams), // Next page fetch function (nextOffset) => this.workspaces.getWorkspaces({ ...searchParams, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } catch (error: any) { console.error(`Error listing workspaces: ${error.message}`); throw error; } } async searchProjects(workspace: string | undefined, namePattern: string, archived: boolean = false, opts: any = {}) { try { // Extrage parametrii noi const { team, limit, offset, auto_paginate = false, max_pages = 10, opt_fields, ...otherOpts } = opts; // Pregătește obiectul de parametri pentru cerere const searchParams: any = { ...otherOpts, archived }; // Adaugă workspace dacă este furnizat sau folosește cel implicit if (workspace) { searchParams.workspace = workspace; } else if (this.defaultWorkspaceId) { searchParams.workspace = this.defaultWorkspaceId; } // Adaugă team dacă este furnizat if (team) { searchParams.team = team; } // Verifică dacă avem cel puțin un parametru de filtrare (workspace sau team) if (!searchParams.workspace && !searchParams.team) { throw new Error("No workspace or team specified and no default workspace ID set"); } // Adaugă câmpuri opționale dacă sunt furnizate if (opt_fields) { searchParams.opt_fields = opt_fields; } else { searchParams.opt_fields = 'name,archived,created_at,modified_at,public,current_status'; } // Adaugă parametri de paginare if (limit !== undefined) { // Asigură-te că limita este între 1 și 100 searchParams.limit = Math.min(Math.max(1, Number(limit)), 100); } if (offset) { searchParams.offset = offset; } // Folosește handlePaginatedResults pentru a gestiona paginarea const projects = await this.handlePaginatedResults( // Funcția de fetch inițială () => this.projects.getProjects(searchParams), // Funcția de fetch pentru pagina următoare (nextOffset) => this.projects.getProjects({ ...searchParams, offset: nextOffset }), // Opțiuni de paginare { auto_paginate, max_pages } ); // Filtrează proiectele pe baza pattern-ului de nume if (namePattern) { const pattern = new RegExp(namePattern, 'i'); return projects.filter((project: any) => pattern.test(project.name)); } return projects; } catch (error: any) { console.error(`Error searching projects: ${error.message}`); // Adaugă gestionare detaliată a erorilor pentru situații comune if (error.message?.includes('Bad Request')) { if (opts.limit && (opts.limit < 1 || opts.limit > 100)) { throw new Error(`Invalid limit parameter: ${opts.limit}. Limit must be between 1 and 100.`); } throw new Error(`Error searching projects: ${error.message}. Check that all parameters are valid.`); } throw error; } } async searchTasks(workspace: string | undefined, searchOpts: any = {}) { try { // Use default workspace if not specified and available if (!workspace && this.defaultWorkspaceId) { workspace = this.defaultWorkspaceId; } if (!workspace) { throw new Error("No workspace specified and no default workspace ID set"); } // Extract pagination parameters const { auto_paginate = false, max_pages = 10, limit, offset, // Extract other known parameters text, resource_subtype, completed, is_subtask, has_attachment, is_blocked, is_blocking, sort_by, sort_ascending, opt_fields, ...otherOpts } = searchOpts; // Build search parameters const searchParams: any = { ...otherOpts // Include any additional filter parameters }; // Handle custom fields if provided if (searchOpts.custom_fields) { if (typeof searchOpts.custom_fields == "string") { try { searchOpts.custom_fields = JSON.parse(searchOpts.custom_fields); } catch (err) { if (err instanceof Error) { err.message = "custom_fields must be a JSON object : " + err.message; } throw err; } } Object.entries(searchOpts.custom_fields).forEach(([key, value]) => { searchParams[`custom_fields.${key}`] = value; }); delete searchParams.custom_fields; // Remove the custom_fields object since we've processed it } // Add optional parameters if provided if (text) searchParams.text = text; if (resource_subtype) searchParams.resource_subtype = resource_subtype; if (completed !== undefined) searchParams.completed = completed; if (is_subtask !== undefined) searchParams.is_subtask = is_subtask; if (has_attachment !== undefined) searchParams.has_attachment = has_attachment; if (is_blocked !== undefined) searchParams.is_blocked = is_blocked; if (is_blocking !== undefined) searchParams.is_blocking = is_blocking; if (sort_by) searchParams.sort_by = sort_by; if (sort_ascending !== undefined) searchParams.sort_ascending = sort_ascending; if (opt_fields) searchParams.opt_fields = opt_fields; // Add pagination parameters if (limit !== undefined) { // Ensure limit is between 1 and 100 searchParams.limit = Math.min(Math.max(1, Number(limit)), 100); } if (offset) searchParams.offset = offset; // Use the paginated results handler for more reliable pagination const results = await this.handlePaginatedResults( // Initial fetch function () => this.tasks.searchTasksForWorkspace(workspace, searchParams), // Next page fetch function (nextOffset) => this.tasks.searchTasksForWorkspace(workspace, { ...searchParams, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); // Transform the response to simplify custom fields if present return results.map((task: any) => { if (!task.custom_fields) return task; return { ...task, custom_fields: task.custom_fields.reduce((acc: any, field: any) => { const key = `${field.name} (${field.gid})`; let value = field.display_value; // For enum fields with a value, include the enum option GID if (field.type === 'enum' && field.enum_value) { value = `${field.display_value} (${field.enum_value.gid})`; } acc[key] = value; return acc; }, {}) }; }); } catch (error: any) { console.error(`Error searching tasks: ${error.message}`); // Add more detailed error handling for common issues if (error.message?.includes('Bad Request')) { // Check for common causes of Bad Request if (searchOpts.projects_any) { throw new Error(`Error searching tasks with projects_any: ${error.message}. Try using 'projects.any' instead of 'projects_any', or use getTasksForProject directly.`); } if (searchOpts.limit && (searchOpts.limit < 1 || searchOpts.limit > 100)) { throw new Error(`Invalid limit parameter: ${searchOpts.limit}. Limit must be between 1 and 100.`); } if (searchOpts.offset && !searchOpts.offset.startsWith('eyJ')) { throw new Error(`Invalid offset parameter: ${searchOpts.offset}. Offset must be a valid pagination token from a previous response.`); } // Generic bad request error with suggestions throw new Error(`Bad Request error when searching tasks. Check that all parameters are valid. Common issues: invalid workspace ID, invalid project reference, or incompatible search filters. ${error.message}`); } throw error; } } async getTask(taskId: string, opts: any = {}) { try { const response = await this.tasks.getTask(taskId, opts); return response.data; } catch (error: any) { console.error(`Error retrieving task ${taskId}: ${error.message}`); // Adăugăm informații utile pentru diagnosticare if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } throw error; } } async createTask(projectId: string, data: any) { try { // Ensure projects array includes the projectId const projects = data.projects || []; if (!projects.includes(projectId)) { projects.push(projectId); } const taskData = { data: { ...data, projects, // Handle resource_subtype if provided resource_subtype: data.resource_subtype || 'default_task', // Handle custom_fields if provided custom_fields: data.custom_fields || {}, // Asigură-te că task-ul este adăugat la sfârșitul listei insert_before: null } }; const response = await this.tasks.createTask(taskData); return response.data; } catch (error: any) { console.error(`Error creating task: ${error.message}`); // Add useful diagnostics information if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } // Provide more context about the error if (error.message.includes("Missing input")) { throw new Error(`Failed to create task: Missing required parameters. ${error.message}`); } if (error.message.includes("Not a valid project")) { throw new Error(`Project ID ${projectId} is not valid or you don't have access to it.`); } throw error; } } async getStoriesForTask(taskId: string, opts: any = {}) { const response = await this.stories.getStoriesForTask(taskId, opts); return response.data; } async updateTask(taskId: string, data: any) { // Create a deep clone of the data to avoid modifying the original const taskData = JSON.parse(JSON.stringify(data)); // Handle custom fields properly if provided if (taskData.custom_fields) { try { // Import utils only when needed (avoiding circular dependencies) const { parseCustomFields } = await import('./utils/field-utils.js'); taskData.custom_fields = parseCustomFields(taskData.custom_fields); } catch (err) { throw new Error(`Error processing custom fields: ${(err as Error).message}`); } } try { const body = { data: { ...taskData, // Handle resource_subtype if provided resource_subtype: taskData.resource_subtype || undefined, } }; const opts = {}; const response = await this.tasks.updateTask(body, taskId, opts); return response.data; } catch (error: any) { // Add more context for custom field errors if (error.message?.includes('custom_field')) { throw new Error(`Error updating custom fields: ${error.message}. Make sure you're using the correct format for each field type.`); } throw error; } } async getProject(projectId: string, opts: any = {}) { // Only include opts if opt_fields was actually provided const options = opts.opt_fields ? opts : {}; const response = await this.projects.getProject(projectId, options); return response.data; } async getProjectTaskCounts(projectId: string, opts: any = {}) { // Ensure we always include essential opt_fields for task counts // See: https://developers.asana.com/reference/gettaskcountsforproject const options = { opt_fields: 'num_tasks,num_incomplete_tasks,num_completed_tasks,num_milestones,num_incomplete_milestones,num_completed_milestones' }; // If caller provided specific opt_fields, use those instead if (opts.opt_fields) { options.opt_fields = opts.opt_fields; } const response = await this.projects.getTaskCountsForProject(projectId, options); return response.data; } async getProjectSections(projectId: string, opts: any = {}) { // Only include opts if opt_fields was actually provided const options = opts.opt_fields ? opts : {}; const sections = new Asana.SectionsApi(); const response = await sections.getSectionsForProject(projectId, options); return response.data; } async createTaskStory(taskId: string, text: string, opts: any = {}) { const options = opts.opt_fields ? opts : {}; const body = { data: { text: text } }; const response = await this.stories.createStoryForTask(body, taskId, options); return response.data; } async addTaskDependencies(taskId: string, dependencies: any) { // Ensure dependencies is an array const dependenciesArray = this.ensureArray(dependencies); const body = { data: { dependencies: dependenciesArray } }; const response = await this.tasks.addDependenciesForTask(body, taskId); return response.data; } async addTaskDependents(taskId: string, dependents: any) { // Ensure dependents is an array const dependentsArray = this.ensureArray(dependents); const body = { data: { dependents: dependentsArray } }; const response = await this.tasks.addDependentsForTask(body, taskId); return response.data; } /** * Add one or more tags to a task * @param taskId Task GID to add tags to * @param tags Single tag GID or array of tag GIDs * @returns Results object with successful and failed tags */ async addTagsToTask(taskId: string, tags: any) { // Ensure tags is an array const tagsArray = this.ensureArray(tags); const results: { task_id: string, successful_tags: string[], failed_tags: Array<{tag_gid: string, error: string}>, task_data: any } = { task_id: taskId, successful_tags: [], failed_tags: [], task_data: null }; if (tagsArray.length === 0) { throw new Error("Tags must be provided as a non-empty array of tag GIDs"); } // The Asana API requires separate requests for each tag for (const tagGid of tagsArray) { try { // Corect format for Asana API const body = { data: { tag: tagGid } }; await this.tasks.addTagForTask(body, taskId); results.successful_tags.push(tagGid); } catch (error: any) { results.failed_tags.push({ tag_gid: tagGid, error: error.message }); } } // Return the updated task with tags included try { results.task_data = await this.getTask(taskId, { opt_fields: "name,tags" }); } catch (error: any) { console.error(`Could not fetch updated task data: ${error.message}`); } return results; } // Metoda nouă pentru adăugarea de followers la un task async addFollowersToTask(taskId: string, followers: any) { // Ensure followers is an array const followersArray = this.ensureArray(followers); try { const body = { data: { followers: followersArray } }; const response = await this.tasks.addFollowersForTask(body, taskId); return response.data; } catch (error) { console.error(`Error adding followers to task: ${error}`); // Dacă metoda standard eșuează, încercăm metoda alternativă cu callApi direct try { const client = Asana.ApiClient.instance; const response = await client.callApi( `/tasks/${taskId}/addFollowers`, 'POST', { task_gid: taskId }, {}, {}, {}, { data: { followers: followersArray } }, ['token'], ['application/json'], ['application/json'], 'Blob' ); return response.data; } catch (fallbackError) { console.error("Error in fallback method for adding followers:", fallbackError); throw fallbackError; } } } async createSubtask(parentTaskId: string, data: any, opts: any = {}) { const taskData = { data: { ...data, // Asigură-te că subtask-ul este adăugat la sfârșitul listei insert_before: null } }; const response = await this.tasks.createSubtaskForTask(taskData, parentTaskId, opts); return response.data; } async setParentForTask(data: any, taskId: string, opts: any = {}) { const response = await this.tasks.setParentForTask({ data }, taskId, opts); return response.data; } async getSubtasksForTask(taskId: string, opts: any = {}) { try { const response = await this.tasks.getSubtasksForTask(taskId, opts); return response.data; } catch (error) { console.error("Error in getSubtasksForTask:", error); throw error; } } async getProjectStatus(statusId: string, opts: any = {}) { const response = await this.projectStatuses.getProjectStatus(statusId, opts); return response.data; } async getProjectStatusesForProject(projectId: string, opts: any = {}) { const response = await this.projectStatuses.getProjectStatusesForProject(projectId, opts); return response.data; } async createProjectStatus(projectId: string, data: any) { const body = { data }; const response = await this.projectStatuses.createProjectStatusForProject(body, projectId); return response.data; } async deleteProjectStatus(statusId: string) { const response = await this.projectStatuses.deleteProjectStatus(statusId); return response.data; } async getMultipleTasksByGid(taskIds: any, opts: any = {}) { const taskIdsArray = this.ensureArray(taskIds); if (taskIdsArray.length > 25) { throw new Error("Maximum of 25 task IDs allowed"); } // Use Promise.all to fetch tasks in parallel const tasks = await Promise.all( taskIdsArray.map(taskId => this.getTask(taskId, opts)) ); return tasks; } async getTasksForTag(tag_gid: string, opts: any = {}) { const response = await this.tasks.getTasksForTag(tag_gid, opts); return response.data; } async getTagsForWorkspace(workspace_gid: string | undefined, opts: any = {}) { // Use default workspace if not specified and available if (!workspace_gid && this.defaultWorkspaceId) { workspace_gid = this.defaultWorkspaceId; } if (!workspace_gid) { throw new Error("No workspace specified and no default workspace ID set"); } const response = await this.tags.getTagsForWorkspace(workspace_gid, opts); return response.data; } async getTeamsForUser(user_gid: string, opts: any = {}) { try { const response = await this.teams.getTeamsForUser(user_gid, opts); return response.data; } catch (error) { console.error(`Error in getTeamsForUser: ${error}`); throw error; } } async getTeamsForWorkspace(workspace_gid: string | undefined, opts: any = {}) { try { // Use default workspace if not specified and available if (!workspace_gid && this.defaultWorkspaceId) { workspace_gid = this.defaultWorkspaceId; } if (!workspace_gid) { throw new Error("No workspace specified and no default workspace ID set"); } const response = await this.teams.getTeamsForWorkspace(workspace_gid, opts); return response.data; } catch (error) { console.error(`Error in getTeamsForWorkspace: ${error}`); throw error; } } /** * Gets a list of users in a workspace with support for pagination * @param workspaceId The workspace ID to get users for * @param opts Additional options including pagination parameters * @returns List of users in the workspace */ async getUsersForWorkspace(workspaceId: string | undefined, opts: any = {}) { try { // Use default workspace if not specified and available if (!workspaceId && this.defaultWorkspaceId) { workspaceId = this.defaultWorkspaceId; } if (!workspaceId) { throw new Error("No workspace specified and no default workspace ID set"); } // Extract pagination parameters const { auto_paginate = false, max_pages = 10, limit, offset, opt_fields, ...otherOpts } = opts; // Build search parameters const searchParams: any = { ...otherOpts }; // Add default opt_fields if not specified if (!opt_fields) { searchParams.opt_fields = "name,email"; } else { searchParams.opt_fields = opt_fields; } // Add pagination parameters if provided if (limit !== undefined) { // Ensure limit is between 1 and 100 searchParams.limit = Math.min(Math.max(1, Number(limit)), 100); } if (offset) searchParams.offset = offset; // Use the paginated results handler for more reliable pagination return await this.handlePaginatedResults( // Initial fetch function () => this.users.getUsersForWorkspace(workspaceId, searchParams), // Next page fetch function (nextOffset) => this.users.getUsersForWorkspace(workspaceId, { ...searchParams, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } catch (error: any) { console.error(`Error getting users for workspace ${workspaceId}: ${error.message}`); // Add detailed error handling for common issues if (error.message?.includes('Not Found')) { throw new Error(`Workspace with ID ${workspaceId} not found or inaccessible.`); } if (error.message?.includes('Bad Request')) { if (opts.limit && (opts.limit < 1 || opts.limit > 100)) { throw new Error(`Invalid limit parameter: ${opts.limit}. Limit must be between 1 and 100.`); } throw new Error(`Error retrieving users for workspace: ${error.message}. Check that all parameters are valid.`); } throw error; } } // Metodă nouă pentru crearea unei secțiuni într-un proiect async createSectionForProject(projectId: string, name: string, opts: any = {}) { try { const body = { data: { name } }; // Schimbăm ordinea parametrilor conform documentației Asana const response = await this.sections.createSectionForProject(projectId, body, opts); return response.data; } catch (error) { console.error(`Error creating section for project: ${error}`); // Dacă obținem eroare, încercăm metoda alternativă folosind callApi direct try { const client = Asana.ApiClient.instance; const response = await client.callApi( `/projects/${projectId}/sections`, 'POST', { project_gid: projectId }, {}, {}, {}, { data: { name } }, ['token'], ['application/json'], ['application/json'], 'Blob' ); return response.data; } catch (fallbackError) { console.error("Error in fallback method:", fallbackError); throw fallbackError; } } } // Metodă nouă pentru adăugarea unui task într-o secțiune async addTaskToSection(sectionId: string, taskId: string, opts: any = {}) { const data = { data: { task: taskId, insert_before: null } }; try { const response = await this.sections.addTaskForSection(sectionId, data, opts); return response.data; } catch (error) { console.error("Error in addTaskToSection:", error); // Dacă obținem eroare, încercăm metoda alternativă folosind callApi direct try { const client = Asana.ApiClient.instance; const response = await client.callApi( `/sections/${sectionId}/addTask`, 'POST', { section_gid: sectionId }, {}, {}, {}, { data: { task: taskId, insert_before: null } }, ['token'], ['application/json'], ['application/json'], 'Blob' ); return response.data; } catch (fallbackError) { console.error("Error in fallback method:", fallbackError); throw fallbackError; } } } // Metodă nouă pentru a obține task-urile dintr-o secțiune async getTasksForSection(sectionId: string, opts: any = {}) { try { const response = await this.tasks.getTasksForSection(sectionId, opts); return response.data; } catch (error) { console.error("Error in getTasksForSection:", error); throw error; } } // Metodă pentru obținerea structurii ierarhice complete a unui proiect async getProjectHierarchy(projectId: string, opts: any = {}) { /** * Get the complete hierarchical structure of an Asana project * Pagination features: * 1. Auto pagination: Set auto_paginate=true to get all results automatically * 2. Manual pagination: * - First request: Set limit=N (without offset) * - Subsequent requests: Use limit=N with offset token from previous response * 3. Pagination metadata is provided at multiple levels: * - Global: result.pagination_info * - Section level: section.pagination_info (contains next_offset token) * - Task level: task.subtasks_pagination_info (for subtasks pagination) * * @param projectId - The project GID * @param opts - Options including pagination params (limit, offset, auto_paginate) */ try { // Extrage opțiunile de paginare const { auto_paginate = false, max_pages = 10, limit, offset, include_subtasks = true, include_completed_subtasks, max_subtask_depth = 1, ...otherOpts } = opts; // Initialize stats object for tracking counts const stats = { total_sections: 0, total_tasks: 0, total_subtasks: 0 }; // Pasul 1: Obține informații despre proiect const projectFields = "name,gid" + (opts.opt_fields_project ? `,${opts.opt_fields_project}` : ""); const project = await this.getProject(projectId, { opt_fields: projectFields }); // Pasul 2: Obține secțiunile proiectului const sectionFields = "name,gid" + (opts.opt_fields_sections ? `,${opts.opt_fields_sections}` : ""); const sections = await this.getProjectSections(projectId, { opt_fields: sectionFields }); // Update stats with section count stats.total_sections = sections ? sections.length : 0; // Verifică dacă avem secțiuni if (!sections || sections.length === 0) { return { project: project, sections: [], stats }; } // Calculăm limita efectivă pentru task-uri, dacă este specificată // Dacă nu este specificată, folosim o valoare implicită care va permite API-ului să decidă const effectiveLimit = limit ? Math.min(Math.max(1, Number(limit)), 100) : undefined; // Pasul 3: Pentru fiecare secțiune, obține task-urile const sectionsWithTasks = await Promise.all(sections.map(async (section: any) => { const taskFields = "name,gid,completed,resource_subtype,num_subtasks" + (opts.opt_fields_tasks ? `,${opts.opt_fields_tasks}` : ""); // Pregătim parametrii pentru task-uri const taskOpts: any = { opt_fields: taskFields }; // Adăugăm limita doar dacă este specificată if (effectiveLimit) { taskOpts.limit = effectiveLimit; } // Adăugăm offset doar dacă este specificat și pare valid (începe cu 'eyJ') if (offset && typeof offset === 'string' && offset.startsWith('eyJ')) { taskOpts.offset = offset; } // Include sau exclude task-urile completate if (opts.include_completed_tasks === false) { taskOpts.completed_since = "now"; } // Obținem task-urile din secțiune cu sau fără paginare let tasks; if (auto_paginate) { // Folosim handlePaginatedResults pentru a obține toate task-urile cu paginare automată tasks = await this.handlePaginatedResults( // Initial fetch function () => this.tasks.getTasksForSection(section.gid, taskOpts), // Next page fetch function (nextOffset) => this.tasks.getTasksForSection(section.gid, { ...taskOpts, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } else { // Obținem doar o pagină de task-uri try { const response = await this.tasks.getTasksForSection(section.gid, taskOpts); tasks = response.data || []; // Includem informații despre paginare în rezultat if (response.next_page) { section.pagination_info = { has_more: true, next_offset: response.next_page.offset }; } else { section.pagination_info = { has_more: false }; } } catch (error) { console.error(`Error fetching tasks for section ${section.gid}:`, error); tasks = []; section.error = "Could not fetch tasks for this section"; } } // Update total tasks count stats.total_tasks += tasks ? tasks.length : 0; // Pasul 4: Pentru fiecare task, obține subtask-urile dacă acestea există și dacă utilizatorul dorește const tasksWithSubtasks = await Promise.all(tasks.map(async (task: any) => { // Verifică dacă avem nevoie de subtask-uri și dacă task-ul are subtask-uri if (include_subtasks && task.num_subtasks && task.num_subtasks > 0) { try { // Pregătim câmpurile pentru subtask-uri const subtaskFields = "name,gid,completed,resource_subtype,num_subtasks" + (opts.opt_fields_subtasks ? `,${opts.opt_fields_subtasks}` : opts.opt_fields_tasks ? `,${opts.opt_fields_tasks}` : ""); // Pregătim parametrii pentru subtask-uri const subtaskOpts: any = { opt_fields: subtaskFields }; // Adăugăm limita doar dacă este specificată if (effectiveLimit) { subtaskOpts.limit = effectiveLimit; } // Aplicăm filtrarea pentru task-uri completate (dacă este specificată) if (include_completed_subtasks === false) { subtaskOpts.completed_since = "now"; } // Folosim metoda corectă pentru a obține subtask-urile let subtasks; if (auto_paginate) { // Cu paginare automată subtasks = await this.handlePaginatedResults( // Initial fetch function () => this.tasks.getSubtasksForTask(task.gid, subtaskOpts), // Next page fetch function (nextOffset) => this.tasks.getSubtasksForTask(task.gid, { ...subtaskOpts, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } else { // Fără paginare automată, doar o singură pagină try { const response = await this.tasks.getSubtasksForTask(task.gid, subtaskOpts); subtasks = response.data || []; // Includem informații despre paginare în rezultat if (response.next_page) { task.subtasks_pagination_info = { has_more: true, next_offset: response.next_page.offset }; } else { task.subtasks_pagination_info = { has_more: false }; } } catch (error) { console.error(`Error fetching subtasks for task ${task.gid}:`, error); subtasks = []; task.subtasks_error = "Could not fetch subtasks for this task"; } } // Update subtasks count stats.total_subtasks += subtasks ? subtasks.length : 0; // If max_subtask_depth > 1, recursively fetch deeper subtasks if (max_subtask_depth > 1 && subtasks && subtasks.length > 0) { await this.fetchSubtasksRecursively( subtasks, max_subtask_depth, 1, // Current depth subtaskOpts, auto_paginate, max_pages, stats ); } return { ...task, subtasks }; } catch (error) { console.error(`Error fetching subtasks for task ${task.gid}:`, error); return { ...task, subtasks: [], subtasks_error: "Error fetching subtasks" }; } } return { ...task, subtasks: [] }; })); return { ...section, tasks: tasksWithSubtasks }; })); // Adăugăm informații despre paginare la nivelul rezultatului const result = { project: project, sections: sectionsWithTasks, stats, // Include the statistics in the result pagination_info: { auto_paginate_used: auto_paginate, effective_limit: effectiveLimit, offset_provided: offset ? true : false } }; // Returnează structura ierarhică completă return result; } catch (error: any) { // Oferim un mesaj de eroare mai util pentru probleme comune if (error.message && error.message.includes('offset')) { console.error("Error in getProjectHierarchy with pagination:", error); throw new Error(`Invalid pagination parameters: ${error.message}. Asana requires offset tokens to be obtained from previous responses.`); } else { console.error("Error in getProjectHierarchy:", error); throw error; } } } // Helper method to recursively fetch subtasks to a specified depth private async fetchSubtasksRecursively( tasks: any[], maxDepth: number, currentDepth: number, opts: any, auto_paginate: boolean, max_pages: number, stats: { total_subtasks: number } ) { // If we've reached the maximum depth, stop recursion if (currentDepth >= maxDepth) { return; } // For each task at the current depth for (const task of tasks) { // Skip if task has no subtasks if (!task.num_subtasks || task.num_subtasks <= 0) { continue; } try { // Fetch subtasks for this task let subtasks; if (auto_paginate) { // With auto pagination subtasks = await this.handlePaginatedResults( // Initial fetch function () => this.tasks.getSubtasksForTask(task.gid, opts), // Next page fetch function (nextOffset) => this.tasks.getSubtasksForTask(task.gid, { ...opts, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } else { // Without auto pagination, just get one page const response = await this.tasks.getSubtasksForTask(task.gid, opts); subtasks = response.data || []; // Add pagination info to the task if (response.next_page) { task.subtasks_pagination_info = { has_more: true, next_offset: response.next_page.offset }; } else { task.subtasks_pagination_info = { has_more: false }; } } // Update subtasks count stats.total_subtasks += subtasks ? subtasks.length : 0; // Add subtasks to the current task task.subtasks = subtasks; // Recursively fetch the next level of subtasks if (subtasks && subtasks.length > 0) { await this.fetchSubtasksRecursively( subtasks, maxDepth, currentDepth + 1, opts, auto_paginate, max_pages, stats ); } } catch (error) { console.error(`Error recursively fetching subtasks for task ${task.gid}:`, error); task.subtasks = []; task.subtasks_error = "Error fetching subtasks recursively"; } } } async createProjectForWorkspace(workspaceId: string | undefined, data: any, opts: any = {}) { try { // Use default workspace if not specified and available if (!workspaceId && this.defaultWorkspaceId) { workspaceId = this.defaultWorkspaceId; } if (!workspaceId) { throw new Error("No workspace specified and no default workspace ID set"); } // Pregătim datele pentru cerere const requestData = { ...data }; // Redenumim team_id în team dacă există if (requestData.team_id) { requestData.team = requestData.team_id; delete requestData.team_id; } // Asana API are nevoie de parametrii corecți const body = { data: { // Includem workspace direct workspace: workspaceId, // Includem toate celelalte date dar fără workspace sau workspace_id // deoarece sunt deja transmise prin parametrul workspaceId ...requestData } }; // Eliminăm parametrii redundanți dacă există delete body.data.workspace_id; delete body.data.workspace; // Evităm duplicarea - am pus deja workspaceId ca workspace // Folosim metoda standard createProject const response = await this.projects.createProject(body, opts); return response.data; } catch (error: any) { console.error(`Error creating project for workspace: ${error}`); // Adăugăm mai multe detalii despre eroare pentru debugging if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } throw error; } } async updateProject(projectId: string, data: any, opts: any = {}) { try { // Pregătim datele pentru cerere const body = { data: { ...data } }; // Folosim metoda standard updateProject pentru a actualiza proiectul // Ordinea parametrilor este importantă: body, projectId, opts const response = await this.projects.updateProject(body, projectId, opts); return response.data; } catch (error: any) { console.error(`Error updating project: ${error}`); // Adăugăm mai multe detalii despre eroare pentru debugging if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } throw error; } } // Metodă pentru adăugarea de membri la un proiect async addMembersForProject(projectId: string, members: any) { try { const membersArray = this.ensureArray(members); const body = { data: { members: membersArray } }; const response = await this.projects.addMembersForProject(body, projectId); return response.data; } catch (error: any) { console.error(`Error adding members to project: ${error}`); // Adăugăm mai multe detalii despre eroare pentru debugging if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } // Dacă metoda standard eșuează, încercăm metoda alternativă cu callApi direct try { const client = Asana.ApiClient.instance; const membersArray = this.ensureArray(members); const response = await client.callApi( `/projects/${projectId}/addMembers`, 'POST', { project_gid: projectId }, {}, {}, {}, { data: { members: membersArray } }, ['token'], ['application/json'], ['application/json'], 'Blob' ); return response.data; } catch (fallbackError) { console.error("Error in fallback method for adding members:", fallbackError); throw fallbackError; } } } // Metodă pentru adăugarea de urmăritori la un proiect async addFollowersForProject(projectId: string, followers: any) { try { const followersArray = this.ensureArray(followers); const body = { data: { followers: followersArray } }; const response = await this.projects.addFollowersForProject(body, projectId); return response.data; } catch (error: any) { console.error(`Error adding followers to project: ${error}`); // Adăugăm mai multe detalii despre eroare pentru debugging if (error.response && error.response.body) { console.error(`Response error details: ${JSON.stringify(error.response.body, null, 2)}`); } // Dacă metoda standard eșuează, încercăm metoda alternativă cu callApi direct try { const client = Asana.ApiClient.instance; const followersArray = this.ensureArray(followers); const response = await client.callApi( `/projects/${projectId}/addFollowers`, 'POST', { project_gid: projectId }, {}, {}, {}, { data: { followers: followersArray } }, ['token'], ['application/json'], ['application/json'], 'Blob' ); return response.data; } catch (fallbackError) { console.error("Error in fallback method for adding followers:", fallbackError); throw fallbackError; } } } /** * Get tasks for a specific project with pagination support * @param projectId Project GID * @param opts Additional options including pagination options * @returns List of tasks in the project */ async getTasksForProject(projectId: string, opts: any = {}) { try { // Extract pagination parameters const { auto_paginate = false, max_pages = 10, limit, offset, opt_fields, completed, ...otherOpts } = opts; // Build search parameters const searchParams: any = { ...otherOpts }; // Add specific filters if (completed !== undefined) searchParams.completed = completed; if (opt_fields) searchParams.opt_fields = opt_fields; // Add pagination parameters if (limit !== undefined) { // Ensure limit is between 1 and 100 searchParams.limit = Math.min(Math.max(1, Number(limit)), 100); } if (offset) searchParams.offset = offset; // Use the paginated results handler for more reliable pagination return await this.handlePaginatedResults( // Initial fetch function () => this.tasks.getTasksForProject(projectId, searchParams), // Next page fetch function (nextOffset) => this.tasks.getTasksForProject(projectId, { ...searchParams, offset: nextOffset }), // Pagination options { auto_paginate, max_pages } ); } catch (error: any) { console.error(`Error getting tasks for project ${projectId}: ${error.message}`); // Add detailed error handling for common issues if (error.message?.includes('Not Found')) { throw new Error(`Project with ID ${projectId} not found or inaccessible.`); } if (error.message?.includes('Bad Request')) { if (opts.limit && (opts.limit < 1 || opts.limit > 100)) { throw new Error(`Invalid limit parameter: ${opts.limit}. Limit must be between 1 and 100.`); } throw new Error(`Error retrieving tasks for project: ${error.message}. Check that all parameters are valid.`); } throw error; } } // Metodă pentru reordonarea unei secțiuni într-un proiect async reorderSections(projectId: string, sectionId: string, beforeSectionId?: string | null, afterSectionId?: string | null) { try { if (!sectionId) { throw new Error("Section ID cannot be empty"); } // Nu putem specifica atât before_section cât și after_section simultan if (beforeSectionId !== undefined && beforeSectionId !== null && afterSectionId !== undefined && afterSectionId !== null) { throw new Error("Cannot specify both before_section and after_section. Choose one."); } const body: any = { data: { section: sectionId } }; // Adăugăm before_section sau after_section la body, dacă sunt specificate if (beforeSectionId !== undefined) { body.data.before_section = beforeSectionId === null ? null : beforeSectionId; } else if (afterSectionId !== undefined) { body.data.after_section = afterSectionId === null ? null : afterSectionId; } else { throw new Error("Must specify either before_section_id or after_section_id"); } // Apelăm API-ul Asana pentru a muta secțiunea const response = await this.sections.insertSectionForProject(projectId, body); return { project_id: projectId, section_id: sectionId, status: "success", before_section: beforeSectionId, after_section: afterSectionId, result: response.data }; } catch (error) { console.error(`Error reordering section for project: ${error}`); // Dacă metoda standard eșuează, încercăm metoda alternativă cu callApi direct try { const client = Asana.ApiClient.instance; const body: any = { data: { section: sectionId } }; // Adăugăm before_section sau after_section la body, dacă sunt specificate if (beforeSectionId !== undefined) { body.data.before_section = beforeSectionId === null ? null : beforeSectionId; } else if (afterSectionId !== undefined) { body.data.after_section = afterSectionId === null ? null : afterSectionId; } const response = await client.callApi( `/projects/${projectId}/sections/insert`, 'POST', { project_gid: projectId }, {}, {}, {}, body, ['token'], ['application/json'], ['application/json'], 'Blob' ); return { project_id: projectId, section_id: sectionId, status: "success (fallback method)", before_section: beforeSectionId, after_section: afterSectionId, result: response.data }; } catch (fallbackError) { console.error("Error in fallback method:", fallbackError); throw error; // Aruncăm eroarea originală } } } async getAttachmentsForObject(objectId: string, opts: any = {}) { const response = await this.attachments.getAttachmentsForObject(objectId, opts); return response.data; } async getAttachment(attachmentId: string, opts: any = {}) { const response = await this.attachments.getAttachment(attachmentId, opts); return response.data; } async uploadAttachmentForObject(objectId: string, filePath: string, fileName?: string, fileType?: string) { const fs = await import('fs'); const path = await import('path'); if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const form = new FormData(); const name = fileName || path.basename(filePath); const buffer = await fs.promises.readFile(filePath); const file = new File([buffer], name, { type: fileType || 'application/octet-stream' }); form.append('parent', objectId); form.append('file', file); const token = Asana.ApiClient.instance.authentications['token'].accessToken; const response = await fetch('https://app.asana.com/api/1.0/attachments', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form as any }); if (!response.ok) { throw new Error(`Upload failed: ${response.status} ${await response.text()}`); } const result = await response.json(); // Am adăugat tipare explicită pentru a rezolva avertismentul TypeScript const typedResult = result as { data: any }; return typedResult.data; } private extensionForMime(mime: string): string { const map: Record<string, string> = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'application/pdf': '.pdf', 'text/plain': '.txt', 'application/zip': '.zip', 'application/json': '.json' }; return map[mime] || ''; } async downloadAttachment(attachmentId: string, outputDir?: string) { const fs = await import('fs'); const path = await import('path'); const os = await import('os'); const { pipeline } = await import('stream/promises'); outputDir = outputDir || path.join(os.homedir(), 'downloads'); const attachment = await this.getAttachment(attachmentId); const downloadUrl = attachment.download_url || attachment.downloadUrl; if (!downloadUrl) { throw new Error('Attachment does not have a download_url'); } await fs.promises.mkdir(outputDir, { recursive: true }); const res = await fetch(downloadUrl); if (!res.ok || !res.body) { throw new Error(`Failed to download attachment: ${res.status}`); } let filename: string = attachment.name || attachment.gid; const contentType = res.headers.get('content-type') || attachment.mime_type; if (!path.extname(filename) && contentType) { filename += this.extensionForMime(contentType); } const filePath = path.join(outputDir, filename); const fileStream = fs.createWriteStream(filePath); await pipeline(res.body, fileStream); return { attachment_id: attachmentId, file_path: filePath, mime_type: contentType }; } }

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/cristip73/mcp-server-asana'

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