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 @@