UNPKG

donobu

Version:

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

161 lines 6.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestsPersistenceSqlite = void 0; const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException"); const FlowMetadata_1 = require("../../models/FlowMetadata"); const TestMetadata_1 = require("../../models/TestMetadata"); const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata"); class TestsPersistenceSqlite { constructor(db) { this.db = db; } static async create(db) { return new TestsPersistenceSqlite(db); } async createTest(testMetadata) { const stmt = this.db.prepare(` INSERT INTO test_metadata (id, name, metadata, created_at, suite_id, next_run_mode) VALUES (@id, @name, @metadata, @createdAt, @suiteId, @nextRunMode) `); const insert = this.db.transaction((test) => { stmt.run({ id: test.id, name: test.name, metadata: JSON.stringify(test), createdAt: Date.now(), suiteId: test.suiteId ?? null, nextRunMode: test.nextRunMode, }); }); insert.immediate(testMetadata); } async updateTest(testMetadata) { const stmt = this.db.prepare(` UPDATE test_metadata SET name = @name, metadata = @metadata, suite_id = @suiteId, next_run_mode = @nextRunMode WHERE id = @id `); const result = stmt.run({ id: testMetadata.id, name: testMetadata.name, metadata: JSON.stringify(testMetadata), suiteId: testMetadata.suiteId ?? null, nextRunMode: testMetadata.nextRunMode, }); if (result.changes === 0) { throw TestNotFoundException_1.TestNotFoundException.forId(testMetadata.id); } } async getTestById(testId) { const stmt = this.db.prepare('SELECT metadata FROM test_metadata WHERE id = ?'); const row = stmt.get(testId); if (!row) { throw TestNotFoundException_1.TestNotFoundException.forId(testId); } return TestMetadata_1.TestMetadataSchema.parse(JSON.parse(row.metadata)); } async getTests(query) { const validLimit = Math.max(1, Math.min(100, query.limit ?? 100)); const offset = query.pageToken ? parseInt(query.pageToken, 10) || 0 : 0; const whereConditions = []; const params = []; // partialName takes precedence over name if both are provided. if (query.partialName) { whereConditions.push('name LIKE ?'); params.push(`%${query.partialName}%`); } else if (query.name) { whereConditions.push('name = ?'); params.push(query.name); } if (query.suiteId) { whereConditions.push('suite_id = ?'); params.push(query.suiteId); } // LEFT JOIN against a per-test aggregate of flow_metadata so the // `flow_count` and `latest_flow_created_at` columns are queryable for // ORDER BY without a separate round-trip per test. A second LEFT JOIN // uses ROW_NUMBER() OVER (PARTITION BY test_id …) to pick the single // most-recent flow per test, so we can return its full metadata blob // alongside the count without an N+1 fetch. let sql = ` SELECT t.metadata AS metadata, COALESCE(fc.flow_count, 0) AS flow_count, fc.latest_flow_created_at AS latest_flow_created_at, lf.metadata AS latest_flow_metadata FROM test_metadata t LEFT JOIN ( SELECT test_id, COUNT(*) AS flow_count, MAX(created_at) AS latest_flow_created_at FROM flow_metadata WHERE test_id IS NOT NULL GROUP BY test_id ) fc ON fc.test_id = t.id LEFT JOIN ( SELECT test_id, metadata, ROW_NUMBER() OVER ( PARTITION BY test_id ORDER BY created_at DESC, id DESC ) AS rn FROM flow_metadata WHERE test_id IS NOT NULL ) lf ON lf.test_id = t.id AND lf.rn = 1 `; // The WHERE clauses still target test_metadata columns; rewrite them to // the aliased table. const aliasedWhere = whereConditions.map((c) => `t.${c}`); if (aliasedWhere.length > 0) { sql += ' WHERE ' + aliasedWhere.join(' AND '); } // Map the API-level sortBy to the actual SQL column. test_metadata // columns are referenced through the `t.` alias; the join-derived // columns are referenced by the alias defined in the SELECT list. const sortColMap = { created_at: 't.created_at', name: 't.name', suite_id: 't.suite_id', next_run_mode: 't.next_run_mode', flow_count: 'flow_count', latest_flow_created_at: 'latest_flow_created_at', }; const sortCol = sortColMap[query.sortBy ?? 'created_at'] ?? 't.created_at'; const sortDir = query.sortOrder === 'asc' ? 'ASC' : 'DESC'; // Stable secondary sort by created_at so flows with identical sort keys // (e.g. flow_count = 0 across many tests) come back in a deterministic // order across requests. sql += ` ORDER BY ${sortCol} ${sortDir}, t.created_at DESC LIMIT ? OFFSET ?`; params.push(validLimit + 1); params.push(offset); const stmt = this.db.prepare(sql); const rows = stmt.all(...params); const hasMore = rows.length > validLimit; const results = hasMore ? rows.slice(0, validLimit) : rows; const nextPageToken = hasMore ? `${offset + validLimit}` : undefined; return { items: results.map((row) => { const test = TestMetadata_1.TestMetadataSchema.parse(JSON.parse(row.metadata)); const latestFlow = row.latest_flow_metadata ? FlowMetadata_1.FlowMetadataSchema.parse((0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.latest_flow_metadata))) : null; return { ...test, flowCount: row.flow_count, latestFlow, }; }), nextPageToken, }; } async deleteTest(testId) { // Flows are cascade-deleted by the DB FK constraint. const stmt = this.db.prepare('DELETE FROM test_metadata WHERE id = ?'); const { changes } = stmt.run(testId); if (changes === 0) { throw TestNotFoundException_1.TestNotFoundException.forId(testId); } } } exports.TestsPersistenceSqlite = TestsPersistenceSqlite; //# sourceMappingURL=TestsPersistenceSqlite.js.map