Skip to main content
Glama
documentation.js9.79 kB
const { supabase, cachedQuery } = require('./database'); /** * Search for WebDNA documentation based on a query * @param {string} query - The search query * @param {Object} options - Search options * @param {number} options.limit - Maximum number of results (default: 20) * @param {number} options.offset - Offset for pagination (default: 0) * @param {string} options.category - Filter by category (optional) * @returns {Promise<Array>} - Array of matching documentation entries */ async function searchDocumentation(query, options = {}) { const { limit = 20, offset = 0, category = null } = options; try { // Normalize query const normalizedQuery = query.trim().toLowerCase(); // Generate cache key const cacheKey = `search:${normalizedQuery}:${limit}:${offset}:${category || 'all'}`; return await cachedQuery(cacheKey, async () => { // First try exact match on instruction name let exactMatchQuery = supabase .from('documentation') .select(` id, instruction, description, url, webdna_id, categories(id, name) `) .ilike('instruction', `%${normalizedQuery}%`) .order('instruction'); // Add category filter if provided if (category) { exactMatchQuery = exactMatchQuery .eq('categories.name', category); } const { data: exactMatches, error: exactError } = await exactMatchQuery; if (exactError) throw exactError; // Then try full-text search let ftsQuery = supabase .from('documentation') .select(` id, instruction, description, url, webdna_id, categories(id, name) `) .textSearch('search_vector', normalizedQuery, { type: 'websearch', config: 'english' }); // Add category filter if provided if (category) { ftsQuery = ftsQuery .eq('categories.name', category); } const { data: ftsMatches, error: ftsError } = await ftsQuery; if (ftsError) throw ftsError; // Combine results, removing duplicates const seen = new Set(); const results = []; // Process exact matches first (higher priority) exactMatches.forEach(match => { seen.add(match.id); results.push({ id: match.id, instruction: match.instruction, category: match.categories?.name || 'Uncategorized', category_id: match.categories?.id, description: match.description, url: match.url, webdna_id: match.webdna_id, match_type: 'exact', relevance_score: 1.0 // Highest score for exact matches }); }); // Process full-text search matches with relevance scoring ftsMatches.forEach(match => { if (!seen.has(match.id)) { seen.add(match.id); // Calculate relevance score based on position of query in text // This is a simplified approach - could be more sophisticated let relevanceScore = 0.5; // Base score for FTS matches if (match.instruction.toLowerCase().includes(normalizedQuery)) { // Boost score if query appears in instruction name relevanceScore += 0.3; } if (match.description?.toLowerCase().includes(normalizedQuery)) { // Boost score if query appears in description relevanceScore += 0.1; } results.push({ id: match.id, instruction: match.instruction, category: match.categories?.name || 'Uncategorized', category_id: match.categories?.id, description: match.description, url: match.url, webdna_id: match.webdna_id, match_type: 'content', relevance_score: relevanceScore }); } }); // Sort by relevance score (descending) results.sort((a, b) => b.relevance_score - a.relevance_score); // Apply pagination return { results: results.slice(offset, offset + limit), total_count: results.length, offset, limit, query: normalizedQuery }; }, 5 * 60 * 1000); // Cache for 5 minutes } catch (error) { console.error('Error searching documentation:', error); throw error; } } /** * Get a specific WebDNA documentation entry by ID or name * @param {number|string} idOrName - The documentation ID, WebDNA ID, or instruction name * @returns {Promise<Object>} - The documentation entry */ async function getDocumentationById(idOrName) { try { // Generate cache key const cacheKey = `doc:${idOrName}`; return await cachedQuery(cacheKey, async () => { // Determine if id is numeric (database id), webdna_id, or instruction name const isNumeric = /^\d+$/.test(idOrName); let query = supabase .from('documentation') .select(` *, categories(id, name) `); if (isNumeric) { query = query.eq('id', idOrName); } else { // Try webdna_id first, then instruction name query = query.eq('webdna_id', idOrName); } let { data, error } = await query.maybeSingle(); // If not found by webdna_id, try instruction name if (!data && !isNumeric) { ({ data, error } = await supabase .from('documentation') .select(` *, categories(id, name) `) .ilike('instruction', idOrName) .maybeSingle()); } if (error) { if (error.code === 'PGRST116') { // No rows returned return null; } throw error; } if (!data) return null; // Process related documentation if available let relatedDocs = []; if (data.related && Array.isArray(data.related) && data.related.length > 0) { const { data: related, error: relatedError } = await supabase .from('documentation') .select('id, instruction, description, url, webdna_id') .in('id', data.related); if (!relatedError && related) { relatedDocs = related; } } // Format the response return { ...data, category_name: data.categories?.name || 'Uncategorized', category_id: data.categories?.id, related_docs: relatedDocs }; }, 15 * 60 * 1000); // Cache for 15 minutes } catch (error) { console.error('Error getting documentation by ID:', error); throw error; } } /** * Get all WebDNA documentation categories * @returns {Promise<Array>} - Array of categories */ async function getCategories() { try { return await cachedQuery('categories', async () => { // Get all categories const { data: categories, error: categoriesError } = await supabase .from('categories') .select('*') .order('name'); if (categoriesError) throw categoriesError; // Get count of instructions per category const { data: counts, error: countsError } = await supabase .from('documentation') .select('category_id, count(*)', { count: 'exact' }) .group('category_id'); if (countsError) throw countsError; // Map counts to categories const countMap = {}; counts.forEach(item => { countMap[item.category_id] = parseInt(item.count); }); // Combine data return categories.map(category => ({ ...category, instruction_count: countMap[category.id] || 0 })); }, 30 * 60 * 1000); // Cache for 30 minutes } catch (error) { console.error('Error getting categories:', error); throw error; } } /** * Get random WebDNA documentation entries * @param {number} limit - Maximum number of entries to return * @returns {Promise<Array>} - Array of random documentation entries */ async function getRandomDocumentation(limit = 5) { try { // Generate new results every time, don't cache const { data, error } = await supabase .from('documentation') .select(` id, instruction, description, url, webdna_id, categories(id, name) `) .order('id', { ascending: false }) // Using 'random' doesn't work well in RLS .limit(limit); if (error) throw error; return data.map(doc => ({ id: doc.id, instruction: doc.instruction, category: doc.categories?.name || 'Uncategorized', category_id: doc.categories?.id, description: doc.description, url: doc.url, webdna_id: doc.webdna_id })); } catch (error) { console.error('Error getting random documentation:', error); throw error; } } /** * Get the total count of documentation entries * @returns {Promise<number>} - Total count */ async function getDocumentationCount() { try { return await cachedQuery('doc_count', async () => { const { count, error } = await supabase .from('documentation') .select('*', { count: 'exact', head: true }); if (error) throw error; return count || 0; }, 60 * 60 * 1000); // Cache for 1 hour } catch (error) { console.error('Error getting documentation count:', error); throw error; } } module.exports = { searchDocumentation, getDocumentationById, getCategories, getRandomDocumentation, getDocumentationCount };

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/jacgood/webdna-mcp-server'

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