Remove plugin data query projection

This commit is contained in:
npc0-hue
2026-08-21 15:36:49 +08:00
parent fc86b24699
commit 8aeb488179
20 changed files with 91 additions and 703 deletions
@@ -273,29 +273,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-player-profile.result.schema.json",
"sqlRef": "sql/scum-db-v57/users.sql",
"pollIntervalSeconds": 3,
"rowTarget": {
"collection": "scum_users",
"upsertKeys": [
"steamId"
],
"writeMode": "merge",
"columnMappings": {
"userProfileId": "userProfileId",
"steamId": "steamId",
"gamePlayerId": "gamePlayerId",
"displayName": "displayName",
"squadId": "squadId",
"squadName": "squadName",
"famePoints": "famePoints",
"normalBalance": "normalBalance",
"goldBalance": "goldBalance",
"x": "x",
"y": "y",
"z": "z",
"lastLoginTime": "lastLoginTime",
"lastSaveTime": "lastSaveTime"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -310,25 +287,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-squads.result.schema.json",
"sqlRef": "sql/scum-db-v57/squads.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_squads",
"upsertKeys": [
"squadId"
],
"writeMode": "replace",
"columnMappings": {
"squadId": "squadId",
"name": "name",
"leaderProfileId": "leaderProfileId",
"leaderPlayerId": "leaderPlayerId",
"memberCount": "memberCount",
"score": "score",
"memberLimit": "memberLimit",
"message": "message",
"info": "info",
"lastMemberLoginTime": "lastMemberLoginTime"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -343,23 +301,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-squad-members.result.schema.json",
"sqlRef": "sql/scum-db-v57/squad-members.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_squad_members",
"upsertKeys": [
"squadId",
"steamId"
],
"writeMode": "replace",
"columnMappings": {
"squadId": "squadId",
"userProfileId": "userProfileId",
"gamePlayerId": "gamePlayerId",
"steamId": "steamId",
"displayName": "displayName",
"rank": "rank",
"isLeader": "isLeader"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -374,24 +315,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-vehicles.result.schema.json",
"sqlRef": "sql/scum-db-v57/vehicles.sql",
"pollIntervalSeconds": 3,
"rowTarget": {
"collection": "scum_vehicles",
"upsertKeys": [
"vehicleId"
],
"writeMode": "replace",
"columnMappings": {
"vehicleId": "vehicleId",
"entityId": "entityId",
"className": "className",
"label": "label",
"x": "x",
"y": "y",
"z": "z",
"lastAccessTime": "lastAccessTime",
"isFunctional": "isFunctional"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -406,28 +329,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-flags.result.schema.json",
"sqlRef": "sql/scum-db-v57/flags.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_flags",
"upsertKeys": [
"flagId"
],
"writeMode": "replace",
"columnMappings": {
"flagId": "flagId",
"entityId": "entityId",
"baseId": "baseId",
"ownerProfileId": "ownerProfileId",
"ownerPlayerId": "ownerPlayerId",
"ownerSquadId": "ownerSquadId",
"ownerSquadName": "ownerSquadName",
"overtakerProfileId": "overtakerProfileId",
"overtakeEndTime": "overtakeEndTime",
"ownershipConfidence": "ownershipConfidence",
"x": "x",
"y": "y",
"z": "z"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -442,27 +343,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-positions.result.schema.json",
"sqlRef": "sql/scum-db-v57/map-points.sql",
"pollIntervalSeconds": 3,
"rowTarget": {
"collection": "scum_map_points",
"upsertKeys": [
"subjectType",
"subjectId"
],
"writeMode": "replace",
"columnMappings": {
"subjectType": "subjectType",
"subjectId": "subjectId",
"userProfileId": "userProfileId",
"gamePlayerId": "gamePlayerId",
"vehicleId": "vehicleId",
"entityId": "entityId",
"baseId": "baseId",
"x": "x",
"y": "y",
"z": "z",
"observedAt": "observedAt"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -477,25 +357,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-tasks.result.schema.json",
"sqlRef": "sql/scum-db-v57/tasks.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_tasks",
"upsertKeys": [
"taskRecordId"
],
"writeMode": "replace",
"columnMappings": {
"taskRecordId": "taskRecordId",
"taskKind": "taskKind",
"userProfileId": "userProfileId",
"mapId": "mapId",
"trackingDataSetId": "trackingDataSetId",
"dataAssetPath": "dataAssetPath",
"sequenceIndex": "sequenceIndex",
"isTracked": "isTracked",
"state": "state",
"completionDeadline": "completionDeadline"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -510,28 +371,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-events.result.schema.json",
"sqlRef": "sql/scum-db-v57/events.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_native_event_rounds",
"upsertKeys": [
"eventRecordId"
],
"writeMode": "replace",
"columnMappings": {
"eventRecordId": "eventRecordId",
"eventId": "eventId",
"roundId": "roundId",
"userProfileId": "userProfileId",
"startTime": "startTime",
"endTime": "endTime",
"state": "state",
"score": "score",
"enemyKills": "enemyKills",
"teamKills": "teamKills",
"deaths": "deaths",
"assists": "assists",
"headshots": "headshots"
}
},
"maxRows": 500,
"timeoutSeconds": 15
},
@@ -546,20 +385,6 @@
"resultSchemaRef": "schemas/bridge/queries/scum-native-timed-gifts.result.schema.json",
"sqlRef": "sql/scum-db-v57/native-timed-gifts.sql",
"pollIntervalSeconds": 1800,
"rowTarget": {
"collection": "scum_timed_gift_events",
"upsertKeys": [
"timedGiftId"
],
"writeMode": "replace",
"columnMappings": {
"timedGiftId": "timedGiftId",
"userProfileId": "userProfileId",
"mapId": "mapId",
"spawnTime": "spawnTime",
"spawnAt": "spawnAt"
}
},
"maxRows": 500,
"timeoutSeconds": 15
}
@@ -335,23 +335,11 @@
"parameterSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
"sqlRef": { "$ref": "#/$defs/relativeSqlRef" },
"rowTarget": { "$ref": "#/$defs/pluginDataRowTarget" },
"maxRows": { "type": "integer", "minimum": 1, "maximum": 500 },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 60 },
"pollIntervalSeconds": { "type": "integer", "minimum": 0, "maximum": 86400 }
}
},
"pluginDataRowTarget": {
"type": "object",
"required": ["collection", "upsertKeys", "columnMappings", "writeMode"],
"additionalProperties": false,
"properties": {
"collection": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,119}$" },
"upsertKeys": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" }, "uniqueItems": true, "minItems": 1, "maxItems": 8 },
"columnMappings": { "type": "object", "minProperties": 1, "maxProperties": 64, "propertyNames": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" }, "additionalProperties": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" } },
"writeMode": { "enum": ["merge", "replace"] }
}
},
"gameClientBridgeLogProjection": {
"type": "object",
"required": ["key", "streamKeys", "steps", "correlationFields", "maxInterveningLines", "target"],
+1 -12
View File
@@ -685,7 +685,6 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
parameterSchemaRef?: string;
resultSchemaRef?: string;
sqlRef?: string;
rowTarget?: { collection?: string; upsertKeys?: string[]; columnMappings?: Record<string, string>; writeMode?: string };
maxRows?: number;
timeoutSeconds?: number;
pollIntervalSeconds?: number;
@@ -817,17 +816,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
errors.push(`${location}.${field}: raw host paths and unsafe schema references are not allowed`);
}
}
const projectsRows = queryTemplate.sqlRef !== undefined || queryTemplate.rowTarget !== undefined;
if (projectsRows) {
if (!queryTemplate.sqlRef || !isSafeRelativeSqlRef(queryTemplate.sqlRef)) errors.push(`${location}.sqlRef: projected queries require a package-relative SQL asset`);
const target = queryTemplate.rowTarget;
if (!target || !/^[A-Za-z][A-Za-z0-9._-]{0,119}$/.test(target.collection ?? "")) errors.push(`${location}.rowTarget.collection: projected queries require a safe collection`);
if (!Array.isArray(target?.upsertKeys) || target.upsertKeys.length === 0 || !target.upsertKeys.every((key) => /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(key))) errors.push(`${location}.rowTarget.upsertKeys: projected queries require safe upsert keys`);
const mappings = target?.columnMappings;
if (!mappings || Array.isArray(mappings) || Object.keys(mappings).length === 0 || !Object.entries(mappings).every(([destination, source]) => /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(destination) && typeof source === "string" && /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(source))) errors.push(`${location}.rowTarget.columnMappings: projected queries require safe field mappings`);
if (mappings && Array.isArray(target?.upsertKeys) && !target.upsertKeys.every((key) => key in mappings)) errors.push(`${location}.rowTarget.upsertKeys: every upsert key must be declared in columnMappings`);
if (!new Set(["merge", "replace"]).has(target?.writeMode ?? "")) errors.push(`${location}.rowTarget.writeMode: projected queries require merge or replace`);
}
if (queryTemplate.sqlRef !== undefined && !isSafeRelativeSqlRef(queryTemplate.sqlRef)) errors.push(`${location}.sqlRef: query templates require a package-relative SQL asset`);
if (!Number.isInteger(queryTemplate.maxRows) || (queryTemplate.maxRows ?? 0) < 1 || (queryTemplate.maxRows ?? 0) > 500) {
errors.push(`${location}.maxRows: must be an integer between 1 and 500`);
}
-8
View File
@@ -249,14 +249,6 @@ export interface GameClientBridgeQueryTemplateDeclaration {
maxRows: number;
timeoutSeconds: number;
pollIntervalSeconds?: number;
rowTarget?: PluginDataRowTargetDeclaration;
}
export interface PluginDataRowTargetDeclaration {
collection: string;
upsertKeys: string[];
columnMappings: Record<string, string>;
writeMode: "merge" | "replace";
}
export interface GameClientBridgeLogProjectionStepDeclaration {
@@ -637,7 +637,6 @@ describe("plugin manifest validation", () => {
resultSchemaRef: string;
sqlRef: string;
pollIntervalSeconds: number;
rowTarget: { collection: string; upsertKeys: string[]; writeMode: "merge" | "replace"; columnMappings: Record<string, string> };
maxRows: number;
timeoutSeconds: number;
}>;
@@ -673,11 +672,7 @@ describe("plugin manifest validation", () => {
expect(template.transportKey).toBe("scum-database");
expect(template.targetKey).toBe("scum-database");
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/.+\.sql$/);
expect(template.rowTarget.collection).toMatch(/^scum_/);
expect(template.pollIntervalSeconds).toBe(fastTemplates.has(key) ? 3 : 1800);
expect(template.rowTarget.writeMode).toBe(key === "scum.player.profile" ? "merge" : "replace");
expect(template.rowTarget.upsertKeys.length).toBeGreaterThan(0);
expect(template.rowTarget.upsertKeys.every((upsertKey) => upsertKey in template.rowTarget.columnMappings)).toBe(true);
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"));
@@ -687,15 +682,12 @@ describe("plugin manifest validation", () => {
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(template.rowTarget.columnMappings).toEqual(Object.fromEntries(expectedColumns.map((column) => [column, column])));
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"));
}
}
expect(templatesByKey.get("scum.player.profile")?.rowTarget.upsertKeys).toEqual(["steamId"]);
expect(templatesByKey.get("scum.squad-members")?.rowTarget.upsertKeys).toEqual(["squadId", "steamId"]);
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);