~james/core

core/web/src/lib/api/ticketSearch.ts -rw-r--r-- 3.0 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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 };