@upstart.gg/sdk
Version:
You can test the CLI without recompiling by running:
256 lines (255 loc) • 8.88 kB
JavaScript
//#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