donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
161 lines • 6.81 kB
JavaScript
;
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