donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
215 lines • 9.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowsPersistenceSupabase = void 0;
const FlowNotFoundException_1 = require("../exceptions/FlowNotFoundException");
const JsonUtils_1 = require("../utils/JsonUtils");
/**
* A persistence implementation that uses Supabase for storage via
* its PostgREST endpoints. This implementation assumes that row-Level Security (RLS) is enabled and
* enforced by Supabase.
*/
class FlowsPersistenceSupabase {
constructor(supabaseHttpClient) {
this.supabaseHttpClient = supabaseHttpClient;
}
async saveMetadata(flowMetadata) {
const startedAtEpochMs = flowMetadata.startedAt || new Date().getTime();
const root = {
id: flowMetadata.id,
startedat: startedAtEpochMs,
runmode: flowMetadata.runMode,
state: flowMetadata.state,
name: flowMetadata.name,
jsonrecord: flowMetadata,
};
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.FLOW_METADATA_TABLE}?on_conflict=id`, 'POST', JSON.stringify(root));
}
async getMetadataByFlowId(flowId) {
const response = await this.supabaseHttpClient.executeGet(`rest/v1/${FlowsPersistenceSupabase.FLOW_METADATA_TABLE}?id=eq.${flowId}`);
if (!response) {
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
}
const records = JSON.parse(response);
if (records.length === 0) {
throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
}
return records[0].jsonrecord;
}
/**
* Get flow metadata by name using the indexed name column
*/
async getMetadataByFlowName(flowName) {
if (!flowName) {
throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName);
}
// Use the indexed name column for direct lookup.
const encodedName = encodeURIComponent(flowName);
const response = await this.supabaseHttpClient.executeGet(`rest/v1/${FlowsPersistenceSupabase.FLOW_METADATA_TABLE}?name=eq.${encodedName}`);
if (!response) {
throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName);
}
const records = JSON.parse(response);
if (records.length === 0) {
throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName);
}
return records[0].jsonrecord;
}
async savePngScreenShot(flowId, bytes) {
const fileId = `${new Date().toISOString()}.screenshot.png`;
await this.setFlowFile(flowId, fileId, bytes);
return fileId;
}
async getPngScreenShot(flowId, screenShotId) {
return this.getFlowFile(flowId, screenShotId);
}
async saveToolCall(flowId, toolCall) {
const root = {
id: toolCall.id,
flowid: flowId,
startedat: toolCall.startedAt,
jsonrecord: toolCall,
};
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.TOOL_CALLS_TABLE}`, 'POST', JSON.stringify(root));
}
async getToolCalls(flowId) {
const response = await this.supabaseHttpClient.executeGet(`rest/v1/${FlowsPersistenceSupabase.TOOL_CALLS_TABLE}?flowid=eq.${flowId}`);
if (!response) {
await this.getMetadataByFlowId(flowId); // Verify flow exists
return [];
}
const records = JSON.parse(response);
const toolCalls = records
.map((record) => record.jsonrecord)
.sort((a, b) => a.startedAt - b.startedAt);
return toolCalls;
}
async getFlows(query) {
const { limit = 20, pageToken, name, runMode, state, startedAfter, startedBefore, } = query || {};
// Validate and sanitize inputs.
const validLimit = Math.min(Math.max(1, limit), 100);
// Create query parameters
const queryParams = new URLSearchParams();
// Set ordering by most recent first.
queryParams.append('order', 'startedat.desc');
// Set pagination limit.
queryParams.append('limit', validLimit.toString());
// Apply offset from page token if available.
if (pageToken) {
queryParams.append('offset', pageToken);
}
// Add filters for each query parameter
if (name) {
queryParams.append('name', `eq.${encodeURIComponent(name)}`);
}
if (runMode) {
queryParams.append('runmode', `eq.${encodeURIComponent(runMode)}`);
}
if (state) {
queryParams.append('state', `eq.${encodeURIComponent(state)}`);
}
if (startedAfter !== undefined && startedBefore !== undefined) {
// Range query with both boundaries.
queryParams.append('startedat', `gte.${startedAfter},lte.${startedBefore}`);
}
else if (startedAfter !== undefined) {
// Only lower boundary.
queryParams.append('startedat', `gte.${startedAfter}`);
}
else if (startedBefore !== undefined) {
// Only upper boundary.
queryParams.append('startedat', `lte.${startedBefore}`);
}
// Build the URL with all parameters
const url = `rest/v1/${FlowsPersistenceSupabase.FLOW_METADATA_TABLE}?${queryParams.toString()}`;
// Include the total count in headers.
const response = await this.supabaseHttpClient.executeGet(url, {
Prefer: 'count=exact',
});
if (!response) {
return { items: [] };
}
// Parse the response.
const records = JSON.parse(response);
const flows = records.map((record) => record.jsonrecord);
// Check if we have more results.
const hasMore = flows.length === validLimit;
// Calculate next page token (offset).
const nextOffset = pageToken
? parseInt(pageToken, 10) + validLimit
: validLimit;
return {
items: flows,
nextPageToken: hasMore ? nextOffset.toString() : undefined,
};
}
async setVideo(flowId, bytes) {
await this.setFlowFile(flowId, 'video.webm', bytes);
}
async getVideoSegment(flowId, startOffset, length) {
const video = await this.getFlowFile(flowId, 'video.webm');
if (!video) {
return null;
}
const totalLength = video.length;
const adjustedLength = Math.min(length, totalLength - startOffset);
if (adjustedLength <= 0) {
return null;
}
const segment = video.subarray(startOffset, startOffset + adjustedLength);
return {
bytes: segment,
totalLength: totalLength,
startOffset: startOffset,
};
}
async getFlowFile(flowId, fileId) {
const response = await this.supabaseHttpClient.executeGet(`rest/v1/${FlowsPersistenceSupabase.FLOW_FILES_TABLE}?flowid=eq.${flowId}&id=eq.${encodeURIComponent(fileId)}&select=data`);
if (!response) {
await this.getMetadataByFlowId(flowId); // Verify flow exists
return null;
}
const records = JSON.parse(response);
if (records.length === 0) {
return null;
}
return Buffer.from(records[0].data, 'base64');
}
async setFlowFile(flowId, fileId, fileBytes) {
const root = {
id: fileId,
flowid: flowId,
data: fileBytes.toString('base64'),
};
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.FLOW_FILES_TABLE}?on_conflict=id`, 'POST', JSON.stringify(root));
}
async setBrowserState(flowId, browserState) {
const serializedBrowserState = Buffer.from(JSON.stringify(browserState), 'utf-8');
await this.setFlowFile(flowId, FlowsPersistenceSupabase.BROWSER_STATE_FILENAME, serializedBrowserState);
}
async getBrowserState(flowId) {
const browserStateRaw = await this.getFlowFile(flowId, FlowsPersistenceSupabase.BROWSER_STATE_FILENAME);
if (browserStateRaw) {
const browserState = JsonUtils_1.JsonUtils.jsonStringToJsonObject(browserStateRaw.toString('utf-8'));
if (browserState) {
return browserState;
}
else {
throw new Error(`Cannot load malformed browser state from flow ${flowId}`);
}
}
else {
return null;
}
}
async deleteFlow(flowId) {
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.TOOL_CALLS_TABLE}?flowid=eq.${flowId}`, 'DELETE');
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.FLOW_FILES_TABLE}?flowid=eq.${flowId}`, 'DELETE');
await this.supabaseHttpClient.executeMethod(`rest/v1/${FlowsPersistenceSupabase.FLOW_METADATA_TABLE}?id=eq.${flowId}`, 'DELETE');
}
}
exports.FlowsPersistenceSupabase = FlowsPersistenceSupabase;
FlowsPersistenceSupabase.FLOW_METADATA_TABLE = 'flowmetadatav1';
FlowsPersistenceSupabase.BROWSER_STATE_FILENAME = 'browserstate.json';
FlowsPersistenceSupabase.TOOL_CALLS_TABLE = 'toolcallsv1';
FlowsPersistenceSupabase.FLOW_FILES_TABLE = 'flowfilesv1';
//# sourceMappingURL=FlowsPersistenceSupabase.js.map