All files / src/lib/db repo-cache.ts

100% Statements 23/23
100% Branches 6/6
100% Functions 3/3
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                            153x                               10x 10x       10x 3x 3x     7x 7x 1x 1x     6x 6x 6x 1x 1x   4x 4x     1x 1x                       7x 7x                           2x 2x    
/**
 * Cached Sentry repository list (per org).
 *
 * Powers `issue resolve --in @commit` — avoids a `GET /organizations/{org}/repos/`
 * round trip on every invocation when the cache is fresh. The cache stores
 * the entire repo list as a JSON blob since the typical lookup pattern is
 * "match git origin → find one repo", not "get one repo by ID".
 *
 * TTL matches other caches (~7 days via {@link CACHE_TTL_MS}). A stale
 * cache is refreshed on the next call path that hits the API anyway.
 */
 
import type { SentryRepository } from "../../types/index.js";
import { recordCacheHit } from "../telemetry.js";
import { getDatabase } from "./index.js";
import { runUpsert } from "./utils.js";
 
/**
 * How long cached repo lists are considered fresh. Kept shorter than the
 * project cache (30 days) because repo-to-integration links change more
 * often than project listings do.
 */
export const REPO_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
 
type RepoCacheRow = {
  org_slug: string;
  repos_json: string;
  cached_at: number;
};
 
/**
 * Fetch the cached repo list for an org. Returns `null` when no cache
 * entry exists or the entry is older than {@link REPO_CACHE_TTL_MS}.
 *
 * Unparseable JSON (from a corrupted or stale schema) is treated as a
 * cache miss — the caller refetches and the broken row is overwritten.
 */
export function getCachedRepos(orgSlug: string): SentryRepository[] | null {
  const db = getDatabase();
  const row = db
    .query("SELECT * FROM repo_cache WHERE org_slug = ?")
    .get(orgSlug) as RepoCacheRow | undefined;
 
  if (!row) {
    recordCacheHit("repo", false);
    return null;
  }
 
  const age = Date.now() - row.cached_at;
  if (age > REPO_CACHE_TTL_MS) {
    recordCacheHit("repo", false);
    return null;
  }
 
  try {
    const repos = JSON.parse(row.repos_json) as SentryRepository[];
    if (!Array.isArray(repos)) {
      recordCacheHit("repo", false);
      return null;
    }
    recordCacheHit("repo", true);
    return repos;
  } catch {
    // Corrupted cache — treat as miss; overwritten on next setCachedRepos.
    recordCacheHit("repo", false);
    return null;
  }
}
 
/**
 * Upsert the cached repo list for an org. Overwrites the previous entry
 * (there's only ever one row per org).
 */
export function setCachedRepos(
  orgSlug: string,
  repos: SentryRepository[]
): void {
  const db = getDatabase();
  runUpsert(
    db,
    "repo_cache",
    {
      org_slug: orgSlug,
      repos_json: JSON.stringify(repos),
      cached_at: Date.now(),
    },
    ["org_slug"]
  );
}
 
/** Clear the cached repo list for one org (for tests and manual refresh). */
export function clearCachedRepos(orgSlug: string): void {
  const db = getDatabase();
  db.query("DELETE FROM repo_cache WHERE org_slug = ?").run(orgSlug);
}