From 1a4471e837c92a7ab95cbcc422d095e4557bcb38 Mon Sep 17 00:00:00 2001 From: Rishabh Gusain Date: Sat, 22 Aug 2026 17:18:49 +0530 Subject: [PATCH 1/2] fix(validation): add missing analytics date range schema --- packages/validation/src/schemas/analytics.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/validation/src/schemas/analytics.ts b/packages/validation/src/schemas/analytics.ts index 79dd5c0e6..d777b3cb5 100644 --- a/packages/validation/src/schemas/analytics.ts +++ b/packages/validation/src/schemas/analytics.ts @@ -65,6 +65,23 @@ const timestampSchema = z } ); +const analyticsDateOnlySchema = z.iso.date(); + +export const analyticsDateRangeSchema = z + .object({ + startDate: analyticsDateOnlySchema.optional(), + endDate: analyticsDateOnlySchema.optional(), + }) + .superRefine((range, ctx) => { + if (range.startDate && range.endDate && range.startDate > range.endDate) { + ctx.addIssue({ + code: "custom", + message: "startDate must be on or before endDate", + path: ["startDate"], + }); + } + }); + export const analyticsEventSchema = z.object({ eventId: z.string().max(VALIDATION_LIMITS.EVENT_ID_MAX_LENGTH), name: z.string().min(1).max(VALIDATION_LIMITS.NAME_MAX_LENGTH), From fb5a3fa92c57d9515993fb9b35fbb5ab5c5022bb Mon Sep 17 00:00:00 2001 From: Rishabh Gusain Date: Sat, 22 Aug 2026 18:12:32 +0530 Subject: [PATCH 2/2] fix(validation): reject partial analytics date ranges Partial ranges (only startDate or only endDate) were accepted by the schema but silently replaced with a default range downstream, returning analytics for dates the caller did not request. Require both endpoints or neither. --- .../validation/src/schemas/analytics.test.ts | 73 ++++++++++++++++++- packages/validation/src/schemas/analytics.ts | 12 +++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/validation/src/schemas/analytics.test.ts b/packages/validation/src/schemas/analytics.test.ts index cb87d405d..8f69a4407 100644 --- a/packages/validation/src/schemas/analytics.test.ts +++ b/packages/validation/src/schemas/analytics.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "bun:test"; -import { analyticsEventSchema } from "./analytics"; +import { + analyticsDateRangeSchema, + analyticsEventSchema, +} from "./analytics"; const validEvent = { eventId: "test-id", @@ -96,3 +99,71 @@ describe("analyticsEventSchema referrer validation", () => { expect(result.success).toBe(false); }); }); + +describe("analyticsDateRangeSchema", () => { + it("accepts a complete range", () => { + const result = analyticsDateRangeSchema.safeParse({ + startDate: "2026-01-01", + endDate: "2026-01-31", + }); + expect(result.success).toBe(true); + }); + + it("accepts no dates at all", () => { + const result = analyticsDateRangeSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + it("rejects a start date without an end date", () => { + const result = analyticsDateRangeSchema.safeParse({ + startDate: "2026-01-01", + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "endDate is required when startDate is provided" + ); + } + }); + + it("rejects an end date without a start date", () => { + const result = analyticsDateRangeSchema.safeParse({ + endDate: "2026-01-31", + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "startDate is required when endDate is provided" + ); + } + }); + + it("rejects an inverted range", () => { + const result = analyticsDateRangeSchema.safeParse({ + startDate: "2026-02-01", + endDate: "2026-01-31", + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + "startDate must be on or before endDate" + ); + } + }); + + it("accepts equal start and end dates", () => { + const result = analyticsDateRangeSchema.safeParse({ + startDate: "2026-01-31", + endDate: "2026-01-31", + }); + expect(result.success).toBe(true); + }); + + it("rejects non-ISO date strings", () => { + const result = analyticsDateRangeSchema.safeParse({ + startDate: "01/2026", + endDate: "2026-01-31", + }); + expect(result.success).toBe(false); + }); +}); diff --git a/packages/validation/src/schemas/analytics.ts b/packages/validation/src/schemas/analytics.ts index d777b3cb5..66e3603fb 100644 --- a/packages/validation/src/schemas/analytics.ts +++ b/packages/validation/src/schemas/analytics.ts @@ -73,6 +73,18 @@ export const analyticsDateRangeSchema = z endDate: analyticsDateOnlySchema.optional(), }) .superRefine((range, ctx) => { + const hasStart = Boolean(range.startDate); + const hasEnd = Boolean(range.endDate); + if (hasStart !== hasEnd) { + ctx.addIssue({ + code: "custom", + message: hasStart + ? "endDate is required when startDate is provided" + : "startDate is required when endDate is provided", + path: [hasStart ? "endDate" : "startDate"], + }); + return; + } if (range.startDate && range.endDate && range.startDate > range.endDate) { ctx.addIssue({ code: "custom",