tableId column added
This commit is contained in:
16
server/migrations/20250207133740-added_table_id.js
Normal file
16
server/migrations/20250207133740-added_table_id.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up (queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('robot', 'airtable_table_id', {
|
||||||
|
type: Sequelize.TEXT
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down (queryInterface, Sequelize) {
|
||||||
|
await queryInterface.removeColumn('robot', 'airtable_table_id', {
|
||||||
|
type: Sequelize.TEXT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ interface RobotAttributes {
|
|||||||
airtable_access_token?: string | null; // New field for Airtable access token
|
airtable_access_token?: string | null; // New field for Airtable access token
|
||||||
airtable_refresh_token?: string | null; // New field for Airtable refresh token
|
airtable_refresh_token?: string | null; // New field for Airtable refresh token
|
||||||
schedule?: ScheduleConfig | null;
|
schedule?: ScheduleConfig | null;
|
||||||
|
airtable_table_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScheduleConfig {
|
interface ScheduleConfig {
|
||||||
@@ -61,6 +62,7 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
|
|||||||
public airtable_table_name!: string | null; // New field for Airtable table name
|
public airtable_table_name!: string | null; // New field for Airtable table name
|
||||||
public airtable_access_token!: string | null; // New field for Airtable access token
|
public airtable_access_token!: string | null; // New field for Airtable access token
|
||||||
public airtable_refresh_token!: string | null; // New field for Airtable refresh token
|
public airtable_refresh_token!: string | null; // New field for Airtable refresh token
|
||||||
|
public airtable_table_id!: string | null;
|
||||||
public schedule!: ScheduleConfig | null;
|
public schedule!: ScheduleConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +113,10 @@ Robot.init(
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
airtable_table_id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
airtable_access_token: {
|
airtable_access_token: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -749,7 +749,7 @@ router.get("/airtable/bases", async (req: AuthenticatedRequest, res) => {
|
|||||||
|
|
||||||
// Update robot with selected base
|
// Update robot with selected base
|
||||||
router.post("/airtable/update", async (req: AuthenticatedRequest, res) => {
|
router.post("/airtable/update", async (req: AuthenticatedRequest, res) => {
|
||||||
const { baseId, robotId , tableName} = req.body;
|
const { baseId, robotId , tableName,tableId} = req.body;
|
||||||
|
|
||||||
if (!baseId || !robotId) {
|
if (!baseId || !robotId) {
|
||||||
return res.status(400).json({ message: "Base ID and Robot ID are required" });
|
return res.status(400).json({ message: "Base ID and Robot ID are required" });
|
||||||
@@ -767,6 +767,7 @@ router.post("/airtable/update", async (req: AuthenticatedRequest, res) => {
|
|||||||
await robot.update({
|
await robot.update({
|
||||||
airtable_base_id: baseId,
|
airtable_base_id: baseId,
|
||||||
airtable_table_name: tableName,
|
airtable_table_name: tableName,
|
||||||
|
airtable_table_id: tableId,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function updateAirtable(robotId: string, runId: string) {
|
|||||||
robotId,
|
robotId,
|
||||||
plainRobot.airtable_base_id,
|
plainRobot.airtable_base_id,
|
||||||
plainRobot.airtable_table_name,
|
plainRobot.airtable_table_name,
|
||||||
|
plainRobot.airtable_table_id || '',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
console.log(`Data written to Airtable for ${robotId}`);
|
console.log(`Data written to Airtable for ${robotId}`);
|
||||||
@@ -58,6 +59,7 @@ export async function writeDataToAirtable(
|
|||||||
robotId: string,
|
robotId: string,
|
||||||
baseId: string,
|
baseId: string,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
|
tableId: string,
|
||||||
data: any[]
|
data: any[]
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
@@ -70,17 +72,20 @@ export async function writeDataToAirtable(
|
|||||||
const airtable = new Airtable({ apiKey: accessToken });
|
const airtable = new Airtable({ apiKey: accessToken });
|
||||||
const base = airtable.base(baseId);
|
const base = airtable.base(baseId);
|
||||||
|
|
||||||
|
// Dynamic field creation logic
|
||||||
const existingFields = await getExistingFields(base, tableName);
|
const existingFields = await getExistingFields(base, tableName);
|
||||||
const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))];
|
const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))];
|
||||||
const missingFields = dataFields.filter(field => !existingFields.includes(field));
|
const missingFields = dataFields.filter(field => !existingFields.includes(field));
|
||||||
|
|
||||||
for (const field of missingFields) {
|
for (const field of missingFields) {
|
||||||
const sampleValue = data.find(row => row[field])?.[field];
|
const sampleRow = data.find(row => field in row);
|
||||||
if (sampleValue) {
|
if (sampleRow) {
|
||||||
await createAirtableField(baseId, tableName, field, sampleValue, accessToken);
|
const sampleValue = sampleRow[field];
|
||||||
|
await createAirtableField(baseId, tableName, field, sampleValue, accessToken, tableId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch processing with retries
|
||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
for (let i = 0; i < data.length; i += batchSize) {
|
for (let i = 0; i < data.length; i += batchSize) {
|
||||||
const batch = data.slice(i, i + batchSize);
|
const batch = data.slice(i, i + batchSize);
|
||||||
@@ -94,6 +99,7 @@ export async function writeDataToAirtable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
async function getExistingFields(base: Airtable.Base, tableName: string): Promise<string[]> {
|
async function getExistingFields(base: Airtable.Base, tableName: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const records = await base(tableName).select({ maxRecords: 1 }).firstPage();
|
const records = await base(tableName).select({ maxRecords: 1 }).firstPage();
|
||||||
@@ -109,48 +115,61 @@ async function createAirtableField(
|
|||||||
fieldName: string,
|
fieldName: string,
|
||||||
sampleValue: any,
|
sampleValue: any,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
tableId: string,
|
||||||
|
|
||||||
retries = MAX_RETRIES
|
retries = MAX_RETRIES
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let fieldType = inferFieldType(sampleValue);
|
const sanitizedFieldName = sanitizeFieldName(fieldName);
|
||||||
|
const fieldType = inferFieldType(sampleValue);
|
||||||
|
|
||||||
// Fallback if field type is unknown
|
|
||||||
if (!fieldType) {
|
|
||||||
fieldType = 'singleLineText';
|
|
||||||
logger.log('warn', `Unknown field type for ${fieldName}, defaulting to singleLineText`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Creating field: ${fieldName}, Type: ${fieldType}`);
|
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableName}/fields`,
|
`https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableId}/fields`,
|
||||||
{ name: fieldName, type: fieldType },
|
{ name: sanitizedFieldName, type: fieldType },
|
||||||
{
|
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
logger.log('info', `Created field: ${sanitizedFieldName} (${fieldType})`);
|
||||||
logger.log('info', `Created field: ${fieldName} (${fieldType})`);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (retries > 0 && error.response?.status === 429) {
|
if (retries > 0 && error.response?.status === 429) {
|
||||||
await delay(BASE_API_DELAY * (MAX_RETRIES - retries + 2));
|
await delay(BASE_API_DELAY * (MAX_RETRIES - retries + 1));
|
||||||
return createAirtableField(baseId, tableName, fieldName, sampleValue, accessToken, retries - 1);
|
return createAirtableField(baseId, tableName, fieldName, sampleValue, accessToken, tableId, retries - 1);
|
||||||
}
|
}
|
||||||
throw new Error(`Field creation failed: ${error.response?.data?.error?.message || 'Unknown error'}`);
|
|
||||||
|
const errorMessage = error.response?.data?.error?.message || error.message;
|
||||||
|
const statusCode = error.response?.status || 'No Status Code';
|
||||||
|
throw new Error(`Field creation failed (${statusCode}): ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeFieldName(fieldName: string): string {
|
||||||
|
return fieldName
|
||||||
|
.trim()
|
||||||
|
.replace(/^[^a-zA-Z]+/, '')
|
||||||
|
.replace(/[^\w\s]/gi, ' ')
|
||||||
|
.substring(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
function inferFieldType(value: any): string {
|
function inferFieldType(value: any): string {
|
||||||
|
if (value === null || value === undefined) return 'singleLineText';
|
||||||
if (typeof value === 'number') return 'number';
|
if (typeof value === 'number') return 'number';
|
||||||
if (typeof value === 'boolean') return 'checkbox';
|
if (typeof value === 'boolean') return 'checkbox';
|
||||||
if (value instanceof Date) return 'dateTime';
|
if (value instanceof Date) return 'dateTime';
|
||||||
if (Array.isArray(value)) return 'multipleSelects';
|
if (Array.isArray(value)) {
|
||||||
|
return value.length > 0 && typeof value[0] === 'object' ? 'multipleRecordLinks' : 'multipleSelects';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' && isValidUrl(value)) return 'url';
|
||||||
return 'singleLineText';
|
return 'singleLineText';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidUrl(str: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(str);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function retryableAirtableWrite(
|
async function retryableAirtableWrite(
|
||||||
base: Airtable.Base,
|
base: Airtable.Base,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@@ -188,7 +207,7 @@ export const processAirtableUpdates = async () => {
|
|||||||
task.retries += 1;
|
task.retries += 1;
|
||||||
if (task.retries >= MAX_RETRIES) {
|
if (task.retries >= MAX_RETRIES) {
|
||||||
task.status = 'failed';
|
task.status = 'failed';
|
||||||
logger.log('error', `Permanent failure for run ${runId}`);
|
logger.log('error', `Permanent failure for run ${runId}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,4 +215,4 @@ export const processAirtableUpdates = async () => {
|
|||||||
if (!hasPendingTasks) break;
|
if (!hasPendingTasks) break;
|
||||||
await delay(5000);
|
await delay(5000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -18,6 +18,7 @@ import Cookies from "js-cookie";
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SignalCellularConnectedNoInternet0BarSharp } from "@mui/icons-material";
|
import { SignalCellularConnectedNoInternet0BarSharp } from "@mui/icons-material";
|
||||||
|
import { table } from "console";
|
||||||
|
|
||||||
interface IntegrationProps {
|
interface IntegrationProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -257,6 +258,7 @@ export const IntegrationSettingsModal = ({
|
|||||||
baseName: settings.airtableBaseName,
|
baseName: settings.airtableBaseName,
|
||||||
robotId: recordingId,
|
robotId: recordingId,
|
||||||
tableName: settings.airtableTableName,
|
tableName: settings.airtableTableName,
|
||||||
|
tableId: settings.airtableTableId,
|
||||||
},
|
},
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user