import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getLinearApi } from "@automate.ax/integration-contracts/linear" import { LINEAR_ATTACHMENT_FIELDS } from "../lib/fragments" import { LINEAR_ATTACHMENT_SCHEMA, LINEAR_PAGE_INFO_SCHEMA, LINEAR_PAGE_INPUT_SCHEMA, linearConnectionSchema, } from "@automate.ax/integration-contracts/linear" import { LINEAR_ISSUE_CREATE_ACCOUNT_OPTIONS, LINEAR_READ_ACCOUNT_OPTIONS, LINEAR_WRITE_ACCOUNT_OPTIONS, } from "../lib/scopes" const ATTACHMENT_CONNECTION_SCHEMA = linearConnectionSchema( LINEAR_ATTACHMENT_SCHEMA, ) const ATTACHMENT_PAGE_SCHEMA = z.object({ attachments: LINEAR_ATTACHMENT_SCHEMA.array(), pageInfo: LINEAR_PAGE_INFO_SCHEMA, }) const ATTACHMENT_MUTATION_RESPONSE_SCHEMA = z.object({ attachmentMutation: z.object({ attachment: LINEAR_ATTACHMENT_SCHEMA, success: z.literal(true), }), }) const ATTACHMENT_DELETE_RESPONSE_SCHEMA = z.object({ attachmentMutation: z.object({ entityId: z.string(), success: z.literal(true), }), }) /** Lists external attachments linked to one Linear issue. */ export const listLinearIssueAttachments = defineAction( "List Linear issue attachments", ) .describe("Lists a cursor-paginated page of external issue attachments.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input( LINEAR_PAGE_INPUT_SCHEMA.extend({ /** UUID or human-readable identifier of the issue. */ issueId: z.string().min(1), }), ) .output(ATTACHMENT_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { issue } = await getLinearApi(account.secret).graphql( `query LinearIssueAttachments( $id: String! $first: Int! $after: String $includeArchived: Boolean ) { issue(id: $id) { attachments( first: $first after: $after includeArchived: $includeArchived ) { nodes { ${LINEAR_ATTACHMENT_FIELDS} } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } }`, { responseSchema: z.object({ issue: z.object({ attachments: ATTACHMENT_CONNECTION_SCHEMA }), }), variables: { first: input.limit, id: input.issueId, ...(input.after && { after: input.after }), ...(input.includeArchived !== undefined && { includeArchived: input.includeArchived, }), }, }, ) return { attachments: issue.attachments.nodes, pageInfo: issue.attachments.pageInfo!, } }) /** Creates or idempotently updates an external attachment on a Linear issue. */ export const createLinearIssueAttachment = defineAction( "Create Linear issue attachment", ) .describe("Links an external resource to a Linear issue by its unique URL.") .account("linear", LINEAR_ISSUE_CREATE_ACCOUNT_OPTIONS) .input( z.object({ /** Optional Markdown comment created with the attachment. */ commentBody: z.string().min(1).optional(), /** Group attachments from the same integration source. */ groupBySource: z.boolean().optional(), /** PNG or JPEG icon URL overriding the OAuth application icon. */ iconUrl: z.url().optional(), /** UUID or human-readable identifier of the target issue. */ issueId: z.string().min(1), /** Integration-specific string or numeric metadata. */ metadata: z .record(z.string(), z.union([z.string(), z.number()])) .optional(), /** Secondary attachment status or detail. */ subtitle: z.string().optional(), /** Attachment title shown in Linear. */ title: z.string().min(1), /** Unique external URL; recreating it updates the existing attachment. */ url: z.url(), }), ) .output(LINEAR_ATTACHMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { attachmentMutation } = await getLinearApi(account.secret).graphql( `mutation LinearAttachmentCreate($input: AttachmentCreateInput!) { attachmentMutation: attachmentCreate(input: $input) { success attachment { ${LINEAR_ATTACHMENT_FIELDS} } } }`, { responseSchema: ATTACHMENT_MUTATION_RESPONSE_SCHEMA, variables: { input }, }, ) return attachmentMutation.attachment }) /** Updates a Linear issue attachment's presentation metadata. */ export const updateLinearIssueAttachment = defineAction( "Update Linear issue attachment", ) .describe("Updates an external attachment's title and metadata.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( z.object({ /** Stable Linear attachment ID. */ attachmentId: z.string().min(1), /** Square JPG or PNG icon URL, or `null` to clear it. */ iconUrl: z.url().nullable().optional(), /** Replacement integration-defined metadata. */ metadata: z .record(z.string(), z.union([z.string(), z.number()])) .optional(), /** Replacement attachment subtitle, or `null` to clear it. */ subtitle: z.string().nullable().optional(), /** Replacement attachment title. */ title: z.string().min(1), }), ) .output(LINEAR_ATTACHMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { attachmentId, ...update } = input const { attachmentMutation } = await getLinearApi(account.secret).graphql( `mutation LinearAttachmentUpdate( $id: String! $input: AttachmentUpdateInput! ) { attachmentMutation: attachmentUpdate(id: $id, input: $input) { success attachment { ${LINEAR_ATTACHMENT_FIELDS} } } }`, { responseSchema: ATTACHMENT_MUTATION_RESPONSE_SCHEMA, variables: { id: attachmentId, input: update }, }, ) return attachmentMutation.attachment }) /** Deletes an external Linear issue attachment. */ export const deleteLinearIssueAttachment = defineAction( "Delete Linear issue attachment", ) .describe("Permanently deletes an external issue attachment by ID.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( z.object({ /** Stable Linear attachment ID. */ attachmentId: z.string().min(1), }), ) .output( z.object({ /** ID of the deleted Linear attachment. */ attachmentId: z.string(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { attachmentMutation } = await getLinearApi(account.secret).graphql( `mutation LinearAttachmentDelete($id: String!) { attachmentMutation: attachmentDelete(id: $id) { success entityId } }`, { responseSchema: ATTACHMENT_DELETE_RESPONSE_SCHEMA, variables: { id: input.attachmentId }, }, ) return { attachmentId: attachmentMutation.entityId } })