342 lines
9.9 KiB
TypeScript
342 lines
9.9 KiB
TypeScript
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 };
|
|
}),
|
|
}),
|
|
});
|