import * as z from "zod" import { defineAction } from "../../../automation/actions" import { ASANA_JOB_SCHEMA, ASANA_PROJECT_SCHEMA, ASANA_PROJECT_TASK_COUNTS_SCHEMA, ASANA_PROVIDER_JOB_SCHEMA, ASANA_PROVIDER_PROJECT_SCHEMA, ASANA_PROVIDER_PROJECT_TASK_COUNTS_SCHEMA, getAsanaApi, toAsanaJob, toAsanaProject, toAsanaProjectTaskCounts, } from "@automate.ax/integration-contracts/asana" import { ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE } from "./scopes" const PROJECT_FIELDS = "gid,name,resource_type,resource_subtype,archived,color,created_at,default_view,due_on,html_notes,modified_at,notes,permalink_url,public,start_on,team.gid,team.name,team.resource_type,workspace.gid,workspace.name,workspace.resource_type" const JOB_FIELDS = "gid,resource_type,resource_subtype,status,new_project.gid,new_project.name,new_project.resource_type" const PROJECT_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_PROJECT_SCHEMA, }) const PROJECT_PAGE_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_PROJECT_SCHEMA.array(), }) const JOB_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_JOB_SCHEMA }) const PROJECT_TASK_COUNTS_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_PROJECT_TASK_COUNTS_SCHEMA, }) /** Searches projects in an Asana workspace. */ export const searchAsanaProjects = defineAction("Search Asana projects") .describe("Searches projects by name in an Asana workspace.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z.object({ completed: z.boolean().optional(), limit: z.number().int().min(1).max(100).prefault(100), teamId: z.string().min(1).optional(), text: z.string().min(1), workspaceId: z.string().min(1), }), ) .output(z.object({ projects: ASANA_PROJECT_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ projects: ( await getAsanaApi(account.secret).call( `/workspaces/${input.workspaceId}/projects/search`, { query: { completed: input.completed, limit: input.limit, opt_fields: PROJECT_FIELDS, "teams.any": input.teamId, text: input.text, }, responseSchema: PROJECT_PAGE_RESPONSE_SCHEMA, }, ) ).data.map(toAsanaProject), })) /** Gets task and milestone counts for an Asana project. */ export const getAsanaProjectTaskCounts = defineAction( "Get Asana project task counts", ) .describe("Gets complete and incomplete task and milestone counts.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(z.object({ projectId: z.string().min(1) })) .output(ASANA_PROJECT_TASK_COUNTS_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toAsanaProjectTaskCounts( ( await getAsanaApi(account.secret).call( `/projects/${input.projectId}/task_counts`, { query: { opt_fields: "num_completed_milestones,num_completed_tasks,num_incomplete_milestones,num_incomplete_tasks,num_milestones,num_tasks", }, responseSchema: PROJECT_TASK_COUNTS_RESPONSE_SCHEMA, }, ) ).data, ), ) const PROJECT_DUPLICATE_FIELD_SCHEMA = z.enum([ "allocations", "forms", "members", "notes", "permissions", "task_assignee", "task_attachments", "task_dates", "task_dependencies", "task_followers", "task_notes", "task_projects", "task_subtasks", "task_tags", "task_templates", "task_type_default", ]) /** Duplicates an Asana project asynchronously. */ export const duplicateAsanaProject = defineAction("Duplicate Asana project") .describe("Starts an asynchronous job that duplicates an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z .object({ dueOn: z.iso.date().optional(), include: PROJECT_DUPLICATE_FIELD_SCHEMA.array().optional(), name: z.string().min(1), projectId: z.string().min(1), shouldSkipWeekends: z.boolean().optional(), startOn: z.iso.date().optional(), }) .refine(({ dueOn, startOn }) => !dueOn || !startOn, { message: "Provide only one date-shift anchor.", path: ["startOn"], }) .refine( ({ dueOn, include, startOn }) => (!dueOn && !startOn) || include?.includes("task_dates") === true, { message: "Include task_dates when shifting duplicated dates.", path: ["include"], }, ), ) .output(ASANA_JOB_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaJob( ( await getAsanaApi(account.secret).call( `/projects/${input.projectId}/duplicate`, { body: { ...(input.include && { include: input.include.join(",") }), name: input.name, ...((input.dueOn || input.startOn) && { schedule_dates: { ...(input.dueOn && { due_on: input.dueOn }), should_skip_weekends: input.shouldSkipWeekends ?? false, ...(input.startOn && { start_on: input.startOn }), }, }), }, query: { opt_fields: JOB_FIELDS }, responseSchema: JOB_RESPONSE_SCHEMA, }, ) ).data, ), ) const PROJECT_USERS_INPUT_SCHEMA = z.object({ projectId: z.string().min(1), userIds: z.string().min(1).array().min(1), }) /** Adds users as members of an Asana project. */ export const addAsanaProjectMembers = defineAction("Add Asana project members") .describe("Adds users as members of an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PROJECT_USERS_INPUT_SCHEMA) .output(ASANA_PROJECT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateProjectUsers(account.secret, input, "addMembers", "members"), ) /** Removes users from an Asana project. */ export const removeAsanaProjectMembers = defineAction( "Remove Asana project members", ) .describe("Removes users from an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PROJECT_USERS_INPUT_SCHEMA) .output(ASANA_PROJECT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateProjectUsers(account.secret, input, "removeMembers", "members"), ) /** Adds followers to an Asana project. */ export const addAsanaProjectFollowers = defineAction( "Add Asana project followers", ) .describe("Subscribes users to updates from an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PROJECT_USERS_INPUT_SCHEMA) .output(ASANA_PROJECT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateProjectUsers(account.secret, input, "addFollowers", "followers"), ) /** Removes followers from an Asana project. */ export const removeAsanaProjectFollowers = defineAction( "Remove Asana project followers", ) .describe("Unsubscribes users from updates from an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PROJECT_USERS_INPUT_SCHEMA) .output(ASANA_PROJECT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateProjectUsers(account.secret, input, "removeFollowers", "followers"), ) /** * Applies one project member or follower mutation. * * @param secret - Resolved Asana account secret. * @param input - Project and user identifiers. * @param operation - Provider operation name. * @param field - Provider request field. */ async function mutateProjectUsers( secret: Record, input: z.output, operation: | "addFollowers" | "addMembers" | "removeFollowers" | "removeMembers", field: "followers" | "members", ) { return toAsanaProject( ( await getAsanaApi(secret).call( `/projects/${input.projectId}/${operation}`, { body: { [field]: input.userIds }, query: { opt_fields: PROJECT_FIELDS }, responseSchema: PROJECT_RESPONSE_SCHEMA, }, ) ).data, ) }