diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index c37607bd6..8543f4bab 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -94,6 +94,7 @@ export const OTHER_INDEX_LOST = 3; // Structures/warships destroyed/captured by export const OTHER_INDEX_UPGRADE = 4; // Structures upgraded export const BigIntStringSchema = z.preprocess((val) => { + if (val === null) return 0n; if (typeof val === "string" && /^-?\d+$/.test(val)) return BigInt(val); if (typeof val === "bigint") return val; return val; diff --git a/tests/StatsSchema.test.ts b/tests/StatsSchema.test.ts index 8b8f6b5dd..b513e9537 100644 --- a/tests/StatsSchema.test.ts +++ b/tests/StatsSchema.test.ts @@ -1,3 +1,4 @@ +import { PlayerStatsLeafSchema } from "../src/core/ApiSchemas"; import { PlayerStatsSchema } from "../src/core/StatsSchemas"; function testPlayerSchema( @@ -45,4 +46,32 @@ describe("StatsSchema", () => { testPlayerSchema("{", false, true); testPlayerSchema("{}}", false, true); }); + + test("null array elements coerce to 0n (LEFT JOIN rows with no stats)", () => { + // Postgres SUM() over all-NULL rows returns NULL. These should parse as 0n. + testPlayerSchema( + '{"attacks":[null,null,null],"betrayals":null,"gold":[null,null,null,null,null,null]}', + ); + }); +}); + +describe("PlayerStatsLeafSchema", () => { + test("null stat values coerce to 0n", () => { + const result = PlayerStatsLeafSchema.safeParse({ + wins: "0", + losses: "1", + total: "1", + stats: { attacks: [null, null, null], betrayals: null }, + }); + expect(result.success).toBe(true); + }); + + test("missing required field (wins) still fails — undefined is not coerced", () => { + const result = PlayerStatsLeafSchema.safeParse({ + losses: "1", + total: "1", + stats: {}, + }); + expect(result.success).toBe(false); + }); });