import * as z from "zod" import { defineAction } from "../../../automation/actions" import { ASANA_JOB_SCHEMA, ASANA_PAGE_INFO_SCHEMA, ASANA_PROVIDER_JOB_SCHEMA, ASANA_PROVIDER_PAGE_SCHEMA, ASANA_PROVIDER_TASK_SCHEMA, ASANA_TASK_FIELDS, ASANA_TASK_SCHEMA, getAsanaApi, toAsanaJob, toAsanaPageInfo, toAsanaTask, } from "@automate.ax/integration-contracts/asana" import { ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE } from "./scopes" const JOB_FIELDS = "gid,resource_type,resource_subtype,status,new_task.gid,new_task.name,new_task.resource_type,new_project.gid,new_project.name,new_project.resource_type" const TASK_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_TASK_SCHEMA }) const TASK_PAGE_RESPONSE_SCHEMA = ASANA_PROVIDER_PAGE_SCHEMA.extend({ data: ASANA_PROVIDER_TASK_SCHEMA.array(), }) const JOB_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_JOB_SCHEMA }) const EMPTY_RESPONSE_SCHEMA = z.looseObject({ data: z.looseObject({}) }) const PAGE_INPUT_SCHEMA = z.object({ limit: z.number().int().min(1).max(100).prefault(100), offset: z.string().min(1).optional(), taskId: z.string().min(1), }) const TASK_PAGE_OUTPUT_SCHEMA = z.object({ pageInfo: ASANA_PAGE_INFO_SCHEMA, tasks: ASANA_TASK_SCHEMA.array(), }) /** Gets one asynchronous Asana job. */ export const getAsanaJob = defineAction("Get Asana job") .describe("Gets the current state and created resource for an Asana job.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(z.object({ jobId: z.string().min(1) })) .output(ASANA_JOB_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toAsanaJob( ( await getAsanaApi(account.secret).call(`/jobs/${input.jobId}`, { query: { opt_fields: JOB_FIELDS }, responseSchema: JOB_RESPONSE_SCHEMA, }) ).data, ), ) /** Duplicates an Asana task asynchronously. */ export const duplicateAsanaTask = defineAction("Duplicate Asana task") .describe("Starts an asynchronous job that duplicates an Asana task.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z.object({ include: z .enum([ "assignee", "attachments", "dates", "dependencies", "followers", "notes", "parent", "projects", "subtasks", "tags", ]) .array() .optional(), name: z.string().min(1).optional(), taskId: z.string().min(1), }), ) .output(ASANA_JOB_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaJob( ( await getAsanaApi(account.secret).call( `/tasks/${input.taskId}/duplicate`, { body: { ...(input.include && { include: input.include.join(",") }), ...(input.name && { name: input.name }), }, query: { opt_fields: JOB_FIELDS }, responseSchema: JOB_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Changes or clears an Asana task's parent. */ export const setAsanaTaskParent = defineAction("Set Asana task parent") .describe("Moves a task under a parent task or clears its parent.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z .object({ insertAfterTaskId: z.string().min(1).optional(), insertBeforeTaskId: z.string().min(1).optional(), parentTaskId: z.string().min(1).nullable(), taskId: z.string().min(1), }) .refine( ({ insertAfterTaskId, insertBeforeTaskId }) => !insertAfterTaskId || !insertBeforeTaskId, { message: "Provide only one subtask position.", path: ["insertBeforeTaskId"], }, ), ) .output(ASANA_TASK_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaTask( ( await getAsanaApi(account.secret).call( `/tasks/${input.taskId}/setParent`, { body: { ...(input.insertAfterTaskId && { insert_after: input.insertAfterTaskId, }), ...(input.insertBeforeTaskId && { insert_before: input.insertBeforeTaskId, }), parent: input.parentTaskId, }, query: { opt_fields: ASANA_TASK_FIELDS }, responseSchema: TASK_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Lists direct dependencies of an Asana task. */ export const listAsanaTaskDependencies = defineAction( "List Asana task dependencies", ) .describe("Lists tasks that block an Asana task.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PAGE_INPUT_SCHEMA) .output(TASK_PAGE_OUTPUT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getTaskPage(account.secret, `/tasks/${input.taskId}/dependencies`, input), ) /** Lists direct dependents of an Asana task. */ export const listAsanaTaskDependents = defineAction( "List Asana task dependents", ) .describe("Lists tasks blocked by an Asana task.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PAGE_INPUT_SCHEMA) .output(TASK_PAGE_OUTPUT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getTaskPage(account.secret, `/tasks/${input.taskId}/dependents`, input), ) const DEPENDENCY_MUTATION_SCHEMA = z.object({ dependencyTaskIds: z.string().min(1).array().min(1), taskId: z.string().min(1), }) /** Adds blocking dependencies to an Asana task. */ export const addAsanaTaskDependencies = defineAction( "Add Asana task dependencies", ) .describe("Adds tasks that must finish before an Asana task.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(DEPENDENCY_MUTATION_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await mutateDependencies(account.secret, input, "addDependencies") }) /** Removes blocking dependencies from an Asana task. */ export const removeAsanaTaskDependencies = defineAction( "Remove Asana task dependencies", ) .describe("Removes blocking relationships from an Asana task.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(DEPENDENCY_MUTATION_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await mutateDependencies(account.secret, input, "removeDependencies") }) /** * Loads one page of task graph neighbors. * * @param secret - Resolved Asana account secret. * @param path - Provider graph endpoint. * @param input - Pagination and task identifiers. */ async function getTaskPage( secret: Record, path: string, input: z.output, ) { const result = await getAsanaApi(secret).call(path, { query: { limit: input.limit, offset: input.offset, opt_fields: ASANA_TASK_FIELDS, }, responseSchema: TASK_PAGE_RESPONSE_SCHEMA, }) return { pageInfo: toAsanaPageInfo(result), tasks: result.data.map(toAsanaTask), } } /** * Applies one task dependency mutation. * * @param secret - Resolved Asana account secret. * @param input - Task and dependency identifiers. * @param operation - Provider operation name. */ async function mutateDependencies( secret: Record, input: z.output, operation: "addDependencies" | "removeDependencies", ) { await getAsanaApi(secret).call(`/tasks/${input.taskId}/${operation}`, { body: { dependencies: input.dependencyTaskIds }, responseSchema: EMPTY_RESPONSE_SCHEMA, }) }