Files
supabase/apps/ui-library/app/api/registry/tanstack-db/route.ts
2026-03-09 14:32:44 +01:00

154 lines
4.9 KiB
TypeScript

import fs from 'fs/promises'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
import { RegistryItem } from 'shadcn/schema'
import {
generateDbContent,
generateListContent,
generatePageContent,
generateSchemasContent,
generateSheetContent,
} from './_generators'
import { OpenAPISchema, RegistryFile } from './types'
import { safeFileSegment, toSingular, validateDefinitionNames } from './utils'
// Read the base registry JSON
async function readBaseRegistry(): Promise<RegistryItem> {
const fullPath = path.join(process.cwd(), 'public/r/tanstack-db-nextjs.json')
const content = await fs.readFile(fullPath, 'utf-8')
return JSON.parse(content) as RegistryItem
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const ref = searchParams.get('ref')
const anonKey = searchParams.get('anonKey')
if (!ref || !anonKey) {
return NextResponse.json(
{ error: 'Missing required parameters: ref and anonKey' },
{ status: 400 }
)
}
// Validate Supabase project ref to prevent SSRF via crafted hostnames
// Typical Supabase refs are lowercase alphabetic; adjust if needed.
const refPattern = /^[a-z]{1,64}$/
if (!refPattern.test(ref)) {
return NextResponse.json({ error: 'Invalid project ref format.' }, { status: 400 })
}
try {
// Fetch OpenAPI spec from Supabase
const openApiUrl = `https://${ref}.supabase.co/rest/v1/`
const response = await fetch(openApiUrl, {
headers: {
apikey: anonKey,
},
})
if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch OpenAPI spec: ${response.statusText}` },
{ status: response.status }
)
}
const openApiSpec: OpenAPISchema = await response.json()
if (!openApiSpec.definitions) {
return NextResponse.json(
{ error: 'No table definitions found in OpenAPI spec' },
{ status: 400 }
)
}
// Validate all table/column names before generating any code
try {
validateDefinitionNames(openApiSpec.definitions)
} catch (validationError) {
return NextResponse.json(
{
error: `Invalid table or column name: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
},
{ status: 400 }
)
}
// Generate dynamic file contents
const schemasContent = generateSchemasContent(openApiSpec.definitions)
const dbContent = generateDbContent(openApiSpec.definitions)
// Get the first table for the example CRUD page
const tableNames = Object.keys(openApiSpec.definitions).filter((name) => !name.startsWith('_'))
// TODO: remove this once we have a way to select the table
const firstTableName = tableNames.length > 0 ? tableNames[0] : null
const firstTableDefinition = firstTableName ? openApiSpec.definitions[firstTableName] : null
// Read the base registry JSON
const baseRegistry = await readBaseRegistry()
// Create dynamic files
const dynamicFiles: RegistryFile[] = [
{
path: 'lib/schemas.ts',
content: schemasContent,
type: 'registry:lib',
},
{
path: 'lib/db.ts',
content: dbContent,
type: 'registry:lib',
},
]
// Add example CRUD pages for the first table
if (firstTableName && firstTableDefinition) {
const singularTableName = toSingular(firstTableName)
const safeTable = safeFileSegment(firstTableName)
const safeSingular = safeFileSegment(singularTableName)
dynamicFiles.push(
{
path: `app/${safeTable}/page.tsx`,
content: generatePageContent(firstTableName),
type: 'registry:page',
target: `app/${safeTable}/page.tsx`,
},
{
path: `app/${safeTable}/${safeSingular}-sheet.tsx`,
content: generateSheetContent(firstTableName, firstTableDefinition),
type: 'registry:component',
target: `app/${safeTable}/${safeSingular}-sheet.tsx`,
},
{
path: `app/${safeTable}/${safeTable}-list.tsx`,
content: generateListContent(firstTableName, firstTableDefinition),
type: 'registry:component',
target: `app/${safeTable}/${safeTable}-list.tsx`,
}
)
}
// Combine dynamic files with base registry files
const registryResponse: RegistryItem = {
...baseRegistry,
files: [...dynamicFiles, ...(baseRegistry.files || [])],
envVars: {
NEXT_PUBLIC_SUPABASE_URL: `https://${ref}.supabase.co`,
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: anonKey,
},
docs: '',
}
return NextResponse.json(registryResponse)
} catch (error) {
console.error('Error generating tanstack-db block:', error)
return NextResponse.json(
{ error: 'Failed to generate block. Please check your project ref and anon key.' },
{ status: 500 }
)
}
}