feat: implement database setup, logging, and workflow management

- Added database configuration and migration setup using Knex.
- Implemented a logging system with Pino, including log persistence and flushing.
- Created a server runner with Fastify, integrating authentication and various API routes.
- Introduced a script sandbox for executing user-defined scripts with restricted access.
- Established initial workflows and scripts for manual and cron triggers.
- Included documentation for API endpoints and workflows.
This commit is contained in:
2026-08-14 14:22:27 +07:00
parent a2f6bcad9c
commit 5574d6f723
18 changed files with 0 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
import { randomUUID } from "node:crypto";
import { db } from "./db.js";
const MAX_JSON_BYTES = 64 * 1024;
/**
* @param {unknown} value
* @returns {string | null}
*/
export function serialize(value) {
if (value === undefined || value === null) return null;
let json;
try {
json = JSON.stringify(value);
} catch {
json = JSON.stringify({ truncated: true, reason: "unserializable" });
}
if (Buffer.byteLength(json, "utf8") <= MAX_JSON_BYTES) return json;
return JSON.stringify({
truncated: true,
preview: json.slice(0, 1024),
});
}
/**
* @param {string | null} value
* @returns {unknown}
*/
function deserialize(value) {
if (value == null) return null;
try {
return JSON.parse(value);
} catch {
return value;
}
}
function nowIso() {
return new Date().toISOString();
}
/**
* @param {{
* owner: string,
* workflow: string,
* workflowName?: string | null,
* trigger: { type: string, detail?: string | null },
* input?: unknown,
* parentRunId?: string | null,
* }} opts
*/
export async function startRun({
owner,
workflow,
workflowName = null,
trigger,
input = null,
parentRunId = null,
}) {
const id = randomUUID();
const started_at = nowIso();
await db("workflow_runs").insert({
id,
owner,
workflow,
workflow_name: workflowName ?? null,
trigger_type: trigger.type,
trigger_detail: trigger.detail ?? null,
status: "running",
started_at,
input: serialize(input),
parent_run_id: parentRunId ?? null,
});
return { id, started_at };
}
/**
* @param {string} id
* @param {"success" | "failed"} status
* @param {unknown} [output]
* @param {Error | unknown} [err]
*/
export async function finishRun(id, status, output = null, err = null) {
const finished_at = nowIso();
const row = await db("workflow_runs").where({ id }).first("started_at");
const duration_ms = row
? Date.parse(finished_at) - Date.parse(row.started_at)
: null;
await db("workflow_runs")
.where({ id })
.update({
status,
finished_at,
duration_ms,
output: serialize(output),
error: err
? err instanceof Error
? err.message
: String(err)
: null,
});
}
/**
* @param {{
* runId: string,
* index: number,
* script: string,
* config?: unknown,
* }} opts
*/
export async function startStep({ runId, index, script, config = null }) {
const id = randomUUID();
const started_at = nowIso();
await db("step_runs").insert({
id,
run_id: runId,
step_index: index,
script,
config: serialize(config),
status: "running",
started_at,
});
return { id, started_at };
}
/**
* @param {string} id
* @param {"success" | "failed"} status
* @param {unknown} [output]
* @param {Error | unknown} [err]
*/
export async function finishStep(id, status, output = null, err = null) {
const finished_at = nowIso();
const row = await db("step_runs").where({ id }).first("started_at");
const duration_ms = row
? Date.parse(finished_at) - Date.parse(row.started_at)
: null;
await db("step_runs")
.where({ id })
.update({
status,
finished_at,
duration_ms,
output: serialize(output),
error: err
? err instanceof Error
? err.message
: String(err)
: null,
});
}
/**
* @param {Array<{
* runId: string,
* stepId?: string | null,
* ts: string,
* level: number,
* msg?: string | null,
* payload?: unknown,
* }>} rows
*/
export async function insertLogs(rows) {
if (!rows.length) return;
await db("logs").insert(
rows.map((r) => ({
run_id: r.runId,
step_id: r.stepId ?? null,
ts: r.ts,
level: r.level,
msg: r.msg ?? null,
payload: serialize(r.payload),
})),
);
}
/**
* @param {{
* owner?: string,
* workflow?: string,
* status?: string,
* limit?: number,
* before?: string,
* }} [filters]
*/
export async function listRuns(filters = {}) {
const limit = Math.min(Math.max(filters.limit ?? 50, 1), 200);
let q = db("workflow_runs").select("*").orderBy("started_at", "desc");
if (filters.owner) q = q.where("owner", filters.owner);
if (filters.workflow) q = q.where("workflow", filters.workflow);
if (filters.status) q = q.where("status", filters.status);
if (filters.before) q = q.where("started_at", "<", filters.before);
const rows = await q.limit(limit);
return rows.map((row) => ({
...row,
input: deserialize(row.input),
output: deserialize(row.output),
}));
}
/**
* @param {string} id
*/
export async function getRun(id) {
const run = await db("workflow_runs").where({ id }).first();
if (!run) return null;
const steps = await db("step_runs")
.where({ run_id: id })
.orderBy("step_index", "asc");
const logs = await db("logs").where({ run_id: id }).orderBy("ts", "asc").orderBy("id", "asc");
return {
...run,
input: deserialize(run.input),
output: deserialize(run.output),
steps: steps.map((s) => ({
...s,
config: deserialize(s.config),
output: deserialize(s.output),
})),
logs: logs.map((l) => ({
...l,
payload: deserialize(l.payload),
})),
};
}
/**
* @param {number} days
* @returns {Promise<number>} number of deleted runs
*/
export async function pruneOlderThan(days) {
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
return db("workflow_runs").where("started_at", "<", cutoff).del();
}