Keep artifact list and beta queries free of file bytes

This commit is contained in:
2026-10-06 14:01:12 -04:00
parent 27be1abf41
commit ddb3db9537
21 changed files with 197 additions and 101 deletions
+3 -2
View File
@@ -21,9 +21,10 @@ export function archiveBetaRevision(id: number, revision: number, startedAt: str
export function betaRevisionHistory(id: number): BetaRevision[] {
const rows = db().prepare('SELECT revision, started_at, notes, results_json FROM beta_revisions WHERE beta_id = ? ORDER BY revision DESC')
.all(id) as Array<{ revision: number; started_at: string; notes: string | null; results_json: string | null }>;
const files = db().prepare(`SELECT revision, script_id, name, kind, path, version_id, version, release_version, sha256
FROM beta_revision_files WHERE beta_id = ? ORDER BY script_id`).all(id) as Array<BetaRevision['files'][number] & { revision: number }>;
return rows.map(({ results_json, ...row }) => ({ ...row,
results: results_json ? JSON.parse(results_json) : null,
files: db().prepare(`SELECT script_id, name, kind, path, version_id, version, release_version, sha256
FROM beta_revision_files WHERE beta_id = ? AND revision = ? ORDER BY script_id`).all(id, row.revision) as BetaRevision['files']
files: files.filter(file => file.revision === row.revision).map(({ revision, ...file }) => file)
}));
}
+11 -10
View File
@@ -1,3 +1,4 @@
import { getVersionContent } from './scripts.js';
import { parseBetaPlan, type BetaPlan } from '../../beta-plan.js';
import { storedPlan, testView, activity, setOutcome } from './beta-tests.js';
import { archiveBetaRevision, currentBetaRevision } from './beta-revisions.js';
@@ -89,7 +90,7 @@ function composeCfgForServer(
if (inputs.inBetaBaseline) {
const s = getScript(inputs.inBetaBaseline.script_id);
if (s && scopeAppliesToServer(s, { game: serverGame, map: serverMap })) {
baselineText = inputs.inBetaBaseline.content.toString('utf8');
baselineText = getVersionContent(inputs.inBetaBaseline.id).toString('utf8');
baselineScriptId = s.id;
}
}
@@ -99,7 +100,7 @@ function composeCfgForServer(
if (promoted?.current_version_id) {
const ver = getVersion(promoted.current_version_id);
if (ver) {
baselineText = ver.content.toString('utf8');
baselineText = getVersionContent(ver.id).toString('utf8');
baselineScriptId = promoted.id;
}
}
@@ -122,7 +123,7 @@ function composeCfgForServer(
if (!s.current_version_id) continue;
const ver = getVersion(s.current_version_id);
if (!ver) continue;
promotedOverlays.push({ id: s.id, text: ver.content.toString('utf8') });
promotedOverlays.push({ id: s.id, text: getVersionContent(ver.id).toString('utf8') });
promotedIdsSeen.add(s.id);
}
};
@@ -132,7 +133,7 @@ function composeCfgForServer(
// In-beta overlays scoped to this server.
const inBetaScopedOverlays = inputs.inBetaOverlays
.filter(({ script }) => scopeAppliesToServer(script, { game: serverGame, map: serverMap }))
.map(({ script, version }) => ({ id: script.id, text: version.content.toString('utf8') }));
.map(({ script, version }) => ({ id: script.id, text: getVersionContent(version.id).toString('utf8') }));
const composed = composeServerCfg(baselineText, [...promotedOverlays, ...inBetaScopedOverlays], serverGame, serverMap, getServer(serverName)?.map_slug);
return { text: composed, baselineScriptId };
@@ -604,7 +605,7 @@ function revisionFileChanges(betaId: number, name: string): Parameters<typeof qu
const server = getServer(name)!;
const changes: Parameters<typeof queueFileChange>[0][] = resolved.filter(row => !isCollapsingKind(row.script.kind))
.map(({ script, version }) => ({ sourceKind: 'beta', sourceId: String(betaId), server: name, kind: script.kind,
relPath: script.target_rel_path, content: version.content, scriptVersionId: version.id, inventorySource: `beta:${betaId}` }));
relPath: script.target_rel_path, content: getVersionContent(version.id), scriptVersionId: version.id, inventorySource: `beta:${betaId}` }));
if (resolved.some(row => isCollapsingKind(row.script.kind))) {
const composed = composeCfgForServer(name, server.game, server.map, {
inBetaBaseline: resolved.find(row => row.script.kind === 'baseline')?.version,
@@ -639,7 +640,7 @@ async function deployBetaServersInternal(betaId: number, names: string[]): Promi
await mkdir(archive, { recursive: true });
for (const r of nonCollapsing) {
const base = posix.basename(r.script.target_rel_path);
await writeFile(join(archive, base), r.version.content);
await writeFile(join(archive, base), getVersionContent(r.version.id));
}
// Persist the requested bytes without touching any live destination. Provisioning
@@ -650,7 +651,7 @@ async function deployBetaServersInternal(betaId: number, names: string[]): Promi
try {
const changes: Parameters<typeof queueFileChange>[0][] = nonCollapsing.map(({ script, version }) => ({
sourceKind: 'beta' as const, sourceId: String(betaId), server: name,
kind: script.kind, relPath: script.target_rel_path, content: version.content,
kind: script.kind, relPath: script.target_rel_path, content: getVersionContent(version.id),
scriptVersionId: version.id, inventorySource: `beta:${betaId}`
}));
if (collapsingBaseline || collapsingOverlays.length > 0) {
@@ -990,7 +991,7 @@ async function promoteBetaInternal(betaId: number, reviewedToken?: string): Prom
priorSha = snap.prior_sha256;
} else if (bss.prior_version_id) {
const pv = getVersion(bss.prior_version_id);
if (pv) { priorContent = pv.content; priorSha = pv.sha256; }
if (pv) { priorContent = getVersionContent(pv.id); priorSha = pv.sha256; }
}
plan = {
script, version, server: name,
@@ -1121,12 +1122,12 @@ async function promoteBetaInternal(betaId: number, reviewedToken?: string): Prom
p.priorSha,
p.version.sha256,
p.priorContent,
p.version.content,
getVersionContent(p.version.id),
betaServerSet.has(p.server) ? 0 : 1
);
if (plansToActivate.has(p)) {
queueFileChange({ sourceKind: 'campaign', sourceId: depId, server: p.server,
kind: p.script.kind, relPath: p.script.target_rel_path, content: p.version.content,
kind: p.script.kind, relPath: p.script.target_rel_path, content: getVersionContent(p.version.id),
scriptVersionId: p.version.id, inventorySource: `promote:${depId}`,
deploymentFileId: Number(file.lastInsertRowid), preserveBeforeImage: betaServerSet.has(p.server) });
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { getVersionContent } from './scripts.js';
import { readdir, readFile, stat, nativeFileManifest, type FileFingerprint } from '../backends/files.js';
import { randomUUID } from 'node:crypto';
import type { DriftPlan, DriftPlanFile } from '$lib/drift-plan.js';
@@ -275,7 +276,7 @@ async function resolveManagedEntry(server: string, entry: DriftEntry): Promise<B
}
if (!entry.scriptVersionId) return null;
const ver = getVersion(entry.scriptVersionId);
return ver?.content ?? null;
return ver ? getVersionContent(ver.id) : null;
}
function composeExpectedCfg(
+46
View File
@@ -0,0 +1,46 @@
import { db } from '../db/index.js';
import type { ArtifactKind, ScriptRow, ScriptVersionRow } from './scripts.js';
/** Fixed query count regardless of the number of artifacts or retained versions. */
export function scriptCatalog(kind?: ArtifactKind) {
const version = (alias: string) => `CASE WHEN ${alias}.id IS NOT NULL THEN json_object(
'id',${alias}.id,'version',${alias}.version,'release_version',${alias}.release_version,
'sha256',${alias}.sha256,'size',${alias}.size,'state',${alias}.state,'uploaded_at',${alias}.uploaded_at) END`;
const rows = db().prepare(`SELECT s.*, ${version('c')} current_json, ${version('v')} latest_json,
COALESCE(counts.n,0) versionCount, active.beta_id runningBetaId
FROM scripts s
LEFT JOIN (SELECT script_id, COUNT(*) n, MAX(version) latest FROM script_versions GROUP BY script_id) counts ON counts.script_id=s.id
LEFT JOIN script_versions v ON v.script_id=s.id AND v.version=counts.latest
LEFT JOIN script_versions c ON c.id=s.current_version_id
LEFT JOIN (SELECT bs.script_id, MIN(b.id) beta_id FROM beta_scripts bs JOIN betas b ON b.id=bs.beta_id
WHERE b.state IN ('running','ready-promote') GROUP BY bs.script_id) active ON active.script_id=s.id
${kind ? 'WHERE s.kind=?' : ''} ORDER BY s.name`).all(...(kind ? [kind] : [])) as Array<ScriptRow & {
current_json: string | null; latest_json: string | null; versionCount: number; runningBetaId: number | null;
}>;
const servers = db().prepare('SELECT name, game, map FROM servers ORDER BY name').all() as { name: string; game: string | null; map: string | null }[];
return rows.map(({ current_json, latest_json, ...row }) => ({ ...row,
currentVersion: current_json ? JSON.parse(current_json) as ScriptVersionRow : null,
latestVersion: latest_json ? JSON.parse(latest_json) as ScriptVersionRow : null,
servers: row.scope_type && row.scope_value ? servers.filter(server => server[row.scope_type!] === row.scope_value).map(server => server.name) : []
}));
}
export function betaScriptMetadata(ids: number[]) {
const rows = db().prepare(`SELECT bs.beta_id, bs.script_id id, COALESCE(s.name,'(deleted)') name,
COALESCE(s.target_rel_path,'') target_rel_path, COALESCE(s.kind,bs.kind,'script') kind,
v.id version_id, v.version, v.release_version, v.sha256
FROM beta_scripts bs LEFT JOIN scripts s ON s.id=bs.script_id
LEFT JOIN script_versions v ON v.id=bs.script_version_id
WHERE bs.beta_id IN (SELECT value FROM json_each(?)) ORDER BY bs.script_id`).all(JSON.stringify(ids)) as Array<{
beta_id: number; id: number; name: string; target_rel_path: string; kind: ArtifactKind;
version_id: number | null; version: number; release_version: string | null; sha256: string;
}>;
const grouped = new Map<number, Array<{ id: number; name: string; target_rel_path: string; kind: ArtifactKind;
version: { id: number; version: number; release_version: string | null; sha256: string } | null }>>();
for (const { beta_id, version_id, version, release_version, sha256, ...script } of rows) {
const group = grouped.get(beta_id) ?? [];
group.push({ ...script, version: version_id === null ? null : { id: version_id, version, release_version, sha256 } });
grouped.set(beta_id, group);
}
return grouped;
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { getVersion, getVersionMetadata, listScripts } from './scripts.js';
import { getVersion, getVersionContent, getVersionMetadata, listScripts } from './scripts.js';
function selectedArtifacts<Version>(game: string | null, map: string | null, readVersion: (id: number) => Version | undefined) {
return (['script', 'plugin'] as const).flatMap((kind) =>
@@ -14,7 +14,10 @@ function selectedArtifacts<Version>(game: string | null, map: string | null, rea
/** Published file artifacts shared by provisioning and drift reconciliation. */
export function promotedArtifacts(game: string | null, map: string | null) {
return selectedArtifacts(game, map, getVersion);
return selectedArtifacts(game, map, id => {
const version = getVersion(id);
return version ? { ...version, content: getVersionContent(id) } : undefined;
});
}
/** Metadata uses the same promotion and scope rules without reading file content. */
+11 -4
View File
@@ -32,7 +32,6 @@ export interface ScriptVersionRow {
release_version: string | null;
sha256: string;
size: number;
content: Buffer;
notes: string | null;
state: ScriptVersionState;
kind: ArtifactKind;
@@ -137,14 +136,22 @@ export function listPromotedOverlaysForScope(
return rows.filter((r) => !skip.has(r.id));
}
export const VERSION_COLUMNS = 'id, script_id, version, release_version, sha256, size, notes, state, kind, uploaded_at';
export function getVersionContent(id: number): Buffer {
const row = db().prepare('SELECT content FROM script_versions WHERE id = ?').get(id) as { content: Buffer } | undefined;
if (!row) throw new ScriptError(`no such version (id=${id})`, 'no-such-version');
return row.content;
}
export function listVersions(scriptId: number): ScriptVersionRow[] {
return db()
.prepare('SELECT * FROM script_versions WHERE script_id = ? ORDER BY version DESC')
.prepare(`SELECT ${VERSION_COLUMNS} FROM script_versions WHERE script_id = ? ORDER BY version DESC`)
.all(scriptId) as ScriptVersionRow[];
}
export function getVersion(id: number): ScriptVersionRow | undefined {
return db().prepare('SELECT * FROM script_versions WHERE id = ?').get(id) as
return db().prepare(`SELECT ${VERSION_COLUMNS} FROM script_versions WHERE id = ?`).get(id) as
| ScriptVersionRow
| undefined;
}
@@ -162,7 +169,7 @@ export function getVersionMetadata(id: number): ScriptVersionMetadata | undefine
// upload, whether or not it has been promoted (current_version_id).
export function getLatestVersion(scriptId: number): ScriptVersionRow | undefined {
return db()
.prepare('SELECT * FROM script_versions WHERE script_id = ? ORDER BY version DESC LIMIT 1')
.prepare(`SELECT ${VERSION_COLUMNS} FROM script_versions WHERE script_id = ? ORDER BY version DESC LIMIT 1`)
.get(scriptId) as ScriptVersionRow | undefined;
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { getVersionContent } from './scripts.js';
import { composeServerCfg } from './config-compose.js';
import { getBaselineForScope, getVersion, listPromotedOverlaysForScope } from './scripts.js';
import { selectedZombieMap, mapRotation } from './map-aliases.js';
@@ -23,7 +24,7 @@ export function getPromotedServerConfig(game: string | null, map: string | null,
...(map ? listPromotedOverlaysForScope('map', map) : [])
].flatMap((overlay) => {
const current = overlay.current_version_id ? getVersion(overlay.current_version_id) : undefined;
return current ? [{ id: overlay.id, text: current.content.toString('utf8') }] : [];
return current ? [{ id: overlay.id, text: getVersionContent(current.id).toString('utf8') }] : [];
});
return Buffer.from(composeServerCfg(version.content.toString('utf8'), overlays, game, map, mapSlug), 'utf8');
return Buffer.from(composeServerCfg(getVersionContent(version.id).toString('utf8'), overlays, game, map, mapSlug), 'utf8');
}