Skip to main content
Glama
contentDb.ts10.9 kB
import { query } from '$lib/server/db' import type { DbContent, DbContentDistilled, CreateContentInput, ContentFilter, ContentStats } from '$lib/types/db' import { logAlways, logErrorAlways } from '$lib/log' // Type mapping for table names to their corresponding types type TableTypeMap = { content: DbContent content_distilled: DbContentDistilled } // Union type for valid table names type TableName = keyof TableTypeMap export class ContentDbService { static extractFilename(path: string): string { return path.split('/').pop() || path } static async upsertContent(input: CreateContentInput): Promise<DbContent> { try { const result = await query( `INSERT INTO content ( path, filename, content, size_bytes, metadata ) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (path) DO UPDATE SET content = EXCLUDED.content, size_bytes = EXCLUDED.size_bytes, metadata = EXCLUDED.metadata, updated_at = CURRENT_TIMESTAMP RETURNING *`, [ input.path, input.filename, input.content, input.size_bytes, input.metadata ? JSON.stringify(input.metadata) : '{}' ] ) logAlways(`Upserted content for ${input.path}`) return result.rows[0] as DbContent } catch (error) { logErrorAlways(`Failed to upsert content for ${input.path}:`, error) throw new Error( `Failed to upsert content: ${error instanceof Error ? error.message : String(error)}` ) } } static async getContentByPath(path: string): Promise<DbContent | null> { try { const result = await query('SELECT * FROM content WHERE path = $1', [path]) return result.rows.length > 0 ? (result.rows[0] as DbContent) : null } catch (error) { logErrorAlways(`Failed to get content ${path}:`, error) throw new Error( `Failed to get content: ${error instanceof Error ? error.message : String(error)}` ) } } static async getAllContent(): Promise<DbContent[]> { try { const result = await query('SELECT * FROM content ORDER BY path') return result.rows as DbContent[] } catch (error) { logErrorAlways('Failed to get all content:', error) throw new Error( `Failed to get content: ${error instanceof Error ? error.message : String(error)}` ) } } /** * Generic search method that works with both content and content_distilled tables */ static async searchContent<T extends TableName>( searchQuery: string, tableName: T, pathPattern: string = 'apps/svelte.dev/content/docs/%' ): Promise<TableTypeMap[T] | null> { try { const lowerQuery = searchQuery.toLowerCase() // Build table-specific WHERE clauses let baseWhereClause = '' let params: (string | number)[] = [] let paramIndex = 1 if (tableName === 'content') { // For content table, include path filter baseWhereClause = `WHERE path LIKE $${paramIndex}` params = [pathPattern] paramIndex = 2 } else { // For content_distilled table, no additional filters needed baseWhereClause = '' paramIndex = 1 } // First, try exact title match using JSON operators const exactTitleQueryStr = ` SELECT * FROM ${tableName} ${baseWhereClause}${baseWhereClause ? ' AND' : 'WHERE'} LOWER(metadata->>'title') = $${paramIndex} LIMIT 1 ` const exactTitleParams = [...params, lowerQuery] const exactTitleResult = await query(exactTitleQueryStr, exactTitleParams) if (exactTitleResult.rows.length > 0) { return exactTitleResult.rows[0] as TableTypeMap[T] } // Then try partial title match const partialTitleQueryStr = ` SELECT * FROM ${tableName} ${baseWhereClause}${baseWhereClause ? ' AND' : 'WHERE'} LOWER(metadata->>'title') LIKE $${paramIndex} LIMIT 1 ` const partialTitleParams = [...params, `%${lowerQuery}%`] const partialTitleResult = await query(partialTitleQueryStr, partialTitleParams) if (partialTitleResult.rows.length > 0) { return partialTitleResult.rows[0] as TableTypeMap[T] } // Finally try path match for backward compatibility const pathMatchQueryStr = ` SELECT * FROM ${tableName} ${baseWhereClause}${baseWhereClause ? ' AND' : 'WHERE'} LOWER(path) LIKE $${paramIndex} LIMIT 1 ` const pathMatchParams = [...params, `%${lowerQuery}%`] const pathMatchResult = await query(pathMatchQueryStr, pathMatchParams) return pathMatchResult.rows.length > 0 ? (pathMatchResult.rows[0] as TableTypeMap[T]) : null } catch (error) { logErrorAlways(`Failed to search ${tableName} for "${searchQuery}":`, error) throw new Error( `Failed to search ${tableName}: ${error instanceof Error ? error.message : String(error)}` ) } } static async searchAllContent( searchQuery: string, pathPattern: string = 'apps/svelte.dev/content/docs/%', limit: number = 50 ): Promise<DbContent[]> { try { const lowerQuery = searchQuery.toLowerCase() // Combine all search types into one query with UNION const combinedQueryStr = ` -- Exact title matches first (SELECT * FROM content WHERE path LIKE $1 AND LOWER(metadata->>'title') = $2 ORDER BY path LIMIT $3) UNION -- Then partial title matches (SELECT * FROM content WHERE path LIKE $1 AND LOWER(metadata->>'title') LIKE $4 AND LOWER(metadata->>'title') != $2 ORDER BY path LIMIT $3) UNION -- Finally path matches (SELECT * FROM content WHERE path LIKE $1 AND LOWER(path) LIKE $4 AND (metadata->>'title' IS NULL OR LOWER(metadata->>'title') NOT LIKE $4) ORDER BY path LIMIT $3) ORDER BY path LIMIT $3 ` const params = [pathPattern, lowerQuery, limit, `%${lowerQuery}%`] const result = await query(combinedQueryStr, params) return result.rows as DbContent[] } catch (error) { logErrorAlways(`Failed to search all content for "${searchQuery}":`, error) throw new Error( `Failed to search content: ${error instanceof Error ? error.message : String(error)}` ) } } static async getDocumentationSections( pathPattern: string = 'apps/svelte.dev/content/docs/%', minContentLength: number = 100 ): Promise<Array<{ path: string; metadata: Record<string, unknown>; content: string }>> { try { const sectionsQueryStr = ` SELECT path, metadata, content FROM content WHERE path LIKE $1 AND LENGTH(content) >= $2 ORDER BY path ` const params = [pathPattern, minContentLength] const result = await query(sectionsQueryStr, params) return result.rows.map((row) => ({ path: row.path, metadata: row.metadata, content: row.content })) } catch (error) { logErrorAlways('Failed to get documentation sections:', error) throw new Error( `Failed to get sections: ${error instanceof Error ? error.message : String(error)}` ) } } static async getFilteredContent( pathPattern: string = 'apps/svelte.dev/content/docs/%', minContentLength: number = 200 ): Promise<DbContent[]> { try { const filterQueryStr = ` SELECT * FROM content WHERE path LIKE $1 AND LENGTH(content) >= $2 ORDER BY path ` const params = [pathPattern, minContentLength] const result = await query(filterQueryStr, params) return result.rows as DbContent[] } catch (error) { logErrorAlways('Failed to get filtered content:', error) throw new Error( `Failed to get filtered content: ${error instanceof Error ? error.message : String(error)}` ) } } static async getContentStats(): Promise<ContentStats> { try { const totalResult = await query( `SELECT COUNT(*) as total_files, COALESCE(SUM(size_bytes), 0) as total_size_bytes, MAX(updated_at) as last_updated FROM content` ) return { total_files: parseInt(totalResult.rows[0].total_files), total_size_bytes: parseInt(totalResult.rows[0].total_size_bytes), last_updated: totalResult.rows[0].last_updated } } catch (error) { logErrorAlways('Failed to get content stats:', error) throw new Error( `Failed to get stats: ${error instanceof Error ? error.message : String(error)}` ) } } static async deleteContent(path: string): Promise<boolean> { try { const result = await query('DELETE FROM content WHERE path = $1', [path]) return (result.rowCount ?? 0) > 0 } catch (error) { logErrorAlways(`Failed to delete content ${path}:`, error) throw new Error( `Failed to delete content: ${error instanceof Error ? error.message : String(error)}` ) } } static async deleteAllContent(): Promise<number> { try { const result = await query('DELETE FROM content') return result.rowCount ?? 0 } catch (error) { logErrorAlways('Failed to delete all content:', error) throw new Error( `Failed to delete content: ${error instanceof Error ? error.message : String(error)}` ) } } static async hasContentChanged(path: string, newContent: string): Promise<boolean> { try { const existing = await ContentDbService.getContentByPath(path) if (!existing) return true return existing.content !== newContent } catch (error) { logErrorAlways(`Failed to check content change for ${path}:`, error) return true // Assume changed on error } } static async batchUpsertContent(contents: CreateContentInput[]): Promise<DbContent[]> { try { const results: DbContent[] = [] // Process in chunks to avoid overwhelming the database const chunkSize = 200 for (let i = 0; i < contents.length; i += chunkSize) { const chunk = contents.slice(i, i + chunkSize) const chunkResults = await Promise.all( chunk.map((content) => ContentDbService.upsertContent(content)) ) results.push(...chunkResults) } logAlways(`Batch upserted ${results.length} content items`) return results } catch (error) { logErrorAlways('Failed to batch upsert content:', error) throw new Error( `Failed to batch upsert: ${error instanceof Error ? error.message : String(error)}` ) } } static extractFrontmatter(content: string): Record<string, unknown> { const metadata: Record<string, unknown> = {} if (!content.startsWith('---\n')) { return metadata } const endIndex = content.indexOf('\n---\n', 4) if (endIndex === -1) { return metadata } const frontmatter = content.substring(4, endIndex) const lines = frontmatter.split('\n') for (const line of lines) { const colonIndex = line.indexOf(':') if (colonIndex > 0) { const key = line.substring(0, colonIndex).trim() const value = line.substring(colonIndex + 1).trim() // Remove quotes if present const cleanValue = value.replace(/^["'](.*)["']$/, '$1') // Try to parse as JSON for nested structures try { metadata[key] = JSON.parse(cleanValue) } catch { metadata[key] = cleanValue } } } return metadata } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/khromov/llmctx'

If you have feedback or need assistance with the MCP directory API, please join our Discord server