import { ClientResponseError } from "@atcute/client"; import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts"; import { enrich, type Enriched, type LinkDescriptor } from "#lib/api/enrich.ts"; import type { TicketRecord, TicketState } from "#lib/api/records.ts"; import { resolveMiniDoc } from "#lib/api/identity.ts"; import { TICKET_COLLECTION } from "#lib/api/ticket.ts"; import { resolveAuthors } from "#lib/ticketQuery.ts"; export interface TicketHit { uri: string; cid?: string; nsid: string; score: number; value: TicketRecord; state: TicketState; commentCount: number; } export interface StateCounts { open: number; closed: number; completed: number; } export interface TicketSearchPage { hits: TicketHit[]; cursor?: string | null; total?: number; stateCounts?: StateCounts; } export interface TicketSearchOptions { repoDid: string; kind: "issue" | "pull"; /** the search text, empty lists all tickets */ query: string; limit: number; offset?: number; enrich: LinkDescriptor[]; } export const searchTickets = ( ctx: BobbinContext, { repoDid, kind, query, limit, offset, enrich: descriptors }: TicketSearchOptions, init?: XrpcRequestInit ): Promise> => enrich( ctx, { xrpc: "org.tangled.search.query", params: { ...(query ? { q: query } : {}), repo: repoDid, nsid: TICKET_COLLECTION, kind, limit, ...(offset ? { offset } : {}) }, enrich: descriptors }, init ); export interface TicketSearchOutcome { result: Enriched | null; error?: string; } export type SearchOutcome = { result: T; error?: never } | { result: null; error: string }; export const searchWithAuthors = async ( ctx: BobbinContext, q: string, search: (query: string) => Promise ): Promise> => { const { query, unresolved } = await resolveAuthors(q, (handle) => resolveMiniDoc(ctx, handle).then( (doc) => doc.did, () => undefined ) ); if (unresolved.length > 0) { const error = unresolved.map((handle) => `\`author:${handle}\` could not be found`); return { result: null, error: error.join("; ") }; } try { return { result: await search(query) }; } catch (cause) { const error = invalidQueryMessage(cause); if (error === undefined) throw cause; return { result: null, error }; } }; /** searches tickets and returns an InvalidQuery message as an error */ export const trySearchTickets = ( ctx: BobbinContext, options: TicketSearchOptions ): Promise => searchWithAuthors(ctx, options.query, (query) => searchTickets(ctx, { ...options, query })); /** the message of an InvalidQuery error or undefined */ export const invalidQueryMessage = (cause: unknown): string | undefined => cause instanceof ClientResponseError && cause.error === "InvalidQuery" ? (cause.description ?? "this search could not be understood") : undefined; export const EMPTY_COUNTS: StateCounts = { open: 0, closed: 0, completed: 0 };