Remove SCUM sqlite user projections

This commit is contained in:
npc0-hue
2026-09-14 13:41:44 +08:00
parent b1f102bafb
commit ecdca8e28b
76 changed files with 2264 additions and 1558 deletions
+34 -99
View File
@@ -140,21 +140,19 @@ describe("plugin manifest validation", () => {
it("removes raw SQL command surfaces", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates: Array<{ key: string }> } };
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates?: Array<{ key: string }> } };
expect(manifest.gameClientBridge.commands.some((command) => command.type === "diagnostic.ping")).toBe(false);
expect(manifest.gameClientBridge.commands.map((command) => command.type)).not.toEqual(expect.arrayContaining(["config.read", "config.patch", "database.request", "management.rcon.request", "management.program.request"]));
expect(manifest.gameClientBridge.queryTemplates.map((query) => query.key)).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"]));
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(false);
});
it("declares SCUM SQLite templates without platform projections", () => {
it("does not declare SCUM direct database templates or bridge projections", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
gameClientBridge: { lifecycleProjections?: unknown[]; queryTemplates: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: unknown[] }> };
gameClientBridge: { lifecycleProjections?: unknown[]; queryTemplates?: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: unknown[] }> };
};
const users = manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.player.profile");
expect(users).toMatchObject({ engine: "sqlite", transportKey: "scum-database", targetKey: "scum-database" });
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
});
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
@@ -215,22 +213,28 @@ describe("plugin manifest validation", () => {
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
});
it("declares database and management transports for direct run jobs", () => {
it("declares management transports without SCUM database access", () => {
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
remoteAccess?: { databaseEngines?: string[]; runCapabilities?: string[] };
runtimeProfiles?: {
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
dataTargets?: Array<{ key?: string; kind?: string }>;
};
};
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
expect(local?.capabilities).toContain("remote.run.rcon.command");
expect(local?.transportKeys).toContain("scum-management");
expect(local?.transportKeys).not.toContain("scum-database");
expect(manifest.remoteAccess?.databaseEngines).toEqual([]);
expect(manifest.remoteAccess?.runCapabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.runtimeProfiles?.dataTargets).toEqual([]);
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]) }),
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.rcon.command"] }),
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
]));
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.key === "scum-database" || profile.kind === "sqlite")).toBe(false);
});
it("covers the SCUM 4.1 bridge and lifecycle declarations", () => {
@@ -247,7 +251,7 @@ describe("plugin manifest validation", () => {
maxPayloadBytes: number;
}>;
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
queryTemplates: Array<{ key: string; projections?: unknown[] }>;
queryTemplates?: Array<{ key: string; projections?: unknown[] }>;
lifecycleProjections?: unknown[];
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
};
@@ -280,10 +284,10 @@ describe("plugin manifest validation", () => {
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
expect(manifest.gameClientBridge.pages.some((page) => page.queryTemplateKeys?.length)).toBe(false);
expect(manifest.pages.map((page) => page.key)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
expect(manifest.pages.map((page) => page.key)).not.toContain("files-config");
expect(manifest.pages.find((page) => page.key === "players")?.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
@@ -404,108 +408,39 @@ describe("plugin manifest validation", () => {
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "vehicles", "flags"]));
});
it("declares typed SCUM.db query templates without browser-visible SQL", () => {
it("keeps SCUM user and vehicle data off direct database query templates", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
permissions: string[];
capabilities: string[];
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[]; rcon?: boolean };
gameClientBridge: {
queryTemplates: Array<{
key: string;
title?: string;
permission: string;
engine: string;
transportKey: string;
targetKey: string;
parameterSchemaRef: string;
resultSchemaRef: string;
sqlRef: string;
pollIntervalSeconds: number;
maxRows: number;
timeoutSeconds: number;
projections?: unknown[];
}>;
queryTemplates?: Array<Record<string, unknown>>;
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
};
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
runtimeProfiles?: {
transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }>;
dataTargets?: Array<{ key: string; kind: string; transportKey: string; sourceRootKey: string; sourcePath: string; workspaceKey: string; refreshPolicy: string; maxBytes: number; platforms?: string[] }>;
dataTargets?: Array<{ key: string; kind: string }>;
};
assetFiles?: Array<{ path: string }>;
};
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.tasks", "scum.events", "scum.native-timed-gifts"];
const expectedColumnsByKey: Record<string, string[]> = {
"scum.player.profile": ["userProfileId", "steamId", "gamePlayerId", "displayName", "lastLoginIp", "registeredAt", "squadId", "squadName", "famePoints", "moneyBalance", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastLogoutTime", "lastSaveTime"],
"scum.squads": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
"scum.squad-members": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
"scum.vehicles": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional", "existsInGame", "mountedPrisonerIds", "mountedUserProfileIds", "mountedSteamIds"],
"scum.flags": ["flagId", "entityId", "baseId", "ownerProfileId", "ownerPlayerId", "ownerSquadId", "ownerSquadName", "overtakerProfileId", "overtakeEndTime", "ownershipConfidence", "x", "y", "z"],
"scum.positions": ["subjectType", "subjectId", "userProfileId", "gamePlayerId", "vehicleId", "entityId", "baseId", "x", "y", "z", "observedAt"],
"scum.tasks": ["taskRecordId", "taskKind", "userProfileId", "mapId", "trackingDataSetId", "dataAssetPath", "sequenceIndex", "isTracked", "state", "completionDeadline"],
"scum.events": ["eventRecordId", "eventId", "roundId", "userProfileId", "startTime", "endTime", "state", "score", "enemyKills", "teamKills", "deaths", "assists", "headshots"],
"scum.native-timed-gifts": ["timedGiftId", "userProfileId", "mapId", "spawnTime", "spawnAt"]
};
const fastTemplates = new Set(["scum.player.profile", "scum.vehicles", "scum.positions"]);
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
expect([...templatesByKey.values()].some((template) => Boolean(template.projections))).toBe(false);
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.capabilities).toContain("remote.run.db.sqlite.execute");
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.execute");
expect(manifest.remoteAccess?.databaseEngines).toContain("sqlite");
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
expect(manifest.capabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.remoteAccess?.runCapabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.remoteAccess?.databaseEngines).toEqual([]);
expect(manifest.remoteAccess?.rcon).toBe(true);
const sqliteTransport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "scum-database");
expect(sqliteTransport).toMatchObject({ kind: "sqlite", targetKey: "scum-database" });
expect(sqliteTransport?.capabilities).toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([
expect.objectContaining({
key: "scum-database",
kind: "sqlite.snapshot",
transportKey: "scum-database",
sourceRootKey: "server-root",
sourcePath: "SCUM/Saved/SaveFiles/SCUM.db",
workspaceKey: "databases/scum-database",
refreshPolicy: "on-demand-snapshot",
maxBytes: 1073741824,
platforms: ["windows"]
})
]));
for (const key of expectedKeys) {
const template = templatesByKey.get(key)!;
expect(template.engine).toBe("sqlite");
expect(template.transportKey).toBe("scum-database");
expect(template.targetKey).toBe("scum-database");
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/.+\.sql$/);
expect(template.pollIntervalSeconds).toBe(fastTemplates.has(key) ? 3 : 1800);
expect(fs.existsSync(path.join(pluginDir, template.sqlRef))).toBe(true);
expect(JSON.stringify(template).toLowerCase()).not.toMatch(/select\s|from\s|sqlite:|scum\.db|databasepath|hostpath|dsn/);
const parameters = JSON.parse(fs.readFileSync(path.join(pluginDir, template.parameterSchemaRef), "utf8"));
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, template.resultSchemaRef), "utf8"));
const sql = fs.readFileSync(path.join(pluginDir, template.sqlRef), "utf8");
const expectedColumns = expectedColumnsByKey[key];
expect(parameters).toMatchObject({ type: "object", additionalProperties: false });
expect(result).toMatchObject({ type: "object", additionalProperties: false, required: ["rows"] });
expect(result.properties.rows.maxItems).toBeLessThanOrEqual(template.maxRows);
expect(Object.keys(result.properties.rows.items.properties).sort()).toEqual([...expectedColumns].sort());
expect([...result.properties.rows.items.required].sort()).toEqual([...expectedColumns].sort());
for (const column of expectedColumns) {
expect(sql).toMatch(new RegExp(`\\bAS\\s+${column}\\b`, "i"));
}
}
const userSQL = fs.readFileSync(path.join(pluginDir, templatesByKey.get("scum.player.profile")!.sqlRef), "utf8");
const positionSQL = fs.readFileSync(path.join(pluginDir, templatesByKey.get("scum.positions")!.sqlRef), "utf8");
expect(userSQL).toMatch(/FROM user account\s+LEFT JOIN user_profile profile/i);
expect(positionSQL).toMatch(/account\.id AS subjectId/i);
expect(manifest.runtimeProfiles?.dataTargets).toEqual([]);
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.kind === "sqlite" || profile.key === "scum-database")).toBe(false);
expect((manifest.assetFiles ?? []).map((file) => file.path).some((assetPath) => assetPath.startsWith("sql/") || assetPath.includes("SCUM_DB_CONTRACT"))).toBe(false);
expect(fs.existsSync(path.join(pluginDir, "sql/scum-db-v57"))).toBe(false);
const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players");
const squadsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "squads");
const mapPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map");
const giftsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "gifts");
const workflowsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows");
expect(playersPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.positions"]));
expect(squadsPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.squads", "scum.squad-members", "scum.flags"]));
expect(mapPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.vehicles", "scum.flags", "scum.positions"]));
expect(playersPage?.queryTemplateKeys).toBeUndefined();
expect(squadsPage?.queryTemplateKeys).toBeUndefined();
expect(mapPage?.queryTemplateKeys).toBeUndefined();
expect(giftsPage?.commandTypes).toEqual(["reward.deliver"]);
expect(workflowsPage?.commandTypes).toEqual(["event.start"]);
for (const pageKey of ["players", "squads", "live-map", "gifts", "workflows"]) {
@@ -519,12 +454,12 @@ describe("plugin manifest validation", () => {
}
});
it("packages SCUM v57 config and gift metadata inside the plugin", () => {
it("packages SCUM v57 config and gift metadata without database query assets", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; configMapRefs: string[]; dataRefs?: string[] }> };
};
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-db-v57");
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-config-v57");
expect(pack).toMatchObject({ databaseUserVersion: 57 });
const configMaps = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.configMapRefs[0]), "utf8"));
const giftMetadata = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![0]), "utf8"));