feat(webapp): add Dexie-backed hooks for saved queries, history, tabs, and UI state

This commit is contained in:
Rohith Gilla 2026-04-06 17:10:55 +05:30
parent 2d36590e6e
commit e7c72de246
No known key found for this signature in database
4 changed files with 318 additions and 0 deletions

View file

@ -0,0 +1,91 @@
'use client'
import { useLiveQuery } from 'dexie-react-hooks'
import { useAuth } from '@clerk/nextjs'
import { getDB } from '@/lib/dexie'
export function useQueryHistory(connectionId?: string, status?: 'success' | 'error') {
const { userId } = useAuth()
const history = useLiveQuery(async () => {
if (!userId) return []
const db = getDB(userId)
let results = await db.queryHistory
.where('_syncStatus')
.notEqual('deleted')
.reverse()
.sortBy('executedAt')
if (connectionId) {
results = results.filter((h) => h.connectionId === connectionId)
}
if (status) {
results = results.filter((h) => h.status === status)
}
return results.slice(0, 200)
}, [userId, connectionId, status])
const addEntry = async (entry: {
connectionId: string
query: string
status: 'success' | 'error'
durationMs?: number
rowCount?: number
errorMessage?: string
}) => {
if (!userId) return
const db = getDB(userId)
await db.queryHistory.add({
id: crypto.randomUUID(),
...entry,
executedAt: new Date().toISOString(),
_syncStatus: 'pending',
})
}
const remove = async (id: string) => {
if (!userId) return
const db = getDB(userId)
const item = await db.queryHistory.get(id)
if (!item) return
if (item._syncStatus === 'pending') {
await db.queryHistory.delete(id)
} else {
await db.queryHistory.update(id, { _syncStatus: 'deleted' })
}
}
const clearAll = async (connectionId?: string) => {
if (!userId) return
const db = getDB(userId)
if (connectionId) {
const entries = await db.queryHistory
.where('connectionId')
.equals(connectionId)
.toArray()
for (const entry of entries) {
if (entry._syncStatus === 'pending') {
await db.queryHistory.delete(entry.id)
} else {
await db.queryHistory.update(entry.id, { _syncStatus: 'deleted' })
}
}
} else {
const entries = await db.queryHistory.toArray()
for (const entry of entries) {
if (entry._syncStatus === 'pending') {
await db.queryHistory.delete(entry.id)
} else {
await db.queryHistory.update(entry.id, { _syncStatus: 'deleted' })
}
}
}
}
return { history: history ?? [], addEntry, remove, clearAll }
}

View file

@ -0,0 +1,101 @@
'use client'
import { useLiveQuery } from 'dexie-react-hooks'
import { useAuth } from '@clerk/nextjs'
import { useCallback, useRef, useSyncExternalStore } from 'react'
import { getDB } from '@/lib/dexie'
let activeTabId: string | null = null
const activeTabListeners = new Set<() => void>()
function setActiveTabId(id: string) {
activeTabId = id
activeTabListeners.forEach((l) => l())
}
function useActiveTabId() {
return useSyncExternalStore(
(cb) => {
activeTabListeners.add(cb)
return () => activeTabListeners.delete(cb)
},
() => activeTabId,
() => activeTabId
)
}
let tabCounter = 1
export function useQueryTabs() {
const { userId } = useAuth()
const currentActiveTabId = useActiveTabId()
const initializedRef = useRef(false)
const tabs = useLiveQuery(async () => {
if (!userId) return []
const db = getDB(userId)
const all = await db.queryTabs.orderBy('updatedAt').toArray()
if (all.length === 0 && !initializedRef.current) {
initializedRef.current = true
const id = crypto.randomUUID()
const now = new Date().toISOString()
await db.queryTabs.add({ id, title: `Query ${tabCounter++}`, sql: '', updatedAt: now })
setActiveTabId(id)
return db.queryTabs.orderBy('updatedAt').toArray()
}
if (!currentActiveTabId && all.length > 0) {
setActiveTabId(all[all.length - 1].id)
}
return all
}, [userId, currentActiveTabId])
const addTab = useCallback(async () => {
if (!userId) return
const db = getDB(userId)
const id = crypto.randomUUID()
const now = new Date().toISOString()
await db.queryTabs.add({ id, title: `Query ${tabCounter++}`, sql: '', updatedAt: now })
setActiveTabId(id)
}, [userId])
const removeTab = useCallback(
async (id: string) => {
if (!userId) return
const db = getDB(userId)
const all = await db.queryTabs.orderBy('updatedAt').toArray()
if (all.length <= 1) return
await db.queryTabs.delete(id)
if (currentActiveTabId === id) {
const remaining = all.filter((t) => t.id !== id)
setActiveTabId(remaining[remaining.length - 1].id)
}
},
[userId, currentActiveTabId]
)
const setActiveTab = useCallback((id: string) => {
setActiveTabId(id)
}, [])
const updateSql = useCallback(
async (id: string, sql: string) => {
if (!userId) return
const db = getDB(userId)
await db.queryTabs.update(id, { sql, updatedAt: new Date().toISOString() })
},
[userId]
)
return {
tabs: tabs ?? [],
activeTabId: currentActiveTabId ?? '',
addTab,
removeTab,
setActiveTab,
updateSql,
}
}

