SPB Git

spb/autoscholar

Public
8.2 KB · 240 lines typescript
1import {2  corrections,3  type Db,4  papers,5  paperTopics,6  paperVersions,7  publicationEvents,8  retractions,9  series,10  topics,11} from "@autoscholar/db";12import type { PublicPaperSummary, PublicSearchQuery } from "@autoscholar/schemas";13import { and, arrayOverlaps, asc, desc, eq, gte, ilike, inArray, lte, or, sql } from "drizzle-orm";14import { notFound } from "./errors.js";1516export const STAGING_FILTER = () =>17  process.env.STAGING_MODE === "1" ? undefined : eq(papers.synthetic, false);1819function toSummary(20  row: { p: typeof papers.$inferSelect; v: typeof paperVersions.$inferSelect; s: { slug: string } | null },21  topicList: Array<{ slug: string; name: string }>,22): PublicPaperSummary {23  const set = row.v.artifactSet ?? [];24  return {25    publicId: row.p.publicId,26    slug: row.p.slug,27    title: row.v.title,28    abstract: row.v.abstract,29    keywords: row.v.keywords,30    discipline: row.p.discipline,31    researchMode: row.p.researchMode,32    status: row.p.status as PublicPaperSummary["status"],33    versionNo: row.v.versionNo,34    firstPublishedAt: row.p.firstPublishedAt.toISOString(),35    updatedAt: row.p.updatedAt.toISOString(),36    seriesSlug: row.s?.slug ?? null,37    topics: topicList,38    hasCode: set.some((a) => a.kind === "code"),39    hasData: set.some((a) => a.kind === "data" && !/README|manifest/.test(a.path)),40    hasLatex: set.some((a) => a.kind === "manuscript" && a.path.endsWith(".tex")),41    synthetic: row.p.synthetic,42  };43}4445async function topicsFor(db: Db, paperIds: string[]) {46  if (paperIds.length === 0) return new Map<string, Array<{ slug: string; name: string }>>();47  const rows = await db48    .select({ paperId: paperTopics.paperId, slug: topics.slug, name: topics.name })49    .from(paperTopics)50    .innerJoin(topics, eq(paperTopics.topicId, topics.id))51    .where(inArray(paperTopics.paperId, paperIds));52  const m = new Map<string, Array<{ slug: string; name: string }>>();53  for (const r of rows) m.set(r.paperId, [...(m.get(r.paperId) ?? []), { slug: r.slug, name: r.name }]);54  return m;55}5657export async function searchPapers(58  db: Db,59  q: PublicSearchQuery,60): Promise<{ items: PublicPaperSummary[]; total: number; page: number; pageSize: number }> {61  const conds = [];62  const staging = STAGING_FILTER();63  if (staging) conds.push(staging);64  if (q.discipline) conds.push(eq(papers.discipline, q.discipline));65  if (q.mode) conds.push(eq(papers.researchMode, q.mode));66  if (q.status) conds.push(eq(papers.status, q.status));67  if (q.series) conds.push(eq(series.slug, q.series));68  if (q.from) conds.push(gte(papers.firstPublishedAt, new Date(q.from)));69  if (q.to) conds.push(lte(papers.firstPublishedAt, new Date(q.to)));70  if (q.code)71    conds.push(72      sql`exists (select 1 from jsonb_array_elements(${paperVersions.artifactSet}) a where a->>'kind' = 'code')`,73    );74  if (q.data)75    conds.push(76      sql`exists (select 1 from jsonb_array_elements(${paperVersions.artifactSet}) a where a->>'kind' = 'data' and a->>'path' !~ '(README|manifest)')`,77    );78  const term = q.q?.trim();79  let rank = sql<number>`0`;80  if (term) {81    const tsq = sql`websearch_to_tsquery('english', ${term})`;82    conds.push(83      or(84        sql`${paperVersions.search} @@ ${tsq}`,85        ilike(paperVersions.title, `%${term}%`),86        sql`${papers.publicId} ilike ${`%${term}%`}`,87      )!,88    );89    rank = sql<number>`ts_rank_cd(${paperVersions.search}, ${tsq})`;90  }91  const where = conds.length ? and(...conds) : undefined;92  const base = db93    .select({ p: papers, v: paperVersions, s: { slug: series.slug }, rank })94    .from(papers)95    .innerJoin(paperVersions, eq(papers.currentVersionId, paperVersions.id))96    .leftJoin(series, eq(papers.seriesId, series.id))97    .where(where);98  const order =99    q.sort === "title"100      ? [asc(paperVersions.title)]101      : q.sort === "relevance" && term102        ? [desc(rank), desc(papers.firstPublishedAt)]103        : [desc(papers.firstPublishedAt)];104  const rows = await base105    .orderBy(...order)106    .limit(q.pageSize)107    .offset((q.page - 1) * q.pageSize);108  const [cnt] = await db109    .select({ n: sql<number>`count(*)::int` })110    .from(papers)111    .innerJoin(paperVersions, eq(papers.currentVersionId, paperVersions.id))112    .leftJoin(series, eq(papers.seriesId, series.id))113    .where(where);114  const tmap = await topicsFor(115    db,116    rows.map((r) => r.p.id),117  );118  return {119    items: rows.map((r) => toSummary(r, tmap.get(r.p.id) ?? [])),120    total: cnt?.n ?? 0,121    page: q.page,122    pageSize: q.pageSize,123  };124}125126export async function getPaperBySlug(db: Db, slug: string) {127  const staging = STAGING_FILTER();128  const [row] = await db129    .select({ p: papers, v: paperVersions, s: series })130    .from(papers)131    .innerJoin(paperVersions, eq(papers.currentVersionId, paperVersions.id))132    .leftJoin(series, eq(papers.seriesId, series.id))133    .where(and(or(eq(papers.slug, slug), eq(papers.publicId, slug.toUpperCase())), staging))134    .limit(1);135  if (!row) throw notFound("paper");136  const versions = await db137    .select({138      id: paperVersions.id,139      versionNo: paperVersions.versionNo,140      title: paperVersions.title,141      publishedAt: paperVersions.publishedAt,142      changelog: paperVersions.changelog,143      pdfPublicPath: paperVersions.pdfPublicPath,144      runId: paperVersions.runId,145    })146    .from(paperVersions)147    .where(eq(paperVersions.paperId, row.p.id))148    .orderBy(desc(paperVersions.versionNo));149  const tmap = await topicsFor(db, [row.p.id]);150  const corr = await db151    .select()152    .from(corrections)153    .where(eq(corrections.paperId, row.p.id))154    .orderBy(desc(corrections.createdAt));155  const [retr] = await db.select().from(retractions).where(eq(retractions.paperId, row.p.id)).limit(1);156  const events = await db157    .select()158    .from(publicationEvents)159    .where(eq(publicationEvents.paperId, row.p.id))160    .orderBy(asc(publicationEvents.createdAt));161  const related = await db162    .select({ p: papers, v: paperVersions, s: { slug: series.slug } })163    .from(papers)164    .innerJoin(paperVersions, eq(papers.currentVersionId, paperVersions.id))165    .leftJoin(series, eq(papers.seriesId, series.id))166    .where(167      and(168        sql`${papers.id} <> ${row.p.id}`,169        eq(papers.status, "published"),170        staging,171        or(172          eq(papers.discipline, row.p.discipline),173          row.v.keywords.length ? arrayOverlaps(paperVersions.keywords, row.v.keywords) : sql`false`,174        ),175      ),176    )177    .orderBy(desc(papers.firstPublishedAt))178    .limit(4);179  const rtmap = await topicsFor(180    db,181    related.map((r) => r.p.id),182  );183  return {184    summary: toSummary(row, tmap.get(row.p.id) ?? []),185    paper: row.p,186    version: row.v,187    series: row.s,188    versions,189    corrections: corr,190    retraction: retr ?? null,191    events,192    related: related.map((r) => toSummary(r, rtmap.get(r.p.id) ?? [])),193  };194}195196export async function listTopics(db: Db) {197  const staging = STAGING_FILTER();198  const rows = await db199    .select({200      t: topics,201      n: sql<number>`(select count(*)::int from paper_topics pt join papers p on p.id = pt.paper_id where pt.topic_id = ${topics.id} ${staging ? sql`and p.synthetic = false` : sql``})`,202    })203    .from(topics)204    .orderBy(asc(topics.sortOrder), asc(topics.name));205  return rows.map((r) => ({ ...r.t, paperCount: r.n }));206}207208export async function listSeries(db: Db) {209  const staging = STAGING_FILTER();210  const rows = await db211    .select({212      s: series,213      n: sql<number>`(select count(*)::int from papers p where p.series_id = ${series.id} ${staging ? sql`and p.synthetic = false` : sql``})`,214    })215    .from(series)216    .orderBy(asc(series.name));217  return rows.map((r) => ({ ...r.s, paperCount: r.n }));218}219220export async function disciplineCounts(db: Db) {221  const staging = STAGING_FILTER();222  return db223    .select({ discipline: papers.discipline, n: sql<number>`count(*)::int` })224    .from(papers)225    .where(staging)226    .groupBy(papers.discipline)227    .orderBy(desc(sql`count(*)`));228}229230export async function allPublishedForFeeds(db: Db, limit = 100) {231  const staging = STAGING_FILTER();232  return db233    .select({ p: papers, v: paperVersions })234    .from(papers)235    .innerJoin(paperVersions, eq(papers.currentVersionId, paperVersions.id))236    .where(staging)237    .orderBy(desc(papers.updatedAt))238    .limit(limit);239}240
Files · main
Menu

Theme