Skip to main content
Glama
MisterSandFR

Supabase MCP Server - Self-Hosted Edition

by MisterSandFR
generate_crud_api.ts48.7 kB
import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { ToolContext } from "./types.js"; import { executeSqlWithFallback } from "./utils.js"; const GenerateCrudApiInputSchema = z.object({ action: z.enum(['generate', 'analyze', 'list', 'deploy', 'test', 'document', 'validate', 'optimize', 'batch_generate']).describe("CRUD API action"), tableName: z.string().optional().describe("Table name to generate API for"), schemaName: z.string().optional().default('public').describe("Schema name"), apiType: z.enum(['rest', 'graphql', 'rpc', 'all']).optional().default('rest').describe("API type to generate"), authRequired: z.boolean().optional().default(true).describe("Require authentication"), operations: z.array(z.enum(['create', 'read', 'update', 'delete', 'list', 'search', 'bulk'])).optional().default(['create', 'read', 'update', 'delete', 'list']).describe("Operations to include"), outputFormat: z.enum(['typescript', 'javascript', 'python', 'curl', 'openapi']).optional().default('typescript').describe("Output format"), includeValidation: z.boolean().optional().default(true).describe("Include input validation"), includeFiltering: z.boolean().optional().default(true).describe("Include filtering capabilities"), includePagination: z.boolean().optional().default(true).describe("Include pagination"), includeRLS: z.boolean().optional().default(true).describe("Include RLS policies"), customEndpoints: z.array(z.object({ name: z.string(), description: z.string(), method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), path: z.string(), handler: z.string() })).optional().describe("Custom endpoints to add"), middleware: z.array(z.enum(['cors', 'helmet', 'ratelimit', 'logging', 'compression'])).optional().default(['cors', 'helmet']).describe("Middleware to include"), testGeneration: z.boolean().optional().default(true).describe("Generate test cases"), documentationGeneration: z.boolean().optional().default(true).describe("Generate API documentation"), performanceOptimization: z.boolean().optional().default(false).describe("Include performance optimizations"), batchOperations: z.boolean().optional().default(false).describe("Include batch operations"), searchOperations: z.boolean().optional().default(false).describe("Include search operations"), relationshipHandling: z.enum(['none', 'basic', 'advanced']).optional().default('basic').describe("Relationship handling level"), cacheIntegration: z.boolean().optional().default(false).describe("Include cache integration"), webhookIntegration: z.boolean().optional().default(false).describe("Include webhook integration"), deploymentTarget: z.enum(['supabase', 'vercel', 'netlify', 'docker', 'standalone']).optional().describe("Deployment target"), tables: z.array(z.string()).optional().describe("Multiple tables for batch generation"), customConfig: z.record(z.any()).optional().describe("Custom configuration options") }); type GenerateCrudApiInput = z.infer<typeof GenerateCrudApiInputSchema>; const generateCrudApiOutputSchema = z.object({ content: z.array(z.object({ type: z.literal("text"), text: z.string() })) }); export const generateCrudApiTool = { name: "generate_crud_api", description: "Comprehensive CRUD API generator for Supabase with TypeScript, validation, authentication, testing, and deployment support", inputSchema: GenerateCrudApiInputSchema, mcpInputSchema: { type: "object", properties: { action: { type: "string", enum: ['generate', 'analyze', 'list', 'deploy', 'test', 'document', 'validate', 'optimize', 'batch_generate'], description: "CRUD API action" }, tableName: { type: "string", description: "Table name to generate API for" }, schemaName: { type: "string", description: "Schema name" }, apiType: { type: "string", enum: ["rest", "graphql", "rpc", "all"], description: "API type" }, authRequired: { type: "boolean", description: "Require authentication" }, operations: { type: "array", items: { type: "string", enum: ["create", "read", "update", "delete", "list", "search", "bulk"] }, description: "Operations to include" }, outputFormat: { type: "string", enum: ["typescript", "javascript", "python", "curl", "openapi"], description: "Output format" }, includeValidation: { type: "boolean", description: "Include validation" }, includeFiltering: { type: "boolean", description: "Include filtering" }, includePagination: { type: "boolean", description: "Include pagination" }, includeRLS: { type: "boolean", description: "Include RLS policies" }, customEndpoints: { type: "array", items: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE", "PATCH"] }, path: { type: "string" }, handler: { type: "string" } } } }, middleware: { type: "array", items: { type: "string", enum: ["cors", "helmet", "ratelimit", "logging", "compression"] } }, testGeneration: { type: "boolean", description: "Generate tests" }, documentationGeneration: { type: "boolean", description: "Generate docs" }, performanceOptimization: { type: "boolean", description: "Performance optimization" }, batchOperations: { type: "boolean", description: "Batch operations" }, searchOperations: { type: "boolean", description: "Search operations" }, relationshipHandling: { type: "string", enum: ["none", "basic", "advanced"], description: "Relationship handling" }, cacheIntegration: { type: "boolean", description: "Cache integration" }, webhookIntegration: { type: "boolean", description: "Webhook integration" }, deploymentTarget: { type: "string", enum: ["supabase", "vercel", "netlify", "docker", "standalone"], description: "Deployment target" }, tables: { type: "array", items: { type: "string" }, description: "Tables for batch generation" }, customConfig: { type: "object", description: "Custom configuration" } }, required: ["action"] }, outputSchema: generateCrudApiOutputSchema, execute: async (input: unknown, context: ToolContext) => { const validatedInput = GenerateCrudApiInputSchema.parse(input); switch (validatedInput.action) { case 'analyze': { if (!validatedInput.tableName) { throw new Error("Table name is required for analysis"); } const tableAnalysis = await analyzeTableForAPI( validatedInput.tableName, validatedInput.schemaName, context ); return { content: [{ type: "text", text: JSON.stringify({ table_analysis: tableAnalysis, api_recommendations: generateApiRecommendations(tableAnalysis), complexity_score: calculateComplexityScore(tableAnalysis) }, null, 2) }] }; } case 'generate': { if (!validatedInput.tableName) { throw new Error("Table name is required for generation"); } const tableInfo = await getTableInfo( validatedInput.tableName, validatedInput.schemaName, context ); const apiCode = await generateAPICode(tableInfo, validatedInput, context); return { content: [{ type: "text", text: JSON.stringify({ generated_api: apiCode, table: `${validatedInput.schemaName}.${validatedInput.tableName}`, operations: validatedInput.operations, output_format: validatedInput.outputFormat, files_generated: Object.keys(apiCode.files).length }, null, 2) }] }; } case 'batch_generate': { if (!validatedInput.tables || validatedInput.tables.length === 0) { throw new Error("Tables list is required for batch generation"); } const batchResults = []; for (const tableName of validatedInput.tables) { try { const tableInfo = await getTableInfo(tableName, validatedInput.schemaName, context); const apiCode = await generateAPICode(tableInfo, { ...validatedInput, tableName }, context); batchResults.push({ table: tableName, success: true, files_generated: Object.keys(apiCode.files).length, api_code: apiCode }); } catch (error: any) { batchResults.push({ table: tableName, success: false, error: error.message }); } } return { content: [{ type: "text", text: JSON.stringify({ batch_results: batchResults, total_tables: validatedInput.tables.length, successful: batchResults.filter(r => r.success).length, failed: batchResults.filter(r => !r.success).length }, null, 2) }] }; } case 'document': { if (!validatedInput.tableName) { throw new Error("Table name is required for documentation"); } const tableInfo = await getTableInfo( validatedInput.tableName, validatedInput.schemaName, context ); const documentation = await generateAPIDocumentation(tableInfo, validatedInput); return { content: [{ type: "text", text: JSON.stringify({ api_documentation: documentation, format: validatedInput.outputFormat === 'openapi' ? 'OpenAPI 3.0' : 'Markdown', table: `${validatedInput.schemaName}.${validatedInput.tableName}` }, null, 2) }] }; } case 'test': { if (!validatedInput.tableName) { throw new Error("Table name is required for test generation"); } const tableInfo = await getTableInfo( validatedInput.tableName, validatedInput.schemaName, context ); const testCases = await generateTestCases(tableInfo, validatedInput); return { content: [{ type: "text", text: JSON.stringify({ test_cases: testCases, test_framework: validatedInput.outputFormat === 'typescript' ? 'Jest' : 'pytest', coverage_areas: ['authentication', 'validation', 'crud_operations', 'error_handling'] }, null, 2) }] }; } case 'list': { const availableTablesSql = ` SELECT t.table_name, t.table_type, COUNT(c.column_name) as column_count, array_agg( c.column_name || ':' || c.data_type ORDER BY c.ordinal_position ) as columns, CASE WHEN EXISTS( SELECT 1 FROM pg_tables pt WHERE pt.schemaname = t.table_schema AND pt.tablename = t.table_name AND pt.rowsecurity = true ) THEN true ELSE false END as has_rls, CASE WHEN COUNT(tc.constraint_name) > 0 THEN true ELSE false END as has_constraints FROM information_schema.tables t LEFT JOIN information_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema LEFT JOIN information_schema.table_constraints tc ON t.table_name = tc.table_name AND t.table_schema = tc.table_schema WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' GROUP BY t.table_name, t.table_type, t.table_schema ORDER BY t.table_name `; const tablesResult = await executeSqlWithFallback(availableTablesSql, context, [validatedInput.schemaName]); const tablesWithRecommendations = tablesResult.data.map((table: any) => ({ ...table, api_suitability: assessApiSuitability(table), recommended_operations: getRecommendedOperations(table), estimated_complexity: estimateApiComplexity(table) })); return { content: [{ type: "text", text: JSON.stringify({ available_tables: tablesWithRecommendations, schema: validatedInput.schemaName, total_tables: tablesWithRecommendations.length, suitable_for_api: tablesWithRecommendations.filter(t => t.api_suitability === 'high').length }, null, 2) }] }; } case 'validate': { if (!validatedInput.tableName) { throw new Error("Table name is required for validation"); } const validationResults = await validateAPIGeneration( validatedInput.tableName, validatedInput.schemaName, context, validatedInput ); return { content: [{ type: "text", text: JSON.stringify({ validation_results: validationResults, is_valid: validationResults.errors.length === 0, can_generate_api: validationResults.errors.length === 0 && validationResults.warnings.length < 3 }, null, 2) }] }; } case 'optimize': { if (!validatedInput.tableName) { throw new Error("Table name is required for optimization"); } const optimizations = await generateOptimizations( validatedInput.tableName, validatedInput.schemaName, context, validatedInput ); return { content: [{ type: "text", text: JSON.stringify({ optimizations: optimizations, performance_improvements: optimizations.filter((o: any) => o.category === 'performance').length, security_improvements: optimizations.filter((o: any) => o.category === 'security').length }, null, 2) }] }; } default: throw new Error(`Unknown action: ${validatedInput.action}`); } } }; async function analyzeTableForAPI(tableName: string, schemaName: string, context: ToolContext): Promise<any> { const analysisSql = ` WITH table_info AS ( SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale, c.ordinal_position FROM information_schema.columns c WHERE c.table_schema = $1 AND c.table_name = $2 ORDER BY c.ordinal_position ), constraints_info AS ( SELECT tc.constraint_type, kcu.column_name, tc.constraint_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 ), foreign_keys AS ( SELECT kcu.column_name as local_column, ccu.table_name as foreign_table, ccu.column_name as foreign_column FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 AND tc.constraint_type = 'FOREIGN KEY' ) SELECT ti.column_name, ti.data_type, ti.is_nullable, ti.column_default, ti.character_maximum_length, ti.numeric_precision, ti.numeric_scale, ti.ordinal_position, COALESCE(ci.constraint_type, '') as constraint_type, fk.foreign_table, fk.foreign_column FROM table_info ti LEFT JOIN constraints_info ci ON ti.column_name = ci.column_name LEFT JOIN foreign_keys fk ON ti.column_name = fk.local_column ORDER BY ti.ordinal_position `; const result = await executeSqlWithFallback(analysisSql, context, [schemaName, tableName]); const analysis = { table_name: tableName, schema_name: schemaName, columns: result.data, primary_key: result.data.find((c: any) => c.constraint_type === 'PRIMARY KEY')?.column_name, foreign_keys: result.data.filter((c: any) => c.foreign_table), nullable_columns: result.data.filter((c: any) => c.is_nullable === 'YES'), required_columns: result.data.filter((c: any) => c.is_nullable === 'NO' && !c.column_default), data_types: result.data.reduce((acc: any, col: any) => { acc[col.data_type] = (acc[col.data_type] || 0) + 1; return acc; }, {}), relationships: { has_foreign_keys: result.data.some((c: any) => c.foreign_table), foreign_key_count: result.data.filter((c: any) => c.foreign_table).length } }; return analysis; } async function getTableInfo(tableName: string, schemaName: string, context: ToolContext): Promise<any> { const tableInfoSql = ` SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, c.character_maximum_length, tc.constraint_type FROM information_schema.columns c LEFT JOIN information_schema.key_column_usage kcu ON c.column_name = kcu.column_name AND c.table_name = kcu.table_name AND c.table_schema = kcu.table_schema LEFT JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name WHERE c.table_schema = $1 AND c.table_name = $2 ORDER BY c.ordinal_position `; const result = await executeSqlWithFallback(tableInfoSql, context, [schemaName, tableName]); return { table_name: tableName, schema_name: schemaName, columns: result.data }; } async function generateAPICode(tableInfo: any, options: GenerateCrudApiInput, context: ToolContext): Promise<any> { const apiFiles = {}; // Generate TypeScript interfaces if (options.outputFormat === 'typescript') { apiFiles['types.ts'] = generateTypeScriptTypes(tableInfo, options); apiFiles['api.ts'] = generateTypeScriptAPI(tableInfo, options); if (options.includeValidation) { apiFiles['validation.ts'] = generateValidationSchema(tableInfo, options); } } // Generate JavaScript version if (options.outputFormat === 'javascript') { apiFiles['api.js'] = generateJavaScriptAPI(tableInfo, options); } // Generate Python version if (options.outputFormat === 'python') { apiFiles['api.py'] = generatePythonAPI(tableInfo, options); } // Generate cURL examples if (options.outputFormat === 'curl') { apiFiles['examples.sh'] = generateCurlExamples(tableInfo, options); } // Generate OpenAPI specification if (options.outputFormat === 'openapi' || options.documentationGeneration) { apiFiles['openapi.yaml'] = generateOpenAPISpec(tableInfo, options); } // Generate tests if (options.testGeneration) { apiFiles['tests.ts'] = generateTestFile(tableInfo, options); } // Generate deployment files if (options.deploymentTarget) { const deploymentFiles = generateDeploymentFiles(tableInfo, options); Object.assign(apiFiles, deploymentFiles); } return { table: tableInfo.table_name, files: apiFiles, operations: options.operations, metadata: { auth_required: options.authRequired, includes_validation: options.includeValidation, includes_pagination: options.includePagination, includes_filtering: options.includeFiltering, middleware: options.middleware } }; } function generateTypeScriptTypes(tableInfo: any, options: GenerateCrudApiInput): string { const interfaceName = toPascalCase(tableInfo.table_name); const properties = tableInfo.columns.map((col: any) => { const optional = col.is_nullable === 'YES' || col.column_default ? '?' : ''; const type = mapPostgresToTypeScript(col.data_type); return ` ${col.column_name}${optional}: ${type};`; }).join('\n'); return `// Generated TypeScript types for ${tableInfo.table_name} export interface ${interfaceName} { ${properties} } export interface Create${interfaceName}Request { ${tableInfo.columns .filter((col: any) => !isAutoGenerated(col)) .map((col: any) => { const optional = col.is_nullable === 'YES' || col.column_default ? '?' : ''; const type = mapPostgresToTypeScript(col.data_type); return ` ${col.column_name}${optional}: ${type};`; }).join('\n')} } export interface Update${interfaceName}Request { ${tableInfo.columns .filter((col: any) => !isAutoGenerated(col) && !isPrimaryKey(col)) .map((col: any) => { const type = mapPostgresToTypeScript(col.data_type); return ` ${col.column_name}?: ${type};`; }).join('\n')} } export interface ${interfaceName}Response { data: ${interfaceName}[]; count?: number; error?: string; }`; } function generateTypeScriptAPI(tableInfo: any, options: GenerateCrudApiInput): string { const className = toPascalCase(tableInfo.table_name); const tableName = tableInfo.table_name; let apiCode = `// Generated API for ${tableName} import { createClient } from '@supabase/supabase-js'; import { ${className}, Create${className}Request, Update${className}Request, ${className}Response } from './types'; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); export class ${className}API { `; // Generate CRUD operations if (options.operations.includes('create')) { apiCode += ` static async create(data: Create${className}Request): Promise<${className}Response> { try { const { data: result, error } = await supabase .from('${tableName}') .insert([data]) .select(); if (error) throw error; return { data: result }; } catch (error: any) { return { data: [], error: error.message }; } } `; } if (options.operations.includes('read')) { const primaryKey = getPrimaryKeyColumn(tableInfo); apiCode += ` static async getById(id: ${mapPostgresToTypeScript(primaryKey?.data_type || 'bigint')}): Promise<${className}Response> { try { const { data, error } = await supabase .from('${tableName}') .select('*') .eq('${primaryKey?.column_name || 'id'}', id) .single(); if (error) throw error; return { data: [data] }; } catch (error: any) { return { data: [], error: error.message }; } } `; } if (options.operations.includes('list')) { apiCode += ` static async list(options?: { limit?: number; offset?: number; orderBy?: string; orderDirection?: 'asc' | 'desc'; filters?: Record<string, any>; }): Promise<${className}Response> { try { let query = supabase.from('${tableName}').select('*', { count: 'exact' }); // Apply filters if (options?.filters) { Object.entries(options.filters).forEach(([key, value]) => { query = query.eq(key, value); }); } // Apply ordering if (options?.orderBy) { query = query.order(options.orderBy, { ascending: options.orderDirection !== 'desc' }); } // Apply pagination if (options?.limit) { query = query.limit(options.limit); } if (options?.offset) { query = query.range(options.offset, options.offset + (options.limit || 10) - 1); } const { data, error, count } = await query; if (error) throw error; return { data: data || [], count: count || 0 }; } catch (error: any) { return { data: [], error: error.message }; } } `; } if (options.operations.includes('update')) { const primaryKey = getPrimaryKeyColumn(tableInfo); apiCode += ` static async update(id: ${mapPostgresToTypeScript(primaryKey?.data_type || 'bigint')}, data: Update${className}Request): Promise<${className}Response> { try { const { data: result, error } = await supabase .from('${tableName}') .update(data) .eq('${primaryKey?.column_name || 'id'}', id) .select(); if (error) throw error; return { data: result }; } catch (error: any) { return { data: [], error: error.message }; } } `; } if (options.operations.includes('delete')) { const primaryKey = getPrimaryKeyColumn(tableInfo); apiCode += ` static async delete(id: ${mapPostgresToTypeScript(primaryKey?.data_type || 'bigint')}): Promise<{ success: boolean; error?: string }> { try { const { error } = await supabase .from('${tableName}') .delete() .eq('${primaryKey?.column_name || 'id'}', id); if (error) throw error; return { success: true }; } catch (error: any) { return { success: false, error: error.message }; } } `; } if (options.operations.includes('search')) { apiCode += ` static async search(query: string, columns?: string[]): Promise<${className}Response> { try { const searchColumns = columns || ${JSON.stringify(getSearchableColumns(tableInfo))}; let supabaseQuery = supabase.from('${tableName}').select('*'); // Use text search across specified columns const orConditions = searchColumns .map(col => \`\${col}.ilike.%\${query}%\`) .join(','); const { data, error } = await supabaseQuery.or(orConditions); if (error) throw error; return { data: data || [] }; } catch (error: any) { return { data: [], error: error.message }; } } `; } apiCode += ` }`; return apiCode; } function generateValidationSchema(tableInfo: any, options: GenerateCrudApiInput): string { const className = toPascalCase(tableInfo.table_name); let validationCode = `// Validation schemas for ${tableInfo.table_name} import { z } from 'zod'; `; // Generate Zod schemas for each operation const createSchema = tableInfo.columns .filter((col: any) => !isAutoGenerated(col)) .map((col: any) => generateZodField(col)) .join(',\n '); validationCode += `export const create${className}Schema = z.object({ ${createSchema} }); `; const updateSchema = tableInfo.columns .filter((col: any) => !isAutoGenerated(col) && !isPrimaryKey(col)) .map((col: any) => generateZodField(col, true)) .join(',\n '); validationCode += `export const update${className}Schema = z.object({ ${updateSchema} }); `; return validationCode; } async function generateAPIDocumentation(tableInfo: any, options: GenerateCrudApiInput): Promise<any> { const className = toPascalCase(tableInfo.table_name); const tableName = tableInfo.table_name; if (options.outputFormat === 'openapi') { return generateOpenAPISpec(tableInfo, options); } // Generate Markdown documentation const docs = `# ${className} API ## Table Information - **Table**: \`${tableName}\` - **Schema**: \`${tableInfo.schema_name}\` - **Columns**: ${tableInfo.columns.length} ## Columns ${tableInfo.columns.map((col: any) => ` - **${col.column_name}**: ${col.data_type}${col.is_nullable === 'NO' ? ' (required)' : ' (optional)'}`).join('')} ## Available Operations ${options.operations.map((op: string) => `- ${op.toUpperCase()}`).join('\n')} ## API Endpoints ### Create ${className} \`\`\` POST /api/${tableName} Content-Type: application/json { // Request body based on Create${className}Request interface } \`\`\` ### Get ${className} by ID \`\`\` GET /api/${tableName}/:id \`\`\` ### List ${className}s \`\`\` GET /api/${tableName}?limit=10&offset=0&orderBy=id&orderDirection=asc \`\`\` ### Update ${className} \`\`\` PUT /api/${tableName}/:id Content-Type: application/json { // Request body based on Update${className}Request interface } \`\`\` ### Delete ${className} \`\`\` DELETE /api/${tableName}/:id \`\`\` `; return { format: 'markdown', content: docs, table: tableName, operations: options.operations }; } async function generateTestCases(tableInfo: any, options: GenerateCrudApiInput): Promise<any> { const className = toPascalCase(tableInfo.table_name); const testContent = `// Generated tests for ${tableInfo.table_name} import { ${className}API } from './api'; import { create${className}Schema, update${className}Schema } from './validation'; describe('${className}API', () => { const mockData = ${generateMockData(tableInfo)}; describe('create', () => { it('should create a new ${tableInfo.table_name}', async () => { const result = await ${className}API.create(mockData); expect(result.data).toBeDefined(); expect(result.error).toBeUndefined(); }); it('should validate input data', () => { const validationResult = create${className}Schema.safeParse(mockData); expect(validationResult.success).toBe(true); }); }); describe('list', () => { it('should list ${tableInfo.table_name}s with pagination', async () => { const result = await ${className}API.list({ limit: 10, offset: 0 }); expect(result.data).toBeInstanceOf(Array); expect(result.count).toBeDefined(); }); }); describe('getById', () => { it('should get ${tableInfo.table_name} by id', async () => { const result = await ${className}API.getById(1); expect(result.data).toBeDefined(); }); }); describe('update', () => { it('should update ${tableInfo.table_name}', async () => { const updateData = { /* partial update data */ }; const result = await ${className}API.update(1, updateData); expect(result.data).toBeDefined(); }); }); describe('delete', () => { it('should delete ${tableInfo.table_name}', async () => { const result = await ${className}API.delete(1); expect(result.success).toBe(true); }); }); });`; return { framework: 'jest', content: testContent, test_cases: options.operations.length * 2, // Assuming 2 tests per operation coverage_areas: ['crud_operations', 'validation', 'error_handling'] }; } // Helper functions function toPascalCase(str: string): string { return str.replace(/(?:^|_)(.)/g, (_, char) => char.toUpperCase()); } function mapPostgresToTypeScript(pgType: string): string { const typeMap = { 'integer': 'number', 'bigint': 'number', 'smallint': 'number', 'decimal': 'number', 'numeric': 'number', 'real': 'number', 'double precision': 'number', 'serial': 'number', 'bigserial': 'number', 'varchar': 'string', 'char': 'string', 'text': 'string', 'uuid': 'string', 'boolean': 'boolean', 'timestamp': 'string', 'timestamptz': 'string', 'date': 'string', 'time': 'string', 'json': 'any', 'jsonb': 'any', 'array': 'any[]' }; return typeMap[pgType as keyof typeof typeMap] || 'any'; } function isAutoGenerated(col: any): boolean { return col.column_default && ( col.column_default.includes('nextval') || col.column_default.includes('uuid_generate') || col.column_default.includes('now()') ); } function isPrimaryKey(col: any): boolean { return col.constraint_type === 'PRIMARY KEY'; } function getPrimaryKeyColumn(tableInfo: any): any { return tableInfo.columns.find((col: any) => isPrimaryKey(col)) || tableInfo.columns.find((col: any) => col.column_name === 'id'); } function getSearchableColumns(tableInfo: any): string[] { return tableInfo.columns .filter((col: any) => ['varchar', 'char', 'text'].includes(col.data_type)) .map((col: any) => col.column_name); } function generateZodField(col: any, optional: boolean = false): string { const baseType = mapPostgresToZod(col.data_type); const isOptional = optional || col.is_nullable === 'YES' || col.column_default; let field = `${col.column_name}: ${baseType}`; if (col.character_maximum_length) { field = field.replace('z.string()', `z.string().max(${col.character_maximum_length})`); } if (isOptional) { field += '.optional()'; } return field; } function mapPostgresToZod(pgType: string): string { const typeMap = { 'integer': 'z.number().int()', 'bigint': 'z.number().int()', 'smallint': 'z.number().int()', 'decimal': 'z.number()', 'numeric': 'z.number()', 'varchar': 'z.string()', 'char': 'z.string()', 'text': 'z.string()', 'uuid': 'z.string().uuid()', 'boolean': 'z.boolean()', 'timestamp': 'z.string().datetime()', 'date': 'z.string().date()', 'json': 'z.any()', 'jsonb': 'z.any()' }; return typeMap[pgType as keyof typeof typeMap] || 'z.any()'; } function generateMockData(tableInfo: any): string { const mockFields = tableInfo.columns .filter((col: any) => !isAutoGenerated(col)) .map((col: any) => { const mockValue = generateMockValue(col); return ` ${col.column_name}: ${mockValue}`; }) .join(',\n'); return `{\n${mockFields}\n }`; } function generateMockValue(col: any): string { switch (col.data_type) { case 'varchar': case 'char': case 'text': return `'test ${col.column_name}'`; case 'integer': case 'bigint': case 'smallint': return '1'; case 'boolean': return 'true'; case 'uuid': return "'123e4567-e89b-12d3-a456-426614174000'"; case 'timestamp': case 'timestamptz': return "'2023-01-01T00:00:00Z'"; default: return 'null'; } } function generateOpenAPISpec(tableInfo: any, options: GenerateCrudApiInput): string { // Generate OpenAPI 3.0 specification const spec = { openapi: '3.0.0', info: { title: `${toPascalCase(tableInfo.table_name)} API`, version: '1.0.0', description: `Generated API for ${tableInfo.table_name} table` }, paths: {}, components: { schemas: {} } }; return JSON.stringify(spec, null, 2); } function generateJavaScriptAPI(tableInfo: any, options: GenerateCrudApiInput): string { // Convert TypeScript to JavaScript (simplified) const tsCode = generateTypeScriptAPI(tableInfo, options); return tsCode .replace(/: [^=\s,)]+/g, '') // Remove type annotations .replace(/interface \w+ \{[^}]+\}/gs, '') // Remove interfaces .replace(/export interface[^}]+\}/gs, ''); // Remove interface exports } function generatePythonAPI(tableInfo: any, options: GenerateCrudApiInput): string { const className = toPascalCase(tableInfo.table_name); return `# Generated Python API for ${tableInfo.table_name} from supabase import create_client, Client from typing import Dict, List, Optional, Any import os class ${className}API: def __init__(self): url = os.environ.get("SUPABASE_URL") key = os.environ.get("SUPABASE_ANON_KEY") self.supabase: Client = create_client(url, key) def create(self, data: Dict[str, Any]) -> Dict[str, Any]: try: result = self.supabase.table('${tableInfo.table_name}').insert(data).execute() return {"data": result.data, "error": None} except Exception as e: return {"data": [], "error": str(e)} def get_by_id(self, id: int) -> Dict[str, Any]: try: result = self.supabase.table('${tableInfo.table_name}').select("*").eq('id', id).single().execute() return {"data": [result.data], "error": None} except Exception as e: return {"data": [], "error": str(e)} def list(self, limit: int = 10, offset: int = 0) -> Dict[str, Any]: try: result = self.supabase.table('${tableInfo.table_name}').select("*").range(offset, offset + limit - 1).execute() return {"data": result.data, "count": len(result.data), "error": None} except Exception as e: return {"data": [], "error": str(e)} `; } function generateCurlExamples(tableInfo: any, options: GenerateCrudApiInput): string { const tableName = tableInfo.table_name; return `#!/bin/bash # cURL examples for ${tableName} API # Create ${tableName} curl -X POST "http://localhost:3000/api/${tableName}" \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer \$SUPABASE_ANON_KEY" \\ -d '{ "name": "example name", "description": "example description" }' # Get ${tableName} by ID curl -X GET "http://localhost:3000/api/${tableName}/1" \\ -H "Authorization: Bearer \$SUPABASE_ANON_KEY" # List ${tableName}s curl -X GET "http://localhost:3000/api/${tableName}?limit=10&offset=0" \\ -H "Authorization: Bearer \$SUPABASE_ANON_KEY" # Update ${tableName} curl -X PUT "http://localhost:3000/api/${tableName}/1" \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer \$SUPABASE_ANON_KEY" \\ -d '{ "name": "updated name" }' # Delete ${tableName} curl -X DELETE "http://localhost:3000/api/${tableName}/1" \\ -H "Authorization: Bearer \$SUPABASE_ANON_KEY" `; } function generateDeploymentFiles(tableInfo: any, options: GenerateCrudApiInput): any { const files: any = {}; if (options.deploymentTarget === 'docker') { files['Dockerfile'] = `FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "start"]`; files['docker-compose.yml'] = `version: '3.8' services: api: build: . ports: - "3000:3000" environment: - SUPABASE_URL=\${SUPABASE_URL} - SUPABASE_ANON_KEY=\${SUPABASE_ANON_KEY}`; } if (options.deploymentTarget === 'vercel') { files['vercel.json'] = JSON.stringify({ version: 2, builds: [ { src: "api/**/*.ts", use: "@vercel/node" } ], routes: [ { src: "/api/(.*)", dest: "/api/$1" } ] }, null, 2); } return files; } function generateTestFile(tableInfo: any, options: GenerateCrudApiInput): string { return generateTestCases(tableInfo, options).then((tests: any) => tests.content); } function generateApiRecommendations(analysis: any): string[] { const recommendations = []; if (!analysis.primary_key) { recommendations.push('Consider adding a primary key for better API performance'); } if (analysis.required_columns.length > 5) { recommendations.push('Many required columns detected - consider making some optional'); } if (analysis.relationships.foreign_key_count > 0) { recommendations.push('Include relationship handling for connected tables'); } if (analysis.nullable_columns.length > analysis.columns.length * 0.7) { recommendations.push('Many nullable columns - ensure proper validation'); } return recommendations; } function calculateComplexityScore(analysis: any): number { let score = 1; // Base complexity // Add complexity for number of columns score += Math.floor(analysis.columns.length / 5); // Add complexity for relationships score += analysis.relationships.foreign_key_count * 2; // Add complexity for different data types score += Object.keys(analysis.data_types).length; return Math.min(score, 10); // Cap at 10 } function assessApiSuitability(table: any): 'low' | 'medium' | 'high' { let score = 0; if (table.has_rls) score += 2; if (table.has_constraints) score += 1; if (table.column_count >= 3 && table.column_count <= 15) score += 2; if (table.column_count > 15) score -= 1; if (score >= 4) return 'high'; if (score >= 2) return 'medium'; return 'low'; } function getRecommendedOperations(table: any): string[] { const operations = ['create', 'read', 'list']; if (table.has_constraints) { operations.push('update', 'delete'); } if (table.column_count > 5) { operations.push('search'); } return operations; } function estimateApiComplexity(table: any): 'low' | 'medium' | 'high' { let complexity = 0; complexity += Math.floor(table.column_count / 5); if (table.has_rls) complexity += 1; if (table.columns.some((c: string) => c.includes('json'))) complexity += 2; if (complexity >= 4) return 'high'; if (complexity >= 2) return 'medium'; return 'low'; } async function validateAPIGeneration(tableName: string, schemaName: string, context: ToolContext, options: GenerateCrudApiInput): Promise<any> { const errors = []; const warnings = []; // Check if table exists const tableExistsSql = ` SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 `; const tableExists = await executeSqlWithFallback(tableExistsSql, context, [schemaName, tableName]); if (tableExists.data[0].count === 0) { errors.push(`Table ${schemaName}.${tableName} does not exist`); } // Check for primary key const primaryKeySql = ` SELECT COUNT(*) as count FROM information_schema.table_constraints WHERE table_schema = $1 AND table_name = $2 AND constraint_type = 'PRIMARY KEY' `; const hasPrimaryKey = await executeSqlWithFallback(primaryKeySql, context, [schemaName, tableName]); if (hasPrimaryKey.data[0].count === 0) { warnings.push('Table has no primary key - some operations may not work as expected'); } return { table: `${schemaName}.${tableName}`, errors, warnings, validation_passed: errors.length === 0 }; } async function generateOptimizations(tableName: string, schemaName: string, context: ToolContext, options: GenerateCrudApiInput): Promise<any[]> { const optimizations = []; // Check for missing indexes const indexSql = ` SELECT t.attname as column_name, pg_catalog.format_type(t.atttypid, t.atttypmod) as data_type FROM pg_catalog.pg_attribute t JOIN pg_catalog.pg_class c ON t.attrelid = c.oid JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 AND c.relname = $2 AND t.attnum > 0 AND NOT EXISTS ( SELECT 1 FROM pg_catalog.pg_index i WHERE i.indrelid = c.oid AND t.attnum = ANY(i.indkey) ) AND t.atttypid IN (23, 25, 1043) -- int4, text, varchar `; const missingIndexes = await executeSqlWithFallback(indexSql, context, [schemaName, tableName]); for (const col of missingIndexes.data) { optimizations.push({ category: 'performance', type: 'index', description: `Consider adding index on column ${col.column_name} for better query performance`, sql: `CREATE INDEX idx_${tableName}_${col.column_name} ON ${schemaName}.${tableName}(${col.column_name});`, impact: 'medium' }); } return optimizations; }

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/MisterSandFR/Supabase-MCP-SelfHosted'

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