import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { createTableContract, listTablesQuerySchema } from '@/lib/api/contracts/tables' import { isZodError, parseRequest, validationErrorResponse } from '@/lib/api/server/validation' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { createTable, getWorkspaceTableLimits, listTables, type TableSchema, type TableScope, } from '@/lib/table' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { normalizeColumn } from '@/app/api/table/utils' const logger = createLogger('TableAPI') interface WorkspaceAccessResult { hasAccess: boolean canWrite: boolean } async function checkWorkspaceAccess( workspaceId: string, userId: string ): Promise { const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission === null) { return { hasAccess: false, canWrite: false } } const canWrite = permission === 'admin' || permission === 'write' return { hasAccess: true, canWrite } } /** POST /api/table - Creates a new user-defined table. */ export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } const parsed = await parseRequest( createTableContract, request, {}, { validationErrorResponse: (error) => validationErrorResponse(error), } ) if (!parsed.success) return parsed.response const params = parsed.data.body const { hasAccess, canWrite } = await checkWorkspaceAccess( params.workspaceId, authResult.userId ) if (!hasAccess || !canWrite) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } const planLimits = await getWorkspaceTableLimits(params.workspaceId) const normalizedSchema: TableSchema = { columns: params.schema.columns.map(normalizeColumn), } const table = await createTable( { name: params.name, description: params.description, schema: normalizedSchema, workspaceId: params.workspaceId, userId: authResult.userId, maxTables: planLimits.maxTables, initialRowCount: params.initialRowCount, }, requestId ) captureServerEvent( authResult.userId, 'table_created', { table_id: table.id, workspace_id: params.workspaceId, column_count: params.schema.columns.length, }, { groups: { workspace: params.workspaceId }, setOnce: { first_table_created_at: new Date().toISOString() }, } ) recordAudit({ workspaceId: params.workspaceId, actorId: authResult.userId, actorName: authResult.userName ?? undefined, actorEmail: authResult.userEmail ?? undefined, action: AuditAction.TABLE_CREATED, resourceType: AuditResourceType.TABLE, resourceId: table.id, resourceName: table.name, description: `Created table "${table.name}"`, request, }) return NextResponse.json({ success: true, data: { table: { id: table.id, name: table.name, description: table.description, schema: { columns: (table.schema as TableSchema).columns.map(normalizeColumn), }, rowCount: table.rowCount, maxRows: table.maxRows, createdAt: table.createdAt instanceof Date ? table.createdAt.toISOString() : String(table.createdAt), updatedAt: table.updatedAt instanceof Date ? table.updatedAt.toISOString() : String(table.updatedAt), }, message: 'Table created successfully', }, }) } catch (error) { if (error instanceof Error) { if (error.message.includes('maximum table limit')) { return NextResponse.json({ error: error.message }, { status: 403 }) } if ( error.message.includes('Invalid table name') || error.message.includes('Invalid schema') || error.message.includes('already exists') ) { return NextResponse.json({ error: error.message }, { status: 400 }) } } logger.error(`[${requestId}] Error creating table:`, error) return NextResponse.json({ error: 'Failed to create table' }, { status: 500 }) } }) /** GET /api/table - Lists all tables in a workspace. */ export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } const { searchParams } = new URL(request.url) const workspaceId = searchParams.get('workspaceId') const scope = searchParams.get('scope') const validation = listTablesQuerySchema.safeParse({ workspaceId, scope: scope ?? undefined, }) if (!validation.success) { return NextResponse.json( { error: 'Validation error', details: validation.error.issues }, { status: 400 } ) } const params = validation.data const { hasAccess } = await checkWorkspaceAccess(params.workspaceId, authResult.userId) if (!hasAccess) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } const tables = await listTables(params.workspaceId, { scope: params.scope as TableScope }) logger.info(`[${requestId}] Listed ${tables.length} tables in workspace ${params.workspaceId}`) const responseTables = tables.map((t) => { const schemaData = t.schema as TableSchema return { id: t.id, name: t.name, description: t.description, schema: { columns: schemaData.columns.map(normalizeColumn), }, rowCount: t.rowCount, maxRows: t.maxRows, workspaceId: t.workspaceId, createdBy: t.createdBy, createdAt: t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt), updatedAt: t.updatedAt instanceof Date ? t.updatedAt.toISOString() : String(t.updatedAt), archivedAt: t.archivedAt instanceof Date ? t.archivedAt.toISOString() : t.archivedAt ? String(t.archivedAt) : null, jobStatus: t.jobStatus ?? null, jobId: t.jobId ?? null, jobType: t.jobType ?? null, jobError: t.jobError ?? null, jobRowsProcessed: t.jobRowsProcessed ?? 0, } }) return NextResponse.json({ success: true, data: { tables: responseTables, totalCount: tables.length, }, }) } catch (error) { if (isZodError(error)) { return validationErrorResponse(error) } logger.error(`[${requestId}] Error listing tables:`, error) return NextResponse.json({ error: 'Failed to list tables' }, { status: 500 }) } })