Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/linear

src/update-issue.ts

134 lines · 4.4 KB · TypeScript
import { linearGraphql } from './client.ts'
import { ISSUE_FIELDS, mapIssue } from './models.ts'
import type {
	DryRunResult,
	JsonRecord,
	LinearAuthInput,
	LinearIssueSummary,
} from './types.ts'
import {
	compactRecord,
	optionalBoolean,
	optionalNumber,
	optionalPriority,
	optionalString,
	optionalStringArray,
	requireRecord,
	requireString,
} from './types.ts'
import { requireAuthMode } from './auth.ts'

export type UpdateIssueInput = LinearAuthInput & {
	/** Issue UUID or identifier (`ENG-123`). */
	id: string
	title?: string
	description?: string
	assigneeId?: string
	stateId?: string
	projectId?: string
	teamId?: string
	priority?: number
	labelIds?: Array<string>
	dueDate?: string
	parentId?: string
	estimate?: number
	confirm?: boolean
	dryRun?: boolean
}

function issueUpdatePayload(input: UpdateIssueInput): JsonRecord {
	const payload = compactRecord({
		title: optionalString(input.title, 'title'),
		description: optionalString(input.description, 'description'),
		assigneeId: optionalString(input.assigneeId, 'assigneeId'),
		stateId: optionalString(input.stateId, 'stateId'),
		projectId: optionalString(input.projectId, 'projectId'),
		teamId: optionalString(input.teamId, 'teamId'),
		priority: optionalPriority(input.priority),
		labelIds: optionalStringArray(input.labelIds, 'labelIds'),
		dueDate: optionalString(input.dueDate, 'dueDate'),
		parentId: optionalString(input.parentId, 'parentId'),
		estimate: optionalNumber(input.estimate, 'estimate'),
	})
	if (Object.keys(payload).length === 0) {
		throw new Error('update-issue requires at least one field to change.')
	}
	return payload
}

/**
 * Update a Linear issue. Requires `confirm: true`, or use `dryRun: true`.
 * Needs `write`.
 * @example
 * import updateIssue from 'kody:@kody/linear/update-issue'
 * const preview = await updateIssue({
 *   id: 'ENG-123',
 *   stateId: 'state-uuid',
 *   dryRun: true,
 * })
 */
export async function updateIssue(
	input: UpdateIssueInput,
): Promise<LinearIssueSummary | DryRunResult<{ mutation: string; id: string; input: JsonRecord }>> {
	const id = requireString(input.id, 'id')
	const payload = issueUpdatePayload(input)
	if (input.dryRun) {
		return { dryRun: true, wouldCall: { mutation: 'issueUpdate', id, input: payload } }
	}
	if (input.confirm !== true) {
		throw new Error(
			'Updating a Linear issue requires confirm: true after explicit user approval, or dryRun: true.',
		)
	}

	const result = await linearGraphql<{
		issueUpdate: { success?: boolean; issue?: unknown }
	}>({
		...input,
		operation: 'issueUpdate',
		query: `mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
			issueUpdate(id: $id, input: $input) {
				success
				issue { ${ISSUE_FIELDS} }
			}
		}`,
		variables: { id, input: payload },
	})
	if (!result.data.issueUpdate?.success || !result.data.issueUpdate.issue) {
		throw new Error('Linear issueUpdate did not return an issue.')
	}
	return mapIssue(result.data.issueUpdate.issue)
}

/**
 * Update a Linear issue. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import updateIssue from 'kody:@kody/linear/update-issue'
 * const preview = await updateIssue({ id: 'ENG-123', title: 'Renamed', dryRun: true })
 */
export default async function updateIssueEntrypoint(
	params: Partial<UpdateIssueInput> & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'update-issue')
	return updateIssue({
		id: requireString(input.id, 'id'),
		title: optionalString(input.title, 'title'),
		description: optionalString(input.description, 'description'),
		assigneeId: optionalString(input.assigneeId, 'assigneeId'),
		stateId: optionalString(input.stateId, 'stateId'),
		projectId: optionalString(input.projectId, 'projectId'),
		teamId: optionalString(input.teamId, 'teamId'),
		priority: optionalPriority(input.priority),
		labelIds: optionalStringArray(input.labelIds, 'labelIds'),
		dueDate: optionalString(input.dueDate, 'dueDate'),
		parentId: optionalString(input.parentId, 'parentId'),
		estimate: optionalNumber(input.estimate, 'estimate'),
		integrationName: optionalString(input.integrationName, 'integrationName'),
		integration: optionalString(input.integration, 'integration'),
		account: optionalString(input.account, 'account'),
		secretName: optionalString(input.secretName, 'secretName'),
		auth: requireAuthMode(input.auth),
		confirm: optionalBoolean(input.confirm, 'confirm'),
		dryRun: optionalBoolean(input.dryRun, 'dryRun'),
	})
}