From 93720c2bae58219032a1783fa92c177f904658bd Mon Sep 17 00:00:00 2001 From: Devon Wells Date: Tue, 2 Dec 2025 06:11:59 -0500 Subject: [PATCH] fix(dashboard): resolve all TypeScript errors with proper Zero type inference - Add function overloads to useQuery/useQueryOne for both direct queries and getter functions - Types now inferred automatically from Zero's Query - Remove explicit type arguments from all component useQuery calls - Update formatProperties to accept unknown (compatible with ReadonlyJSONValue) - Use minimal interface types in chart components for flexibility - Add @types/d3-scale and @types/d3-shape dependencies - Fix schema.ts: replace inferSelect with manual interfaces, use json() without type param - Fix queries.ts: use z.number().default() instead of z.number().optional() - Remove identifiedUsers query (Zero doesn't support null comparisons in .where()) - Fix a11y warnings in FlagsList (aria-label, self-closing span) - Remove unused Tooltip components from charts (LayerChart pre-release typing issues) --- apps/dashboard/package.json | 2 + .../src/lib/components/EventsList.svelte | 4 +- .../src/lib/components/FlagsList.svelte | 7 +- .../lib/components/OverviewDashboard.svelte | 10 +-- .../src/lib/components/SessionDetail.svelte | 6 +- .../src/lib/components/SessionsList.svelte | 4 +- .../charts/EventsOverTimeChart.svelte | 17 ++--- .../charts/SessionsTrendChart.svelte | 17 ++--- .../components/charts/TopEventsChart.svelte | 15 ++--- apps/dashboard/src/lib/utils/formatters.ts | 7 +- apps/dashboard/src/lib/zero/client.svelte.ts | 53 +++++++++------ apps/dashboard/src/lib/zero/queries.ts | 32 ++++----- apps/dashboard/src/lib/zero/schema.ts | 65 ++++++++++++++++--- apps/dashboard/src/routes/+page.svelte | 5 +- apps/dashboard/src/routes/events/+page.svelte | 5 +- apps/dashboard/src/routes/flags/+page.svelte | 5 +- .../src/routes/sessions/+page.svelte | 5 +- pnpm-lock.yaml | 30 +++++++++ 18 files changed, 173 insertions(+), 116 deletions(-) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 5460b73..d41dde3 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -18,6 +18,8 @@ "@sveltejs/kit": "catalog:", "@sveltejs/vite-plugin-svelte": "catalog:", "@tailwindcss/vite": "catalog:", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.7", "@types/node": "catalog:", "dotenv-cli": "catalog:", "eslint": "catalog:", diff --git a/apps/dashboard/src/lib/components/EventsList.svelte b/apps/dashboard/src/lib/components/EventsList.svelte index 38cbadb..36a88af 100644 --- a/apps/dashboard/src/lib/components/EventsList.svelte +++ b/apps/dashboard/src/lib/components/EventsList.svelte @@ -1,14 +1,14 @@
diff --git a/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte b/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte index b5989ac..0579536 100644 --- a/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte +++ b/apps/dashboard/src/lib/components/charts/EventsOverTimeChart.svelte @@ -1,10 +1,11 @@ * @@ -128,10 +121,21 @@ type QueryInput = * {/each} * ``` */ -export function useQuery< - TTable extends keyof Schema["tables"] & string, - TReturn, ->(queryInput: QueryInput): QueryResult { +/** Table names in the schema */ +type TableName = keyof Schema["tables"] & string; + +// Overload: direct query (infers types from query) +export function useQuery( + query: Query, +): QueryResult; +// Overload: getter function for reactive params (infers types from query) +export function useQuery( + queryGetter: () => Query, +): QueryResult; +// Implementation +export function useQuery( + queryInput: Query | (() => Query), +): QueryResult { const zero = getZero(); let data = $state([]); @@ -193,6 +197,7 @@ export interface QueryOneResult { /** * Create a reactive query that returns a single item. * Similar to useQuery but expects .one() query results. + * Types are inferred automatically from the query. * * @example * ```svelte @@ -200,10 +205,10 @@ export interface QueryOneResult { * import { useQueryOne } from '$lib/zero/client.svelte'; * import { projectById } from '$lib/zero/queries'; * - * // Static query: + * // Static query - types inferred automatically * const project = useQueryOne(projectById('project-123')); * - * // Reactive query (re-runs when projectId changes): + * // Reactive query - re-runs when projectId changes * const project = useQueryOne(() => projectById(projectId)); * * @@ -212,10 +217,18 @@ export interface QueryOneResult { * {/if} * ``` */ -export function useQueryOne< - TTable extends keyof Schema["tables"] & string, - TReturn, ->(queryInput: QueryInput): QueryOneResult { +// Overload: direct query (infers types from query) +export function useQueryOne( + query: Query, +): QueryOneResult; +// Overload: getter function for reactive params (infers types from query) +export function useQueryOne( + queryGetter: () => Query, +): QueryOneResult; +// Implementation +export function useQueryOne( + queryInput: Query | (() => Query), +): QueryOneResult { const zero = getZero(); let data = $state(undefined); diff --git a/apps/dashboard/src/lib/zero/queries.ts b/apps/dashboard/src/lib/zero/queries.ts index 460d3e0..a1cb3e4 100644 --- a/apps/dashboard/src/lib/zero/queries.ts +++ b/apps/dashboard/src/lib/zero/queries.ts @@ -55,8 +55,8 @@ export const projectById = syncedQuery( */ export const recentEvents = syncedQuery( "recentEvents", - z.tuple([z.string(), z.number().optional()]), - (projectId, limit = 100) => + z.tuple([z.string(), z.number().default(100)]), + (projectId, limit) => builder.events .where("project_id", projectId) .orderBy("timestamp", "desc") @@ -68,8 +68,8 @@ export const recentEvents = syncedQuery( */ export const eventsInWindow = syncedQuery( "eventsInWindow", - z.tuple([z.string(), z.number().optional()]), - (projectId, days = 7) => { + z.tuple([z.string(), z.number().default(7)]), + (projectId, days) => { const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000; return builder.events .where("project_id", projectId) @@ -86,8 +86,8 @@ export const eventsInWindow = syncedQuery( */ export const recentSessions = syncedQuery( "recentSessions", - z.tuple([z.string(), z.number().optional()]), - (projectId, limit = 50) => + z.tuple([z.string(), z.number().default(50)]), + (projectId, limit) => builder.sessions .where("project_id", projectId) .orderBy("started_at", "desc") @@ -99,8 +99,8 @@ export const recentSessions = syncedQuery( */ export const sessionsInWindow = syncedQuery( "sessionsInWindow", - z.tuple([z.string(), z.number().optional()]), - (projectId, days = 7) => { + z.tuple([z.string(), z.number().default(7)]), + (projectId, days) => { const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000; return builder.sessions .where("project_id", projectId) @@ -163,23 +163,16 @@ export const enabledFlags = syncedQuery( */ export const usersForProject = syncedQuery( "usersForProject", - z.tuple([z.string(), z.number().optional()]), - (projectId, limit = 50) => + z.tuple([z.string(), z.number().default(50)]), + (projectId, limit) => builder.users .where("project_id", projectId) .orderBy("last_seen_at", "desc") .limit(limit), ); -/** - * Get identified users (those with user_id set) - */ -export const identifiedUsers = syncedQuery( - "identifiedUsers", - z.tuple([z.string()]), - (projectId) => - builder.users.where("project_id", projectId).where("user_id", "!=", null), -); +// Note: identifiedUsers query removed - Zero doesn't support null comparisons in .where() +// TODO: Re-add when Zero supports IS NOT NULL or use client-side filtering // ============================================ // EXPORT ALL QUERIES @@ -201,5 +194,4 @@ export const queries = [ flagsForProject, enabledFlags, usersForProject, - identifiedUsers, ] as const; diff --git a/apps/dashboard/src/lib/zero/schema.ts b/apps/dashboard/src/lib/zero/schema.ts index cfe1c1e..bbe980c 100644 --- a/apps/dashboard/src/lib/zero/schema.ts +++ b/apps/dashboard/src/lib/zero/schema.ts @@ -47,7 +47,7 @@ const events = table("events") anon_id: string(), user_id: string().optional(), event_name: string(), - properties: json>().optional(), + properties: json().optional(), timestamp: number(), // TIMESTAMPTZ -> number (ms since epoch) received_at: number(), // TIMESTAMPTZ -> number (part of composite PK) }) @@ -94,7 +94,7 @@ const users = table("users") project_id: string(), anon_id: string(), user_id: string().optional(), - traits: json>().optional(), + traits: json().optional(), first_seen_at: number(), // TIMESTAMPTZ -> number (ms since epoch) last_seen_at: number(), // TIMESTAMPTZ -> number }) @@ -140,11 +140,60 @@ export const permissions = definePermissions, Schema>( ); // ============================================ -// TABLE TYPES +// TABLE TYPES (manually defined) // ============================================ -export type Project = typeof projects.inferSelect; -export type Event = typeof events.inferSelect; -export type Session = typeof sessions.inferSelect; -export type Flag = typeof flags.inferSelect; -export type User = typeof users.inferSelect; +/** + * Row types for each table. + * Manually defined since Zero's table builder doesn't have inferSelect. + */ +export interface Project { + id: string; + name: string; + api_key: string; + created_at: number; +} + +export interface Event { + id: string; + project_id: string; + session_id: string; + anon_id: string; + user_id: string | null; + event_name: string; + properties: Record | null; + timestamp: number; + received_at: number; +} + +export interface Session { + id: string; + project_id: string; + anon_id: string; + user_id: string | null; + started_at: number; + last_event_at: number; + event_count: number | null; + entry_url: string | null; + last_url: string | null; +} + +export interface Flag { + id: string; + project_id: string; + key: string; + name: string; + enabled: boolean; + created_at: number; + updated_at: number; +} + +export interface User { + id: string; + project_id: string; + anon_id: string; + user_id: string | null; + traits: Record | null; + first_seen_at: number; + last_seen_at: number; +} diff --git a/apps/dashboard/src/routes/+page.svelte b/apps/dashboard/src/routes/+page.svelte index 2bd2a86..45b7630 100644 --- a/apps/dashboard/src/routes/+page.svelte +++ b/apps/dashboard/src/routes/+page.svelte @@ -1,11 +1,10 @@