import * as z from "zod" import { defineAction, terminalActionError } from "../../../automation/actions" import { TRELLO_READ_SCOPE, TRELLO_WRITE_SCOPE } from "./scopes" import { getTrelloApi, normalizeTrelloCardId, } from "@automate.ax/integration-contracts/trello" import { TRELLO_CHECKLIST_SCHEMA, TRELLO_CHECK_ITEM_SCHEMA, TRELLO_PROVIDER_CHECKLIST_SCHEMA, TRELLO_PROVIDER_CHECK_ITEM_SCHEMA, toTrelloChecklist, toTrelloCheckItem, } from "@automate.ax/integration-contracts/trello" const TRELLO_ACCOUNT = "trello" const POSITION_SCHEMA = z.union([ z.enum(["top", "bottom"]), z.number().nonnegative(), ]) const DATE_INPUT_SCHEMA = z.union([ z.date(), z.iso.datetime({ offset: true }).transform((value) => new Date(value)), ]) /** Lists checklists and checklist items on a Trello card. */ export const listTrelloChecklists = defineAction("List Trello checklists") .describe("Lists every checklist and item on a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_READ_SCOPE) .input(z.object({ card: z.string().trim().min(1) })) .output(TRELLO_CHECKLIST_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(normalizeTrelloCardId(input.card))}/checklists`, { query: { checkItem_fields: "id,name,state,pos,due,dueReminder,idMember", checkItems: "all", fields: "id,idBoard,idCard,name,pos", }, responseSchema: TRELLO_PROVIDER_CHECKLIST_SCHEMA.array(), }, ) ).map(toTrelloChecklist), ) /** Creates a checklist on a Trello card. */ export const createTrelloChecklist = defineAction("Create Trello checklist") .describe("Creates a checklist, optionally copying an existing checklist.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ /** Card ID, short link, or standard Trello card URL. */ card: z.string().trim().min(1), /** Checklist name. */ name: z.string().trim().min(1).optional(), /** Initial checklist position. */ position: POSITION_SCHEMA.prefault("bottom"), /** Existing checklist ID to copy. */ sourceChecklistId: z.string().trim().min(1).optional(), }), ) .output(TRELLO_CHECKLIST_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toTrelloChecklist( await getTrelloApi(account.secret).request("checklists", { method: "POST", query: { idCard: normalizeTrelloCardId(input.card), idChecklistSource: input.sourceChecklistId, name: input.name, pos: input.position, }, responseSchema: TRELLO_PROVIDER_CHECKLIST_SCHEMA, }), ), ) /** Updates a Trello checklist's name or position. */ export const updateTrelloChecklist = defineAction("Update Trello checklist") .describe("Renames or reorders a Trello checklist.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z .object({ checklistId: z.string().trim().min(1), name: z.string().trim().min(1).optional(), position: POSITION_SCHEMA.optional(), }) .refine( ({ name, position }) => name !== undefined || position !== undefined, "Provide a checklist name or position to update.", ), ) .output(TRELLO_CHECKLIST_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toTrelloChecklist( await getTrelloApi(account.secret).request( `checklists/${encodeURIComponent(input.checklistId)}`, { method: "PUT", query: { name: input.name, pos: input.position }, responseSchema: TRELLO_PROVIDER_CHECKLIST_SCHEMA, }, ), ), ) /** Deletes a checklist from a Trello card. */ export const deleteTrelloChecklist = defineAction("Delete Trello checklist") .describe("Deletes a checklist and its items from a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input(z.object({ checklistId: z.string().trim().min(1) })) .output(z.object({ checklistId: z.string(), deleted: z.literal(true) })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getTrelloApi(account.secret).request( `checklists/${encodeURIComponent(input.checklistId)}`, { method: "DELETE", responseSchema: z.unknown() }, ) return { checklistId: input.checklistId, deleted: true as const } }) /** Adds an item to a Trello checklist. */ export const createTrelloChecklistItem = defineAction( "Create Trello checklist item", ) .describe("Adds a dated or assigned item to a Trello checklist.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ /** Whether the item begins completed. */ completed: z.boolean().prefault(false), /** Stable Trello checklist ID. */ checklistId: z.string().trim().min(1), /** Item due time. */ dueAt: DATE_INPUT_SCHEMA.optional(), /** Reminder offset in minutes. */ dueReminder: z.number().int().min(-1).optional(), /** Member ID assigned to the item. */ memberId: z.string().trim().min(1).optional(), /** Checklist item name. */ name: z.string().trim().min(1), /** Initial item position. */ position: POSITION_SCHEMA.prefault("bottom"), }), ) .output(TRELLO_CHECK_ITEM_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getTrelloApi(account.secret) const item = toTrelloCheckItem( await api.request( `checklists/${encodeURIComponent(input.checklistId)}/checkItems`, { method: "POST", query: { checked: input.completed, due: input.dueAt?.toISOString(), dueReminder: input.dueReminder, idMember: input.memberId, name: input.name, pos: input.position, }, responseSchema: TRELLO_PROVIDER_CHECK_ITEM_SCHEMA, }, ), ) const ignoredFields = getIgnoredAdvancedChecklistFields(input, item) if (ignoredFields.length === 0) return item const message = getAdvancedChecklistFieldsError(ignoredFields) try { await api.request( `checklists/${encodeURIComponent(input.checklistId)}/checkItems/${encodeURIComponent(item.id)}`, { method: "DELETE", responseSchema: z.unknown() }, ) } catch (error) { throw new Error( `${message} Trello created checklist item ${item.id}, and its automatic cleanup failed.`, { cause: error }, ) } throw terminalActionError(new Error(message)) }) /** Updates a Trello checklist item. */ export const updateTrelloChecklistItem = defineAction( "Update Trello checklist item", ) .describe("Updates an item's text, state, order, due time, or assignee.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z .object({ /** Card ID, short link, or standard Trello card URL. */ card: z.string().trim().min(1), /** Clear the existing due time. */ clearDue: z.boolean().optional(), /** New due time. */ dueAt: DATE_INPUT_SCHEMA.optional(), /** New reminder offset in minutes. */ dueReminder: z.number().int().min(-1).optional(), /** Stable Trello checklist item ID. */ itemId: z.string().trim().min(1), /** Assign the item to this member. */ memberId: z.string().trim().min(1).optional(), /** Replacement item name. */ name: z.string().trim().min(1).optional(), /** New item position. */ position: POSITION_SCHEMA.optional(), /** New item completion state. */ state: z.enum(["complete", "incomplete"]).optional(), }) .refine(({ clearDue, dueAt }) => !(clearDue && dueAt), { message: "Provide dueAt or clearDue, not both.", path: ["clearDue"], }) .refine( (input) => input.clearDue !== undefined || input.dueAt !== undefined || input.dueReminder !== undefined || input.memberId !== undefined || input.name !== undefined || input.position !== undefined || input.state !== undefined, "Provide at least one checklist-item field to update.", ), ) .output(TRELLO_CHECK_ITEM_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const item = toTrelloCheckItem( await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(normalizeTrelloCardId(input.card))}/checkItem/${encodeURIComponent(input.itemId)}`, { method: "PUT", query: { due: input.clearDue ? "null" : input.dueAt?.toISOString(), dueReminder: input.dueReminder, idMember: input.memberId, name: input.name, pos: input.position, state: input.state, }, responseSchema: TRELLO_PROVIDER_CHECK_ITEM_SCHEMA, }, ), ) const ignoredFields = getIgnoredAdvancedChecklistFields(input, item) if (ignoredFields.length > 0) throw terminalActionError( new Error(getAdvancedChecklistFieldsError(ignoredFields)), ) return item }) /** Deletes an item from a Trello checklist. */ export const deleteTrelloChecklistItem = defineAction( "Delete Trello checklist item", ) .describe("Deletes one item from a Trello checklist.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ checklistId: z.string().trim().min(1), itemId: z.string().trim().min(1), }), ) .output(z.object({ itemId: z.string(), deleted: z.literal(true) })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getTrelloApi(account.secret).request( `checklists/${encodeURIComponent(input.checklistId)}/checkItems/${encodeURIComponent(input.itemId)}`, { method: "DELETE", responseSchema: z.unknown() }, ) return { itemId: input.itemId, deleted: true as const } }) interface AdvancedChecklistFields { clearDue?: boolean dueAt?: Date dueReminder?: number memberId?: string } /** * Returns advanced checklist fields Trello did not persist as requested. * * @param input - Requested advanced field values. * @param item - Normalized item returned by Trello. */ function getIgnoredAdvancedChecklistFields( input: AdvancedChecklistFields, item: z.output, ) { const ignoredFields: string[] = [] if ( input.dueAt !== undefined && item.dueAt?.getTime() !== input.dueAt.getTime() ) ignoredFields.push("dueAt") if (input.clearDue && item.dueAt !== null) ignoredFields.push("clearDue") if ( input.dueReminder !== undefined && !( item.dueReminder === input.dueReminder || (input.dueReminder === -1 && item.dueReminder === null) ) ) ignoredFields.push("dueReminder") if (input.memberId !== undefined && item.memberId !== input.memberId) ignoredFields.push("memberId") return ignoredFields } /** * Explains Trello's paid-Workspace requirement for advanced checklist fields. * * @param ignoredFields - Requested fields absent from the provider response. */ function getAdvancedChecklistFieldsError(ignoredFields: string[]) { return `Trello did not apply advanced checklist fields (${ignoredFields.join(", ")}). Checklist-item dates, reminders, and assignments require a Standard, Premium, or Enterprise Trello Workspace and a valid advanced-checklist combination.` }