import { getLinearApi } from "@automate.ax/integration-contracts/linear" import { LINEAR_DOCUMENT_SCHEMA, LINEAR_PAGE_INFO_SCHEMA, LINEAR_PAGE_INPUT_SCHEMA, linearConnectionSchema, } from "@automate.ax/integration-contracts/linear" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { LINEAR_DOCUMENT_FIELDS } from "../lib/fragments" import { LINEAR_READ_ACCOUNT_OPTIONS, LINEAR_WRITE_ACCOUNT_OPTIONS, } from "../lib/scopes" const DOCUMENT_RESPONSE_SCHEMA = z.object({ document: LINEAR_DOCUMENT_SCHEMA }) const DOCUMENT_CONNECTION_SCHEMA = linearConnectionSchema( LINEAR_DOCUMENT_SCHEMA, ) const DOCUMENT_PAGE_SCHEMA = z.object({ documents: LINEAR_DOCUMENT_SCHEMA.array(), pageInfo: LINEAR_PAGE_INFO_SCHEMA, }) const DOCUMENT_MUTATION_SCHEMA = z.object({ documentMutation: z.object({ document: LINEAR_DOCUMENT_SCHEMA, success: z.literal(true), }), }) const DOCUMENT_ARCHIVE_SCHEMA = z.object({ documentMutation: z.object({ entity: LINEAR_DOCUMENT_SCHEMA.nullable(), success: z.literal(true), }), }) const DOCUMENT_FIELDS_INPUT_SCHEMA = z.object({ /** Document icon color, or `null` to clear it. */ color: z.string().nullable().optional(), /** Markdown document content, or `null` to clear it. */ content: z.string().nullable().optional(), /** Document icon or emoji, or `null` to clear it. */ icon: z.string().nullable().optional(), /** Issue to contain the document, or `null` to detach it. */ issueId: z.string().min(1).nullable().optional(), /** Workspace user who owns the document, or `null` for no owner. */ ownerId: z.string().min(1).nullable().optional(), /** Project to contain the document, or `null` to detach it. */ projectId: z.string().min(1).nullable().optional(), /** Relative order among sibling resources. */ sortOrder: z.number().optional(), /** Document title. */ title: z.string().optional(), }) /** Lists Linear documents, optionally limited to one project. */ export const listLinearDocuments = defineAction("List Linear documents") .describe("Lists a cursor-paginated page of Linear documents.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input( LINEAR_PAGE_INPUT_SCHEMA.extend({ /** Sort by creation time or most recent update. */ orderBy: z.enum(["createdAt", "updatedAt"]).prefault("updatedAt"), /** Project containing returned documents. */ projectId: z.string().min(1).optional(), }), ) .output(DOCUMENT_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { documents } = await getLinearApi(account.secret).graphql( `query LinearDocuments( $first: Int! $after: String $includeArchived: Boolean $orderBy: PaginationOrderBy! $filter: DocumentFilter ) { documents( first: $first after: $after includeArchived: $includeArchived orderBy: $orderBy filter: $filter ) { nodes { ${LINEAR_DOCUMENT_FIELDS} } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } }`, { responseSchema: z.object({ documents: DOCUMENT_CONNECTION_SCHEMA }), variables: { first: input.limit, orderBy: input.orderBy, ...(input.after && { after: input.after }), ...(input.includeArchived !== undefined && { includeArchived: input.includeArchived, }), ...(input.projectId && { filter: { project: { id: { eq: input.projectId } } }, }), }, }, ) return { documents: documents.nodes, pageInfo: documents.pageInfo! } }) /** Gets one Linear document by ID or slug. */ export const getLinearDocument = defineAction("Get Linear document") .describe("Gets one Linear document by ID or URL slug.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input(z.object({ documentId: z.string().min(1) })) .output(LINEAR_DOCUMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getLinearApi(account.secret) .graphql( `query LinearDocument($id: String!) { document(id: $id) { ${LINEAR_DOCUMENT_FIELDS} } }`, { responseSchema: DOCUMENT_RESPONSE_SCHEMA, variables: { id: input.documentId }, }, ) .then(({ document }) => document), ) /** Creates a Linear document. */ export const createLinearDocument = defineAction("Create Linear document") .describe("Creates a Markdown document, optionally attached to work.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( DOCUMENT_FIELDS_INPUT_SCHEMA.extend({ title: z.string() }).refine( ({ issueId, projectId }) => !(issueId && projectId), { message: "A Linear document can select either an issue or project." }, ), ) .output(LINEAR_DOCUMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { documentMutation } = await getLinearApi(account.secret).graphql( `mutation LinearDocumentCreate($input: DocumentCreateInput!) { documentMutation: documentCreate(input: $input) { success document { ${LINEAR_DOCUMENT_FIELDS} } } }`, { responseSchema: DOCUMENT_MUTATION_SCHEMA, variables: { input } }, ) return documentMutation.document }) /** Updates a Linear document's content and placement. */ export const updateLinearDocument = defineAction("Update Linear document") .describe("Updates a Linear document's title, content, owner, or parent.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( DOCUMENT_FIELDS_INPUT_SCHEMA.extend({ documentId: z.string().min(1), }).refine(({ issueId, projectId }) => !(issueId && projectId), { message: "A Linear document can select either an issue or project.", }), ) .output(LINEAR_DOCUMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { documentId, ...update } = input const { documentMutation } = await getLinearApi(account.secret).graphql( `mutation LinearDocumentUpdate( $id: String! $input: DocumentUpdateInput! ) { documentMutation: documentUpdate(id: $id, input: $input) { success document { ${LINEAR_DOCUMENT_FIELDS} } } }`, { responseSchema: DOCUMENT_MUTATION_SCHEMA, variables: { id: documentId, input: update }, }, ) return documentMutation.document }) /** Moves a Linear document to reversible trash. */ export const deleteLinearDocument = defineAction("Delete Linear document") .describe("Moves a Linear document to reversible trash and archives it.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ documentId: z.string().min(1) })) .output(LINEAR_DOCUMENT_SCHEMA.nullable()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => archiveDocument(account.secret, "documentDelete", input.documentId), ) /** Restores a trashed Linear document. */ export const unarchiveLinearDocument = defineAction("Unarchive Linear document") .describe("Restores a previously trashed Linear document.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ documentId: z.string().min(1) })) .output(LINEAR_DOCUMENT_SCHEMA.nullable()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => archiveDocument(account.secret, "documentUnarchive", input.documentId), ) /** * Executes a reversible document archive mutation. * * @param secret - Resolved Linear credential. * @param mutation - Provider archive lifecycle mutation. * @param documentId - Document to mutate. */ async function archiveDocument( secret: Record, mutation: "documentDelete" | "documentUnarchive", documentId: string, ) { const { documentMutation } = await getLinearApi(secret).graphql( mutation === "documentDelete" ? `mutation LinearDocumentDelete($id: String!) { documentMutation: documentDelete(id: $id) { success entity { ${LINEAR_DOCUMENT_FIELDS} } } }` : `mutation LinearDocumentUnarchive($id: String!) { documentMutation: documentUnarchive(id: $id) { success entity { ${LINEAR_DOCUMENT_FIELDS} } } }`, { responseSchema: DOCUMENT_ARCHIVE_SCHEMA, variables: { id: documentId }, }, ) return documentMutation.entity }