2026-04-07 13:23:29 -04:00
|
|
|
const MONDAY_API = "https://api.monday.com/v2";
|
|
|
|
|
const MONDAY_FILE_API = "https://api.monday.com/v2/file";
|
|
|
|
|
|
|
|
|
|
type MondayColumn = { id: string; title: string; type: string };
|
|
|
|
|
|
|
|
|
|
async function gql(token: string, query: string, variables?: Record<string, unknown>) {
|
|
|
|
|
const res = await fetch(MONDAY_API, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
Authorization: token,
|
2026-04-07 14:07:46 -04:00
|
|
|
"API-Version": "2024-10",
|
2026-04-07 13:23:29 -04:00
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ query, variables }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await res.text().catch(() => "");
|
|
|
|
|
throw new Error(`Monday.com API ${res.status}: ${text}`);
|
|
|
|
|
}
|
|
|
|
|
const json = await res.json();
|
|
|
|
|
if (json.errors?.length) {
|
2026-04-07 14:07:46 -04:00
|
|
|
const err = json.errors[0];
|
|
|
|
|
const detail = err.extensions ? ` (${JSON.stringify(err.extensions)})` : "";
|
|
|
|
|
throw new Error(`Monday.com GraphQL: ${err.message}${detail}`);
|
|
|
|
|
}
|
|
|
|
|
if (json.error_message) {
|
|
|
|
|
throw new Error(`Monday.com: ${json.error_message}`);
|
2026-04-07 13:23:29 -04:00
|
|
|
}
|
|
|
|
|
return json.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchBoardColumns(token: string, boardId: string): Promise<MondayColumn[]> {
|
|
|
|
|
const data = await gql(token, `
|
|
|
|
|
query ($boardId: [ID!]!) {
|
|
|
|
|
boards(ids: $boardId) {
|
|
|
|
|
columns { id title type }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`, { boardId: [boardId] });
|
|
|
|
|
return data?.boards?.[0]?.columns ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createItem(
|
|
|
|
|
token: string,
|
|
|
|
|
boardId: string,
|
|
|
|
|
itemName: string,
|
|
|
|
|
columnValues: Record<string, unknown>
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const data = await gql(token, `
|
|
|
|
|
mutation ($boardId: ID!, $itemName: String!, $columnValues: JSON!) {
|
|
|
|
|
create_item(
|
|
|
|
|
board_id: $boardId,
|
|
|
|
|
item_name: $itemName,
|
|
|
|
|
column_values: $columnValues,
|
|
|
|
|
create_labels_if_missing: true
|
|
|
|
|
) { id }
|
|
|
|
|
}
|
|
|
|
|
`, {
|
|
|
|
|
boardId,
|
|
|
|
|
itemName,
|
|
|
|
|
columnValues: JSON.stringify(columnValues),
|
|
|
|
|
});
|
|
|
|
|
return String(data.create_item.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateItem(
|
|
|
|
|
token: string,
|
|
|
|
|
boardId: string,
|
|
|
|
|
itemId: string,
|
|
|
|
|
columnValues: Record<string, unknown>
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await gql(token, `
|
|
|
|
|
mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) {
|
|
|
|
|
change_multiple_column_values(
|
|
|
|
|
board_id: $boardId,
|
|
|
|
|
item_id: $itemId,
|
|
|
|
|
column_values: $columnValues,
|
|
|
|
|
create_labels_if_missing: true
|
|
|
|
|
) { id }
|
|
|
|
|
}
|
|
|
|
|
`, {
|
|
|
|
|
boardId,
|
|
|
|
|
itemId,
|
|
|
|
|
columnValues: JSON.stringify(columnValues),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadFileToItem(
|
|
|
|
|
token: string,
|
|
|
|
|
itemId: string,
|
|
|
|
|
columnId: string,
|
|
|
|
|
fileBuffer: Buffer,
|
|
|
|
|
fileName: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const query = `mutation ($file: File!) { add_file_to_column(file: $file, item_id: ${itemId}, column_id: "${columnId}") { id } }`;
|
|
|
|
|
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
form.append("query", query);
|
|
|
|
|
form.append("variables[file]", new Blob([new Uint8Array(fileBuffer)]), fileName);
|
|
|
|
|
|
|
|
|
|
const res = await fetch(MONDAY_FILE_API, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { Authorization: token },
|
|
|
|
|
body: form,
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await res.text().catch(() => "");
|
|
|
|
|
throw new Error(`Monday.com file upload ${res.status}: ${text}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ItemColumnValue = { id: string; text: string; value: string | null };
|
|
|
|
|
|
|
|
|
|
export async function readItem(
|
|
|
|
|
token: string,
|
|
|
|
|
itemId: string
|
|
|
|
|
): Promise<{ name: string; columnValues: ItemColumnValue[] } | null> {
|
|
|
|
|
const data = await gql(token, `
|
|
|
|
|
query ($itemId: [ID!]!) {
|
|
|
|
|
items(ids: $itemId) {
|
|
|
|
|
name
|
|
|
|
|
column_values { id text value }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`, { itemId: [itemId] });
|
|
|
|
|
const item = data?.items?.[0];
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
return {
|
|
|
|
|
name: item.name,
|
|
|
|
|
columnValues: item.column_values.map((cv: { id: string; text: string; value: string | null }) => ({
|
|
|
|
|
id: cv.id,
|
|
|
|
|
text: cv.text,
|
|
|
|
|
value: cv.value,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mapCardToColumnValues(
|
|
|
|
|
card: Record<string, unknown>,
|
|
|
|
|
columnMap: Record<string, string>
|
|
|
|
|
): Record<string, unknown> {
|
|
|
|
|
const values: Record<string, unknown> = {};
|
|
|
|
|
|
|
|
|
|
for (const [cardField, colId] of Object.entries(columnMap)) {
|
|
|
|
|
if (!colId || !cardField) continue;
|
|
|
|
|
const val = card[cardField];
|
|
|
|
|
if (val === null || val === undefined) continue;
|
|
|
|
|
values[colId] = String(val);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return values;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mapItemToCardFields(
|
|
|
|
|
columnValues: ItemColumnValue[],
|
|
|
|
|
columnMap: Record<string, string>
|
|
|
|
|
): Record<string, string> {
|
|
|
|
|
const reverseMap: Record<string, string> = {};
|
|
|
|
|
for (const [cardField, colId] of Object.entries(columnMap)) {
|
|
|
|
|
reverseMap[colId] = cardField;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cardData: Record<string, string> = {};
|
|
|
|
|
for (const cv of columnValues) {
|
|
|
|
|
const cardField = reverseMap[cv.id];
|
|
|
|
|
if (cardField && cv.text) {
|
|
|
|
|
cardData[cardField] = cv.text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return cardData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createWebhookSubscription(
|
|
|
|
|
token: string,
|
|
|
|
|
boardId: string,
|
|
|
|
|
callbackUrl: string
|
|
|
|
|
): Promise<string> {
|
2026-04-07 14:07:46 -04:00
|
|
|
const escapedUrl = callbackUrl.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
2026-04-07 13:23:29 -04:00
|
|
|
const data = await gql(token, `
|
2026-04-07 14:07:46 -04:00
|
|
|
mutation {
|
|
|
|
|
create_webhook(board_id: ${boardId}, url: "${escapedUrl}", event: change_column_value) {
|
|
|
|
|
id
|
|
|
|
|
board_id
|
|
|
|
|
}
|
2026-04-07 13:23:29 -04:00
|
|
|
}
|
2026-04-07 14:07:46 -04:00
|
|
|
`);
|
2026-04-07 13:23:29 -04:00
|
|
|
return String(data.create_webhook.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function deleteWebhookSubscription(
|
|
|
|
|
token: string,
|
|
|
|
|
webhookId: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await gql(token, `
|
2026-04-07 14:07:46 -04:00
|
|
|
mutation {
|
|
|
|
|
delete_webhook(id: ${webhookId}) {
|
|
|
|
|
id
|
|
|
|
|
}
|
2026-04-07 13:23:29 -04:00
|
|
|
}
|
2026-04-07 14:07:46 -04:00
|
|
|
`);
|
2026-04-07 13:23:29 -04:00
|
|
|
}
|