Skip to content

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

Package listing

@kody/planetscale

src/types.ts

217 lines · 5.2 KB · TypeScript
export type JsonRecord = Record<string, unknown>

export type PlanetscaleAuthInput = {
	/**
	 * Short account alias. `work` → secret `planetscaleApiToken-work`.
	 * `default` / `planetscale` / omitted → `planetscaleApiToken`.
	 */
	account?: string
	/** User secret name. Value must be `SERVICE_TOKEN_ID:SERVICE_TOKEN`. */
	secretName?: string
}

export type PlanetscalePageInfo = {
	currentPage: number | null
	nextPage: number | null
	prevPage: number | null
}

export type PlanetscaleOrganizationSummary = {
	id: string
	name: string | null
	billingEmail: string | null
}

export type PlanetscaleDatabaseSummary = {
	id: string
	name: string | null
	kind: string | null
	region: string | null
	htmlUrl: string | null
}

export type PlanetscaleBranchSummary = {
	id: string
	name: string | null
	production: boolean | null
	ready: boolean | null
	htmlUrl: string | null
	parentBranch: string | null
}

export type PlanetscaleQuerySummary = {
	id: string
	fingerprint: string | null
	normalizedSql: string | null
	statementType: string | null
	keyspace: string | null
	tables: Array<string>
	queryCount: number | null
	p99Latency: number | null
	timePerQuery: number | null
	sumRowsRead: number | null
	errorCount: number | null
	lastRunAt: string | null
}

export type PlanetscaleCorrelation = {
	id: string
	fingerprint: string | null
	normalizedSql: string | null
	keyspace: string | null
	r: number | null
}

export type PlanetscaleAnomalySummary = {
	id: string
	active: boolean | null
	periodStart: string | null
	periodEnd: string | null
	minutesInViolation: number | null
	duration: number | null
	correlations: Array<PlanetscaleCorrelation>
}

export type LoopTarget = {
	organization: string
	database: string
	branch: string
	githubOwner?: string
	githubRepo?: string
	repositoryUrl?: string
	labels?: Array<string>
}

export type LoopConfig = Partial<LoopTarget> & {
	cursorRef?: string
	account?: string
	githubIntegrationName?: string
	githubSecretName?: string
	loopEnabled?: boolean
	lastProcessedAnomalyId?: string
}

export type PlanetscaleWebhookPayload = {
	timestamp?: number
	event?: string
	organization?: string
	database?: string
	resource?: {
		id?: string
		name?: string
		html_url?: string
		parent_branch?: string
	}
}

export type LoopDraft = {
	title: string
	issueBody: string
	implementerPrompt: string
	reviewerPrompt: string
	humanPing: string
	insightsUrl: string
	anomaly: PlanetscaleAnomalySummary | null
	topQueries: Array<PlanetscaleQuerySummary>
	target: LoopTarget
}

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 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 = 'perPage',
): 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 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
}

export function stringField(value: unknown): string | null {
	return typeof value === 'string' && value.length > 0 ? value : null
}

export function booleanField(value: unknown): boolean | null {
	return typeof value === 'boolean' ? value : null
}

export function numberField(value: unknown): number | null {
	return typeof value === 'number' && Number.isFinite(value) ? value : null
}

/**
 * Shared PlanetScale types for this package.
 * @example
 * import types from 'kody:@kody/planetscale/types'
 * const catalog = await types()
 */
export default async function typesOverview() {
	return {
		auth: ['PlanetscaleAuthInput'],
		models: [
			'PlanetscaleAnomalySummary',
			'PlanetscaleQuerySummary',
			'LoopDraft',
			'LoopTarget',
			'LoopConfig',
			'PlanetscaleWebhookPayload',
		],
	}
}