import type { IDataObject } from 'n8n-workflow'; export const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export interface AlgaApiError { statusCode?: number; code: string; message: string; details?: unknown; } function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } export function compactObject(input: IDataObject): IDataObject { return Object.entries(input).reduce((acc, [key, value]) => { if (value === undefined || value === null || value === '') { return acc; } if (Array.isArray(value) && value.length === 0) { return acc; } acc[key] = value; return acc; }, {} as IDataObject); } export function parseCsvList(value: unknown): string[] { if (!value) { return []; } return String(value) .split(',') .map((entry) => entry.trim()) .filter(Boolean); } export function parseTags(value: unknown): string[] | undefined { const tags = parseCsvList(value); return tags.length > 0 ? tags : undefined; } export function parseAttributes(value: unknown): IDataObject | undefined { if (!value) { return undefined; } if (isObject(value)) { return value as IDataObject; } const raw = String(value).trim(); if (!raw) { return undefined; } const parsed = JSON.parse(raw); if (!isObject(parsed)) { throw new Error('Attributes must be a JSON object'); } return parsed as IDataObject; } export function ensureNonEmpty(value: unknown, fieldName: string): string { const normalized = String(value ?? '').trim(); if (!normalized) { throw new Error(`${fieldName} is required`); } return normalized; } export function ensureUuid(value: unknown, fieldName: string): string { const normalized = ensureNonEmpty(value, fieldName); if (!UUID_REGEX.test(normalized)) { throw new Error(`${fieldName} must be a valid UUID`); } return normalized; } export function extractResourceLocatorValue(value: unknown): string { if (typeof value === 'string') { return value.trim(); } if (isObject(value) && 'value' in value) { return String(value.value ?? '').trim(); } return ''; } export function buildTicketCreatePayload(input: { title: string; clientId: string; boardId: string; statusId: string; priorityId: string; additionalFields?: IDataObject; }): IDataObject { const additional = input.additionalFields ?? {}; const payload: IDataObject = { title: input.title, client_id: input.clientId, board_id: input.boardId, status_id: input.statusId, priority_id: input.priorityId, location_id: additional.location_id, contact_name_id: additional.contact_name_id, category_id: additional.category_id, subcategory_id: additional.subcategory_id, assigned_to: additional.assigned_to, url: additional.url, attributes: parseAttributes(additional.attributes), tags: parseTags(additional.tags), }; return compactObject(payload); } export function buildTicketUpdatePayload(additionalFields: IDataObject = {}): IDataObject { const payload: IDataObject = { title: additionalFields.title, client_id: additionalFields.client_id, board_id: additionalFields.board_id, status_id: additionalFields.status_id, priority_id: additionalFields.priority_id, location_id: additionalFields.location_id, contact_name_id: additionalFields.contact_name_id, category_id: additionalFields.category_id, subcategory_id: additionalFields.subcategory_id, assigned_to: additionalFields.assigned_to, url: additionalFields.url, attributes: parseAttributes(additionalFields.attributes), tags: parseTags(additionalFields.tags), }; return compactObject(payload); } function normalizeOptionalUuid(value: unknown, fieldName: string): string | undefined { if (value === undefined || value === null || String(value).trim() === '') { return undefined; } return ensureUuid(value, fieldName); } function normalizeJsonInput(value: unknown, fieldName: string): unknown { if (value === undefined || value === null) { return undefined; } if (typeof value !== 'string') { return value; } const raw = value.trim(); if (!raw) { return undefined; } try { return JSON.parse(raw); } catch { throw new Error(`${fieldName} must be valid JSON`); } } export function parseContactPhoneNumbers(value: unknown): IDataObject[] | undefined { const parsed = normalizeJsonInput(value, 'phone_numbers'); if (parsed === undefined) { return undefined; } if (!Array.isArray(parsed)) { throw new Error('phone_numbers must be a JSON array'); } return parsed.map((entry, index) => { if (!isObject(entry)) { throw new Error(`phone_numbers[${index}] must be an object`); } const phoneNumber = ensureNonEmpty(entry.phone_number, `phone_numbers[${index}].phone_number`); const contactPhoneNumberId = normalizeOptionalUuid( entry.contact_phone_number_id, `phone_numbers[${index}].contact_phone_number_id`, ); const canonicalType = entry.canonical_type === undefined || entry.canonical_type === null ? undefined : ensureNonEmpty(entry.canonical_type, `phone_numbers[${index}].canonical_type`); const customType = entry.custom_type === undefined || entry.custom_type === null ? undefined : ensureNonEmpty(entry.custom_type, `phone_numbers[${index}].custom_type`); if (entry.is_default !== undefined && typeof entry.is_default !== 'boolean') { throw new Error(`phone_numbers[${index}].is_default must be a boolean`); } if ( entry.display_order !== undefined && (!Number.isInteger(entry.display_order) || Number(entry.display_order) < 0) ) { throw new Error(`phone_numbers[${index}].display_order must be a non-negative integer`); } const isDefault = entry.is_default === undefined ? undefined : (entry.is_default as boolean); const displayOrder = entry.display_order === undefined ? undefined : Number(entry.display_order); return compactObject({ contact_phone_number_id: contactPhoneNumberId, phone_number: phoneNumber, canonical_type: canonicalType, custom_type: customType, is_default: isDefault, display_order: displayOrder, }); }); } export function parseContactEmailAddresses(value: unknown): IDataObject[] | undefined { const parsed = normalizeJsonInput(value, 'additional_email_addresses'); if (parsed === undefined) { return undefined; } if (!Array.isArray(parsed)) { throw new Error('additional_email_addresses must be a JSON array'); } return parsed.map((entry, index) => { if (!isObject(entry)) { throw new Error(`additional_email_addresses[${index}] must be an object`); } const emailAddress = ensureNonEmpty( entry.email_address, `additional_email_addresses[${index}].email_address`, ); const additionalEmailId = normalizeOptionalUuid( entry.contact_additional_email_address_id, `additional_email_addresses[${index}].contact_additional_email_address_id`, ); const canonicalType = entry.canonical_type === undefined || entry.canonical_type === null ? undefined : ensureNonEmpty(entry.canonical_type, `additional_email_addresses[${index}].canonical_type`); const customType = entry.custom_type === undefined || entry.custom_type === null ? undefined : ensureNonEmpty(entry.custom_type, `additional_email_addresses[${index}].custom_type`); if ( entry.display_order !== undefined && (!Number.isInteger(entry.display_order) || Number(entry.display_order) < 0) ) { throw new Error( `additional_email_addresses[${index}].display_order must be a non-negative integer`, ); } const displayOrder = entry.display_order === undefined ? undefined : Number(entry.display_order); return compactObject({ contact_additional_email_address_id: additionalEmailId, email_address: emailAddress, canonical_type: canonicalType, custom_type: customType, display_order: displayOrder, }); }); } export function buildContactCreatePayload(input: { fullName: string; additionalFields?: IDataObject; }): IDataObject { const additional = input.additionalFields ?? {}; const clientId = normalizeOptionalUuid(additional.client_id, 'client_id'); return compactObject({ full_name: input.fullName, email: additional.email, primary_email_canonical_type: additional.primary_email_canonical_type, primary_email_custom_type: additional.primary_email_custom_type, primary_email_custom_type_id: normalizeOptionalUuid( additional.primary_email_custom_type_id, 'primary_email_custom_type_id', ), additional_email_addresses: parseContactEmailAddresses(additional.additional_email_addresses), client_id: clientId, role: additional.role, notes: additional.notes, is_inactive: additional.is_inactive, phone_numbers: parseContactPhoneNumbers(additional.phone_numbers), }); } export function buildContactUpdatePayload(additionalFields: IDataObject = {}): IDataObject { const clientId = normalizeOptionalUuid(additionalFields.client_id, 'client_id'); return compactObject({ full_name: additionalFields.full_name, email: additionalFields.email, primary_email_canonical_type: additionalFields.primary_email_canonical_type, primary_email_custom_type: additionalFields.primary_email_custom_type, primary_email_custom_type_id: normalizeOptionalUuid( additionalFields.primary_email_custom_type_id, 'primary_email_custom_type_id', ), additional_email_addresses: parseContactEmailAddresses(additionalFields.additional_email_addresses), client_id: clientId, role: additionalFields.role, notes: additionalFields.notes, is_inactive: additionalFields.is_inactive, phone_numbers: parseContactPhoneNumbers(additionalFields.phone_numbers), }); } function normalizeOptionalNumber(value: unknown, fieldName: string): number | undefined { if (value === undefined || value === null || value === '') { return undefined; } const parsed = typeof value === 'number' ? value : Number(value); if (!Number.isFinite(parsed)) { throw new Error(`${fieldName} must be a number`); } if (parsed < 0) { throw new Error(`${fieldName} must be a non-negative number`); } return parsed; } function normalizeOptionalString(value: unknown): string | undefined { if (value === undefined || value === null) { return undefined; } const trimmed = String(value).trim(); return trimmed === '' ? undefined : trimmed; } export function buildProjectTaskCreatePayload(input: { taskName: string; statusMappingId: string; additionalFields?: IDataObject; }): IDataObject { const additional = input.additionalFields ?? {}; return compactObject({ task_name: input.taskName, project_status_mapping_id: input.statusMappingId, description: normalizeOptionalString(additional.description), assigned_to: normalizeOptionalUuid(additional.assigned_to, 'assigned_to'), estimated_hours: normalizeOptionalNumber(additional.estimated_hours, 'estimated_hours'), due_date: normalizeOptionalString(additional.due_date), priority_id: normalizeOptionalUuid(additional.priority_id, 'priority_id'), task_type_key: normalizeOptionalString(additional.task_type_key), wbs_code: normalizeOptionalString(additional.wbs_code), tags: parseTags(additional.tags), }); } export function buildProjectTaskUpdatePayload(additionalFields: IDataObject = {}): IDataObject { return compactObject({ task_name: normalizeOptionalString(additionalFields.task_name), description: normalizeOptionalString(additionalFields.description), assigned_to: normalizeOptionalUuid(additionalFields.assigned_to, 'assigned_to'), estimated_hours: normalizeOptionalNumber(additionalFields.estimated_hours, 'estimated_hours'), due_date: normalizeOptionalString(additionalFields.due_date), priority_id: normalizeOptionalUuid(additionalFields.priority_id, 'priority_id'), task_type_key: normalizeOptionalString(additionalFields.task_type_key), project_status_mapping_id: normalizeOptionalUuid( additionalFields.project_status_mapping_id, 'project_status_mapping_id', ), wbs_code: normalizeOptionalString(additionalFields.wbs_code), tags: parseTags(additionalFields.tags), }); } export function buildProjectTaskListQuery(input: { page: number; limit: number; }): IDataObject { return compactObject({ page: input.page, limit: input.limit, }); } export function buildContactListQuery(input: { page: number; limit: number; filters?: IDataObject; }): IDataObject { const filters = input.filters ?? {}; const clientId = normalizeOptionalUuid(filters.client_id, 'client_id'); return compactObject({ page: input.page, limit: input.limit, client_id: clientId, search_term: filters.search_term, is_inactive: filters.is_inactive, }); } export function buildTicketListQuery(input: { page: number; limit: number; sort?: string; order?: string; filters?: IDataObject; }): IDataObject { return compactObject({ page: input.page, limit: input.limit, sort: input.sort, order: input.order, ...(input.filters ?? {}), }); } export function buildTicketSearchQuery(input: { query: string; limit?: number; includeClosed?: boolean; fields?: string[]; statusIds?: string[]; priorityIds?: string[]; clientIds?: string[]; assignedToIds?: string[]; }): IDataObject { return compactObject({ query: input.query, limit: input.limit, include_closed: input.includeClosed, fields: input.fields && input.fields.length > 0 ? input.fields.join(',') : undefined, status_ids: input.statusIds && input.statusIds.length > 0 ? input.statusIds.join(',') : undefined, priority_ids: input.priorityIds && input.priorityIds.length > 0 ? input.priorityIds.join(',') : undefined, client_ids: input.clientIds && input.clientIds.length > 0 ? input.clientIds.join(',') : undefined, assigned_to_ids: input.assignedToIds && input.assignedToIds.length > 0 ? input.assignedToIds.join(',') : undefined, }); } export function buildTicketCommentListQuery(options: IDataObject = {}): IDataObject { return compactObject({ limit: options.limit, offset: options.offset, order: options.order, }); } export function buildTicketCommentPayload( commentText: string, additionalFields: IDataObject = {}, ): IDataObject { return compactObject({ comment_text: commentText, is_internal: additionalFields.is_internal, }); } export function normalizeSuccessResponse(response: unknown): IDataObject { if (response === undefined || response === null) { return {}; } if (Array.isArray(response)) { return { data: response }; } if (isObject(response) && 'data' in response) { const data = response.data; const pagination = response.pagination; if (pagination !== undefined) { return { data: Array.isArray(data) ? data : data ?? [], pagination, } as IDataObject; } if (Array.isArray(data)) { return { data }; } if (isObject(data)) { return data as IDataObject; } if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { return { data }; } return { data: JSON.stringify(data ?? null) }; } if (isObject(response)) { return response as IDataObject; } return { data: response as string | number | boolean }; } export function normalizeDeleteSuccess(id: string, response?: unknown): IDataObject { const normalized = normalizeSuccessResponse(response); return { success: true, id, deleted: true, ...normalized, }; } function parseErrorBody(value: unknown): Record | undefined { if (isObject(value)) { return value; } if (typeof value === 'string') { try { const parsed = JSON.parse(value); if (isObject(parsed)) { return parsed; } } catch { return undefined; } } return undefined; } export function formatAlgaApiError(error: unknown): AlgaApiError { const raw = (error ?? {}) as Record; const response = isObject(raw.response) ? raw.response : undefined; const responseBody = parseErrorBody(raw.responseBody) ?? parseErrorBody(raw.body); const responseData = response ? parseErrorBody(response.data) : undefined; const merged = responseBody ?? responseData; const nestedError = merged && isObject(merged.error) ? merged.error : undefined; const statusCode = (typeof raw.httpCode === 'number' ? raw.httpCode : undefined) ?? (typeof raw.statusCode === 'number' ? raw.statusCode : undefined) ?? (response && typeof response.status === 'number' ? response.status : undefined); const code = (nestedError && typeof nestedError.code === 'string' ? nestedError.code : undefined) ?? (merged && typeof merged.code === 'string' ? merged.code : undefined) ?? (typeof raw.code === 'string' ? raw.code : undefined) ?? (statusCode ? `HTTP_${statusCode}` : 'UNKNOWN_ERROR'); const message = (nestedError && typeof nestedError.message === 'string' ? nestedError.message : undefined) ?? (merged && typeof merged.message === 'string' ? merged.message : undefined) ?? (typeof raw.message === 'string' ? raw.message : undefined) ?? 'Request failed'; const details = (nestedError && 'details' in nestedError ? nestedError.details : undefined) ?? (merged && 'details' in merged ? merged.details : undefined); return { statusCode, code, message, details, }; }