UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

215 lines 9.4 kB
"use strict"; 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