import { z } from 'zod' import { TRPCError } from '@trpc/server' import { Prisma } from '@prisma/client' import { router, adminProcedure } from '../trpc' import { testNotionConnection, getNotionDatabaseSchema, queryNotionDatabase, } from '@/lib/notion' export const notionImportRouter = router({ /** * Test connection to Notion API */ testConnection: adminProcedure .input( z.object({ apiKey: z.string().min(1), }) ) .mutation(async ({ input }) => { return testNotionConnection(input.apiKey) }), /** * Get database schema (properties) for mapping */ getDatabaseSchema: adminProcedure .input( z.object({ apiKey: z.string().min(1), databaseId: z.string().min(1), }) ) .query(async ({ input }) => { try { return await getNotionDatabaseSchema(input.apiKey, input.databaseId) } catch (error) { throw new TRPCError({ code: 'BAD_REQUEST', message: error instanceof Error ? error.message : 'Failed to fetch database schema', }) } }), /** * Preview data from Notion database */ previewData: adminProcedure .input( z.object({ apiKey: z.string().min(1), databaseId: z.string().min(1), limit: z.number().int().min(1).max(10).default(5), }) ) .query(async ({ input }) => { try { const records = await queryNotionDatabase( input.apiKey, input.databaseId, input.limit ) return { records, count: records.length } } catch (error) { throw new TRPCError({ code: 'BAD_REQUEST', message: error instanceof Error ? error.message : 'Failed to fetch data from Notion', }) } }), /** * Import projects from Notion database */ importProjects: adminProcedure .input( z.object({ apiKey: z.string().min(1), databaseId: z.string().min(1), roundId: z.string(), // Column mappings: Notion property name -> Project field mappings: z.object({ title: z.string(), // Required teamName: z.string().optional(), description: z.string().optional(), tags: z.string().optional(), // Multi-select property }), // Store unmapped columns in metadataJson includeUnmappedInMetadata: z.boolean().default(true), }) ) .mutation(async ({ ctx, input }) => { // Verify round exists const round = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.roundId }, }) // Fetch all records from Notion const records = await queryNotionDatabase(input.apiKey, input.databaseId) if (records.length === 0) { return { imported: 0, skipped: 0, errors: [] } } const results = { imported: 0, skipped: 0, errors: [] as Array<{ recordId: string; error: string }>, } // Process each record for (const record of records) { try { // Get mapped values const title = getPropertyValue(record.properties, input.mappings.title) if (!title || typeof title !== 'string' || !title.trim()) { results.errors.push({ recordId: record.id, error: 'Missing or invalid title', }) results.skipped++ continue } const teamName = input.mappings.teamName ? getPropertyValue(record.properties, input.mappings.teamName) : null const description = input.mappings.description ? getPropertyValue(record.properties, input.mappings.description) : null let tags: string[] = [] if (input.mappings.tags) { const tagsValue = getPropertyValue(record.properties, input.mappings.tags) if (Array.isArray(tagsValue)) { tags = tagsValue.filter((t): t is string => typeof t === 'string') } else if (typeof tagsValue === 'string') { tags = tagsValue.split(',').map((t) => t.trim()).filter(Boolean) } } // Build metadata from unmapped columns let metadataJson: Record | null = null if (input.includeUnmappedInMetadata) { const mappedKeys = new Set([ input.mappings.title, input.mappings.teamName, input.mappings.description, input.mappings.tags, ].filter(Boolean)) metadataJson = {} for (const [key, value] of Object.entries(record.properties)) { if (!mappedKeys.has(key) && value !== null && value !== undefined) { metadataJson[key] = value } } if (Object.keys(metadataJson).length === 0) { metadataJson = null } } // Create project await ctx.prisma.project.create({ data: { roundId: round.id, status: 'SUBMITTED', title: title.trim(), teamName: typeof teamName === 'string' ? teamName.trim() : null, description: typeof description === 'string' ? description : null, tags, metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined, externalIdsJson: { notionPageId: record.id, notionDatabaseId: input.databaseId, } as Prisma.InputJsonValue, }, }) results.imported++ } catch (error) { results.errors.push({ recordId: record.id, error: error instanceof Error ? error.message : 'Unknown error', }) results.skipped++ } } // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'IMPORT', entityType: 'Project', detailsJson: { source: 'notion', databaseId: input.databaseId, roundId: input.roundId, imported: results.imported, skipped: results.skipped, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return results }), }) /** * Helper to get a property value from a record */ function getPropertyValue( properties: Record, propertyName: string ): unknown { return properties[propertyName] ?? null }