Skip to content

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

Package listing

@kody/linear

src/types.ts

216 lines · 5.5 KB · TypeScript
export type JsonRecord = Record<string, unknown>

export type LinearAuthMode = 'oauth' | 'api-key'

export type LinearAuthInput = {
	/** Saved OAuth integration name. Defaults to `linear`. */
	integrationName?: string
	/** Alias of `integrationName` (Notion-style). */
	integration?: string
	/**
	 * Short account alias. `work` → `linear-work`. Values that already start
	 * with `linear-` are used as-is. `default` / `linear` / omitted → `linear`.
	 */
	account?: string
	/** User secret name for a personal API key. Defaults to `linearApiKey`. */
	secretName?: string
	/** Force OAuth or API-key auth when both exist. */
	auth?: LinearAuthMode
}

export type LinearPageInfo = {
	hasNextPage: boolean
	endCursor: string | null
}

export type LinearNamedRef = {
	id: string
	name: string
	key?: string
	type?: string
}

export type LinearIssueSummary = {
	id: string
	identifier: string
	title: string
	url: string | null
	priority: number | null
	createdAt: string | null
	updatedAt: string | null
	state: LinearNamedRef | null
	team: LinearNamedRef | null
	assignee: LinearNamedRef | null
	project: LinearNamedRef | null
}

export type LinearCommentSummary = {
	id: string
	body: string | null
	url: string | null
	createdAt: string | null
	updatedAt: string | null
	user: LinearNamedRef | null
}

export type LinearProjectSummary = {
	id: string
	name: string
	url: string | null
	description: string | null
	state: string | null
	startDate: string | null
	targetDate: string | null
	progress: number | null
}

export type LinearTeamSummary = {
	id: string
	name: string
	key: string
	description: string | null
}

export type LinearWorkflowStateSummary = {
	id: string
	name: string
	type: string
	team: LinearNamedRef | null
}

export type DryRunResult<T> = {
	dryRun: true
	wouldCall: T
}

export function requireRecord(value: unknown, label: string): JsonRecord {
	if (!value || typeof value !== 'object' || Array.isArray(value)) {
		throw new Error(`${label} must be an object.`)
	}
	return value as JsonRecord
}

export function requireString(
	value: unknown,
	label: string,
	options: { allowEmpty?: boolean } = {},
): string {
	if (typeof value !== 'string' || (!options.allowEmpty && value.trim().length === 0)) {
		throw new Error(
			`${label} must be ${options.allowEmpty ? 'a string' : 'a non-empty string'}.`,
		)
	}
	return options.allowEmpty ? value : value.trim()
}

export function optionalString(value: unknown, label: string): string | undefined {
	if (value === undefined || value === null) return undefined
	return requireString(value, label)
}

export function optionalBoolean(value: unknown, label: string): boolean | undefined {
	if (value === undefined) return undefined
	if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`)
	return value
}

export function optionalNumber(value: unknown, label: string): number | undefined {
	if (value === undefined || value === null) return undefined
	if (typeof value !== 'number' || !Number.isFinite(value)) {
		throw new Error(`${label} must be a finite number.`)
	}
	return value
}

export function optionalStringArray(value: unknown, label: string): Array<string> | undefined {
	if (value === undefined || value === null) return undefined
	if (!Array.isArray(value)) throw new Error(`${label} must be an array of strings.`)
	return value.map((item, index) => requireString(item, `${label}[${index}]`))
}

export function clampInt(
	value: unknown,
	min: number,
	max: number,
	fallback: number,
	label = 'first',
): number {
	if (value === undefined || value === null) return fallback
	if (typeof value !== 'number' || !Number.isInteger(value)) {
		throw new Error(`${label} must be an integer.`)
	}
	if (value < min || value > max) {
		throw new Error(`${label} must be between ${min} and ${max}.`)
	}
	return value
}

export function requirePriority(value: unknown, label = 'priority'): number {
	const priority = optionalNumber(value, label)
	if (priority === undefined) {
		throw new Error(`${label} is required.`)
	}
	if (![0, 1, 2, 3, 4].includes(priority)) {
		throw new Error(`${label} must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low).`)
	}
	return priority
}

export function optionalPriority(value: unknown, label = 'priority'): number | undefined {
	if (value === undefined || value === null) return undefined
	return requirePriority(value, label)
}

export const WORKFLOW_STATE_TYPES = [
	'triage',
	'backlog',
	'unstarted',
	'started',
	'completed',
	'canceled',
] as const

export type WorkflowStateType = (typeof WORKFLOW_STATE_TYPES)[number]

export function optionalWorkflowStateType(
	value: unknown,
	label = 'stateType',
): WorkflowStateType | undefined {
	if (value === undefined || value === null) return undefined
	const type = requireString(value, label)
	if (!(WORKFLOW_STATE_TYPES as readonly string[]).includes(type)) {
		throw new Error(
			`${label} must be one of: ${WORKFLOW_STATE_TYPES.join(', ')}.`,
		)
	}
	return type as WorkflowStateType
}

export function compactRecord<T extends JsonRecord>(value: T): T {
	const output: JsonRecord = {}
	for (const [key, item] of Object.entries(value)) {
		if (item === undefined) continue
		output[key] = item
	}
	return output as T
}

/**
 * Shared Linear types for this package.
 * @example
 * import types from 'kody:@kody/linear/types'
 * const catalog = await types()
 */
export default async function typesOverview() {
	return {
		auth: ['LinearAuthInput', 'LinearAuthMode'],
		models: [
			'LinearIssueSummary',
			'LinearProjectSummary',
			'LinearTeamSummary',
			'LinearCommentSummary',
			'LinearWorkflowStateSummary',
		],
	}
}