import { AutotaskConfig, AutotaskHeaders, QueryParams, ApiResponse, ApiError, Resource, Ticket, Task, Company, ConfigurationItem, Attachment, EntityField, PicklistValue, } from '@/lib/types/autotask'; export class AutotaskClient { private config: AutotaskConfig; private rateLimiter: RateLimiter; constructor(config: AutotaskConfig) { this.config = config; this.rateLimiter = new RateLimiter(10); // 10 requests per second } private getAuthHeaders(impersonationResourceId?: number): Record { // Using the direct header authentication method (not Basic auth) const headers: Record = { 'Username': this.config.username, 'Secret': this.config.password, 'APIIntegrationcode': this.config.apiIntegrationCode, 'Content-Type': 'application/json', 'Accept': 'application/json', }; if (impersonationResourceId) { headers.ImpersonationResourceID = impersonationResourceId.toString(); } return headers; } private async makeApiCall( url: string, options: RequestInit ): Promise { await this.rateLimiter.throttle(); try { const response = await fetch(url, options); const responseText = await response.text(); if (!response.ok) { console.error(`Autotask API error: ${response.status} - ${responseText}`); let errorMessage = `API Error: ${response.statusText}`; try { const errorData: ApiError = JSON.parse(responseText); if (errorData.errors && errorData.errors.length > 0) { errorMessage = errorData.errors.map((e) => e.message).join(', '); } else if (errorData.message) { errorMessage = errorData.message; } } catch (parseError) { errorMessage = responseText || errorMessage; } throw new Error(errorMessage); } try { return JSON.parse(responseText); } catch (parseError) { console.error('Failed to parse API response:', parseError); throw new Error('Invalid JSON response from Autotask API'); } } catch (error) { console.error('API call failed:', error); throw error; } } private buildQueryString(params: QueryParams): string { if (!params.filter || params.filter.length === 0) { return ''; } const query = { filter: params.filter }; return `?search=${encodeURIComponent(JSON.stringify(query))}`; } async queryEntity( entityName: string, params: QueryParams = {} ): Promise { const queryString = this.buildQueryString(params); const url = `${this.config.apiUrl}/${entityName}/query${queryString}`; const response = await this.makeApiCall>(url, { method: 'GET', headers: this.getAuthHeaders(), }); return response.items || []; } // Paginated query to handle large datasets async queryEntityPaginated( entityName: string, params: QueryParams = {}, pageSize: number = 500 ): Promise { const allItems: T[] = []; let page = 1; let hasMore = true; while (hasMore) { const paginatedParams = { ...params, MaxRecords: pageSize, // Autotask uses MaxRecords for page size }; console.log(`Fetching ${entityName} page ${page} (max ${pageSize} records)...`); const queryString = this.buildQueryString(paginatedParams); const url = `${this.config.apiUrl}/${entityName}/query${queryString}`; const response = await this.makeApiCall>(url, { method: 'GET', headers: this.getAuthHeaders(), }); const items = response.items || []; allItems.push(...items); console.log(`Fetched ${items.length} ${entityName}, total so far: ${allItems.length}`); // If we got fewer items than pageSize, we've reached the end hasMore = items.length === pageSize; page++; // Safety limit to prevent infinite loops if (page > 100) { console.warn(`Reached page limit (100) for ${entityName}`); break; } } console.log(`Total ${entityName} fetched: ${allItems.length}`); return allItems; } async getEntityById(entityName: string, id: number): Promise { const url = `${this.config.apiUrl}/${entityName}/${id}`; const response = await this.makeApiCall>(url, { method: 'GET', headers: this.getAuthHeaders(), }); return response.item || null; } async createEntity(entityName: string, data: Partial): Promise { const url = `${this.config.apiUrl}/${entityName}`; const response = await this.makeApiCall>(url, { method: 'POST', headers: this.getAuthHeaders(), body: JSON.stringify(data), }); if (!response.item) { throw new Error('Failed to create entity'); } return response.item; } async updateEntity( entityName: string, id: number, data: Partial ): Promise { const url = `${this.config.apiUrl}/${entityName}/${id}`; const response = await this.makeApiCall>(url, { method: 'PATCH', headers: this.getAuthHeaders(), body: JSON.stringify(data), }); if (!response.item) { throw new Error('Failed to update entity'); } return response.item; } // Resource-specific methods async getResourceByEmail(email: string): Promise { const resources = await this.queryEntity('Resources', { filter: [{ op: 'eq', field: 'email', value: email }], }); return resources.length > 0 ? resources[0] : null; } async getAllResources(): Promise { return this.queryEntity('Resources', { filter: [{ op: 'eq', field: 'isActive', value: true }], }); } // Ticket-specific methods async getOpenTicketsByResource(resourceId: number): Promise { return this.queryEntity('Tickets', { filter: [ { op: 'eq', field: 'assignedResourceID', value: resourceId }, { op: 'noteq', field: 'status', value: 5 }, // Exclude completed ], }); } async getTicketsByCompany(companyId: number): Promise { return this.queryEntity('Tickets', { filter: [{ op: 'eq', field: 'companyID', value: companyId }], }); } async createTicket(ticket: Partial): Promise { return this.createEntity('Tickets', ticket); } async updateTicket(id: number, updates: Partial): Promise { return this.updateEntity('Tickets', id, updates); } // Task-specific methods async getTasksByResource(resourceId: number): Promise { return this.queryEntity('Tasks', { filter: [ { op: 'eq', field: 'assignedResourceID', value: resourceId }, { op: 'noteq', field: 'status', value: 5 }, // Exclude completed ], }); } async getTasksByProject(projectId: number): Promise { return this.queryEntity('Tasks', { filter: [{ op: 'eq', field: 'projectID', value: projectId }], }); } async createTask(task: Partial): Promise { return this.createEntity('Tasks', task); } async updateTask(id: number, updates: Partial): Promise { return this.updateEntity('Tasks', id, updates); } // Company-specific methods async getAllCompanies(): Promise { const companies = await this.queryEntity('Companies', { filter: [{ op: 'eq', field: 'isActive', value: true }], }); return companies.sort((a, b) => a.companyName.localeCompare(b.companyName) ); } async getCompanyById(id: number): Promise { return this.getEntityById('Companies', id); } // Configuration Item methods async getConfigurationItemsByCompany(companyId: number): Promise { return this.queryEntity('ConfigurationItems', { filter: [ { op: 'eq', field: 'companyID', value: companyId }, { op: 'eq', field: 'isActive', value: true } ], }); } async getAllConfigurationItems(): Promise { return this.queryEntity('ConfigurationItems', { filter: [{ op: 'eq', field: 'isActive', value: true }], }); } async getConfigurationItemById(id: number): Promise { // Use query instead of direct GET as ConfigurationItems might not support direct ID access const items = await this.queryEntity('ConfigurationItems', { filter: [{ op: 'eq', field: 'id', value: id }], }); return items.length > 0 ? items[0] : null; } async createConfigurationItem(item: Partial): Promise { return this.createEntity('ConfigurationItems', item); } async updateConfigurationItem(id: number, updates: Partial): Promise { return this.updateEntity('ConfigurationItems', id, updates); } // Picklist methods async getPicklistValues( entityName: string, fieldName: string ): Promise> { const url = `${this.config.apiUrl}/${entityName}/entityInformation/fields`; const response = await this.makeApiCall<{ fields: EntityField[] }>(url, { method: 'GET', headers: this.getAuthHeaders(), }); const field = response.fields.find((f) => f.name === fieldName); if (field && field.picklistValues) { const picklistMap: Record = {}; field.picklistValues.forEach((item) => { picklistMap[item.value] = item.label; }); return picklistMap; } return {}; } async getTicketStatusPicklist(): Promise> { return this.getPicklistValues('Tickets', 'status'); } async getTicketPriorityPicklist(): Promise> { return this.getPicklistValues('Tickets', 'priority'); } async getTaskStatusPicklist(): Promise> { return this.getPicklistValues('Tasks', 'status'); } // Attachment methods async uploadAttachment( entityName: string, entityId: number, fileBuffer: Buffer, fileName: string, impersonatorEmail?: string ): Promise { let impersonationResourceId: number | undefined; if (impersonatorEmail) { const resource = await this.getResourceByEmail(impersonatorEmail); if (resource) { impersonationResourceId = resource.id; } } const base64Data = fileBuffer.toString('base64'); const payload: Partial = { id: 0, attachmentType: 'FILE_ATTACHMENT', fullPath: fileName, title: fileName, publish: 1, // All Autotask Users data: base64Data, }; const url = `${this.config.apiUrl}/${entityName}/${entityId}/Attachments`; const response = await this.makeApiCall>(url, { method: 'POST', headers: this.getAuthHeaders(impersonationResourceId), body: JSON.stringify(payload), }); if (!response.item) { throw new Error('Failed to upload attachment'); } return response.item; } async getAttachments( entityName: string, entityId: number ): Promise { const url = `${this.config.apiUrl}/${entityName}/${entityId}/Attachments`; const response = await this.makeApiCall>(url, { method: 'GET', headers: this.getAuthHeaders(), }); return response.items || []; } } // Rate Limiter class class RateLimiter { private maxRequestsPerSecond: number; private requestTimes: number[]; constructor(maxRequestsPerSecond = 10) { this.maxRequestsPerSecond = maxRequestsPerSecond; this.requestTimes = []; } async throttle(): Promise { const now = Date.now(); const oneSecondAgo = now - 1000; // Remove old request times this.requestTimes = this.requestTimes.filter((t) => t > oneSecondAgo); // If at limit, wait if (this.requestTimes.length >= this.maxRequestsPerSecond) { const oldestRequest = this.requestTimes[0]; const waitTime = 1000 - (now - oldestRequest); if (waitTime > 0) { await new Promise((resolve) => setTimeout(resolve, waitTime)); } } this.requestTimes.push(Date.now()); } }