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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | 678x 575x 147x 653x 653x 653x 518x 518x 135x 135x 135x 600x 600x 600x 600x 341x 337x 312x 263x 27x 27x 27x 15x 15x 12x 12x 12x 12x 12x 15x 44x 4x 40x 40x 40x 40x 81x 40x 308x 308x | /**
* Cached project information storage (by orgId:projectId or DSN public key).
*/
import type { CachedProject } from "../../types/index.js";
import { recordCacheHit } from "../telemetry.js";
import { getDatabase, maybeCleanupCaches } from "./index.js";
import { runUpsert, touchCacheEntry } from "./utils.js";
type ProjectCacheRow = {
cache_key: string;
org_slug: string;
org_name: string;
project_slug: string;
project_name: string;
project_id?: string;
cached_at: number;
last_accessed: number;
};
function projectCacheKey(orgId: string, projectId: string): string {
return `${orgId}:${projectId}`;
}
function dsnCacheKey(publicKey: string): string {
return `dsn:${publicKey}`;
}
function rowToCachedProject(row: ProjectCacheRow): CachedProject {
return {
orgSlug: row.org_slug,
orgName: row.org_name,
projectSlug: row.project_slug,
projectName: row.project_name,
projectId: row.project_id,
cachedAt: row.cached_at,
};
}
/** Shared get implementation — looks up by pre-computed cache key. */
function getByKey(key: string): CachedProject | undefined {
const db = getDatabase();
const row = db
.query("SELECT * FROM project_cache WHERE cache_key = ?")
.get(key) as ProjectCacheRow | undefined;
if (!row) {
recordCacheHit("project", false);
return;
}
recordCacheHit("project", true);
touchCacheEntry("project_cache", "cache_key", key);
return rowToCachedProject(row);
}
/** Shared set implementation — writes by pre-computed cache key. */
function setByKey(key: string, info: Omit<CachedProject, "cachedAt">): void {
const db = getDatabase();
const now = Date.now();
runUpsert(
db,
"project_cache",
{
cache_key: key,
org_slug: info.orgSlug,
org_name: info.orgName,
project_slug: info.projectSlug,
project_name: info.projectName,
project_id: info.projectId ?? null,
cached_at: now,
last_accessed: now,
},
["cache_key"]
);
maybeCleanupCaches();
}
export function getCachedProject(
orgId: string,
projectId: string
): CachedProject | undefined {
return getByKey(projectCacheKey(orgId, projectId));
}
export function setCachedProject(
orgId: string,
projectId: string,
info: Omit<CachedProject, "cachedAt">
): void {
setByKey(projectCacheKey(orgId, projectId), info);
}
/** Get cached project by DSN public key (for self-hosted or SaaS DSNs without org ID). */
export function getCachedProjectByDsnKey(
publicKey: string
): CachedProject | undefined {
return getByKey(dsnCacheKey(publicKey));
}
/** Cache project by DSN public key (for self-hosted or SaaS DSNs without org ID). */
export function setCachedProjectByDsnKey(
publicKey: string,
info: Omit<CachedProject, "cachedAt">
): void {
setByKey(dsnCacheKey(publicKey), info);
}
/**
* Look up a cached project by organization and project slug.
*
* Returns the most recently cached entry matching the (org_slug, project_slug)
* pair across ALL cache key shapes (`orgId:projectId`, `dsn:publicKey`,
* `list:{org}/{project}`). This is useful for slug-based lookups where the
* caller doesn't know the numeric org/project IDs upfront — e.g., when
* resolving `<org>/<project>` from CLI arguments in `fetchProjectId`.
*
* Uses `MAX(cached_at)` with a covariant SELECT: SQLite guarantees the other
* columns come from the row that produced the MAX value. This avoids the
* ambiguity that would arise from a plain `LIMIT 1` without an ORDER BY.
*
* @param orgSlug - Organization slug (case-sensitive)
* @param projectSlug - Project slug (case-sensitive)
* @returns Cached project entry, or undefined if no match
*/
export function getCachedProjectBySlug(
orgSlug: string,
projectSlug: string
): CachedProject | undefined {
const db = getDatabase();
const row = db
.query(
"SELECT cache_key, org_slug, org_name, project_slug, project_name, project_id, MAX(cached_at) AS cached_at, last_accessed FROM project_cache WHERE org_slug = ? AND project_slug = ?"
)
.get(orgSlug, projectSlug) as ProjectCacheRow | undefined;
// When no rows match, SQLite still returns a single row with NULL columns
// because of the MAX aggregate — guard on cache_key being populated.
if (!row?.cache_key) {
recordCacheHit("project", false);
return;
}
recordCacheHit("project", true);
touchCacheEntry("project_cache", "cache_key", row.cache_key);
return rowToCachedProject(row);
}
/**
* Get cached project slugs for a specific organization.
*
* Used by shell completions to suggest projects within a known org.
* Matches on `org_slug` (case-sensitive) and deduplicates by project slug.
*
* @param orgSlug - The organization slug to filter by
*/
export function getCachedProjectsForOrg(
orgSlug: string
): { projectSlug: string; projectName: string }[] {
const db = getDatabase();
// Use MAX(cached_at) to deterministically pick the most recently cached
// project_name when the same project appears under different cache keys
// (e.g., both orgId:projectId and dsn:publicKey).
// SQLite guarantees that non-aggregated columns come from the row that
// produced the MAX/MIN aggregate value.
const rows = db
.query(
"SELECT project_slug, project_name, MAX(cached_at) FROM project_cache WHERE org_slug = ? GROUP BY project_slug"
)
.all(orgSlug) as Pick<ProjectCacheRow, "project_slug" | "project_name">[];
return rows.map((row) => ({
projectSlug: row.project_slug,
projectName: row.project_name,
}));
}
/**
* Batch-cache projects for an organization.
*
* Called from `listProjects()` at the API layer so every command that
* lists projects (project list, findProjectsByPattern, etc.) automatically
* seeds the completion cache. Follows the `setOrgRegions()` pattern.
*
* @param orgSlug - Organization slug
* @param orgName - Organization display name
* @param projects - Projects to cache (id, slug, name from SentryProject)
*/
export function cacheProjectsForOrg(
orgSlug: string,
orgName: string,
projects: Array<{ id: string; slug: string; name: string }>
): void {
if (projects.length === 0) {
return;
}
const db = getDatabase();
const now = Date.now();
db.transaction(() => {
for (const p of projects) {
runUpsert(
db,
"project_cache",
{
cache_key: `list:${orgSlug}/${p.slug}`,
org_slug: orgSlug,
org_name: orgName,
project_slug: p.slug,
project_name: p.name,
project_id: p.id,
cached_at: now,
last_accessed: now,
},
["cache_key"]
);
}
})();
maybeCleanupCaches();
}
export function clearProjectCache(): void {
const db = getDatabase();
db.query("DELETE FROM project_cache").run();
}
|