Skip to content

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

Package listing

@kody/linear

src/create-project.ts

126 lines · 4.2 KB · TypeScript
import { linearGraphql } from './client.ts'
import { mapProject, PROJECT_FIELDS } from './models.ts'
import type {
	DryRunResult,
	JsonRecord,
	LinearAuthInput,
	LinearProjectSummary,
} from './types.ts'
import {
	compactRecord,
	optionalBoolean,
	optionalPriority,
	optionalString,
	optionalStringArray,
	requireRecord,
	requireString,
} from './types.ts'
import { requireAuthMode } from './auth.ts'

export type CreateProjectInput = LinearAuthInput & {
	name: string
	/** One or more team UUIDs. Required by Linear. */
	teamIds: Array<string>
	description?: string
	content?: string
	leadId?: string
	memberIds?: Array<string>
	priority?: number
	startDate?: string
	targetDate?: string
	statusId?: string
	confirm?: boolean
	dryRun?: boolean
}

function projectCreatePayload(input: CreateProjectInput): JsonRecord {
	return compactRecord({
		name: requireString(input.name, 'name'),
		teamIds: optionalStringArray(input.teamIds, 'teamIds') ?? [],
		description: optionalString(input.description, 'description'),
		content: optionalString(input.content, 'content'),
		leadId: optionalString(input.leadId, 'leadId'),
		memberIds: optionalStringArray(input.memberIds, 'memberIds'),
		priority: optionalPriority(input.priority),
		startDate: optionalString(input.startDate, 'startDate'),
		targetDate: optionalString(input.targetDate, 'targetDate'),
		statusId: optionalString(input.statusId, 'statusId'),
	})
}

/**
 * Create a Linear project. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import createProject from 'kody:@kody/linear/create-project'
 * const preview = await createProject({
 *   name: 'Launch checklist',
 *   teamIds: ['team-uuid'],
 *   dryRun: true,
 * })
 */
export async function createProject(
	input: CreateProjectInput,
): Promise<LinearProjectSummary | DryRunResult<{ mutation: string; input: JsonRecord }>> {
	const payload = projectCreatePayload(input)
	if (!Array.isArray(payload.teamIds) || payload.teamIds.length === 0) {
		throw new Error('teamIds must include at least one Linear team UUID.')
	}

	if (input.dryRun) {
		return { dryRun: true, wouldCall: { mutation: 'projectCreate', input: payload } }
	}
	if (input.confirm !== true) {
		throw new Error(
			'Creating a Linear project requires confirm: true after explicit user approval, or dryRun: true.',
		)
	}

	const result = await linearGraphql<{
		projectCreate: { success?: boolean; project?: unknown }
	}>({
		...input,
		operation: 'projectCreate',
		query: `mutation CreateProject($input: ProjectCreateInput!) {
			projectCreate(input: $input) {
				success
				project { ${PROJECT_FIELDS} }
			}
		}`,
		variables: { input: payload },
	})
	if (!result.data.projectCreate?.success || !result.data.projectCreate.project) {
		throw new Error('Linear projectCreate did not return a project.')
	}
	return mapProject(result.data.projectCreate.project)
}

/**
 * Create a Linear project. Requires `confirm: true`, or use `dryRun: true`.
 * @example
 * import createProject from 'kody:@kody/linear/create-project'
 * const preview = await createProject({ name: 'Launch', teamIds: ['uuid'], dryRun: true })
 */
export default async function createProjectEntrypoint(
	params: Partial<CreateProjectInput> & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'create-project')
	return createProject({
		name: requireString(input.name, 'name'),
		teamIds: optionalStringArray(input.teamIds, 'teamIds') ?? [],
		description: optionalString(input.description, 'description'),
		content: optionalString(input.content, 'content'),
		leadId: optionalString(input.leadId, 'leadId'),
		memberIds: optionalStringArray(input.memberIds, 'memberIds'),
		priority: optionalPriority(input.priority),
		startDate: optionalString(input.startDate, 'startDate'),
		targetDate: optionalString(input.targetDate, 'targetDate'),
		statusId: optionalString(input.statusId, 'statusId'),
		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'),
	})
}