View file

@ -0,0 +1,93 @@
'use client'
import { useLiveQuery } from 'dexie-react-hooks'
import { useAuth } from '@clerk/nextjs'
import { getDB, type DexieSavedQuery } from '@/lib/dexie'
export function useSavedQueries(connectionId?: string, search?: string) {
const { userId } = useAuth()
const queries = useLiveQuery(async () => {
if (!userId) return []
const db = getDB(userId)
let collection = db.savedQueries
.where('_syncStatus')
.notEqual('deleted')
let results = await collection.reverse().sortBy('updatedAt')
if (connectionId) {
results = results.filter((q) => q.connectionId === connectionId)
}
if (search) {
const term = search.toLowerCase()
results = results.filter(
(q) => q.name.toLowerCase().includes(term) || q.query.toLowerCase().includes(term)
)
}
return results
}, [userId, connectionId, search])
const create = async (input: {
connectionId: string
name: string
query: string
description?: string
category?: string
tags?: string[]
}) => {
if (!userId) return
const db = getDB(userId)
const now = new Date().toISOString()
await db.savedQueries.add({
id: crypto.randomUUID(),
...input,
usageCount: 0,
createdAt: now,
updatedAt: now,
_syncStatus: 'pending',
})
}
const update = async (
id: string,
updates: Partial<Pick<DexieSavedQuery, 'name' | 'query' | 'description' | 'category' | 'tags'>>
) => {
if (!userId) return
const db = getDB(userId)
await db.savedQueries.update(id, {
...updates,
updatedAt: new Date().toISOString(),
_syncStatus: 'pending',
})
}
const remove = async (id: string) => {
if (!userId) return
const db = getDB(userId)
const item = await db.savedQueries.get(id)
if (!item) return
if (item._syncStatus === 'pending') {
await db.savedQueries.delete(id)
} else {
await db.savedQueries.update(id, { _syncStatus: 'deleted' })
}
}
const incrementUsage = async (id: string) => {
if (!userId) return
const db = getDB(userId)
const item = await db.savedQueries.get(id)
if (!item) return
await db.savedQueries.update(id, {
usageCount: item.usageCount + 1,
updatedAt: new Date().toISOString(),
_syncStatus: 'pending',
})
}
return { queries: queries ?? [], create, update, remove, incrementUsage }
}

View file

@ -0,0 +1,33 @@
'use client'
import { useLiveQuery } from 'dexie-react-hooks'
import { useAuth } from '@clerk/nextjs'
import { useCallback } from 'react'
import { getDB } from '@/lib/dexie'
export function useUiState<T>(
key: string,
defaultValue: T
): { value: T; set: (v: T) => Promise<void> } {
const { userId } = useAuth()
const entry = useLiveQuery(async () => {
if (!userId) return undefined
const db = getDB(userId)
return db.uiState.get(key)
}, [userId, key])
const set = useCallback(
async (value: T) => {
if (!userId) return
const db = getDB(userId)
await db.uiState.put({ key, value })
},
[userId, key]
)
return {
value: (entry?.value as T) ?? defaultValue,
set,
}
}