import { TRPCError } from "@trpc/server"; import { z } from "zod"; import * as boardRepo from "@kan/db/repository/board.repo"; import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as importRepo from "@kan/db/repository/import.repo"; import * as integrationsRepo from "@kan/db/repository/integration.repo"; import * as labelRepo from "@kan/db/repository/label.repo"; import * as listRepo from "@kan/db/repository/list.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import { colours } from "@kan/shared/constants"; import { generateUID } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { assertUserInWorkspace } from "../utils/auth"; import { apiKeys, urls } from "./integration"; export interface TrelloBoard { id: string; name: string; labels: TrelloLabel[]; lists: TrelloList[]; cards: TrelloCard[]; } interface TrelloLabel { id: string; name: string; } interface TrelloList { id: string; name: string; } interface TrelloCard { id: string; name: string; desc: string; idList: string; labels: TrelloLabel[]; } export const importRouter = createTRPCRouter({ trello: createTRPCRouter({ getBoards: protectedProcedure .meta({ openapi: { summary: "Get boards from Trello", method: "GET", path: "/integrations/trello/boards", description: "Retrieves all boards from Trello", tags: ["Integrations"], protect: true, }, }) .input(z.void()) .output(z.array(z.object({ id: z.string(), name: z.string() }))) .query(async ({ ctx }) => { const apiKey = apiKeys.trello; if (!apiKey) throw new TRPCError({ message: "Trello API key not found", code: "INTERNAL_SERVER_ERROR", }); const user = ctx.user; if (!user) throw new TRPCError({ message: "User not authenticated", code: "UNAUTHORIZED", }); const integration = await integrationsRepo.getProviderForUser( ctx.db, user.id, "trello", ); const token = integration?.accessToken; if (!token) throw new TRPCError({ message: "Trello token not found", code: "UNAUTHORIZED", }); const response = await fetch( `${urls.trello}/members/me/boards?key=${apiKey}&token=${token}`, ); const data = (await response.json()) as TrelloBoard[]; return data.map((board) => ({ id: board.id, name: board.name, })); }), importBoards: protectedProcedure .meta({ openapi: { summary: "Import boards from Trello", method: "POST", path: "/imports/trello/boards", description: "Imports boards from Trello", tags: ["Imports"], protect: true, }, }) .input( z.object({ boardIds: z.array(z.string()), workspacePublicId: z.string().min(12), }), ) .output(z.object({ boardsCreated: z.number() })) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; const apiKey = apiKeys.trello; if (!apiKey) throw new TRPCError({ message: "Trello API key not found", code: "INTERNAL_SERVER_ERROR", }); if (!userId) throw new TRPCError({ message: `User not authenticated`, code: "UNAUTHORIZED", }); const integration = await integrationsRepo.getProviderForUser( ctx.db, userId, "trello", ); if (!integration) throw new TRPCError({ message: "Trello token not found", code: "UNAUTHORIZED", }); const workspace = await workspaceRepo.getByPublicId( ctx.db, input.workspacePublicId, ); if (!workspace) throw new TRPCError({ message: `Workspace with public ID ${input.workspacePublicId} not found`, code: "NOT_FOUND", }); await assertUserInWorkspace(ctx.db, userId, workspace.id); const newImport = await importRepo.create(ctx.db, { source: "trello", createdBy: userId, }); const newImportId = newImport?.id; let boardsCreated = 0; for (const boardId of input.boardIds) { const response = await fetch( `${urls.trello}/boards/${boardId}?key=${apiKey}&token=${integration.accessToken}&lists=open&cards=open&labels=all`, ); const data = (await response.json()) as TrelloBoard; const formattedData = { name: data.name, labels: data.labels .map((label) => ({ sourceId: label.id, name: label.name, })) .filter((_label) => !!_label.name), lists: data.lists.map((list) => ({ name: list.name, cards: data.cards .filter((card) => card.idList === list.id) .map((_card) => ({ sourceId: _card.id, name: _card.name, description: _card.desc, labels: _card.labels.map((label) => ({ sourceId: label.id, name: label.name, })), })), })), }; const boardPublicId = generateUID(); const newBoard = await boardRepo.create(ctx.db, { publicId: boardPublicId, name: formattedData.name, slug: boardPublicId, createdBy: userId, importId: newImportId, workspaceId: workspace.id, }); const newBoardId = newBoard?.id; if (!newBoardId) throw new TRPCError({ message: "Failed to create new board", code: "INTERNAL_SERVER_ERROR", }); let createdLabels: { id: number; sourceId: string }[] = []; let createdCards: { id: number; sourceId: string }[] = []; if (formattedData.labels.length) { const labelsInsert = formattedData.labels.map((label, index) => ({ publicId: generateUID(), name: label.name, colourCode: colours[index % colours.length]?.code ?? "#0d9488", createdBy: userId, boardId: newBoardId, importId: newImportId, })); const newLabels = await labelRepo.bulkCreate(ctx.db, labelsInsert); if (newLabels.length) createdLabels = newLabels .map((label, index) => ({ id: label.id, sourceId: formattedData.labels[index]?.sourceId ?? "", })) .filter((label) => !!label.sourceId); } for (const list of formattedData.lists) { const newList = await listRepo.create(ctx.db, { name: list.name, createdBy: userId, boardId: newBoardId, importId: newImportId, }); const newListId = newList.id; if (list.cards.length && newListId) { const cardsInsert = list.cards.map((card, index) => ({ publicId: generateUID(), title: card.name, description: card.description, createdBy: userId, listId: newListId, index, importId: newImportId, })); const newCards = await cardRepo.bulkCreate(ctx.db, cardsInsert); if (!newCards.length) throw new TRPCError({ message: "Failed to create new cards", code: "INTERNAL_SERVER_ERROR", }); createdCards = createdCards.concat( newCards .map((card, index) => ({ id: card.id, sourceId: list.cards[index]?.sourceId ?? "", })) .filter((card) => !!card.sourceId), ); const activities = newCards.map((card) => ({ type: "card.created" as const, cardId: card.id, createdBy: userId, })); if (newCards.length > 0) { await cardActivityRepo.bulkCreate(ctx.db, activities); } if (createdLabels.length && createdCards.length) { const cardLabelRelations: { cardId: number; labelId: number; }[] = []; for (const card of list.cards) { const _card = createdCards.find( (c) => c.sourceId === card.sourceId, ); for (const label of card.labels) { const _label = createdLabels.find( (l) => l.sourceId === label.sourceId, ); if (_card && _label) { cardLabelRelations.push({ cardId: _card.id, labelId: _label.id, }); } } } if (cardLabelRelations.length) { await cardRepo.bulkCreateCardLabelRelationship( ctx.db, cardLabelRelations, ); } } } } boardsCreated++; } if (boardsCreated > 0 && newImportId) { await importRepo.update( ctx.db, { status: "success" }, { importId: newImport.id }, ); } return { boardsCreated }; }), }), });