import { ClientResponseError } from "@atcute/client";
import type { Did } from "@atcute/lexicons/syntax";
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;
author?: Did;
}
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<Enriched<TicketSearchPage>> =>
enrich<TicketSearchPage>(
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<TicketSearchPage> | null;
error?: string;
}
export type SearchOutcome<T> = { result: T; error?: never } | { result: null; error: string };
export const searchWithAuthors = async <T>(
ctx: BobbinContext,
q: string,
search: (query: string) => Promise<T>
): Promise<SearchOutcome<T>> => {
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<TicketSearchOutcome> =>
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 };