From c69d96fbd82e4e2f15dd538f8f41e87dbac847e0 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Mon, 2 Feb 2026 14:12:19 +0100 Subject: [PATCH] add tanstack db generator. --- .../api/registry/tanstack-db/generators/db.ts | 64 ++++ .../registry/tanstack-db/generators/index.ts | 5 + .../registry/tanstack-db/generators/list.ts | 187 +++++++++++ .../registry/tanstack-db/generators/page.ts | 32 ++ .../tanstack-db/generators/schemas.ts | 33 ++ .../registry/tanstack-db/generators/sheet.ts | 312 ++++++++++++++++++ .../app/api/registry/tanstack-db/route.ts | 130 ++++++++ .../app/api/registry/tanstack-db/types.ts | 23 ++ .../app/api/registry/tanstack-db/utils.ts | 105 ++++++ apps/ui-library/components/mdx-components.tsx | 2 + .../components/tanstack-db-generator.tsx | 212 ++++++++++++ .../content/docs/nextjs/tanstack-db.mdx | 120 +++++++ apps/ui-library/public/r/registry.json | 43 +++ .../public/r/tanstack-db-nextjs.json | 48 +++ apps/ui-library/registry/blocks.ts | 4 + .../blocks/tanstack-db/app/providers.tsx | 15 + .../blocks/tanstack-db/lib/query-client.ts | 41 +++ .../blocks/tanstack-db/registry-item.json | 25 ++ 18 files changed, 1401 insertions(+) create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/db.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/index.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/list.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/page.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/schemas.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/generators/sheet.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/route.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/types.ts create mode 100644 apps/ui-library/app/api/registry/tanstack-db/utils.ts create mode 100644 apps/ui-library/components/tanstack-db-generator.tsx create mode 100644 apps/ui-library/content/docs/nextjs/tanstack-db.mdx create mode 100644 apps/ui-library/public/r/tanstack-db-nextjs.json create mode 100644 apps/ui-library/registry/default/blocks/tanstack-db/app/providers.tsx create mode 100644 apps/ui-library/registry/default/blocks/tanstack-db/lib/query-client.ts create mode 100644 apps/ui-library/registry/default/blocks/tanstack-db/registry-item.json diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/db.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/db.ts new file mode 100644 index 00000000000..575168b1912 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/db.ts @@ -0,0 +1,64 @@ +import { OpenAPIDefinition } from '../types' +import { findPrimaryKeys, toCamelCase } from '../utils' + +// Generate db.ts content from OpenAPI definitions +export function generateDbContent(definitions: Record): string { + const tableNames = Object.keys(definitions).filter((name) => !name.startsWith('_')) + + const lines: string[] = [ + "import { createSupabaseCollection } from 'supabase-collection'", + "import { createClient } from '@/lib/supabase/client'", + "import { getQueryClient } from '@/lib/query-client'", + 'import {', + ...tableNames.map((name) => ` ${toCamelCase(name)}Schema,`), + "} from './schemas'", + '', + 'const supabase = createClient()', + '', + ] + + for (const [tableName, definition] of Object.entries(definitions)) { + // Skip internal PostgREST tables + if (tableName.startsWith('_')) continue + + const collectionName = `${toCamelCase(tableName)}Collection` + const schemaName = `${toCamelCase(tableName)}Schema` + const properties = definition.properties || {} + const primaryKeys = findPrimaryKeys(properties) + + // Generate getKey function + let getKeyFn: string + if (primaryKeys.length === 1) { + getKeyFn = `(item) => item.${primaryKeys[0]}` + } else if (primaryKeys.length > 1) { + const keyParts = primaryKeys.map((k) => `\${item.${k}}`).join('-') + getKeyFn = `(item) => \`${keyParts}\`` + } else { + // Fallback to JSON stringify if no keys found + getKeyFn = '(item) => JSON.stringify(item)' + } + + // Generate where function + let whereFn: string + if (primaryKeys.length === 1) { + whereFn = `(query, item) => query.eq('${primaryKeys[0]}', item.${primaryKeys[0]})` + } else if (primaryKeys.length > 1) { + const whereParts = primaryKeys.map((k) => `.eq('${k}', item.${k})`).join('') + whereFn = `(query, item) => query${whereParts}` + } else { + whereFn = '(query, item) => query' + } + + lines.push(`export const ${collectionName} = createSupabaseCollection({`) + lines.push(` tableName: '${tableName}',`) + lines.push(` getKey: ${getKeyFn},`) + lines.push(` where: ${whereFn},`) + lines.push(` schema: ${schemaName},`) + lines.push(' queryClient: getQueryClient(),') + lines.push(' supabase: supabase as any,') + lines.push('})') + lines.push('') + } + + return lines.join('\n') +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/index.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/index.ts new file mode 100644 index 00000000000..cc634455b5f --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/index.ts @@ -0,0 +1,5 @@ +export { generateSchemasContent } from './schemas' +export { generateDbContent } from './db' +export { generatePageContent } from './page' +export { generateSheetContent } from './sheet' +export { generateListContent } from './list' diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/list.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/list.ts new file mode 100644 index 00000000000..cffeeb72284 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/list.ts @@ -0,0 +1,187 @@ +import { OpenAPIDefinition } from '../types' +import { findPrimaryKeys, toCamelCase, toPascalCase, toSingular } from '../utils' + +// Generate list component content +export function generateListContent(tableName: string, definition: OpenAPIDefinition): string { + const pascalName = toPascalCase(tableName) + const singularName = toSingular(tableName) + const singularPascal = toPascalCase(singularName) + const camelName = toCamelCase(tableName) + const collectionName = `${camelName}Collection` + const listComponentName = `${pascalName}List` + const sheetComponentName = `${singularPascal}Sheet` + const properties = definition.properties || {} + const primaryKeys = findPrimaryKeys(properties) + const primaryKey = primaryKeys[0] || 'id' + + // Get display fields (non-system fields, first 2 for display) + const editableFields = Object.entries(properties).filter(([name]) => { + const isSystemField = + name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at') + return !isSystemField + }) + const displayField = editableFields[0]?.[0] || primaryKey + const secondaryField = editableFields[1]?.[0] + + // Get all fields for select + const allFieldNames = Object.keys(properties) + const selectFields = allFieldNames.map((name) => ` ${name}: item.${name},`).join('\n') + + // Determine if we have created_at for display + const hasCreatedAt = properties['created_at'] !== undefined + + // Generate secondary info display + let secondaryInfo = '' + if (secondaryField) { + secondaryInfo = ` + {item.${secondaryField} && ( + {item.${secondaryField}} + )} + ${hasCreatedAt ? `•` : ''}` + } + + return `'use client' + +import { useState } from 'react' +import { useLiveQuery } from '@tanstack/react-db' +import { Pencil, Plus, Trash2, FileText } from 'lucide-react' + +import { ${collectionName} } from '@/lib/db' +import { ${pascalName} } from '@/lib/schemas' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { ${sheetComponentName} } from './${singularName}-sheet' + +export function ${listComponentName}() { + const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false) + const [editingItem, setEditingItem] = useState<${pascalName} | null>(null) + + const { data: items } = useLiveQuery( + (q) => + q + .from({ item: ${collectionName} }) + .orderBy(({ item }) => item.${displayField}, 'asc') + .select(({ item }) => ({ +${selectFields} + })), + [] + ) + + const handleDelete = (itemId: string) => { + ${collectionName}.delete([itemId]) + } + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

