Skip to content

Commit 722b97e

Browse files
great progress
1 parent 361b1e6 commit 722b97e

File tree

7 files changed

+322
-24
lines changed

7 files changed

+322
-24
lines changed

config/config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"development": {
3+
"username": "postgres",
4+
"password": "postgres",
5+
"database": "maxun",
6+
"host": "localhost",
7+
"port": 5432,
8+
"dialect": "postgres"
9+
}
10+
}
11+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface, Sequelize) {
6+
// Add new Airtable-related columns to the 'robot' table
7+
await queryInterface.addColumn('robot', 'airtable_base_id', {
8+
type: Sequelize.STRING,
9+
allowNull: true,
10+
});
11+
12+
await queryInterface.addColumn('robot', 'airtable_table_name', {
13+
type: Sequelize.STRING,
14+
allowNull: true,
15+
});
16+
17+
await queryInterface.addColumn('robot', 'airtable_api_key', {
18+
type: Sequelize.STRING,
19+
allowNull: true,
20+
});
21+
22+
await queryInterface.addColumn('robot', 'airtable_access_token', {
23+
type: Sequelize.STRING,
24+
allowNull: true,
25+
});
26+
},
27+
28+
async down(queryInterface, Sequelize) {
29+
// Remove Airtable-related columns from the 'robot' table
30+
await queryInterface.removeColumn('robot', 'airtable_base_id');
31+
await queryInterface.removeColumn('robot', 'airtable_table_name');
32+
await queryInterface.removeColumn('robot', 'airtable_api_key');
33+
await queryInterface.removeColumn('robot', 'airtable_access_token');
34+
},
35+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@types/react": "^18.0.5",
2222
"@types/react-dom": "^18.0.1",
2323
"@types/uuid": "^8.3.4",
24+
"airtable": "^0.12.2",
2425
"axios": "^0.26.0",
2526
"bcrypt": "^5.1.1",
2627
"body-parser": "^1.20.3",

server/src/models/Robot.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ interface RobotAttributes {
2727
google_refresh_token?: string | null;
2828
airtable_base_id?: string | null; // Airtable Base ID
2929
airtable_table_name?: string | null; // Airtable Table Name
30-
airtable_api_key?: string | null; // Airtable API Key
31-
airtable_access_token?: string | null; // Airtable OAuth Access Token
30+
airtable_personal_access_token?: string | null; // Airtable Personal Access Token
31+
airtable_access_token?: string | null; // Airtable OAuth Access Token (if using OAuth)
3232
schedule?: ScheduleConfig | null;
3333
}
3434

@@ -53,14 +53,14 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
5353
public recording_meta!: RobotMeta;
5454
public recording!: RobotWorkflow;
5555
public google_sheet_email!: string | null;
56-
public google_sheet_name?: string | null;
57-
public google_sheet_id?: string | null;
56+
public google_sheet_name!: string | null;
57+
public google_sheet_id!: string | null;
5858
public google_access_token!: string | null;
5959
public google_refresh_token!: string | null;
60-
public airtable_base_id!: string | null;
61-
public airtable_table_name!: string | null;
62-
public airtable_api_key!: string | null;
63-
public airtable_access_token!: string | null;
60+
public airtable_base_id!: string | null; // Airtable Base ID
61+
public airtable_table_name!: string | null; // Airtable Table Name
62+
public airtable_personal_access_token!: string | null; // Airtable Personal Access Token
63+
public airtable_access_token!: string | null; // Airtable OAuth Access Token
6464
public schedule!: ScheduleConfig | null;
6565
}
6666

