92 lines
3.4 KiB
JavaScript
92 lines
3.4 KiB
JavaScript
import { config } from '../config.js';
|
|
import { db, parseJson } from '../db.js';
|
|
import { camelJobOutput } from './mappers.js';
|
|
import { isPathInside, path } from '../utils/pathUtils.js';
|
|
|
|
// Keep output artifact visibility identical to job visibility so logs and
|
|
// generated files do not leak across users.
|
|
function visibleJobClause() {
|
|
return `(
|
|
j.triggered_by = ?
|
|
OR r.visibility = 'shared'
|
|
OR r.owner_user_id = ?
|
|
OR r.group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?)
|
|
)`;
|
|
}
|
|
|
|
function outputSelect() {
|
|
return `
|
|
SELECT jo.*, j.status AS job_status, j.runplan_id, j.script_id, j.triggered_by,
|
|
r.name AS runplan_name, s.name AS script_name,
|
|
COALESCE(hs.host_total_count, 0) AS host_total_count,
|
|
COALESCE(hs.host_success_count, 0) AS host_success_count,
|
|
COALESCE(hs.host_failed_count, 0) AS host_failed_count,
|
|
COALESCE(hs.host_skipped_count, 0) AS host_skipped_count,
|
|
COALESCE(hs.host_canceled_count, 0) AS host_canceled_count
|
|
FROM job_outputs jo
|
|
JOIN jobs j ON j.id = jo.job_id
|
|
LEFT JOIN runplans r ON r.id = j.runplan_id
|
|
LEFT JOIN scripts s ON s.id = j.script_id
|
|
LEFT JOIN (
|
|
SELECT job_id,
|
|
COUNT(*) AS host_total_count,
|
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS host_success_count,
|
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS host_failed_count,
|
|
SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END) AS host_skipped_count,
|
|
SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) AS host_canceled_count
|
|
FROM job_hosts
|
|
GROUP BY job_id
|
|
) hs ON hs.job_id = j.id
|
|
`;
|
|
}
|
|
|
|
export function listJobOutputs(userId, isAdmin = false) {
|
|
const rows = isAdmin
|
|
? db.prepare(`
|
|
${outputSelect()}
|
|
ORDER BY jo.updated_at DESC
|
|
LIMIT 250
|
|
`).all()
|
|
: db.prepare(`
|
|
${outputSelect()}
|
|
WHERE ${visibleJobClause()}
|
|
ORDER BY jo.updated_at DESC
|
|
LIMIT 250
|
|
`).all(userId, userId, userId);
|
|
return rows.map(camelJobOutput);
|
|
}
|
|
|
|
export function listJobOutputsForJob(jobId, userId, isAdmin = false) {
|
|
const rows = isAdmin
|
|
? db.prepare(`
|
|
${outputSelect()}
|
|
WHERE jo.job_id = ?
|
|
ORDER BY jo.kind
|
|
`).all(jobId)
|
|
: db.prepare(`
|
|
${outputSelect()}
|
|
WHERE jo.job_id = ? AND ${visibleJobClause()}
|
|
ORDER BY jo.kind
|
|
`).all(jobId, userId, userId, userId);
|
|
return rows.map(camelJobOutput);
|
|
}
|
|
|
|
export function getJobOutput(outputId, userId, isAdmin = false) {
|
|
const row = isAdmin
|
|
? db.prepare(`${outputSelect()} WHERE jo.id = ?`).get(outputId)
|
|
: db.prepare(`${outputSelect()} WHERE jo.id = ? AND ${visibleJobClause()}`).get(outputId, userId, userId, userId);
|
|
return row ? camelJobOutput(row) : null;
|
|
}
|
|
|
|
export function getJobOutputFile(outputId, userId, isAdmin = false) {
|
|
const row = isAdmin
|
|
? db.prepare(`${outputSelect()} WHERE jo.id = ?`).get(outputId)
|
|
: db.prepare(`${outputSelect()} WHERE jo.id = ? AND ${visibleJobClause()}`).get(outputId, userId, userId, userId);
|
|
if (!row) return null;
|
|
const filePath = path.resolve(row.storage_path);
|
|
if (!isPathInside(config.fileLockerDir, filePath)) {
|
|
throw new Error('Job output path is outside the configured FileLocker directory');
|
|
}
|
|
return { output: camelJobOutput(row), filePath, metadata: parseJson(row.metadata_json, {}) };
|
|
}
|