svg]:size-4 [&>svg]:shrink-0",
- // Increases the hit area of the button on mobile.
- "after:absolute after:-inset-2 md:after:hidden",
- "peer-data-[size=sm]/menu-button:top-1",
- "peer-data-[size=default]/menu-button:top-1.5",
- "peer-data-[size=lg]/menu-button:top-2.5",
- "group-data-[collapsible=icon]:hidden",
- showOnHover &&
- "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
- className
- )}
- {...props}
- />
- )
-}
-
-export function SidebarMenuBadge({
- className,
- ...props
-}: ComponentProps<"div">) {
- return (
-
- )
-}
-
-export function SidebarMenuSkeleton({
- className,
- showIcon = false,
- ...props
-}: ComponentProps<"div"> & {
- showIcon?: boolean
-}) {
- // Random width between 50 to 90%.
- const width = useMemo(() => {
- return `${Math.floor(Math.random() * 40) + 50}%`
- }, [])
-
- return (
-
- {showIcon && (
-
- )}
-
-
- )
-}
-
-export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) {
- return (
-
- )
-}
-
-export function SidebarMenuSubItem({
- className,
- ...props
-}: ComponentProps<"li">) {
- return (
-
- )
-}
-
-export function SidebarMenuSubButton({
- asChild = false,
- size = "md",
- isActive = false,
- className,
- ...props
-}: ComponentProps<"a"> & {
- asChild?: boolean
- size?: "sm" | "md"
- isActive?: boolean
-}) {
- const Comp = asChild ? Slot : "a"
-
- return (
- svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
- "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
- size === "sm" && "text-xs",
- size === "md" && "text-sm",
- "group-data-[collapsible=icon]:hidden",
- className
- )}
- {...props}
- />
- )
-}
diff --git a/src/components/ui/sidebar-menu/SidebarGroupAction.tsx b/src/components/ui/sidebar-menu/SidebarGroupAction.tsx
new file mode 100644
index 0000000..81fc12a
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarGroupAction.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cn } from "@/lib/utils"
+
+export function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarGroupContent.tsx b/src/components/ui/sidebar-menu/SidebarGroupContent.tsx
new file mode 100644
index 0000000..738c767
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarGroupContent.tsx
@@ -0,0 +1,18 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarGroupContent({
+ className,
+ ...props
+}: ComponentProps<"div">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx b/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx
new file mode 100644
index 0000000..b9121c0
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cn } from "@/lib/utils"
+
+export function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenu.tsx b/src/components/ui/sidebar-menu/SidebarMenu.tsx
new file mode 100644
index 0000000..e535d5a
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenu.tsx
@@ -0,0 +1,15 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenu({ className, ...props }: ComponentProps<"ul">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuAction.tsx b/src/components/ui/sidebar-menu/SidebarMenuAction.tsx
new file mode 100644
index 0000000..2259710
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuAction.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx b/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx
new file mode 100644
index 0000000..1e8979d
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuBadge({
+ className,
+ ...props
+}: ComponentProps<"div">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuButton.tsx b/src/components/ui/sidebar-menu/SidebarMenuButton.tsx
new file mode 100644
index 0000000..676864b
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuButton.tsx
@@ -0,0 +1,84 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+import { useSidebar } from "../sidebar-context"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | ComponentProps
+} & VariantProps) {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuItem.tsx b/src/components/ui/sidebar-menu/SidebarMenuItem.tsx
new file mode 100644
index 0000000..cd2a992
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuItem.tsx
@@ -0,0 +1,15 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuItem({ className, ...props }: ComponentProps<"li">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx
new file mode 100644
index 0000000..8a3d870
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import { CSSProperties, ComponentProps, useMemo } from "react"
+import { cn } from "@/lib/utils"
+import { Skeleton } from "@/components/ui/skeleton"
+
+export function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const width = useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuSub.tsx b/src/components/ui/sidebar-menu/SidebarMenuSub.tsx
new file mode 100644
index 0000000..19c13b1
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuSub.tsx
@@ -0,0 +1,19 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx b/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx
new file mode 100644
index 0000000..1168080
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
diff --git a/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx b/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx
new file mode 100644
index 0000000..3dafc35
--- /dev/null
+++ b/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx
@@ -0,0 +1,18 @@
+"use client"
+
+import { ComponentProps } from "react"
+import { cn } from "@/lib/utils"
+
+export function SidebarMenuSubItem({
+ className,
+ ...props
+}: ComponentProps<"li">) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 1fe6940..a8d7a3e 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -9,18 +9,15 @@ export {
SidebarContent,
SidebarGroup,
} from "./sidebar-parts"
-export {
- SidebarGroupLabel,
- SidebarGroupAction,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuItem,
- SidebarMenuButton,
- SidebarMenuAction,
- SidebarMenuBadge,
- SidebarMenuSkeleton,
- SidebarMenuSub,
- SidebarMenuSubItem,
- SidebarMenuSubButton,
-} from "./sidebar-menu"
-
+export { SidebarGroupLabel } from "./sidebar-menu/SidebarGroupLabel"
+export { SidebarGroupAction } from "./sidebar-menu/SidebarGroupAction"
+export { SidebarGroupContent } from "./sidebar-menu/SidebarGroupContent"
+export { SidebarMenu } from "./sidebar-menu/SidebarMenu"
+export { SidebarMenuItem } from "./sidebar-menu/SidebarMenuItem"
+export { SidebarMenuButton } from "./sidebar-menu/SidebarMenuButton"
+export { SidebarMenuAction } from "./sidebar-menu/SidebarMenuAction"
+export { SidebarMenuBadge } from "./sidebar-menu/SidebarMenuBadge"
+export { SidebarMenuSkeleton } from "./sidebar-menu/SidebarMenuSkeleton"
+export { SidebarMenuSub } from "./sidebar-menu/SidebarMenuSub"
+export { SidebarMenuSubItem } from "./sidebar-menu/SidebarMenuSubItem"
+export { SidebarMenuSubButton } from "./sidebar-menu/SidebarMenuSubButton"
diff --git a/src/lib/db-core.ts b/src/lib/db-core.ts
deleted file mode 100644
index 3583fbe..0000000
--- a/src/lib/db-core.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Core database initialization and management
- */
-
-import initSqlJs, { Database } from 'sql.js'
-import { loadFromIndexedDB, saveToIndexedDB, openIndexedDB, deleteFromIndexedDB } from './db-indexeddb'
-import { loadFromLocalStorage, saveToLocalStorage, deleteFromLocalStorage } from './db-localstorage'
-import { validateSchema, createTables } from './db-schema'
-import { getStorageConfig, FlaskStorageAdapter, loadStorageConfig } from './storage'
-import { DB_KEY } from './db-constants'
-
-let dbInstance: Database | null = null
-let sqlInstance: any = null
-let flaskAdapter: FlaskStorageAdapter | null = null
-let configLoaded = false
-
-async function wipeAndRecreateDB(): Promise {
- console.warn('Wiping corrupted database and creating fresh schema...')
-
- await saveToIndexedDB(new Uint8Array())
- saveToLocalStorage(new Uint8Array())
-
- await deleteFromIndexedDB()
- deleteFromLocalStorage()
-
- dbInstance = null
-}
-
-export async function initDB(): Promise {
- if (dbInstance) return dbInstance
-
- if (!sqlInstance) {
- sqlInstance = await initSqlJs({
- locateFile: (file) => `https://sql.js.org/dist/${file}`
- })
- }
-
- let loadedData: Uint8Array | null = null
- let schemaValid = false
-
- loadedData = await loadFromIndexedDB()
-
- if (!loadedData) {
- loadedData = loadFromLocalStorage()
- }
-
- if (loadedData && loadedData.length > 0) {
- try {
- const testDb = new sqlInstance.Database(loadedData)
- schemaValid = await validateSchema(testDb)
-
- if (schemaValid) {
- dbInstance = testDb
- } else {
- console.warn('Schema validation failed, wiping database')
- testDb.close()
- await wipeAndRecreateDB()
- dbInstance = new sqlInstance.Database()
- }
- } catch (error) {
- console.error('Failed to load saved database, creating new one:', error)
- await wipeAndRecreateDB()
- dbInstance = new sqlInstance.Database()
- }
- } else {
- dbInstance = new sqlInstance.Database()
- }
-
- if (!dbInstance) {
- throw new Error('Failed to initialize database')
- }
-
- createTables(dbInstance)
- await saveDB()
-
- return dbInstance
-}
-
-export async function saveDB() {
- if (!dbInstance) return
-
- try {
- const data = dbInstance.export()
-
- const savedToIDB = await saveToIndexedDB(data)
-
- if (!savedToIDB) {
- saveToLocalStorage(data)
- }
- } catch (error) {
- console.error('Failed to save database:', error)
- }
-}
-
-export function getFlaskAdapter(): FlaskStorageAdapter | null {
- if (!configLoaded) {
- loadStorageConfig()
- configLoaded = true
- }
-
- const config = getStorageConfig()
- if (config.backend === 'flask' && config.flaskUrl) {
- try {
- if (!flaskAdapter || flaskAdapter['baseUrl'] !== config.flaskUrl) {
- flaskAdapter = new FlaskStorageAdapter(config.flaskUrl)
- }
- return flaskAdapter
- } catch (error) {
- console.warn('Failed to create Flask adapter:', error)
- return null
- }
- }
- return null
-}
-
-export async function exportDatabase(): Promise {
- const db = await initDB()
- return db.export()
-}
-
-export async function importDatabase(data: Uint8Array): Promise {
- if (!sqlInstance) {
- sqlInstance = await initSqlJs({
- locateFile: (file) => `https://sql.js.org/dist/${file}`
- })
- }
-
- try {
- dbInstance = new sqlInstance.Database(data)
- await saveDB()
- } catch (error) {
- console.error('Failed to import database:', error)
- throw error
- }
-}
-
-export async function getDatabaseStats(): Promise<{
- snippetCount: number
- templateCount: number
- storageType: 'indexeddb' | 'localstorage' | 'none'
- databaseSize: number
-}> {
- const db = await initDB()
-
- const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets')
- const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates')
-
- const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0
- const templateCount = templateResult[0]?.values[0]?.[0] as number || 0
-
- const data = db.export()
- const databaseSize = data.length
-
- const hasIDB = await openIndexedDB()
- const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null
- const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none')
-
- return {
- snippetCount,
- templateCount,
- storageType,
- databaseSize
- }
-}
-
-export async function clearDatabase(): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- await adapter.wipeDatabase()
- return
- }
-
- await deleteFromIndexedDB()
- deleteFromLocalStorage()
-
- dbInstance = null
- await initDB()
-}
diff --git a/src/lib/db-core/clearDatabase.ts b/src/lib/db-core/clearDatabase.ts
new file mode 100644
index 0000000..300fbca
--- /dev/null
+++ b/src/lib/db-core/clearDatabase.ts
@@ -0,0 +1,19 @@
+import { deleteFromIndexedDB } from '../db-indexeddb'
+import { deleteFromLocalStorage } from '../db-localstorage'
+import { getFlaskAdapter } from './getFlaskAdapter'
+import { initDB } from './initDB'
+import { dbState } from './state'
+
+export async function clearDatabase(): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ await adapter.wipeDatabase()
+ return
+ }
+
+ await deleteFromIndexedDB()
+ deleteFromLocalStorage()
+
+ dbState.dbInstance = null
+ await initDB()
+}
diff --git a/src/lib/db-core/exportDatabase.ts b/src/lib/db-core/exportDatabase.ts
new file mode 100644
index 0000000..94937d2
--- /dev/null
+++ b/src/lib/db-core/exportDatabase.ts
@@ -0,0 +1,6 @@
+import { initDB } from './initDB'
+
+export async function exportDatabase(): Promise {
+ const db = await initDB()
+ return db.export()
+}
diff --git a/src/lib/db-core/getDatabaseStats.ts b/src/lib/db-core/getDatabaseStats.ts
new file mode 100644
index 0000000..d545113
--- /dev/null
+++ b/src/lib/db-core/getDatabaseStats.ts
@@ -0,0 +1,32 @@
+import { openIndexedDB } from '../db-indexeddb'
+import { DB_KEY } from '../db-constants'
+import { initDB } from './initDB'
+
+export async function getDatabaseStats(): Promise<{
+ snippetCount: number
+ templateCount: number
+ storageType: 'indexeddb' | 'localstorage' | 'none'
+ databaseSize: number
+}> {
+ const db = await initDB()
+
+ const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets')
+ const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates')
+
+ const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0
+ const templateCount = templateResult[0]?.values[0]?.[0] as number || 0
+
+ const data = db.export()
+ const databaseSize = data.length
+
+ const hasIDB = await openIndexedDB()
+ const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null
+ const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none')
+
+ return {
+ snippetCount,
+ templateCount,
+ storageType,
+ databaseSize,
+ }
+}
diff --git a/src/lib/db-core/getFlaskAdapter.ts b/src/lib/db-core/getFlaskAdapter.ts
new file mode 100644
index 0000000..c2bd475
--- /dev/null
+++ b/src/lib/db-core/getFlaskAdapter.ts
@@ -0,0 +1,23 @@
+import { FlaskStorageAdapter, getStorageConfig, loadStorageConfig } from '../storage'
+import { dbState } from './state'
+
+export function getFlaskAdapter(): FlaskStorageAdapter | null {
+ if (!dbState.configLoaded) {
+ loadStorageConfig()
+ dbState.configLoaded = true
+ }
+
+ const config = getStorageConfig()
+ if (config.backend === 'flask' && config.flaskUrl) {
+ try {
+ if (!dbState.flaskAdapter || dbState.flaskAdapter['baseUrl'] !== config.flaskUrl) {
+ dbState.flaskAdapter = new FlaskStorageAdapter(config.flaskUrl)
+ }
+ return dbState.flaskAdapter
+ } catch (error) {
+ console.warn('Failed to create Flask adapter:', error)
+ return null
+ }
+ }
+ return null
+}
diff --git a/src/lib/db-core/importDatabase.ts b/src/lib/db-core/importDatabase.ts
new file mode 100644
index 0000000..32ddc0a
--- /dev/null
+++ b/src/lib/db-core/importDatabase.ts
@@ -0,0 +1,19 @@
+import initSqlJs from 'sql.js'
+import { saveDB } from './saveDB'
+import { dbState } from './state'
+
+export async function importDatabase(data: Uint8Array): Promise {
+ if (!dbState.sqlInstance) {
+ dbState.sqlInstance = await initSqlJs({
+ locateFile: (file) => `https://sql.js.org/dist/${file}`,
+ })
+ }
+
+ try {
+ dbState.dbInstance = new dbState.sqlInstance.Database(data)
+ await saveDB()
+ } catch (error) {
+ console.error('Failed to import database:', error)
+ throw error
+ }
+}
diff --git a/src/lib/db-core/initDB.ts b/src/lib/db-core/initDB.ts
new file mode 100644
index 0000000..62bd660
--- /dev/null
+++ b/src/lib/db-core/initDB.ts
@@ -0,0 +1,57 @@
+import initSqlJs, { Database } from 'sql.js'
+import { loadFromIndexedDB } from '../db-indexeddb'
+import { loadFromLocalStorage } from '../db-localstorage'
+import { createTables, validateSchema } from '../db-schema'
+import { saveDB } from './saveDB'
+import { dbState } from './state'
+import { wipeAndRecreateDB } from './wipeAndRecreateDB'
+
+export async function initDB(): Promise {
+ if (dbState.dbInstance) return dbState.dbInstance
+
+ if (!dbState.sqlInstance) {
+ dbState.sqlInstance = await initSqlJs({
+ locateFile: (file) => `https://sql.js.org/dist/${file}`,
+ })
+ }
+
+ let loadedData: Uint8Array | null = null
+ let schemaValid = false
+
+ loadedData = await loadFromIndexedDB()
+
+ if (!loadedData) {
+ loadedData = loadFromLocalStorage()
+ }
+
+ if (loadedData && loadedData.length > 0) {
+ try {
+ const testDb = new dbState.sqlInstance.Database(loadedData)
+ schemaValid = await validateSchema(testDb)
+
+ if (schemaValid) {
+ dbState.dbInstance = testDb
+ } else {
+ console.warn('Schema validation failed, wiping database')
+ testDb.close()
+ await wipeAndRecreateDB()
+ dbState.dbInstance = new dbState.sqlInstance.Database()
+ }
+ } catch (error) {
+ console.error('Failed to load saved database, creating new one:', error)
+ await wipeAndRecreateDB()
+ dbState.dbInstance = new dbState.sqlInstance.Database()
+ }
+ } else {
+ dbState.dbInstance = new dbState.sqlInstance.Database()
+ }
+
+ if (!dbState.dbInstance) {
+ throw new Error('Failed to initialize database')
+ }
+
+ createTables(dbState.dbInstance)
+ await saveDB()
+
+ return dbState.dbInstance
+}
diff --git a/src/lib/db-core/saveDB.ts b/src/lib/db-core/saveDB.ts
new file mode 100644
index 0000000..8584a4d
--- /dev/null
+++ b/src/lib/db-core/saveDB.ts
@@ -0,0 +1,19 @@
+import { saveToIndexedDB } from '../db-indexeddb'
+import { saveToLocalStorage } from '../db-localstorage'
+import { dbState } from './state'
+
+export async function saveDB() {
+ if (!dbState.dbInstance) return
+
+ try {
+ const data = dbState.dbInstance.export()
+
+ const savedToIDB = await saveToIndexedDB(data)
+
+ if (!savedToIDB) {
+ saveToLocalStorage(data)
+ }
+ } catch (error) {
+ console.error('Failed to save database:', error)
+ }
+}
diff --git a/src/lib/db-core/state.ts b/src/lib/db-core/state.ts
new file mode 100644
index 0000000..3524251
--- /dev/null
+++ b/src/lib/db-core/state.ts
@@ -0,0 +1,9 @@
+import type { Database } from 'sql.js'
+import type { FlaskStorageAdapter } from '../storage'
+
+export const dbState = {
+ dbInstance: null as Database | null,
+ sqlInstance: null as any,
+ flaskAdapter: null as FlaskStorageAdapter | null,
+ configLoaded: false,
+}
diff --git a/src/lib/db-core/wipeAndRecreateDB.ts b/src/lib/db-core/wipeAndRecreateDB.ts
new file mode 100644
index 0000000..0765b00
--- /dev/null
+++ b/src/lib/db-core/wipeAndRecreateDB.ts
@@ -0,0 +1,15 @@
+import { deleteFromIndexedDB, saveToIndexedDB } from '../db-indexeddb'
+import { deleteFromLocalStorage, saveToLocalStorage } from '../db-localstorage'
+import { dbState } from './state'
+
+export async function wipeAndRecreateDB(): Promise {
+ console.warn('Wiping corrupted database and creating fresh schema...')
+
+ await saveToIndexedDB(new Uint8Array())
+ saveToLocalStorage(new Uint8Array())
+
+ await deleteFromIndexedDB()
+ deleteFromLocalStorage()
+
+ dbState.dbInstance = null
+}
diff --git a/src/lib/db-namespaces.ts b/src/lib/db-namespaces.ts
deleted file mode 100644
index 292e220..0000000
--- a/src/lib/db-namespaces.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * Namespace operations for organizing snippets
- */
-
-import type { Namespace } from './types'
-import { initDB, saveDB, getFlaskAdapter } from './db-core'
-import { mapRowToObject, mapRowsToObjects } from './db-mapper'
-
-export async function getAllNamespaces(): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.getAllNamespaces()
- }
-
- const db = await initDB()
- const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
-
- return mapRowsToObjects(results)
-}
-
-export async function createNamespace(name: string): Promise {
- const namespace: Namespace = {
- id: Date.now().toString(),
- name,
- createdAt: Date.now(),
- isDefault: false
- }
-
- const adapter = getFlaskAdapter()
- if (adapter) {
- await adapter.createNamespace(namespace)
- return namespace
- }
-
- const db = await initDB()
-
- db.run(
- `INSERT INTO namespaces (id, name, createdAt, isDefault)
- VALUES (?, ?, ?, ?)`,
- [namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0]
- )
-
- await saveDB()
- return namespace
-}
-
-export async function deleteNamespace(id: string): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.deleteNamespace(id)
- }
-
- const db = await initDB()
-
- const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
- if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) {
- throw new Error('Default namespace not found')
- }
-
- const defaultId = defaultNamespace[0].values[0][0] as string
-
- const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id])
- if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) {
- throw new Error('Cannot delete default namespace')
- }
-
- db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id])
-
- db.run('DELETE FROM namespaces WHERE id = ?', [id])
-
- await saveDB()
-}
-
-export async function ensureDefaultNamespace(): Promise {
- const db = await initDB()
-
- const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1')
- const count = results[0]?.values[0]?.[0] as number || 0
-
- if (count === 0) {
- const defaultNamespace: Namespace = {
- id: 'default',
- name: 'Default',
- createdAt: Date.now(),
- isDefault: true
- }
-
- db.run(
- `INSERT INTO namespaces (id, name, createdAt, isDefault)
- VALUES (?, ?, ?, ?)`,
- [defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1]
- )
-
- await saveDB()
- }
-}
-
-export async function getNamespaceById(id: string): Promise {
- const db = await initDB()
- const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
-
- if (results.length === 0 || results[0].values.length === 0) return null
-
- const columns = results[0].columns
- const row = results[0].values[0]
-
- return mapRowToObject(row, columns)
-}
diff --git a/src/lib/db-namespaces/createNamespace.ts b/src/lib/db-namespaces/createNamespace.ts
new file mode 100644
index 0000000..5079e22
--- /dev/null
+++ b/src/lib/db-namespaces/createNamespace.ts
@@ -0,0 +1,30 @@
+import type { Namespace } from '../types'
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function createNamespace(name: string): Promise {
+ const namespace: Namespace = {
+ id: Date.now().toString(),
+ name,
+ createdAt: Date.now(),
+ isDefault: false,
+ }
+
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ await adapter.createNamespace(namespace)
+ return namespace
+ }
+
+ const db = await initDB()
+
+ db.run(
+ `INSERT INTO namespaces (id, name, createdAt, isDefault)
+ VALUES (?, ?, ?, ?)`,
+ [namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0]
+ )
+
+ await saveDB()
+ return namespace
+}
diff --git a/src/lib/db-namespaces/deleteNamespace.ts b/src/lib/db-namespaces/deleteNamespace.ts
new file mode 100644
index 0000000..5435acc
--- /dev/null
+++ b/src/lib/db-namespaces/deleteNamespace.ts
@@ -0,0 +1,30 @@
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function deleteNamespace(id: string): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.deleteNamespace(id)
+ }
+
+ const db = await initDB()
+
+ const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
+ if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) {
+ throw new Error('Default namespace not found')
+ }
+
+ const defaultId = defaultNamespace[0].values[0][0] as string
+
+ const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id])
+ if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) {
+ throw new Error('Cannot delete default namespace')
+ }
+
+ db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id])
+
+ db.run('DELETE FROM namespaces WHERE id = ?', [id])
+
+ await saveDB()
+}
diff --git a/src/lib/db-namespaces/ensureDefaultNamespace.ts b/src/lib/db-namespaces/ensureDefaultNamespace.ts
new file mode 100644
index 0000000..5f8a78f
--- /dev/null
+++ b/src/lib/db-namespaces/ensureDefaultNamespace.ts
@@ -0,0 +1,27 @@
+import type { Namespace } from '../types'
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+
+export async function ensureDefaultNamespace(): Promise {
+ const db = await initDB()
+
+ const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1')
+ const count = results[0]?.values[0]?.[0] as number || 0
+
+ if (count === 0) {
+ const defaultNamespace: Namespace = {
+ id: 'default',
+ name: 'Default',
+ createdAt: Date.now(),
+ isDefault: true,
+ }
+
+ db.run(
+ `INSERT INTO namespaces (id, name, createdAt, isDefault)
+ VALUES (?, ?, ?, ?)`,
+ [defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1]
+ )
+
+ await saveDB()
+ }
+}
diff --git a/src/lib/db-namespaces/getAllNamespaces.ts b/src/lib/db-namespaces/getAllNamespaces.ts
new file mode 100644
index 0000000..0001875
--- /dev/null
+++ b/src/lib/db-namespaces/getAllNamespaces.ts
@@ -0,0 +1,16 @@
+import type { Namespace } from '../types'
+import { initDB } from '../db-core/initDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+import { mapRowsToObjects } from '../db-mapper'
+
+export async function getAllNamespaces(): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.getAllNamespaces()
+ }
+
+ const db = await initDB()
+ const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
+
+ return mapRowsToObjects(results)
+}
diff --git a/src/lib/db-namespaces/getNamespaceById.ts b/src/lib/db-namespaces/getNamespaceById.ts
new file mode 100644
index 0000000..d05ca11
--- /dev/null
+++ b/src/lib/db-namespaces/getNamespaceById.ts
@@ -0,0 +1,15 @@
+import type { Namespace } from '../types'
+import { initDB } from '../db-core/initDB'
+import { mapRowToObject } from '../db-mapper'
+
+export async function getNamespaceById(id: string): Promise {
+ const db = await initDB()
+ const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
+
+ if (results.length === 0 || results[0].values.length === 0) return null
+
+ const columns = results[0].columns
+ const row = results[0].values[0]
+
+ return mapRowToObject(row, columns)
+}
diff --git a/src/lib/db-snippets.ts b/src/lib/db-snippets.ts
deleted file mode 100644
index bf3c07f..0000000
--- a/src/lib/db-snippets.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-/**
- * Snippet CRUD operations and templates management
- */
-
-import type { Snippet, SnippetTemplate } from './types'
-import { initDB, saveDB, getFlaskAdapter } from './db-core'
-import { mapRowToObject, mapRowsToObjects } from './db-mapper'
-import { ensureDefaultNamespace } from './db-namespaces'
-import seedSnippetsData from '@/data/seed-snippets.json'
-import seedTemplatesData from '@/data/seed-templates.json'
-
-export async function getAllSnippets(): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.getAllSnippets()
- }
-
- const db = await initDB()
- const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
-
- return mapRowsToObjects(results)
-}
-
-export async function getSnippet(id: string): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.getSnippet(id)
- }
-
- const db = await initDB()
- const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
-
- if (results.length === 0 || results[0].values.length === 0) return null
-
- const columns = results[0].columns
- const row = results[0].values[0]
-
- return mapRowToObject(row, columns)
-}
-
-export async function createSnippet(snippet: Snippet): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.createSnippet(snippet)
- }
-
- const db = await initDB()
-
- db.run(
- `INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- snippet.id,
- snippet.title,
- snippet.description,
- snippet.code,
- snippet.language,
- snippet.category,
- snippet.namespaceId || null,
- snippet.hasPreview ? 1 : 0,
- snippet.functionName || null,
- snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
- snippet.createdAt,
- snippet.updatedAt
- ]
- )
-
- await saveDB()
-}
-
-export async function updateSnippet(snippet: Snippet): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.updateSnippet(snippet)
- }
-
- const db = await initDB()
-
- db.run(
- `UPDATE snippets
- SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
- WHERE id = ?`,
- [
- snippet.title,
- snippet.description,
- snippet.code,
- snippet.language,
- snippet.category,
- snippet.namespaceId || null,
- snippet.hasPreview ? 1 : 0,
- snippet.functionName || null,
- snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
- snippet.updatedAt,
- snippet.id
- ]
- )
-
- await saveDB()
-}
-
-export async function deleteSnippet(id: string): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- return await adapter.deleteSnippet(id)
- }
-
- const db = await initDB()
-
- db.run('DELETE FROM snippets WHERE id = ?', [id])
-
- await saveDB()
-}
-
-export async function getSnippetsByNamespace(namespaceId: string): Promise {
- const db = await initDB()
- const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId])
-
- return mapRowsToObjects(results)
-}
-
-export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- const snippet = await adapter.getSnippet(snippetId)
- if (snippet) {
- snippet.namespaceId = targetNamespaceId
- snippet.updatedAt = Date.now()
- await adapter.updateSnippet(snippet)
- }
- return
- }
-
- const db = await initDB()
-
- db.run(
- 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
- [targetNamespaceId, Date.now(), snippetId]
- )
-
- await saveDB()
-}
-
-export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise {
- const adapter = getFlaskAdapter()
- if (adapter) {
- await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId)
- return
- }
-
- const db = await initDB()
- const now = Date.now()
-
- for (const snippetId of snippetIds) {
- db.run(
- 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
- [targetNamespaceId, now, snippetId]
- )
- }
-
- await saveDB()
-}
-
-export async function getAllTemplates(): Promise {
- const db = await initDB()
- const results = db.exec('SELECT * FROM snippet_templates')
-
- return mapRowsToObjects(results)
-}
-
-export async function createTemplate(template: SnippetTemplate): Promise {
- const db = await initDB()
-
- db.run(
- `INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- template.id,
- template.title,
- template.description,
- template.code,
- template.language,
- template.category,
- template.hasPreview ? 1 : 0,
- template.functionName || null,
- template.inputParameters ? JSON.stringify(template.inputParameters) : null
- ]
- )
-
- await saveDB()
-}
-
-export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise {
- const db = await initDB()
-
- const existingTemplates = db.exec('SELECT id FROM snippet_templates')
- const existingIds = new Set(
- existingTemplates[0]?.values.map(row => row[0] as string) || []
- )
-
- let addedCount = 0
- for (const template of templates) {
- if (!existingIds.has(template.id)) {
- await createTemplate(template)
- addedCount++
- }
- }
-}
-
-export async function seedDatabase(): Promise {
- const db = await initDB()
-
- await ensureDefaultNamespace()
-
- const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets')
- const snippetCount = checkSnippets[0]?.values[0]?.[0] as number
-
- if (snippetCount > 0) {
- return
- }
-
- const now = Date.now()
-
- const seedSnippets: Snippet[] = seedSnippetsData.map((snippet, index) => {
- const timestamp = now - index * 1000
- return {
- ...snippet,
- createdAt: timestamp,
- updatedAt: timestamp
- }
- })
-
- for (const snippet of seedSnippets) {
- await createSnippet(snippet)
- }
-
- const seedTemplates: SnippetTemplate[] = seedTemplatesData
-
- for (const template of seedTemplates) {
- await createTemplate(template)
- }
-}
diff --git a/src/lib/db-snippets/bulkMoveSnippets.ts b/src/lib/db-snippets/bulkMoveSnippets.ts
new file mode 100644
index 0000000..262701c
--- /dev/null
+++ b/src/lib/db-snippets/bulkMoveSnippets.ts
@@ -0,0 +1,23 @@
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId)
+ return
+ }
+
+ const db = await initDB()
+ const now = Date.now()
+
+ for (const snippetId of snippetIds) {
+ db.run(
+ 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
+ [targetNamespaceId, now, snippetId]
+ )
+ }
+
+ await saveDB()
+}
diff --git a/src/lib/db-snippets/createSnippet.ts b/src/lib/db-snippets/createSnippet.ts
new file mode 100644
index 0000000..acc8c90
--- /dev/null
+++ b/src/lib/db-snippets/createSnippet.ts
@@ -0,0 +1,34 @@
+import type { Snippet } from '../types'
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function createSnippet(snippet: Snippet): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.createSnippet(snippet)
+ }
+
+ const db = await initDB()
+
+ db.run(
+ `INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [
+ snippet.id,
+ snippet.title,
+ snippet.description,
+ snippet.code,
+ snippet.language,
+ snippet.category,
+ snippet.namespaceId || null,
+ snippet.hasPreview ? 1 : 0,
+ snippet.functionName || null,
+ snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
+ snippet.createdAt,
+ snippet.updatedAt,
+ ]
+ )
+
+ await saveDB()
+}
diff --git a/src/lib/db-snippets/createTemplate.ts b/src/lib/db-snippets/createTemplate.ts
new file mode 100644
index 0000000..7014b4c
--- /dev/null
+++ b/src/lib/db-snippets/createTemplate.ts
@@ -0,0 +1,25 @@
+import type { SnippetTemplate } from '../types'
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+
+export async function createTemplate(template: SnippetTemplate): Promise {
+ const db = await initDB()
+
+ db.run(
+ `INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [
+ template.id,
+ template.title,
+ template.description,
+ template.code,
+ template.language,
+ template.category,
+ template.hasPreview ? 1 : 0,
+ template.functionName || null,
+ template.inputParameters ? JSON.stringify(template.inputParameters) : null,
+ ]
+ )
+
+ await saveDB()
+}
diff --git a/src/lib/db-snippets/deleteSnippet.ts b/src/lib/db-snippets/deleteSnippet.ts
new file mode 100644
index 0000000..bb89acc
--- /dev/null
+++ b/src/lib/db-snippets/deleteSnippet.ts
@@ -0,0 +1,16 @@
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function deleteSnippet(id: string): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.deleteSnippet(id)
+ }
+
+ const db = await initDB()
+
+ db.run('DELETE FROM snippets WHERE id = ?', [id])
+
+ await saveDB()
+}
diff --git a/src/lib/db-snippets/getAllSnippets.ts b/src/lib/db-snippets/getAllSnippets.ts
new file mode 100644
index 0000000..d924b4e
--- /dev/null
+++ b/src/lib/db-snippets/getAllSnippets.ts
@@ -0,0 +1,16 @@
+import type { Snippet } from '../types'
+import { initDB } from '../db-core/initDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+import { mapRowsToObjects } from '../db-mapper'
+
+export async function getAllSnippets(): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.getAllSnippets()
+ }
+
+ const db = await initDB()
+ const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
+
+ return mapRowsToObjects(results)
+}
diff --git a/src/lib/db-snippets/getAllTemplates.ts b/src/lib/db-snippets/getAllTemplates.ts
new file mode 100644
index 0000000..247af39
--- /dev/null
+++ b/src/lib/db-snippets/getAllTemplates.ts
@@ -0,0 +1,10 @@
+import type { SnippetTemplate } from '../types'
+import { initDB } from '../db-core/initDB'
+import { mapRowsToObjects } from '../db-mapper'
+
+export async function getAllTemplates(): Promise {
+ const db = await initDB()
+ const results = db.exec('SELECT * FROM snippet_templates')
+
+ return mapRowsToObjects(results)
+}
diff --git a/src/lib/db-snippets/getSnippet.ts b/src/lib/db-snippets/getSnippet.ts
new file mode 100644
index 0000000..2943335
--- /dev/null
+++ b/src/lib/db-snippets/getSnippet.ts
@@ -0,0 +1,21 @@
+import type { Snippet } from '../types'
+import { initDB } from '../db-core/initDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+import { mapRowToObject } from '../db-mapper'
+
+export async function getSnippet(id: string): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.getSnippet(id)
+ }
+
+ const db = await initDB()
+ const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
+
+ if (results.length === 0 || results[0].values.length === 0) return null
+
+ const columns = results[0].columns
+ const row = results[0].values[0]
+
+ return mapRowToObject(row, columns)
+}
diff --git a/src/lib/db-snippets/getSnippetsByNamespace.ts b/src/lib/db-snippets/getSnippetsByNamespace.ts
new file mode 100644
index 0000000..ffc2dff
--- /dev/null
+++ b/src/lib/db-snippets/getSnippetsByNamespace.ts
@@ -0,0 +1,13 @@
+import type { Snippet } from '../types'
+import { initDB } from '../db-core/initDB'
+import { mapRowsToObjects } from '../db-mapper'
+
+export async function getSnippetsByNamespace(namespaceId: string): Promise {
+ const db = await initDB()
+ const results = db.exec(
+ 'SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC',
+ [namespaceId, namespaceId]
+ )
+
+ return mapRowsToObjects(results)
+}
diff --git a/src/lib/db-snippets/moveSnippetToNamespace.ts b/src/lib/db-snippets/moveSnippetToNamespace.ts
new file mode 100644
index 0000000..bf87927
--- /dev/null
+++ b/src/lib/db-snippets/moveSnippetToNamespace.ts
@@ -0,0 +1,25 @@
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ const snippet = await adapter.getSnippet(snippetId)
+ if (snippet) {
+ snippet.namespaceId = targetNamespaceId
+ snippet.updatedAt = Date.now()
+ await adapter.updateSnippet(snippet)
+ }
+ return
+ }
+
+ const db = await initDB()
+
+ db.run(
+ 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
+ [targetNamespaceId, Date.now(), snippetId]
+ )
+
+ await saveDB()
+}
diff --git a/src/lib/db-snippets/seedDatabase.ts b/src/lib/db-snippets/seedDatabase.ts
new file mode 100644
index 0000000..5c2647a
--- /dev/null
+++ b/src/lib/db-snippets/seedDatabase.ts
@@ -0,0 +1,41 @@
+import type { Snippet, SnippetTemplate } from '../types'
+import { initDB } from '../db-core/initDB'
+import { createSnippet } from './createSnippet'
+import { createTemplate } from './createTemplate'
+import { ensureDefaultNamespace } from '../db-namespaces/ensureDefaultNamespace'
+import seedSnippetsData from '@/data/seed-snippets.json'
+import seedTemplatesData from '@/data/seed-templates.json'
+
+export async function seedDatabase(): Promise {
+ const db = await initDB()
+
+ await ensureDefaultNamespace()
+
+ const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets')
+ const snippetCount = checkSnippets[0]?.values[0]?.[0] as number
+
+ if (snippetCount > 0) {
+ return
+ }
+
+ const now = Date.now()
+
+ const seedSnippets: Snippet[] = seedSnippetsData.map((snippet, index) => {
+ const timestamp = now - index * 1000
+ return {
+ ...snippet,
+ createdAt: timestamp,
+ updatedAt: timestamp,
+ }
+ })
+
+ for (const snippet of seedSnippets) {
+ await createSnippet(snippet)
+ }
+
+ const seedTemplates: SnippetTemplate[] = seedTemplatesData
+
+ for (const template of seedTemplates) {
+ await createTemplate(template)
+ }
+}
diff --git a/src/lib/db-snippets/syncTemplatesFromJSON.ts b/src/lib/db-snippets/syncTemplatesFromJSON.ts
new file mode 100644
index 0000000..cb31d95
--- /dev/null
+++ b/src/lib/db-snippets/syncTemplatesFromJSON.ts
@@ -0,0 +1,20 @@
+import type { SnippetTemplate } from '../types'
+import { initDB } from '../db-core/initDB'
+import { createTemplate } from './createTemplate'
+
+export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise {
+ const db = await initDB()
+
+ const existingTemplates = db.exec('SELECT id FROM snippet_templates')
+ const existingIds = new Set(
+ existingTemplates[0]?.values.map(row => row[0] as string) || []
+ )
+
+ let addedCount = 0
+ for (const template of templates) {
+ if (!existingIds.has(template.id)) {
+ await createTemplate(template)
+ addedCount++
+ }
+ }
+}
diff --git a/src/lib/db-snippets/updateSnippet.ts b/src/lib/db-snippets/updateSnippet.ts
new file mode 100644
index 0000000..a8afcb3
--- /dev/null
+++ b/src/lib/db-snippets/updateSnippet.ts
@@ -0,0 +1,34 @@
+import type { Snippet } from '../types'
+import { initDB } from '../db-core/initDB'
+import { saveDB } from '../db-core/saveDB'
+import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
+
+export async function updateSnippet(snippet: Snippet): Promise {
+ const adapter = getFlaskAdapter()
+ if (adapter) {
+ return await adapter.updateSnippet(snippet)
+ }
+
+ const db = await initDB()
+
+ db.run(
+ `UPDATE snippets
+ SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
+ WHERE id = ?`,
+ [
+ snippet.title,
+ snippet.description,
+ snippet.code,
+ snippet.language,
+ snippet.category,
+ snippet.namespaceId || null,
+ snippet.hasPreview ? 1 : 0,
+ snippet.functionName || null,
+ snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
+ snippet.updatedAt,
+ snippet.id,
+ ]
+ )
+
+ await saveDB()
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 7059bb4..eeab88e 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -4,32 +4,33 @@
*/
// Re-export core database functions
-export { initDB, saveDB, exportDatabase, importDatabase, getDatabaseStats, clearDatabase } from './db-core'
+export { initDB } from './db-core/initDB'
+export { saveDB } from './db-core/saveDB'
+export { exportDatabase } from './db-core/exportDatabase'
+export { importDatabase } from './db-core/importDatabase'
+export { getDatabaseStats } from './db-core/getDatabaseStats'
+export { clearDatabase } from './db-core/clearDatabase'
// Re-export snippet operations
-export {
- getAllSnippets,
- getSnippet,
- createSnippet,
- updateSnippet,
- deleteSnippet,
- getSnippetsByNamespace,
- moveSnippetToNamespace,
- bulkMoveSnippets,
- getAllTemplates,
- createTemplate,
- syncTemplatesFromJSON,
- seedDatabase
-} from './db-snippets'
+export { getAllSnippets } from './db-snippets/getAllSnippets'
+export { getSnippet } from './db-snippets/getSnippet'
+export { createSnippet } from './db-snippets/createSnippet'
+export { updateSnippet } from './db-snippets/updateSnippet'
+export { deleteSnippet } from './db-snippets/deleteSnippet'
+export { getSnippetsByNamespace } from './db-snippets/getSnippetsByNamespace'
+export { moveSnippetToNamespace } from './db-snippets/moveSnippetToNamespace'
+export { bulkMoveSnippets } from './db-snippets/bulkMoveSnippets'
+export { getAllTemplates } from './db-snippets/getAllTemplates'
+export { createTemplate } from './db-snippets/createTemplate'
+export { syncTemplatesFromJSON } from './db-snippets/syncTemplatesFromJSON'
+export { seedDatabase } from './db-snippets/seedDatabase'
// Re-export namespace operations
-export {
- getAllNamespaces,
- createNamespace,
- deleteNamespace,
- ensureDefaultNamespace,
- getNamespaceById
-} from './db-namespaces'
+export { getAllNamespaces } from './db-namespaces/getAllNamespaces'
+export { createNamespace } from './db-namespaces/createNamespace'
+export { deleteNamespace } from './db-namespaces/deleteNamespace'
+export { ensureDefaultNamespace } from './db-namespaces/ensureDefaultNamespace'
+export { getNamespaceById } from './db-namespaces/getNamespaceById'
// Re-export schema validation
export { validateDatabaseSchema } from './db-schema'