${pascalName}

+

+ {items.length} ${singularName.replace(/_/g, ' ')}{items.length !== 1 ? 's' : ''} in database +

+
+
+ +
+
+ + {/* Items list */} +
+ {items.length > 0 ? ( +
+ {items.map((item) => ( +
+
+
+ +
+
+

+ {item.${displayField} || 'Untitled'} +

+
${secondaryInfo}${hasCreatedAt ? ` + + Created {new Date(item.created_at).toLocaleDateString()} + ` : ` + + {item.${primaryKey}.slice(0, 8)}... + `} +
+
+
+
+ + +
+
+ ))} +
+ ) : ( +
+ +

No ${tableName.replace(/_/g, ' ')} yet

+

Create your first ${singularName.replace(/_/g, ' ')} to get started

+ +
+ )} +
+ + {/* Create Sheet */} + <${sheetComponentName} + open={isCreateSheetOpen} + onOpenChange={setIsCreateSheetOpen} + mode="create" + /> + + {/* Edit Sheet */} + <${sheetComponentName} + open={!!editingItem} + onOpenChange={(open) => !open && setEditingItem(null)} + mode="edit" + item={editingItem} + /> +
+ ) +} +` +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/page.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/page.ts new file mode 100644 index 00000000000..9757a62b7c2 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/page.ts @@ -0,0 +1,32 @@ +import { toCamelCase, toPascalCase } from '../utils' + +// Generate page.tsx content +export function generatePageContent(tableName: string): string { + const pascalName = toPascalCase(tableName) + const camelName = toCamelCase(tableName) + const collectionName = `${camelName}Collection` + const listComponentName = `${pascalName}List` + + return `import { HydrationBoundary, dehydrate } from '@tanstack/react-query' + +import { ${collectionName} } from '@/lib/db' +import { getQueryClient } from '@/lib/query-client' + +import { ${listComponentName} } from './${tableName}-list' + +export default async function ${pascalName}Page() { + const queryClient = getQueryClient() + + // Prefetch ${camelName} on the server + await queryClient.prefetchQuery(${collectionName}.listQueryOptions()) + + return ( + +
+ <${listComponentName} /> +
+
+ ) +} +` +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/schemas.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/schemas.ts new file mode 100644 index 00000000000..9bb4216c453 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/schemas.ts @@ -0,0 +1,33 @@ +import { OpenAPIDefinition } from '../types' +import { openApiTypeToZod, toCamelCase, toPascalCase } from '../utils' + +// Generate schemas.ts content from OpenAPI definitions +export function generateSchemasContent(definitions: Record): string { + const lines: string[] = ["import { z } from 'zod'", ''] + + for (const [tableName, definition] of Object.entries(definitions)) { + // Skip internal PostgREST tables + if (tableName.startsWith('_')) continue + + const typeName = toPascalCase(tableName) + const schemaName = `${toCamelCase(tableName)}Schema` + const properties = definition.properties || {} + const required = definition.required || [] + + lines.push(`// ${typeName} schema`) + lines.push(`export const ${schemaName} = z.object({`) + + for (const [propName, prop] of Object.entries(properties)) { + const isRequired = required.includes(propName) + const zodType = openApiTypeToZod(prop, isRequired) + lines.push(` ${propName}: ${zodType},`) + } + + lines.push('})') + lines.push('') + lines.push(`export type ${typeName} = z.infer`) + lines.push('') + } + + return lines.join('\n') +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/generators/sheet.ts b/apps/ui-library/app/api/registry/tanstack-db/generators/sheet.ts new file mode 100644 index 00000000000..6e343144288 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/generators/sheet.ts @@ -0,0 +1,312 @@ +import { OpenAPIDefinition } from '../types' +import { findPrimaryKeys, toCamelCase, toLabel, toPascalCase, toSingular } from '../utils' + +// Generate sheet component content +export function generateSheetContent(tableName: string, definition: OpenAPIDefinition): string { + const pascalName = toPascalCase(tableName) + const singularName = toSingular(tableName) + const singularPascal = toPascalCase(singularName) + const camelName = toCamelCase(tableName) + const collectionName = `${camelName}Collection` + const sheetComponentName = `${singularPascal}Sheet` + const properties = definition.properties || {} + const required = definition.required || [] + const primaryKeys = findPrimaryKeys(properties) + const primaryKey = primaryKeys[0] || 'id' + + // Separate fields into editable and read-only + const allFields = Object.entries(properties) + const editableFields = allFields.filter(([name]) => { + const isSystemField = + name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at') + return !isSystemField + }) + const readOnlyFields = allFields.filter(([name]) => { + return name === primaryKey || name === 'created_at' || name === 'updated_at' || name.endsWith('_at') + }) + + // Generate state declarations + const stateDeclarations = editableFields + .map(([name, prop]) => { + const stateName = toCamelCase(name) + if (prop.type === 'boolean') { + return ` const [${stateName}, set${toPascalCase(name)}] = useState(false)` + } + if (prop.type === 'integer' || prop.type === 'number') { + return ` const [${stateName}, set${toPascalCase(name)}] = useState(0)` + } + return ` const [${stateName}, set${toPascalCase(name)}] = useState('')` + }) + .join('\n') + + // Generate useEffect reset + const useEffectReset = editableFields + .map(([name, prop]) => { + const stateName = toCamelCase(name) + const setterName = `set${toPascalCase(name)}` + if (prop.type === 'boolean') { + return ` ${setterName}(item?.${name} ?? false)` + } + if (prop.type === 'integer' || prop.type === 'number') { + return ` ${setterName}(item?.${name} ?? 0)` + } + return ` ${setterName}(item?.${name} ?? '')` + }) + .join('\n') + + // Generate form fields for editable fields + const editableFormFields = editableFields + .map(([name, prop]) => { + const stateName = toCamelCase(name) + const setterName = `set${toPascalCase(name)}` + const label = toLabel(name) + const isRequired = required.includes(name) + + if (prop.type === 'boolean') { + return ` {/* ${label} Field */} +
+ ${setterName}(e.target.checked)} + className="h-4 w-4 rounded border" + /> + +
` + } + + if (prop.type === 'integer' || prop.type === 'number') { + return ` {/* ${label} Field */} +
+ + ${setterName}(e.target.value ? Number(e.target.value) : '')} + placeholder="Enter ${label.toLowerCase()}" + /> +
` + } + + if (prop.enum && prop.enum.length > 0) { + const options = prop.enum + .map((e) => ` `) + .join('\n') + return ` {/* ${label} Field */} +
+ + +
` + } + + return ` {/* ${label} Field */} +
+ + ${setterName}(e.target.value)} + placeholder="Enter ${label.toLowerCase()}" + /> +
` + }) + .join('\n\n') + + // Generate read-only fields for edit mode + const readOnlyFormFields = readOnlyFields + .map(([name]) => { + const label = toLabel(name) + const isTimestamp = name.endsWith('_at') + const displayValue = isTimestamp ? `new Date(item.${name}).toLocaleString()` : `item.${name}` + + return ` {/* ${label} - Read Only */} +
+ + +
` + }) + .join('\n\n') + + // Generate insert data + const insertData = [ + ` ${primaryKey}: crypto.randomUUID(),`, + ...editableFields.map(([name, prop]) => { + const stateName = toCamelCase(name) + if (prop.type === 'boolean') { + return ` ${name}: ${stateName},` + } + if (prop.type === 'integer' || prop.type === 'number') { + return ` ${name}: ${stateName} || 0,` + } + return ` ${name}: ${stateName},` + }), + // Add timestamps if they exist + ...readOnlyFields + .filter(([name]) => name === 'created_at') + .map(() => ` created_at: new Date().toISOString(),`), + ].join('\n') + + // Generate update data + const updateData = editableFields + .map(([name, prop]) => { + const stateName = toCamelCase(name) + if (prop.type === 'boolean') { + return ` ${name}: ${stateName},` + } + if (prop.type === 'integer' || prop.type === 'number') { + return ` ${name}: ${stateName} || 0,` + } + return ` ${name}: ${stateName},` + }) + .join('\n') + + // Generate reset states + const resetStates = editableFields + .map(([name, prop]) => { + const setterName = `set${toPascalCase(name)}` + if (prop.type === 'boolean') { + return ` ${setterName}(false)` + } + if (prop.type === 'integer' || prop.type === 'number') { + return ` ${setterName}(0)` + } + return ` ${setterName}('')` + }) + .join('\n') + + // Generate validation (check required fields) + const requiredEditableFields = editableFields.filter(([name]) => required.includes(name)) + const validationCheck = + requiredEditableFields.length > 0 + ? requiredEditableFields + .map(([name, prop]) => { + const stateName = toCamelCase(name) + if (prop.type === 'boolean') return null + if (prop.type === 'integer' || prop.type === 'number') { + return `${stateName} === ''` + } + return `!${stateName}.trim()` + }) + .filter(Boolean) + .join(' || ') + : null + + return `'use client' + +import { useState, useEffect } from 'react' + +import { ${collectionName} } from '@/lib/db' +import { ${pascalName} } from '@/lib/schemas' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' + +interface ${sheetComponentName}Props { + open: boolean + onOpenChange: (open: boolean) => void + mode: 'create' | 'edit' + item?: ${pascalName} | null +} + +export function ${sheetComponentName}({ open, onOpenChange, mode, item }: ${sheetComponentName}Props) { +${stateDeclarations} + + const isCreate = mode === 'create' + const title = isCreate ? 'Create New ${singularPascal}' : 'Edit ${singularPascal}' + const description = isCreate + ? 'Add a new ${singularName.replace(/_/g, ' ')} to your database.' + : 'Update the ${singularName.replace(/_/g, ' ')} information.' + const submitLabel = isCreate ? 'Create ${singularPascal}' : 'Save Changes' + + useEffect(() => { + if (open) { +${useEffectReset} + } + }, [open, item]) + + const handleSubmit = () => { +${validationCheck ? ` if (${validationCheck}) return\n` : ''} + if (isCreate) { + ${collectionName}.insert({ +${insertData} + }) + } else if (item) { + ${collectionName}.update([item.${primaryKey}], (prev) => ({ + ...prev, +${updateData} + })) + } + + handleClose() + } + + const handleClose = () => { +${resetStates} + onOpenChange(false) + } + + return ( + + + + {title} + {description} + + +
+ {/* Read-only fields (only in edit mode) */} + {!isCreate && item && ( + <> +${readOnlyFormFields} + + )} + + {/* Editable fields */} +${editableFormFields} +
+ + + + + +
+
+ ) +} +` +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/route.ts b/apps/ui-library/app/api/registry/tanstack-db/route.ts new file mode 100644 index 00000000000..222177def3b --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server' +import path from 'path' +import { RegistryItem } from 'shadcn/schema' +import fs from 'fs/promises' + +import { OpenAPISchema, RegistryFile } from './types' +import { toSingular } from './utils' +import { + generateSchemasContent, + generateDbContent, + generatePageContent, + generateSheetContent, + generateListContent, +} from './generators' + +// Read the base registry JSON +async function readBaseRegistry(): Promise { + 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 } + ) + } + + 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 } + ) + } + + // 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('_')) + const firstTableName = tableNames[0] + 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) + + dynamicFiles.push( + { + path: `app/${firstTableName}/page.tsx`, + content: generatePageContent(firstTableName), + type: 'registry:page', + target: `app/${firstTableName}/page.tsx`, + }, + { + path: `app/${firstTableName}/${singularTableName}-sheet.tsx`, + content: generateSheetContent(firstTableName, firstTableDefinition), + type: 'registry:component', + target: `app/${firstTableName}/${singularTableName}-sheet.tsx`, + }, + { + path: `app/${firstTableName}/${firstTableName}-list.tsx`, + content: generateListContent(firstTableName, firstTableDefinition), + type: 'registry:component', + target: `app/${firstTableName}/${firstTableName}-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_OR_ANON_KEY: anonKey, + }, + } + + 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 } + ) + } +} diff --git a/apps/ui-library/app/api/registry/tanstack-db/types.ts b/apps/ui-library/app/api/registry/tanstack-db/types.ts new file mode 100644 index 00000000000..e6b5ee3baa7 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/types.ts @@ -0,0 +1,23 @@ +import { RegistryItem } from 'shadcn/schema' + +export interface OpenAPISchema { + definitions?: Record +} + +export interface OpenAPIDefinition { + type: string + properties?: Record + required?: string[] +} + +export interface OpenAPIProperty { + type?: string + format?: string + description?: string + default?: unknown + maxLength?: number + enum?: string[] + items?: { type?: string; format?: string } +} + +export type RegistryFile = NonNullable[number] diff --git a/apps/ui-library/app/api/registry/tanstack-db/utils.ts b/apps/ui-library/app/api/registry/tanstack-db/utils.ts new file mode 100644 index 00000000000..246c5aaf396 --- /dev/null +++ b/apps/ui-library/app/api/registry/tanstack-db/utils.ts @@ -0,0 +1,105 @@ +import { OpenAPIProperty } from './types' + +// Convert OpenAPI type to Zod type +export function openApiTypeToZod(prop: OpenAPIProperty, isRequired: boolean): string { + let zodType: string + + switch (prop.type) { + case 'string': + if (prop.format === 'uuid') { + zodType = 'z.string().uuid()' + } else if (prop.format === 'date-time' || prop.format === 'timestamp with time zone') { + zodType = 'z.string()' + } else if (prop.enum && prop.enum.length > 0) { + const enumValues = prop.enum.map((e) => `'${e}'`).join(', ') + zodType = `z.enum([${enumValues}])` + } else { + zodType = 'z.string()' + } + break + case 'integer': + zodType = 'z.number().int()' + break + case 'number': + zodType = 'z.number()' + break + case 'boolean': + zodType = 'z.boolean()' + break + case 'array': + if (prop.items) { + const itemType = openApiTypeToZod(prop.items as OpenAPIProperty, true) + zodType = `z.array(${itemType})` + } else { + zodType = 'z.array(z.unknown())' + } + break + case 'object': + zodType = 'z.record(z.unknown())' + break + default: + zodType = 'z.unknown()' + } + + // Add nullable if not required + if (!isRequired) { + zodType += '.nullable()' + } + + return zodType +} + +// Convert table name to PascalCase for type names +export function toPascalCase(str: string): string { + return str + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join('') +} + +// Convert table name to camelCase for variable names +export function toCamelCase(str: string): string { + const pascal = toPascalCase(str) + return pascal.charAt(0).toLowerCase() + pascal.slice(1) +} + +// Convert field name to human-readable label +export function toLabel(fieldName: string): string { + return fieldName + .split('_') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' ') +} + +// Get singular form of table name (simple heuristic) +export function toSingular(tableName: string): string { + if (tableName.endsWith('ies')) { + return tableName.slice(0, -3) + 'y' + } + if (tableName.endsWith('ses') || tableName.endsWith('xes') || tableName.endsWith('zes')) { + return tableName.slice(0, -2) + } + if (tableName.endsWith('s') && !tableName.endsWith('ss')) { + return tableName.slice(0, -1) + } + return tableName +} + +// Find primary key columns from properties +export function findPrimaryKeys(properties: Record): string[] { + const primaryKeys: string[] = [] + + for (const [name, prop] of Object.entries(properties)) { + // Check if the description mentions primary key + if (prop.description?.toLowerCase().includes('primary key')) { + primaryKeys.push(name) + } + } + + // If no primary keys found, default to 'id' if it exists + if (primaryKeys.length === 0 && properties['id']) { + primaryKeys.push('id') + } + + return primaryKeys +} diff --git a/apps/ui-library/components/mdx-components.tsx b/apps/ui-library/components/mdx-components.tsx index a735f7892cd..22600e89326 100644 --- a/apps/ui-library/components/mdx-components.tsx +++ b/apps/ui-library/components/mdx-components.tsx @@ -17,6 +17,7 @@ import { DualRealtimeChat } from './dual-realtime-chat' import { RegistryBlock } from './registry-block' import { StyleWrapper } from './style-wrapper' import TanStackBeta from './tanstack-beta' +import { TanstackDBGenerator } from './tanstack-db-generator' import type { Style } from '@/registry/styles' const components = { @@ -181,6 +182,7 @@ const components = { BlockItem, BlockPreview, DualRealtimeChat, + TanstackDBGenerator, } interface MdxProps { diff --git a/apps/ui-library/components/tanstack-db-generator.tsx b/apps/ui-library/components/tanstack-db-generator.tsx new file mode 100644 index 00000000000..39de35aba70 --- /dev/null +++ b/apps/ui-library/components/tanstack-db-generator.tsx @@ -0,0 +1,212 @@ +'use client' + +import { Check, Copy, Loader2 } from 'lucide-react' +import { useState } from 'react' +import { + Button, + Input_Shadcn_ as Input, + Label_Shadcn_ as Label, + Tabs_Shadcn_ as Tabs, + TabsContent_Shadcn_ as TabsContent, + TabsList_Shadcn_ as TabsList, + TabsTrigger_Shadcn_ as TabsTrigger, + cn, + copyToClipboard, +} from 'ui' + +import { useLocalStorage } from './use-local-storage' + +type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' + +const LOCAL_STORAGE_KEY = 'package-manager-tanstack-db' + +const getBaseUrl = () => { + if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') { + return 'https://supabase.com/ui' + } else if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'preview') { + return `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` + } else { + return 'http://localhost:3004/ui' + } +} + +export function TanstackDBGenerator() { + const [projectRef, setProjectRef] = useState('') + const [anonKey, setAnonKey] = useState('') + const [isValidating, setIsValidating] = useState(false) + const [validationError, setValidationError] = useState(null) + const [isGenerated, setIsGenerated] = useState(false) + const [hasCopied, setHasCopied] = useState(false) + const [packageManager, setPackageManager] = useLocalStorage(LOCAL_STORAGE_KEY, 'npm') + + const baseUrl = getBaseUrl() + const apiUrl = `${baseUrl}/api/registry/tanstack-db?ref=${encodeURIComponent(projectRef)}&anonKey=${encodeURIComponent(anonKey)}` + + const commands: Record = { + npm: `npx shadcn@latest add "${apiUrl}"`, + pnpm: `pnpm dlx shadcn@latest add "${apiUrl}"`, + yarn: `yarn dlx shadcn@latest add "${apiUrl}"`, + bun: `bunx --bun shadcn@latest add "${apiUrl}"`, + } + + const handleValidate = async () => { + if (!projectRef.trim() || !anonKey.trim()) { + setValidationError('Please enter both Project Ref and Anon Key') + return + } + + setIsValidating(true) + setValidationError(null) + + try { + // Test the connection by fetching the OpenAPI spec + const response = await fetch(`https://${projectRef}.supabase.co/rest/v1/`, { + headers: { + apikey: anonKey, + }, + }) + + if (!response.ok) { + throw new Error(`Invalid credentials or project not found (${response.status})`) + } + + const data = await response.json() + + if (!data.definitions || Object.keys(data.definitions).length === 0) { + setValidationError('No tables found in your database. Create some tables first.') + return + } + + setIsGenerated(true) + } catch (error) { + setValidationError( + error instanceof Error ? error.message : 'Failed to connect to your Supabase project' + ) + } finally { + setIsValidating(false) + } + } + + const handleCopy = (command: string) => { + copyToClipboard(command) + setHasCopied(true) + setTimeout(() => setHasCopied(false), 2000) + } + + const handleReset = () => { + setIsGenerated(false) + setProjectRef('') + setAnonKey('') + setValidationError(null) + } + + return ( +
+ {!isGenerated ? ( + <> +
+
+ + setProjectRef(e.target.value)} + className="font-mono" + /> +

+ Found in your project settings or URL (e.g., abcdefghijklmnop) +

+
+
+ + setAnonKey(e.target.value)} + className="font-mono" + type="password" + /> +

