donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
254 lines (253 loc) • 10.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDonobuSqliteDatabase = getDonobuSqliteDatabase;
exports.closeDonobuSqliteDatabase = closeDonobuSqliteDatabase;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const MiscUtils_1 = require("../utils/MiscUtils");
const Logger_1 = require("../utils/Logger");
let instance = null;
/**
* ##################################################################
* # #
* # WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! #
* # #
* ##################################################################
*
* NEVER change existing migrations. This list is APPEND-ONLY! If you do not
* abide by this, YOU WILL BREAK OUR USERS!
*/
const migrations = [
{
// Setup the Donobu agent configs table.
version: 1,
up: (db) => {
db.exec(`
CREATE TABLE IF NOT EXISTS agent_configs (
agent TEXT PRIMARY KEY,
config_name TEXT NULL
);
INSERT OR IGNORE INTO agent_configs (agent, config_name)
VALUES ('flow-runner', NULL);`);
},
},
{
version: 2,
up: (db) => {
// Setup the Donobu flows-related tables.
db.exec(`
CREATE TABLE IF NOT EXISTS flow_metadata (
id TEXT PRIMARY KEY,
name TEXT NULL,
metadata TEXT NOT NULL,
created_at INTEGER NOT NULL,
run_mode TEXT NOT NULL,
state TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tool_calls (
id TEXT PRIMARY KEY,
flow_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
tool_call TEXT NOT NULL,
FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS binary_files (
id TEXT PRIMARY KEY,
flow_id TEXT NOT NULL,
file_id TEXT NOT NULL,
mime_type TEXT NOT NULL,
content BLOB,
FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE,
UNIQUE(flow_id, file_id)
);
-- Indices for better performance
CREATE INDEX IF NOT EXISTS idx_flow_metadata_name ON flow_metadata(name);
CREATE INDEX IF NOT EXISTS idx_flow_metadata_created_at ON flow_metadata(created_at);
CREATE INDEX IF NOT EXISTS idx_flow_metadata_run_mode ON flow_metadata(run_mode);
CREATE INDEX IF NOT EXISTS idx_flow_metadata_state ON flow_metadata(state);
CREATE INDEX IF NOT EXISTS idx_flow_metadata_run_mode_state ON flow_metadata(run_mode, state);
CREATE INDEX IF NOT EXISTS idx_tool_calls_flow_id_started ON tool_calls(flow_id, created_at);
CREATE INDEX IF NOT EXISTS idx_tool_calls_created_at ON tool_calls(created_at);
CREATE INDEX IF NOT EXISTS idx_binary_files_flow_file ON binary_files(flow_id, file_id);
-- Enable foreign key support
PRAGMA foreign_keys = ON;`);
},
},
{
// Setup the GPT configs table.
version: 3,
up: (db) => {
db.exec(`
CREATE TABLE IF NOT EXISTS gpt_configs (
name TEXT PRIMARY KEY,
config TEXT NOT NULL
);`);
},
},
{
// Update the flow JSON metadata schema.
version: 4,
up: (db) => {
// For each row in the flow_metadata table...
// 1. Load the metadata field.
// 2. Parse it as JSON data.
// 3. Update the JSON data such that...
// a. There is a new field called "browser" that is an object.
// b. The "initialBrowserState" field is moved to the "browser" object as "initialState".
// c. The "persistBrowserState" field is moved to the "browser" object as "persistState".
// d. There is a field in all "browser" objects called "using" that is an object.
// e. If there is a non-null "remoteBrowserInstanceUrl" field, then...
// i. The "remoteBrowserInstanceUrl" field is moved to the "using" object as "url".
// ii. The "type" field in the "using" object is set to "remoteInstance".
// f. If the "remoteBrowserInstanceUrl" field is null, then...
// i. The "deviceName" field is moved to the "using" object. If "deviceName" is null,
// then it is defaulted to "Desktop Chromium".
// ii. The "headless" field is moved to the "using" object.
// iii. The "type" field in the "using" object is set to "device".
// g. The original top-level "initialBrowserState" field is removed.
// h. The original top-level "persistBrowserState" field is removed.
// i. The original top-level "remoteBrowserInstanceUrl" field is removed.
// j. The original top-level "deviceName" field is removed.
// k. The original top-level "headless" field is removed.
// 4. Save the JSON data back to the metadata field.
// Get all rows from the flow_metadata table.
const flows = db
.prepare('SELECT id, metadata FROM flow_metadata')
.all();
// Prepare the update statement.
const updateStmt = db.prepare('UPDATE flow_metadata SET metadata = ? WHERE id = ?');
// Process each flow's metadata.
for (const flow of flows) {
try {
// Parse the metadata JSON.
const metadata = JSON.parse(flow.metadata);
// Create the new browser object structure.
metadata.browser = {
initialState: metadata.initialBrowserState || null,
persistState: metadata.persistBrowserState || false,
using: {},
};
// Determine the browser type and populate the 'using' object.
if (metadata.remoteBrowserInstanceUrl) {
// Remote browser instance configuration.
metadata.browser.using = {
type: 'remoteInstance',
url: metadata.remoteBrowserInstanceUrl,
};
}
else {
// Local device configuration.
metadata.browser.using = {
type: 'device',
deviceName: metadata.deviceName || 'Desktop Chromium',
headless: metadata.headless !== undefined ? metadata.headless : false,
};
}
// Remove the old fields.
delete metadata.initialBrowserState;
delete metadata.persistBrowserState;
delete metadata.remoteBrowserInstanceUrl;
delete metadata.deviceName;
delete metadata.headless;
// Update the database with the modified metadata.
updateStmt.run(JSON.stringify(metadata), flow.id);
}
catch (error) {
Logger_1.appLogger.error(`Error updating metadata for flow ${flow.id}:`, error);
// Continue processing other flows even if one fails.
}
}
},
},
];
/**
* Create the SQL schema migrations table that can be used to manage table
* schemas across Donobu releases.
*/
function initMigrationsTable(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
`);
}
function applyMigrations(db) {
// First, validate that no duplicate version numbers exist in the migrations
// array.
const versionCounts = new Map();
const duplicateVersions = [];
// Count occurrences of each version.
for (const migration of migrations) {
const count = (versionCounts.get(migration.version) || 0) + 1;
versionCounts.set(migration.version, count);
// If we've seen this version more than once, add it to duplicates.
if (count > 1 && !duplicateVersions.includes(migration.version)) {
duplicateVersions.push(migration.version);
}
}
// If we found duplicates, throw an error.
if (duplicateVersions.length > 0) {
throw new Error(`Duplicate migration version(s) detected: ${duplicateVersions.join(', ')}. ` +
'Each migration must have a unique version number.');
}
// Get current version.
const getCurrentVersionStmt = db.prepare('SELECT MAX(version) as current_version FROM schema_migrations');
const result = getCurrentVersionStmt.get();
const currentVersion = result.current_version || 0;
// Apply pending migrations in transaction.
const pendingMigrations = migrations
.filter((m) => m.version > currentVersion)
.sort((a, b) => a.version - b.version);
if (pendingMigrations.length > 0) {
const transaction = db.transaction(() => {
for (const migration of pendingMigrations) {
Logger_1.appLogger.debug(`Applying migration ${migration.version}...`);
migration.up(db);
// Record migration.
const recordMigrationStmt = db.prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)');
recordMigrationStmt.run(migration.version, new Date().toISOString());
}
});
transaction();
}
}
/**
* Returns the singleton SQLite database instance.
* Creates the database on first call and initializes it with proper settings.
*/
function getDonobuSqliteDatabase() {
if (instance) {
return instance;
}
const dbDirectory = MiscUtils_1.MiscUtils.baseWorkingDirectory();
if (!fs_1.default.existsSync(dbDirectory)) {
fs_1.default.mkdirSync(dbDirectory, { recursive: true });
}
const dbFilepath = path_1.default.join(MiscUtils_1.MiscUtils.baseWorkingDirectory(), 'database.sqlite');
// Initialize database.
const donobuSqliteDb = new better_sqlite3_1.default(dbFilepath);
// Set up database configuration.
donobuSqliteDb.pragma('journal_mode = WAL');
donobuSqliteDb.pragma('foreign_keys = ON');
// Set up tables.
initMigrationsTable(donobuSqliteDb);
applyMigrations(donobuSqliteDb);
// Update global database handle.
instance = donobuSqliteDb;
return instance;
}
/**
* Closes the database connection. Should be called during application shutdown.
*/
function closeDonobuSqliteDatabase() {
if (instance) {
instance.close();
instance = null;
}
}
//# sourceMappingURL=DonobuSqliteDb.js.map