import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getLinearApi } from "@automate.ax/integration-contracts/linear" import { LINEAR_ISSUE_RELATION_FIELDS } from "../lib/fragments" import { LINEAR_ISSUE_RELATION_SCHEMA, LINEAR_PAGE_INFO_SCHEMA, linearConnectionSchema, } from "@automate.ax/integration-contracts/linear" import { LINEAR_READ_ACCOUNT_OPTIONS, LINEAR_WRITE_ACCOUNT_OPTIONS, } from "../lib/scopes" const RELATION_CONNECTION_SCHEMA = linearConnectionSchema( LINEAR_ISSUE_RELATION_SCHEMA, ) const RELATION_MUTATION_RESPONSE_SCHEMA = z.object({ relationMutation: z.object({ issueRelation: LINEAR_ISSUE_RELATION_SCHEMA, success: z.literal(true), }), }) const RELATION_DELETE_RESPONSE_SCHEMA = z.object({ relationMutation: z.object({ entityId: z.string(), success: z.literal(true), }), }) const RELATION_LIST_SCHEMA = z.object({ inversePageInfo: LINEAR_PAGE_INFO_SCHEMA, inverseRelations: LINEAR_ISSUE_RELATION_SCHEMA.array(), pageInfo: LINEAR_PAGE_INFO_SCHEMA, relations: LINEAR_ISSUE_RELATION_SCHEMA.array(), }) /** Lists outgoing and inverse relations for one Linear issue. */ export const listLinearIssueRelations = defineAction( "List Linear issue relations", ) .describe("Lists both directions of an issue's relation graph.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input( z.object({ /** Cursor for the next page of inverse relations. */ inverseAfter: z.string().min(1).optional(), /** UUID or human-readable identifier of the issue. */ issueId: z.string().min(1), /** Maximum relations returned in each direction. */ limit: z.number().int().min(1).max(50).prefault(50), /** Cursor for the next page of outgoing relations. */ after: z.string().min(1).optional(), }), ) .output(RELATION_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { issue } = await getLinearApi(account.secret).graphql( `query LinearIssueRelations( $id: String! $first: Int! $after: String $inverseAfter: String ) { issue(id: $id) { relations(first: $first, after: $after) { nodes { ${LINEAR_ISSUE_RELATION_FIELDS} } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } inverseRelations(first: $first, after: $inverseAfter) { nodes { ${LINEAR_ISSUE_RELATION_FIELDS} } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } }`, { responseSchema: z.object({ issue: z.object({ inverseRelations: RELATION_CONNECTION_SCHEMA, relations: RELATION_CONNECTION_SCHEMA, }), }), variables: { first: input.limit, id: input.issueId, ...(input.after && { after: input.after }), ...(input.inverseAfter && { inverseAfter: input.inverseAfter }), }, }, ) return { inversePageInfo: issue.inverseRelations.pageInfo!, inverseRelations: issue.inverseRelations.nodes, pageInfo: issue.relations.pageInfo!, relations: issue.relations.nodes, } }) /** Creates a directional relationship between two Linear issues. */ export const createLinearIssueRelation = defineAction( "Create Linear issue relation", ) .describe("Creates a blocks, duplicate, related, or similar issue relation.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( z.object({ /** Issue on the source side of the relationship. */ issueId: z.string().min(1), /** Issue on the target side of the relationship. */ relatedIssueId: z.string().min(1), /** Relationship type; `blocks` means the source blocks the target. */ type: z.enum(["blocks", "duplicate", "related", "similar"]), }), ) .output(LINEAR_ISSUE_RELATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { relationMutation } = await getLinearApi(account.secret).graphql( `mutation LinearIssueRelationCreate($input: IssueRelationCreateInput!) { relationMutation: issueRelationCreate(input: $input) { success issueRelation { ${LINEAR_ISSUE_RELATION_FIELDS} } } }`, { responseSchema: RELATION_MUTATION_RESPONSE_SCHEMA, variables: { input }, }, ) return relationMutation.issueRelation }) /** Deletes one Linear issue relation. */ export const deleteLinearIssueRelation = defineAction( "Delete Linear issue relation", ) .describe("Permanently deletes one issue relation by ID.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( z.object({ /** Stable Linear issue relation ID. */ relationId: z.string().min(1), }), ) .output( z.object({ /** ID of the deleted Linear issue relation. */ relationId: z.string(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { relationMutation } = await getLinearApi(account.secret).graphql( `mutation LinearIssueRelationDelete($id: String!) { relationMutation: issueRelationDelete(id: $id) { success entityId } }`, { responseSchema: RELATION_DELETE_RESPONSE_SCHEMA, variables: { id: input.relationId }, }, ) return { relationId: relationMutation.entityId } })