+ Found in your project's API settings +

+
+
+ + {validationError && ( +
+ {validationError} +
+ )} + + + + ) : ( + <> +
+ Successfully connected to your Supabase project. Copy the command below to install the + block. +
+ + +
+
+ + {(Object.keys(commands) as PackageManager[]).map((manager) => ( + + {manager} + + ))} + + + {(Object.keys(commands) as PackageManager[]).map((manager) => ( + +
+
+ $ + {commands[manager]} +
+ +
+
+ ))} +
+
+
+ + + + )} +
+ ) +} diff --git a/apps/ui-library/content/docs/nextjs/tanstack-db.mdx b/apps/ui-library/content/docs/nextjs/tanstack-db.mdx new file mode 100644 index 00000000000..bc78ae9f2f0 --- /dev/null +++ b/apps/ui-library/content/docs/nextjs/tanstack-db.mdx @@ -0,0 +1,120 @@ +--- +title: TanStack DB +description: Type-safe database collections with TanStack Query and Supabase +--- + +This block provides TanStack Query integration with Supabase collections for type-safe database operations. It generates Zod schemas and collection configurations based on your database schema. + +## Installation + +Enter your Supabase project credentials to generate a personalized installation command. The block will include schemas and collections for all tables in your database. + + + + + Your credentials are only used to fetch your database schema. They are not stored or sent to any + server other than your Supabase project. + + +## What's included + +This block installs the following files: + +| File | Description | +| ---------------------------- | ---------------------------------------------------- | +| `lib/schemas.ts` | Zod schemas for all your database tables | +| `lib/db.ts` | Supabase collections for type-safe CRUD operations | +| `lib/query-client.ts` | TanStack Query client configuration with SSR support | +| `app/providers.tsx` | React Query provider component | +| `lib/supabase/client.ts` | Browser Supabase client | +| `lib/supabase/server.ts` | Server-side Supabase client | +| `lib/supabase/middleware.ts` | Middleware for session handling | + +## Getting started + +After installing the block, add the required environment variables to your `.env.local` file: + +```env +NEXT_PUBLIC_SUPABASE_URL=your-project-url +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your-anon-key +``` + +## Usage + +### Wrap your app with the Providers component + +Update your root layout to include the Providers component: + +```tsx +import { Providers } from '@/app/providers' + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} +``` + +### Using collections in your components + +Each table in your database gets a corresponding collection. Here's how to use them: + +```tsx +'use client' + +import { usersCollection } from '@/lib/db' + +export function UserList() { + const { data: users, isLoading } = usersCollection.useList() + + if (isLoading) return
Loading...
+ + return
    {users?.map((user) =>
  • {user.name}
  • )}
