import type { IntegrationProvider, CardData, TestResult, PushResult, ExternalField, JsonValue, } from "../types"; const AIRTABLE_API = "https://api.airtable.com/v0"; interface AirtableConfig { personalAccessToken: string; baseId: string; tableIdOrName: string; } function parseConfig(config: JsonValue): AirtableConfig { const c = config as Record; return { personalAccessToken: (c.personalAccessToken as string) || "", baseId: (c.baseId as string) || "", tableIdOrName: (c.tableIdOrName as string) || "", }; } async function airtableFetch( token: string, path: string, options: RequestInit = {} ) { const res = await fetch(`${AIRTABLE_API}${path}`, { ...options, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(options.headers || {}), }, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`Airtable API ${res.status}: ${text}`); } return res.json(); } export const airtableProvider: IntegrationProvider = { id: "airtable", name: "Airtable", description: "Push response cards as rows in Airtable bases", icon: "airtable", category: "spreadsheet", supportsFieldMapping: true, supportsOAuth: false, configFields: [ { key: "personalAccessToken", label: "Personal Access Token", type: "password", required: true, helpText: "Create at airtable.com/create/tokens", }, { key: "baseId", label: "Base ID", type: "text", required: true, placeholder: "appXXXXXXXXXXXXXX", helpText: "Found in the Airtable API docs for your base", }, { key: "tableIdOrName", label: "Table Name or ID", type: "text", required: true, placeholder: "Response Cards", }, ], async testConnection(config: JsonValue): Promise { try { const { personalAccessToken, baseId, tableIdOrName } = parseConfig(config); if (!personalAccessToken || !baseId || !tableIdOrName) { return { success: false, message: "Token, base ID, and table name are all required", }; } const data = await airtableFetch( personalAccessToken, `/${baseId}/${encodeURIComponent(tableIdOrName)}?maxRecords=1` ); return { success: true, message: `Connected to table (${data.records?.length ?? 0} sample records)`, }; } catch (err) { return { success: false, message: err instanceof Error ? err.message : "Connection failed", }; } }, async getExternalFields(config: JsonValue): Promise { const { personalAccessToken, baseId, tableIdOrName } = parseConfig(config); const data = await airtableFetch( personalAccessToken, `/${baseId}/${encodeURIComponent(tableIdOrName)}?maxRecords=1` ); if (data.records?.[0]?.fields) { return Object.keys(data.records[0].fields).map((key) => ({ id: key, name: key, type: typeof data.records[0].fields[key], })); } return []; }, async pushCard( card: CardData, config: JsonValue, mapping: JsonValue ): Promise { try { const { personalAccessToken, baseId, tableIdOrName } = parseConfig(config); const fieldMap = (mapping as Record) || {}; const fields: Record = {}; for (const [cardField, airtableField] of Object.entries(fieldMap)) { if (!airtableField || cardField.startsWith("_")) continue; const val = card[cardField]; if (val !== null && val !== undefined) { fields[airtableField] = typeof val === "object" ? JSON.stringify(val) : val; } } if (Object.keys(fields).length === 0 && card.name) { fields["Name"] = card.name; if (card.email) fields["Email"] = card.email; if (card.cellPhone) fields["Phone"] = card.cellPhone; } const data = await airtableFetch( personalAccessToken, `/${baseId}/${encodeURIComponent(tableIdOrName)}`, { method: "POST", body: JSON.stringify({ records: [{ fields }] }), } ); const recordId = data.records?.[0]?.id; return { success: true, externalId: recordId, message: `Created record ${recordId}`, }; } catch (err) { return { success: false, message: err instanceof Error ? err.message : "Push failed", }; } }, };