@@ -111,7 +111,7 @@ Robot.init(
111111
type: DataTypes.STRING,
112112
allowNull: true,
113113
},
114-
airtable_api_key: {
114+
airtable_personal_access_token: {
115115
type: DataTypes.STRING,
116116
allowNull: true,
117117
},
@@ -137,4 +137,4 @@ Robot.init(
137137
// as: 'runs', // Alias for the relation
138138
// });
139139

140-
export default Robot;
140+
export default Robot;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Airtable from 'airtable';
2+
import logger from '../../logger';
3+
import Run from '../../models/Run';
4+
import Robot from '../../models/Robot';
5+
6+
interface AirtableUpdateTask {
7+
robotId: string;
8+
runId: string;
9+
status: 'pending' | 'completed' | 'failed';
10+
retries: number;
11+
}
12+
13+
const MAX_RETRIES = 5;
14+
15+
export let airtableUpdateTasks: { [runId: string]: AirtableUpdateTask } = {};
16+
17+
/**
18+
* Updates Airtable with data from a successful run.
19+
* @param robotId - The ID of the robot.
20+
* @param runId - The ID of the run.
21+
*/
22+
export async function updateAirtable(robotId: string, runId: string) {
23+
try {
24+
const run = await Run.findOne({ where: { runId } });
25+
26+
if (!run) {
27+
throw new Error(`Run not found for runId: ${runId}`);
28+
}
29+
30+
const plainRun = run.toJSON();
31+
32+
if (plainRun.status === 'success') {
33+
let data: { [key: string]: any }[] = [];
34+
if (plainRun.serializableOutput && Object.keys(plainRun.serializableOutput).length > 0) {
35+
data = plainRun.serializableOutput['item-0'] as { [key: string]: any }[];
36+
} else if (plainRun.binaryOutput && plainRun.binaryOutput['item-0']) {
37+
const binaryUrl = plainRun.binaryOutput['item-0'] as string;
38+
data = [{ "Screenshot URL": binaryUrl }];
39+
}
40+
41+
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
42+
43+
if (!robot) {
44+
throw new Error(`Robot not found for robotId: ${robotId}`);
45+
}
46+
47+
const plainRobot = robot.toJSON();
48+
49+
const tableName = plainRobot.airtable_table_name;
50+
const baseId = plainRobot.airtable_base_id;
51+
const personalAccessToken = plainRobot.airtable_personal_access_token;
52+
53+
if (tableName && baseId && personalAccessToken) {
54+
console.log(`Preparing to write data to Airtable for robot: ${robotId}, table: ${tableName}`);
55+
56+
await writeDataToAirtable(baseId, tableName, personalAccessToken, data);
57+
console.log(`Data written to Airtable successfully for Robot: ${robotId} and Run: ${runId}`);
58+
} else {
59+
console.log('Airtable integration not configured.');
60+
}
61+
} else {
62+
console.log('Run status is not success or serializableOutput is missing.');
63+
}
64+
} catch (error: any) {
65+
console.error(`Failed to write data to Airtable for Robot: ${robotId} and Run: ${runId}: ${error.message}`);
66+
}
67+
}
68+
69+
/**
70+
* Writes data to Airtable.
71+
* @param baseId - The ID of the Airtable base.
72+
* @param tableName - The name of the Airtable table.
73+
* @param personalAccessToken - The Airtable Personal Access Token.
74+
* @param data - The data to write to Airtable.
75+
*/
76+
export async function writeDataToAirtable(baseId: string, tableName: string, personalAccessToken: string, data: any[]) {
77+
try {
78+
// Initialize Airtable with Personal Access Token
79+
const base = new Airtable({ apiKey: personalAccessToken }).base(baseId);
80+
81+
const table = base(tableName);
82+
83+
// Prepare records for Airtable
84+
const records = data.map((row) => ({ fields: row }));
85+
86+
// Write data to Airtable
87+
const response = await table.create(records);
88+
89+
if (response) {
90+
console.log('Data successfully appended to Airtable.');
91+
} else {
92+
console.error('Airtable append failed:', response);
93+
}
94+
95+
logger.log(`info`, `Data written to Airtable: ${tableName}`);
96+
} catch (error: any) {
97+
logger.log(`error`, `Error writing data to Airtable: ${error.message}`);
98+
throw error;
99+
}
100+
}
101+
102+
/**
103+
* Processes pending Airtable update tasks.
104+
*/
105+
export const processAirtableUpdates = async () => {
106+
while (true) {
107+
let hasPendingTasks = false;
108+
for (const runId in airtableUpdateTasks) {
109+
const task = airtableUpdateTasks[runId];
110+
console.log(`Processing task for runId: ${runId}, status: ${task.status}`);
111+
112+
if (task.status === 'pending') {
113+
hasPendingTasks = true;
114+
try {
115+
await updateAirtable(task.robotId, task.runId);
116+
console.log(`Successfully updated Airtable for runId: ${runId}`);
117+
delete airtableUpdateTasks[runId];
118+
} catch (error: any) {
119+
console.error(`Failed to update Airtable for run ${task.runId}:`, error);
120+
if (task.retries < MAX_RETRIES) {
121+
airtableUpdateTasks[runId].retries += 1;
122+
console.log(`Retrying task for runId: ${runId}, attempt: ${task.retries}`);
123+
} else {
124+
airtableUpdateTasks[runId].status = 'failed';
125+
console.log(`Max retries reached for runId: ${runId}. Marking task as failed.`);
126+
}
127+
}
128+
}
129+
}
130+
131+
if (!hasPendingTasks) {
132+
console.log('No pending tasks. Exiting loop.');
133+
break;
134+
}
135+
136+
console.log('Waiting for 5 seconds before checking again...');
137+
await new Promise((resolve) => setTimeout(resolve, 5000));
138+
}
139+
};

server/src/workflow-management/scheduler/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createRemoteBrowserForRun, destroyRemoteBrowser } from '../../browser-m
66
import logger from '../../logger';
77
import { browserPool } from "../../server";
88
import { googleSheetUpdateTasks, processGoogleSheetUpdates } from "../integrations/gsheet";
9+
import { airtableUpdateTasks, processAirtableUpdates } from "../integrations/airtableintegration"; // Import Airtable functions
910
import Robot from "../../models/Robot";
1011
import Run from "../../models/Run";
1112
import { getDecryptedProxyConfig } from "../../routes/proxy";
@@ -44,7 +45,7 @@ async function createWorkflowAndStoreMetadata(id: string, userId: string) {
4445
};
4546
}
4647

47-
const browserId = createRemoteBrowserForRun( userId);
48+
const browserId = createRemoteBrowserForRun(userId);
4849
const runId = uuid();
4950

5051
const run = await Run.create({
@@ -177,13 +178,24 @@ async function executeRun(id: string) {
177178
}
178179
);
179180

181+
// Add task for Google Sheets update
180182
googleSheetUpdateTasks[id] = {
181183
robotId: plainRun.robotMetaId,
182184
runId: id,
183185
status: 'pending',
184186
retries: 5,
185187
};
186188
processGoogleSheetUpdates();
189+
190+
// Add task for Airtable update
191+
airtableUpdateTasks[id] = {
192+
robotId: plainRun.robotMetaId,
193+
runId: id,
194+
status: 'pending',
195+
retries: 5,
196+
};
197+
processAirtableUpdates();
198+
187199
return true;
188200
} catch (error: any) {
189201
logger.log('info', `Error while running a robot with id: ${id} - ${error.message}`);

0 commit comments

Comments
 (0)