+} +``` + +### Available collection methods + +Each collection provides the following methods: + +| Method | Description | +| ------------- | -------------------------- | +| `useList()` | Fetch all items | +| `useGet(key)` | Fetch a single item by key | +| `useCreate()` | Create a new item | +| `useUpdate()` | Update an existing item | +| `useDelete()` | Delete an item | + +### Server-side data fetching + +For server components, you can prefetch data using the query client: + +```tsx +import { HydrationBoundary, dehydrate } from '@tanstack/react-query' + +import { usersCollection } from '@/lib/db' +import { getQueryClient } from '@/lib/query-client' + +export default async function UsersPage() { + const queryClient = getQueryClient() + + // Prefetch users on the server + await queryClient.prefetchQuery(usersCollection.listQueryOptions()) + + return ( + + + + ) +} +``` + +## Further reading + +- [TanStack Query documentation](https://tanstack.com/query/latest) +- [Supabase JavaScript client](https://supabase.com/docs/reference/javascript) +- [Generating TypeScript types](https://supabase.com/docs/guides/api/rest/generating-types) diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json index a757ca0ae49..9103c974d02 100644 --- a/apps/ui-library/public/r/registry.json +++ b/apps/ui-library/public/r/registry.json @@ -1417,6 +1417,49 @@ } ] }, + { + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "tanstack-db-nextjs", + "type": "registry:block", + "title": "TanStack DB for Next.js and Supabase", + "description": "TanStack Query integration with Supabase collections for type-safe database operations", + "registryDependencies": [], + "dependencies": [ + "@tanstack/react-query@latest", + "github:ivasilov/supabase-collection", + "zod@latest", + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "files": [ + { + "path": "registry/default/blocks/tanstack-db/lib/query-client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/blocks/tanstack-db/app/providers.tsx", + "type": "registry:component", + "target": "app/providers.tsx" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/server.ts", + "type": "registry:lib" + } + ], + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`.", + "envVars": { + "NEXT_PUBLIC_SUPABASE_URL": "", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + } + }, { "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "supabase-client-nextjs", diff --git a/apps/ui-library/public/r/tanstack-db-nextjs.json b/apps/ui-library/public/r/tanstack-db-nextjs.json new file mode 100644 index 00000000000..8449186fb3e --- /dev/null +++ b/apps/ui-library/public/r/tanstack-db-nextjs.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "tanstack-db-nextjs", + "type": "registry:block", + "title": "TanStack DB for Next.js and Supabase", + "description": "TanStack Query integration with Supabase collections for type-safe database operations", + "dependencies": [ + "@tanstack/react-query@latest", + "github:ivasilov/supabase-collection", + "zod@latest", + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "registryDependencies": [], + "files": [ + { + "path": "registry/default/blocks/tanstack-db/lib/query-client.ts", + "content": "import { QueryClient, defaultShouldDehydrateQuery, isServer } from '@tanstack/react-query'\nimport { cache } from 'react'\n\nfunction makeQueryClient() {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 60 * 1000,\n },\n dehydrate: {\n // include pending queries in dehydration\n shouldDehydrateQuery: (query) =>\n defaultShouldDehydrateQuery(query) || query.state.status === 'pending',\n shouldRedactErrors: (error) => {\n // We should not catch Next.js server errors\n // as that's how Next.js detects dynamic pages\n // so we cannot redact them.\n // Next.js also automatically redacts errors for us\n // with better digests.\n return false\n },\n },\n },\n })\n}\n\nlet browserQueryClient: QueryClient | undefined = undefined\nconst serverQueryClient = cache(() => makeQueryClient())\n\nexport function getQueryClient() {\n if (isServer) {\n return serverQueryClient()\n } else {\n // Browser: make a new query client if we don't already have one\n // This is very important, so we don't re-make a new client if React\n // suspends during the initial render. This may not be needed if we\n // have a suspense boundary BELOW the creation of the query client\n if (!browserQueryClient) browserQueryClient = makeQueryClient()\n return browserQueryClient\n }\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/blocks/tanstack-db/app/providers.tsx", + "content": "'use client'\n\nimport { QueryClientProvider } from '@tanstack/react-query'\n\nimport { getQueryClient } from '@/registry/default/blocks/tanstack-db/lib/query-client'\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n // NOTE: Avoid useState when initializing the query client if you don't\n // have a suspense boundary between this and the code that may\n // suspend because React will throw away the client on the initial\n // render if it suspends and there is no boundary\n const queryClient = getQueryClient()\n\n return {children}\n}\n", + "type": "registry:component", + "target": "app/providers.tsx" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/client.ts", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!\n )\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/server.ts", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "type": "registry:lib" + } + ], + "envVars": { + "NEXT_PUBLIC_SUPABASE_URL": "", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY": "" + }, + "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY`." +} \ No newline at end of file diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts index 417af327346..d54c7bcd59e 100644 --- a/apps/ui-library/registry/blocks.ts +++ b/apps/ui-library/registry/blocks.ts @@ -15,6 +15,7 @@ import socialAuthNextjs from './default/blocks/social-auth-nextjs/registry-item. import socialAuthReactRouter from './default/blocks/social-auth-react-router/registry-item.json' with { type: 'json' } import socialAuthReact from './default/blocks/social-auth-react/registry-item.json' with { type: 'json' } import socialAuthTanstack from './default/blocks/social-auth-tanstack/registry-item.json' with { type: 'json' } +import tanstackDbNextjs from './default/blocks/tanstack-db/registry-item.json' with { type: 'json' } import { registryItemAppend } from './utils' const combine = (component: RegistryItem) => { @@ -52,4 +53,7 @@ export const blocks = [ ...combine(realtimeChat as RegistryItem), // infinite query hook is intentionally not combined with the clients since it depends on clients having database types. infiniteQueryHook as RegistryItem, + + // tanstack-db is served dynamically via API route, but we register it here for the static build + registryItemAppend(tanstackDbNextjs as RegistryItem, [nextjsClient!]), ] as RegistryItem[] diff --git a/apps/ui-library/registry/default/blocks/tanstack-db/app/providers.tsx b/apps/ui-library/registry/default/blocks/tanstack-db/app/providers.tsx new file mode 100644 index 00000000000..cdbb3f73591 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/tanstack-db/app/providers.tsx @@ -0,0 +1,15 @@ +'use client' + +import { QueryClientProvider } from '@tanstack/react-query' + +import { getQueryClient } from '@/registry/default/blocks/tanstack-db/lib/query-client' + +export function Providers({ children }: { children: React.ReactNode }) { + // NOTE: Avoid useState when initializing the query client if you don't + // have a suspense boundary between this and the code that may + // suspend because React will throw away the client on the initial + // render if it suspends and there is no boundary + const queryClient = getQueryClient() + + return {children} +} diff --git a/apps/ui-library/registry/default/blocks/tanstack-db/lib/query-client.ts b/apps/ui-library/registry/default/blocks/tanstack-db/lib/query-client.ts new file mode 100644 index 00000000000..f6cc68d88c9 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/tanstack-db/lib/query-client.ts @@ -0,0 +1,41 @@ +import { QueryClient, defaultShouldDehydrateQuery, isServer } from '@tanstack/react-query' +import { cache } from 'react' + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + dehydrate: { + // include pending queries in dehydration + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || query.state.status === 'pending', + shouldRedactErrors: (error) => { + // We should not catch Next.js server errors + // as that's how Next.js detects dynamic pages + // so we cannot redact them. + // Next.js also automatically redacts errors for us + // with better digests. + return false + }, + }, + }, + }) +} + +let browserQueryClient: QueryClient | undefined = undefined +const serverQueryClient = cache(() => makeQueryClient()) + +export function getQueryClient() { + if (isServer) { + return serverQueryClient() + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient() + return browserQueryClient + } +} diff --git a/apps/ui-library/registry/default/blocks/tanstack-db/registry-item.json b/apps/ui-library/registry/default/blocks/tanstack-db/registry-item.json new file mode 100644 index 00000000000..b6579994d7e --- /dev/null +++ b/apps/ui-library/registry/default/blocks/tanstack-db/registry-item.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "tanstack-db-nextjs", + "type": "registry:block", + "title": "TanStack DB for Next.js and Supabase", + "description": "TanStack Query integration with Supabase collections for type-safe database operations", + "registryDependencies": [], + "dependencies": [ + "@tanstack/react-query@latest", + "github:ivasilov/supabase-collection", + "zod@latest" + ], + "files": [ + { + "path": "registry/default/blocks/tanstack-db/lib/query-client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/blocks/tanstack-db/app/providers.tsx", + "type": "registry:component", + "target": "app/providers.tsx" + } + ], + "docs": "This block provides TanStack Query integration with Supabase collections. The schemas and collections are generated based on your database schema." +}