- Added a new module for parsing and resolving configuration references, supporting `$SECRET_`, `$KV_`, and `$CONTEXT_` prefixes. - Implemented functions to handle the resolution of these references within workflow contexts, enhancing the flexibility of configuration management. - Updated existing workflows and scripts to utilize the new config reference resolution, improving data handling and context management. - Introduced a smoke test to validate the functionality of the config reference system, ensuring robustness and reliability. This update significantly enhances the ability to manage dynamic configurations within workflows, improving overall usability and maintainability.
176 lines
5.9 KiB
JavaScript
176 lines
5.9 KiB
JavaScript
import fs from "fs";
|
|
import {
|
|
clearScriptCache,
|
|
inspectScriptSource,
|
|
instantiateScriptSource,
|
|
} from "../../script-sandbox.js";
|
|
import * as fsStore from "../../fs-store.js";
|
|
import { createDryRunLogger, safeSerialize } from "./dry-run-logger.js";
|
|
import { normalizeStepResult } from "../../step-result.js";
|
|
import { resolveConfigRefs } from "../../config-refs.js";
|
|
|
|
/**
|
|
* @param {{ referencedScripts: () => Set<string> }} registry
|
|
*/
|
|
export default function scriptsPluginFactory(registry) {
|
|
/**
|
|
* @param {import("fastify").FastifyInstance} fastify
|
|
*/
|
|
return async function scriptsPlugin(fastify) {
|
|
fastify.get("/scripts", async () => {
|
|
const scripts = fsStore.listScriptFiles().map((name) => {
|
|
const content = fsStore.readScript(name);
|
|
const inspected =
|
|
content == null
|
|
? { meta: null, metaError: "script not found" }
|
|
: inspectScriptSource(name, content);
|
|
return { name, hasIcon: fsStore.scriptHasIcon(name), ...inspected };
|
|
});
|
|
return { scripts };
|
|
});
|
|
|
|
fastify.get("/scripts/:name/icon", async (req, reply) => {
|
|
const { name } = /** @type {{ name: string }} */ (req.params);
|
|
try {
|
|
fsStore.assertScriptName(name);
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
const icon = fsStore.resolveScriptIcon(name);
|
|
if (icon == null) return reply.code(404).send({ error: "icon not found" });
|
|
const body = fs.readFileSync(icon.filePath);
|
|
return reply
|
|
.type(icon.contentType)
|
|
.header("Cache-Control", "private, max-age=60")
|
|
.send(body);
|
|
});
|
|
|
|
fastify.get("/scripts/:name", async (req, reply) => {
|
|
const { name } = /** @type {{ name: string }} */ (req.params);
|
|
try {
|
|
fsStore.assertScriptName(name);
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
const content = fsStore.readScript(name);
|
|
if (content == null) return reply.code(404).send({ error: "script not found" });
|
|
return { name, content, hasIcon: fsStore.scriptHasIcon(name), ...inspectScriptSource(name, content) };
|
|
});
|
|
|
|
fastify.put("/scripts/:name", async (req, reply) => {
|
|
const { name } = /** @type {{ name: string }} */ (req.params);
|
|
try {
|
|
fsStore.assertScriptName(name);
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
const body = /** @type {{ content?: string }} */ (req.body ?? {});
|
|
if (typeof body.content !== "string") {
|
|
return reply.code(400).send({ error: "content is required" });
|
|
}
|
|
const existed = fsStore.readScript(name) != null;
|
|
fsStore.writeScript(name, body.content);
|
|
clearScriptCache();
|
|
return reply.code(existed ? 200 : 201).send({
|
|
name,
|
|
...inspectScriptSource(name, body.content),
|
|
});
|
|
});
|
|
|
|
fastify.delete("/scripts/:name", async (req, reply) => {
|
|
const { name } = /** @type {{ name: string }} */ (req.params);
|
|
try {
|
|
fsStore.assertScriptName(name);
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
if (registry.referencedScripts().has(name)) {
|
|
return reply
|
|
.code(409)
|
|
.send({ error: "script is referenced by a workflow" });
|
|
}
|
|
if (!fsStore.deleteScript(name)) {
|
|
return reply.code(404).send({ error: "script not found" });
|
|
}
|
|
clearScriptCache();
|
|
return { ok: true };
|
|
});
|
|
|
|
fastify.post("/scripts/:name/dry-run", async (req, reply) => {
|
|
const { name } = /** @type {{ name: string }} */ (req.params);
|
|
try {
|
|
fsStore.assertScriptName(name);
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
|
|
const body = /** @type {{ content?: string, data?: unknown, context?: unknown, config?: unknown, owner?: string }} */ (
|
|
req.body ?? {}
|
|
);
|
|
if (typeof body.content !== "string") {
|
|
return reply.code(400).send({ error: "content is required" });
|
|
}
|
|
|
|
let owner = "default";
|
|
if (body.owner != null && body.owner !== "") {
|
|
try {
|
|
owner = fsStore.assertOwner(String(body.owner));
|
|
} catch (err) {
|
|
return reply.code(err.statusCode ?? 400).send({ error: err.message });
|
|
}
|
|
}
|
|
|
|
const incomingContext =
|
|
body.context != null && typeof body.context === "object" && !Array.isArray(body.context)
|
|
? body.context
|
|
: {};
|
|
|
|
const { log, logs } = createDryRunLogger();
|
|
const started = Date.now();
|
|
|
|
try {
|
|
const config = await resolveConfigRefs(body.config ?? null, {
|
|
owner,
|
|
workflowKey: "dry-run",
|
|
context: incomingContext,
|
|
});
|
|
const ctx = {
|
|
data: body.data ?? null,
|
|
context: incomingContext,
|
|
config,
|
|
};
|
|
const { fn, meta, metaError } = instantiateScriptSource(name, body.content, {
|
|
log,
|
|
workflowName: "dry-run",
|
|
owner,
|
|
});
|
|
const raw = await fn(ctx);
|
|
const result = normalizeStepResult(raw, incomingContext, name);
|
|
return {
|
|
status: "success",
|
|
output: safeSerialize(result.output),
|
|
context: safeSerialize(result.context),
|
|
skipRemaining: result.skipRemaining,
|
|
error: null,
|
|
logs,
|
|
durationMs: Date.now() - started,
|
|
meta,
|
|
metaError,
|
|
};
|
|
} catch (err) {
|
|
const inspected = inspectScriptSource(name, body.content);
|
|
return {
|
|
status: "failed",
|
|
output: null,
|
|
context: null,
|
|
skipRemaining: false,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
logs,
|
|
durationMs: Date.now() - started,
|
|
...inspected,
|
|
};
|
|
}
|
|
});
|
|
};
|
|
}
|