import { formatServiceError, isAbortError } from "../shared/errors"; import { fetchJson } from "../shared/http"; import { OPENALEX_RESULTS_PER_PAGE, OPENALEX_URL } from "../types"; export interface OpenAlexWork { id: string; doi: string | null; title: string; display_name: string; publication_year: number | null; publication_date: string | null; type: string | null; cited_by_count: number; open_access: { is_oa: boolean; oa_status: string | null; oa_url: string | null; } | null; primary_location: { source?: { display_name?: string; } | null; landing_page_url?: string; pdf_url?: string | null; } | null; authorships: Array<{ author: { display_name: string; }; institutions: Array<{ display_name?: string; }>; }>; primary_topic: { display_name: string; } | null; } export interface OpenAlexSearchResponse { meta: { count: number; db_response_time_ms: number; page: number; per_page: number; }; results: OpenAlexWork[]; } function formatAuthors(authorships: OpenAlexWork["authorships"]): string { if (!authorships || authorships.length === 0) return ""; const names = authorships.slice(0, 5).map((a) => a.author.display_name); const suffix = authorships.length > 5 ? ` et al.` : ""; return names.join(", ") + suffix; } function formatAbstract(abstract: Record | null): string { if (!abstract) return ""; const words = new Map(); for (const [word, positions] of Object.entries(abstract)) { for (const pos of positions) { words.set(pos, word); } } const sorted = Array.from(words.entries()).sort(([a], [b]) => a - b); return sorted.map(([, w]) => w).join(" ").slice(0, 500); } export async function searchOpenAlex( query: string, apiKey: string, signal?: AbortSignal, type?: string, yearFrom?: number, yearTo?: number, ): Promise<{ results: string; totalResults: number; error?: string }> { try { const params = new URLSearchParams({ search: query, per_page: String(OPENALEX_RESULTS_PER_PAGE), api_key: apiKey, sort: "relevance_score:desc", select: [ "id", "doi", "title", "display_name", "publication_year", "publication_date", "type", "cited_by_count", "open_access", "primary_location", "authorships", "primary_topic", "abstract_inverted_index", ].join(","), }); const filters: string[] = []; if (type) filters.push(`type:${type}`); if (yearFrom) filters.push(`from_publication_date:${yearFrom}-01-01`); if (yearTo) filters.push(`to_publication_date:${yearTo}-12-31`); if (filters.length > 0) params.set("filter", filters.join(",")); const data = await fetchJson( `${OPENALEX_URL}/works?${params}`, { signal }, ); const totalResults = data.meta.count; const lines: string[] = []; lines.push(`Found ${totalResults.toLocaleString()} results`); lines.push(""); for (let i = 0; i < data.results.length; i++) { const work = data.results[i]; const num = `[${i + 1}]`; const title = work.title ?? work.display_name ?? "Untitled"; const year = work.publication_year ?? ""; const typeLabel = work.type ?? ""; const citations = work.cited_by_count ?? 0; const authors = formatAuthors(work.authorships); const source = work.primary_location?.source?.display_name ?? ""; const oaUrl = work.open_access?.oa_url ?? work.primary_location?.pdf_url ?? ""; const doi = work.doi ?? ""; const topic = work.primary_topic?.display_name ?? ""; const abstract = formatAbstract(work.abstract_inverted_index as Record | null); lines.push(`${num} ${title}`); if (authors) lines.push(` Authors: ${authors}`); if (source) lines.push(` Source: ${source}`); const metaParts: string[] = []; if (year) metaParts.push(String(year)); if (typeLabel) metaParts.push(typeLabel); if (citations > 0) metaParts.push(`Cited by ${citations}`); if (work.open_access?.is_oa) metaParts.push("Open Access"); if (metaParts.length > 0) lines.push(` ${metaParts.join(" ยท ")}`); if (topic) lines.push(` Topic: ${topic}`); if (doi) lines.push(` DOI: ${doi}`); if (oaUrl) lines.push(` PDF: ${oaUrl}`); if (abstract) lines.push(` Abstract: ${abstract}...`); lines.push(""); } return { results: lines.join("\n"), totalResults }; } catch (error) { if (isAbortError(error)) throw error; return { results: "", totalResults: 0, error: formatServiceError("OpenAlex", error) }; } }