UNPKG

@upstart.gg/sdk

Version:

You can test the CLI without recompiling by running:

256 lines (255 loc) • 8.88 kB
//#region src/shared/datarecords/external/airtable/handler.ts const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function getClient(token) { if (!token) throw new Error("Missing Airtable API token"); return { async callApi(path, method = "GET", body = null) { const url = `https://api.airtable.com/${path}`; const maxRetries = 5; const retryDelay = 3e4; for (let attempt = 1; attempt <= maxRetries; attempt++) try { const res = await fetch(url, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : void 0 }); if (res.status === 429 && attempt < maxRetries) { console.warn(`Airtable rate limit hit (429) on attempt ${attempt}/${maxRetries}. Waiting ${retryDelay / 1e3}s before retry...`); await sleep(retryDelay); continue; } const data = await res.json(); return { status: res.status, success: res.ok, data }; } catch (error) { if (attempt < maxRetries) { console.warn(`Network error on attempt ${attempt}/${maxRetries}. Waiting ${retryDelay / 1e3}s before retry...`, error); await sleep(retryDelay); continue; } console.error(`Error on attempt ${attempt}/${maxRetries}:`, error); throw error; } throw new Error(`Airtable API rate limit exceeded after ${maxRetries} attempts`); } }; } /** * Convert a FormData value based on Airtable field type */ function convertValueForAirtableField(value, fieldType) { switch (fieldType) { case "checkbox": return value === "true" || value === "1" || value.toLowerCase() === "on"; case "number": { const numValue = Number(value); return Number.isNaN(numValue) ? value : numValue; } case "date": case "dateTime": return value; case "email": case "url": case "singleLineText": case "multilineText": return value; case "singleSelect": return value; case "multipleSelects": if (value.startsWith("[") && value.endsWith("]")) try { return JSON.parse(value); } catch { return [value]; } if (value.includes(",")) return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0); return [value]; default: console.warn(`Unknown Airtable field type: ${fieldType}, treating as text`); return value; } } /** * Fallback conversion based on value patterns (when field type is unknown) */ function convertValueByPattern(value) { if (value === "true" || value === "false") return value === "true"; if (/^\d+\.?\d*$/.test(value)) { const numValue = Number(value); if (!Number.isNaN(numValue)) return numValue; } if (/^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) return value; if (value.includes(",")) { const values = value.split(",").map((v) => v.trim()).filter((v) => v.length > 0); if (values.length > 1) return values; } return value; } async function saveRecord({ formData, options, properties, accessToken }) { try { const client = getClient(accessToken); const records = {}; for (const [key, value] of formData.entries()) { if (value === null || value === void 0 || value === "") continue; if (value instanceof File) { console.warn(`File upload not yet supported for field: ${key}`); continue; } const fieldDef = options.fields?.find((field) => field.name === key); if (fieldDef) records[key] = convertValueForAirtableField(value, fieldDef.type); else records[key] = convertValueByPattern(value); } const response = await client.callApi(`v0/${options.baseId}/${options.tableId}`, "POST", { records: [{ fields: records }] }); if (!response.success) throw new Error(`Failed to push data to Airtable: ${response.status} - ${JSON.stringify(response.data)}`); return response.data.records[0] ? { id: response.data.records[0].id } : null; } catch (error) { console.error("Error pushing data to Airtable:", error); } return null; } /** * Build Airtable table creation data from schema */ function buildAirtableTableData(properties) { return Object.entries(properties).sort(([, fieldA], [, fieldB]) => { const orderA = fieldA.metadata?.order; const orderB = fieldB.metadata?.order; if (orderA !== void 0 && orderB !== void 0) return orderA - orderB; if (orderA !== void 0 && orderB === void 0) return -1; if (orderA === void 0 && orderB !== void 0) return 1; return 0; }).map(([fieldName, field]) => { if (field.type === "string") { if (field.format === "email") return { name: fieldName, type: "email" }; if (field.format === "uri") return { name: fieldName, type: "url" }; if (field.format === "date") return { name: fieldName, type: "date", options: { dateFormat: { name: "local" } } }; if (field.format === "date-time") return { name: fieldName, type: "dateTime", options: { dateFormat: { name: "local" }, timeFormat: { name: "24hour" }, timeZone: "client" } }; if (field.metadata?.["ui:multiline"]) return { name: fieldName, type: "multilineText" }; if (field.enum) { if (field.metadata?.["ui:widget"] === "checkbox") return { name: fieldName, type: "multipleSelects", options: { choices: field.enum.map((value) => ({ name: value })) } }; return { name: fieldName, type: "singleSelect", options: { choices: field.enum.map((value) => ({ name: value })) } }; } return { name: fieldName, type: "singleLineText" }; } if (field.type === "boolean") return { name: fieldName, type: "checkbox", options: { icon: "check", color: "grayBright" } }; if (field.type === "number") return { name: fieldName, type: "number", options: { precision: 8 } }; }); } async function createTable({ name, schema, baseId, accessToken }) { const fields = buildAirtableTableData(schema.properties); const tableCreationData = { name, description: `Table created by Upstart for ${name}`, fields }; try { const response = await getClient(accessToken).callApi(`v0/meta/bases/${baseId}/tables`, "POST", tableCreationData); if (!response.success) { console.error("Error while creating Airtable table with:", JSON.stringify(tableCreationData, null, 2), "from schema: ", JSON.stringify(schema, null, 2)); throw new Error(`Failed to create Airtable table: ${response.status} - ${JSON.stringify(response.data)}`); } return { tableId: response.data.id, fields: response.data.fields, tableName: response.data.name, externalUrl: `https://airtable.com/${baseId}/${response.data.id}` }; } catch (error) { if (error instanceof Error) console.error(error.message); else console.error("Unknown error occurred while creating Airtable table", error); throw error; } } async function updateTable({ baseId, tableId, newName, newProperties, accessToken }) { const client = getClient(accessToken); if (newProperties) try { const fields = buildAirtableTableData(newProperties); for (const field of fields) { const response = await client.callApi(`v0/meta/bases/${baseId}/tables/${tableId}/fields`, "POST", field); if (!response.success) { console.error("Error while adding field to Airtable table with:", JSON.stringify(field, null, 2)); throw new Error(`Failed to update Airtable table: ${response.status} - ${JSON.stringify(response.data)}`); } } } catch (error) { if (error instanceof Error) console.error(error.message); else console.error("Unknown error occurred while updating Airtable table", error); throw error; } try { const tableUpdateData = { name: newName, description: `Table updated by Upstart for ${newName}` }; const response = await client.callApi(`v0/meta/bases/${baseId}/tables/${tableId}`, "PATCH", tableUpdateData); if (!response.success) { console.error("Error while updating Airtable table with:", JSON.stringify(tableUpdateData, null, 2)); throw new Error(`Failed to update Airtable table: ${response.status} - ${JSON.stringify(response.data)}`); } return { tableId: response.data.id, fields: response.data.fields, tableName: response.data.name, externalUrl: `https://airtable.com/${baseId}/${response.data.id}` }; } catch (error) { if (error instanceof Error) console.error(error.message); else console.error("Unknown error occurred while creating Airtable table", error); throw error; } } async function fetchAirtableBases(accessToken) { try { const response = await getClient(accessToken).callApi("v0/meta/bases"); if (response.success) return response.data.bases; else throw new Error(`Failed to fetch bases: ${response.status}`); } catch (error) { console.error("Error fetching Airtable bases:", error); return []; } } //#endregion export { createTable, fetchAirtableBases, saveRecord, updateTable }; //# sourceMappingURL=handler.js.map