import * as z from "zod" import { defineAction } from "../../../automation/actions" import { ASANA_CUSTOM_FIELD_SCHEMA, ASANA_ENUM_OPTION_SCHEMA, ASANA_PAGE_INFO_SCHEMA, ASANA_PROVIDER_CUSTOM_FIELD_SCHEMA, ASANA_PROVIDER_ENUM_OPTION_SCHEMA, ASANA_PROVIDER_PAGE_SCHEMA, getAsanaApi, toAsanaCustomField, toAsanaEnumOption, toAsanaPageInfo, } from "@automate.ax/integration-contracts/asana" import { ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE } from "./scopes" const CUSTOM_FIELD_FIELDS = "gid,name,resource_type,resource_subtype,description,enum_options.gid,enum_options.name,enum_options.resource_type,enum_options.color,enum_options.enabled,precision,format,currency_code,custom_label" const ENUM_OPTION_FIELDS = "gid,name,resource_type,color,enabled" const CUSTOM_FIELD_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_CUSTOM_FIELD_SCHEMA, }) const CUSTOM_FIELD_PAGE_RESPONSE_SCHEMA = ASANA_PROVIDER_PAGE_SCHEMA.extend({ data: ASANA_PROVIDER_CUSTOM_FIELD_SCHEMA.array(), }) const ENUM_OPTION_RESPONSE_SCHEMA = z.looseObject({ data: ASANA_PROVIDER_ENUM_OPTION_SCHEMA, }) const EMPTY_RESPONSE_SCHEMA = z.looseObject({ data: z.looseObject({}) }) const CUSTOM_FIELD_FORMAT_SCHEMA = z.enum([ "currency", "custom", "duration", "identifier", "none", "percentage", ]) const CUSTOM_FIELD_SUBTYPE_SCHEMA = z.enum([ "date", "enum", "multi_enum", "number", "people", "reference", "text", ]) const CUSTOM_FIELD_MUTATION_SCHEMA = z.object({ currencyCode: z.string().length(3).optional(), customLabel: z.string().optional(), description: z.string().optional(), format: CUSTOM_FIELD_FORMAT_SCHEMA.optional(), name: z.string().min(1).optional(), precision: z.number().int().min(0).max(6).optional(), }) const ENUM_OPTION_MUTATION_SCHEMA = z.object({ color: z.string().min(1).optional(), enabled: z.boolean().optional(), name: z.string().min(1).optional(), }) const CUSTOM_FIELD_ENUM_OPTION_INPUT_SCHEMA = z.object({ color: z.string().min(1).optional(), enabled: z.boolean().optional(), name: z.string().min(1), }) /** * Returns whether a custom-field update contains a mutable property. * * @param input - Candidate update. */ function hasCustomFieldMutation( input: z.input, ) { return Object.keys(input).length > 0 } /** * Returns whether an enum-option update contains a mutable property. * * @param input - Candidate update. */ function hasEnumOptionMutation( input: z.input, ) { return Object.keys(input).length > 0 } /** Gets one Asana custom field. */ export const getAsanaCustomField = defineAction("Get Asana custom field") .describe("Gets one Asana custom-field definition and its enum options.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(z.object({ customFieldId: z.string().min(1) })) .output(ASANA_CUSTOM_FIELD_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toAsanaCustomField( ( await getAsanaApi(account.secret).call( `/custom_fields/${input.customFieldId}`, { query: { opt_fields: CUSTOM_FIELD_FIELDS }, responseSchema: CUSTOM_FIELD_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Lists custom fields in an Asana workspace. */ export const listAsanaWorkspaceCustomFields = defineAction( "List Asana workspace custom fields", ) .describe("Lists custom-field definitions available in an Asana workspace.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z.object({ limit: z.number().int().min(1).max(100).prefault(100), offset: z.string().min(1).optional(), workspaceId: z.string().min(1), }), ) .output( z.object({ customFields: ASANA_CUSTOM_FIELD_SCHEMA.array(), pageInfo: ASANA_PAGE_INFO_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getAsanaApi(account.secret).call( `/workspaces/${input.workspaceId}/custom_fields`, { query: { limit: input.limit, offset: input.offset, opt_fields: CUSTOM_FIELD_FIELDS, }, responseSchema: CUSTOM_FIELD_PAGE_RESPONSE_SCHEMA, }, ) return { customFields: result.data.map(toAsanaCustomField), pageInfo: toAsanaPageInfo(result), } }) /** Creates an Asana custom field. */ export const createAsanaCustomField = defineAction("Create Asana custom field") .describe("Creates a custom-field definition in an Asana workspace.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( CUSTOM_FIELD_MUTATION_SCHEMA.extend({ enumOptions: CUSTOM_FIELD_ENUM_OPTION_INPUT_SCHEMA.array() .min(1) .optional(), name: z.string().min(1), subtype: CUSTOM_FIELD_SUBTYPE_SCHEMA, workspaceId: z.string().min(1), }).superRefine(({ enumOptions, subtype }, context) => { const isEnum = subtype === "enum" || subtype === "multi_enum" if (isEnum && enumOptions === undefined) { context.addIssue({ code: "custom", message: "enumOptions is required for enum and multi_enum fields.", path: ["enumOptions"], }) } if (!isEnum && enumOptions !== undefined) { context.addIssue({ code: "custom", message: "enumOptions is only valid for enum and multi_enum fields.", path: ["enumOptions"], }) } }), ) .output(ASANA_CUSTOM_FIELD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaCustomField( ( await getAsanaApi(account.secret).call("/custom_fields", { body: { ...(input.currencyCode && { currency_code: input.currencyCode }), ...(input.customLabel !== undefined && { custom_label: input.customLabel, }), ...(input.description !== undefined && { description: input.description, }), ...(input.enumOptions && { enum_options: input.enumOptions }), ...(input.format && { format: input.format }), name: input.name, ...(input.precision !== undefined && { precision: input.precision, }), resource_subtype: input.subtype, workspace: input.workspaceId, }, query: { opt_fields: CUSTOM_FIELD_FIELDS }, responseSchema: CUSTOM_FIELD_RESPONSE_SCHEMA, }) ).data, ), ) /** Updates an Asana custom field. */ export const updateAsanaCustomField = defineAction("Update Asana custom field") .describe("Updates an Asana custom-field definition.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( CUSTOM_FIELD_MUTATION_SCHEMA.extend({ customFieldId: z.string().min(1), }).refine( ({ customFieldId: _customFieldId, ...mutation }) => hasCustomFieldMutation(mutation), { message: "Provide at least one custom-field change." }, ), ) .output(ASANA_CUSTOM_FIELD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaCustomField( ( await getAsanaApi(account.secret).call( `/custom_fields/${input.customFieldId}`, { body: { ...(input.currencyCode && { currency_code: input.currencyCode }), ...(input.customLabel !== undefined && { custom_label: input.customLabel, }), ...(input.description !== undefined && { description: input.description, }), ...(input.format && { format: input.format }), ...(input.name !== undefined && { name: input.name }), ...(input.precision !== undefined && { precision: input.precision, }), }, httpMethod: "PUT", query: { opt_fields: CUSTOM_FIELD_FIELDS }, responseSchema: CUSTOM_FIELD_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Deletes an Asana custom field. */ export const deleteAsanaCustomField = defineAction("Delete Asana custom field") .describe("Permanently deletes an Asana custom-field definition.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(z.object({ customFieldId: z.string().min(1) })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getAsanaApi(account.secret).call( `/custom_fields/${input.customFieldId}`, { httpMethod: "DELETE", responseSchema: EMPTY_RESPONSE_SCHEMA }, ) }) /** Creates an enum option for an Asana custom field. */ export const createAsanaCustomFieldEnumOption = defineAction( "Create Asana custom field enum option", ) .describe("Creates and positions an enum option on an Asana custom field.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( ENUM_OPTION_MUTATION_SCHEMA.extend({ customFieldId: z.string().min(1), insertAfterOptionId: z.string().min(1).optional(), insertBeforeOptionId: z.string().min(1).optional(), name: z.string().min(1), }).refine( ({ insertAfterOptionId, insertBeforeOptionId }) => !insertAfterOptionId || !insertBeforeOptionId, { message: "Provide only one enum-option position.", path: ["insertBeforeOptionId"], }, ), ) .output(ASANA_ENUM_OPTION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaEnumOption( ( await getAsanaApi(account.secret).call( `/custom_fields/${input.customFieldId}/enum_options`, { body: { ...(input.color && { color: input.color }), ...(input.enabled !== undefined && { enabled: input.enabled }), ...(input.insertAfterOptionId && { insert_after: input.insertAfterOptionId, }), ...(input.insertBeforeOptionId && { insert_before: input.insertBeforeOptionId, }), name: input.name, }, query: { opt_fields: ENUM_OPTION_FIELDS }, responseSchema: ENUM_OPTION_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Updates an Asana custom-field enum option. */ export const updateAsanaCustomFieldEnumOption = defineAction( "Update Asana custom field enum option", ) .describe("Updates or disables an Asana custom-field enum option.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( ENUM_OPTION_MUTATION_SCHEMA.extend({ optionId: z.string().min(1), }).refine( ({ optionId: _optionId, ...mutation }) => hasEnumOptionMutation(mutation), { message: "Provide at least one enum-option change." }, ), ) .output(ASANA_ENUM_OPTION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toAsanaEnumOption( ( await getAsanaApi(account.secret).call( `/enum_options/${input.optionId}`, { body: { ...(input.color && { color: input.color }), ...(input.enabled !== undefined && { enabled: input.enabled }), ...(input.name !== undefined && { name: input.name }), }, httpMethod: "PUT", query: { opt_fields: ENUM_OPTION_FIELDS }, responseSchema: ENUM_OPTION_RESPONSE_SCHEMA, }, ) ).data, ), ) /** Repositions an Asana custom-field enum option. */ export const moveAsanaCustomFieldEnumOption = defineAction( "Move Asana custom field enum option", ) .describe("Moves an enum option before or after another option.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( z .object({ customFieldId: z.string().min(1), insertAfterOptionId: z.string().min(1).optional(), insertBeforeOptionId: z.string().min(1).optional(), optionId: z.string().min(1), }) .refine( ({ insertAfterOptionId, insertBeforeOptionId }) => Boolean(insertAfterOptionId) !== Boolean(insertBeforeOptionId), { message: "Provide exactly one enum-option position.", path: ["insertBeforeOptionId"], }, ), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getAsanaApi(account.secret).call( `/custom_fields/${input.customFieldId}/enum_options/insert`, { body: { ...(input.insertAfterOptionId && { after_enum_option: input.insertAfterOptionId, }), ...(input.insertBeforeOptionId && { before_enum_option: input.insertBeforeOptionId, }), enum_option: input.optionId, }, responseSchema: EMPTY_RESPONSE_SCHEMA, }, ) }) const PROJECT_CUSTOM_FIELD_INPUT_SCHEMA = z.object({ customFieldId: z.string().min(1), projectId: z.string().min(1), }) /** Adds a custom field to an Asana project. */ export const addAsanaProjectCustomField = defineAction( "Add Asana project custom field", ) .describe("Adds a custom field to an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input( PROJECT_CUSTOM_FIELD_INPUT_SCHEMA.extend({ isImportant: z.boolean().optional(), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getAsanaApi(account.secret).call( `/projects/${input.projectId}/addCustomFieldSetting`, { body: { custom_field: input.customFieldId, ...(input.isImportant !== undefined && { is_important: input.isImportant, }), }, responseSchema: EMPTY_RESPONSE_SCHEMA, }, ) }) /** Removes a custom field from an Asana project. */ export const removeAsanaProjectCustomField = defineAction( "Remove Asana project custom field", ) .describe("Removes a custom field from an Asana project.") .account(ASANA_ACCOUNT, ASANA_DEFAULT_SCOPE) .input(PROJECT_CUSTOM_FIELD_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getAsanaApi(account.secret).call( `/projects/${input.projectId}/removeCustomFieldSetting`, { body: { custom_field: input.customFieldId }, responseSchema: EMPTY_RESPONSE_SCHEMA, }, ) })