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) { const body = JSON.stringify({ query, variables }); console.log("[monday gql] request:", body.slice(0, 500)); const res = await fetch(MONDAY_API, { method: "POST", headers: { "Content-Type": "application/json", Authorization: token, "API-Version": "2024-10", }, body, }); 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) { const err = json.errors[0]; const detail = err.extensions ? ` (${JSON.stringify(err.extensions)})` : ""; console.error("[monday gql] GraphQL error:", JSON.stringify(json.errors)); throw new Error(`Monday.com GraphQL: ${err.message}${detail}`); } if (json.error_message) { console.error("[monday gql] API error:", json.error_message, json.error_code); throw new Error(`Monday.com: ${json.error_message} (${json.error_code || "unknown"})`); } return json.data; } export async function fetchBoardColumns(token: string, boardId: string): Promise { 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 ): Promise { 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 ): Promise { 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 { 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, })), }; } function toCleanString(val: unknown): string | null { if (val === null || val === undefined) return null; if (Array.isArray(val)) { const joined = val.filter(Boolean).join(", "); return joined || null; } if (typeof val === "object") { const s = JSON.stringify(val); return s === "{}" || s === "[]" ? null : s; } const str = String(val).trim(); if (str === "" || str === "null" || str === "undefined" || str === "[object Object]") return null; return str; } function formatForColumnType(colType: string, val: unknown): unknown | null { switch (colType) { case "boolean": { const isChecked = val === true || val === "true"; return { checked: isChecked ? "true" : "false" }; } case "color": { const str = toCleanString(val); return str ? { label: str } : null; } case "dropdown": { const str = toCleanString(val); if (!str) return null; const labels = str.split(",").map((s) => s.trim()).filter(Boolean); return labels.length > 0 ? { labels } : null; } case "date": { const str = toCleanString(val); if (!str) return null; const iso = parseToISO(str); return iso ? { date: iso } : str; } case "email": { const str = toCleanString(val); return str ? { email: str, text: str } : null; } case "phone": { const str = toCleanString(val); return str ? { phone: str, countryShortName: "US" } : null; } case "link": { const str = toCleanString(val); return str ? { url: str, text: str } : null; } default: { return toCleanString(val); } } } function parseToISO(dateStr: string): string | null { const parts = dateStr.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2,4})$/); if (!parts) return null; let [, m, d, y] = parts; if (y.length === 2) y = parseInt(y) > 50 ? `19${y}` : `20${y}`; const month = m.padStart(2, "0"); const day = d.padStart(2, "0"); return `${y}-${month}-${day}`; } export function mapCardToColumnValues( card: Record, columnMap: Record ): Record { const values: Record = {}; const columnTypes = ((columnMap._columnTypes as Record) ?? {}); for (const [cardField, rawColId] of Object.entries(columnMap)) { if (!rawColId || !cardField || cardField.startsWith("_") || typeof rawColId !== "string") continue; const colId = rawColId; const val = card[cardField]; if (val === null || val === undefined) continue; const colType = columnTypes[colId] || "text"; const formatted = formatForColumnType(colType, val); if (formatted !== null) { values[colId] = formatted; } } return values; } export function mapItemToCardFields( columnValues: ItemColumnValue[], columnMap: Record ): Record { const reverseMap: Record = {}; for (const [cardField, colId] of Object.entries(columnMap)) { if (cardField.startsWith("_") || typeof colId !== "string") continue; reverseMap[colId] = cardField; } const cardData: Record = {}; 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 { const escapedUrl = callbackUrl.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); const data = await gql(token, ` mutation { create_webhook(board_id: ${boardId}, url: "${escapedUrl}", event: change_column_value) { id board_id } } `); return String(data.create_webhook.id); } export async function deleteWebhookSubscription( token: string, webhookId: string ): Promise { await gql(token, ` mutation { delete_webhook(id: ${webhookId}) { id } } `); }