SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.0 KB · 74 lines typescript
Raw Blame History
1import type { Ranking, RankingRow } from "@dci/core";2import { pg } from "../lib/sql.js";3import { int, json, num, reqIso, reqStr, str, type Row } from "../lib/rows.js";4import { hrefFor } from "../lib/resolve.js";56export interface RankingListItem { key: string; label: string; scope: Ranking["scope"]; unit: string; methodology: string; computedAt: string; total: number; minCoverage?: number }78const SCOPE_TYPE: Record<string, string> = { countries: "country", metros: "metro", operators: "operator", facilities: "facility" };910function asScope(v: unknown): Ranking["scope"] {11  const s = str(v);12  return s === "countries" || s === "metros" || s === "operators" || s === "facilities" ? s : "facilities";13}1415export function toRankingRows(scope: string, raw: unknown, limit?: number): RankingRow[] {16  const arr = json<Array<Record<string, unknown>>>(raw, []);17  const rows: RankingRow[] = arr.map((r, i) => {18    const slug = reqStr(r.slug, reqStr(r.id));19    return {20      rank: int(r.rank, i + 1),21      id: reqStr(r.id),22      slug,23      name: reqStr(r.name, slug),24      href: reqStr(r.href, hrefFor(SCOPE_TYPE[scope] ?? "facility", slug)),25      value: num(r.value),26      secondary: num(r.secondary),27      countryIso2: str(r.countryIso2 ?? r.country_iso2),28      coverage: num(r.coverage),29      delta: num(r.delta),30    };31  });32  rows.sort((a, b) => a.rank - b.rank);33  return limit ? rows.slice(0, limit) : rows;34}3536export async function listRankings(): Promise<RankingListItem[]> {37  const sql = pg();38  const rows = await sql<Row[]>`select key, label, scope, unit, methodology, computed_at, total, min_coverage from rankings where is_current and key not like '%:all' order by scope, key`;39  return rows.map((r) => ({ key: reqStr(r.key), label: reqStr(r.label), scope: asScope(r.scope), unit: reqStr(r.unit), methodology: reqStr(r.methodology), computedAt: reqIso(r.computed_at), total: int(r.total), ...(num(r.min_coverage) != null ? { minCoverage: num(r.min_coverage)! } : {}) }));40}4142export async function getRanking(key: string, limit?: number): Promise<Ranking | null> {43  const sql = pg();44  const rows = await sql<Row[]>`select * from rankings where key = ${key} and is_current order by computed_at desc limit 1`;45  const r = rows[0];46  if (!r) return null;47  const scope = asScope(r.scope);48  const out: Ranking = { key: reqStr(r.key), label: reqStr(r.label), unit: reqStr(r.unit), scope, methodology: reqStr(r.methodology), computedAt: reqIso(r.computed_at), rows: toRankingRows(scope, r.rows, limit), total: int(r.total) };49  const mc = num(r.min_coverage);50  if (mc != null) out.minCoverage = mc;51  return out;52}5354/** Positions of one entity (matched by id or slug) across all current rankings of a scope. */55export async function rankingPositions(scope: Ranking["scope"], ids: string[]): Promise<Array<{ key: string; label: string; rank: number; total: number; value: number | null }>> {56  const sql = pg();57  const rows = await sql<Row[]>`select key, label, total, rows from rankings where is_current and scope = ${scope} and key not like '%:all' order by key`;58  const wanted = new Set(ids.map((s) => s.toLowerCase()));59  const out: Array<{ key: string; label: string; rank: number; total: number; value: number | null }> = [];60  for (const r of rows) {61    const hit = toRankingRows(scope, r.rows).find((row) => wanted.has(row.id.toLowerCase()) || wanted.has(row.slug.toLowerCase()));62    if (hit) out.push({ key: reqStr(r.key), label: reqStr(r.label), rank: hit.rank, total: int(r.total), value: hit.value });63  }64  return out;65}6667/** Top rows of the first current ranking matching one of the keys (dashboard). */68export async function topRows(keys: string[], scope: Ranking["scope"], limit: number): Promise<RankingRow[] | null> {69  const sql = pg();70  const rows = await sql<Row[]>`select key, rows from rankings where is_current and scope = ${scope} and key = any(${keys}) order by array_position(${keys}::text[], key) limit 1`;71  if (!rows[0]) return null;72  return toRankingRows(scope, rows[0].rows, limit);73}74