- Introduced a new contract for script returns, requiring an object with `output`, `context`, and `skipRemaining` fields. - Refactored existing scripts to align with the new return structure, ensuring compatibility with the updated context management. - Added utility functions for normalizing context and step results, improving the handling of script execution context. - Updated various scripts (e.g., fetch-binary, fetch-html, fetch-http) to return structured output and context, enhancing data flow and usability. - Improved error handling for script returns, ensuring clearer feedback when invalid structures are returned. This update significantly enhances the flexibility and clarity of script interactions within workflows.
123 lines
3.5 KiB
JavaScript
123 lines
3.5 KiB
JavaScript
import jsonata from "jsonata";
|
|
|
|
function passContext(ctx) {
|
|
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
|
|
return { ...ctx.context };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function mergeData(data) {
|
|
if (data != null && typeof data === "object" && !Array.isArray(data)) {
|
|
return { ...data };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function fingerprint(ctx) {
|
|
const key =
|
|
typeof ctx.config?.key === "string" && ctx.config.key.length > 0
|
|
? ctx.config.key
|
|
: "fingerprint";
|
|
const jsonataExpr = ctx.config?.jsonata;
|
|
const skipRemaining = ctx.config?.skipRemaining !== false;
|
|
|
|
let source;
|
|
if (typeof jsonataExpr === "string" && jsonataExpr.length > 0) {
|
|
log.info({ jsonata: jsonataExpr }, "fingerprint: evaluating jsonata");
|
|
const expression = jsonata(jsonataExpr);
|
|
source = await expression.evaluate(ctx);
|
|
} else {
|
|
source = ctx.data;
|
|
}
|
|
|
|
log.info({ key }, "fingerprint: claiming");
|
|
const result = await $fingerprint.claim(key, source, {
|
|
maxAge: ctx.config?.maxAge,
|
|
});
|
|
|
|
const extra = {
|
|
fingerprint: result.hash,
|
|
fingerprintChanged: result.changed,
|
|
fingerprintPrevious: result.previous,
|
|
fingerprintAt: result.changed ? result.at : result.previousAt,
|
|
fingerprintAge: result.ageMs,
|
|
fingerprintExpired: result.expired,
|
|
};
|
|
|
|
log.info(
|
|
{
|
|
key,
|
|
changed: result.changed,
|
|
expired: result.expired,
|
|
ageMs: result.ageMs,
|
|
},
|
|
"fingerprint: result",
|
|
);
|
|
|
|
/** @type {{ output: Record<string, unknown>, context: Record<string, unknown>, skipRemaining?: true }} */
|
|
const envelope = {
|
|
output: { ...mergeData(ctx.data), ...extra },
|
|
context: { ...passContext(ctx), ...extra },
|
|
};
|
|
if (!result.changed && skipRemaining) {
|
|
envelope.skipRemaining = true;
|
|
}
|
|
return envelope;
|
|
}
|
|
|
|
fingerprint.meta = {
|
|
description:
|
|
"Hash a value, compare it to the last stored fingerprint, and skip remaining steps when unchanged",
|
|
previewConfigKey: "key",
|
|
reads: "ctx",
|
|
config: {
|
|
key: {
|
|
type: "string",
|
|
default: "fingerprint",
|
|
description: "KV key for the stored fingerprint record",
|
|
},
|
|
jsonata: {
|
|
type: "string",
|
|
required: false,
|
|
description: "JSONata against ctx; omitted hashes ctx.data",
|
|
},
|
|
skipRemaining: {
|
|
type: "boolean",
|
|
default: true,
|
|
description: "Skip later steps when the fingerprint is unchanged",
|
|
},
|
|
maxAge: {
|
|
type: "string",
|
|
required: false,
|
|
description: "Optional age limit (e.g. 24h, 7d, or milliseconds). Older matching hashes fire again",
|
|
},
|
|
},
|
|
input: {},
|
|
output: {
|
|
fingerprint: { type: "string", description: "SHA-256 hex of the source value" },
|
|
fingerprintChanged: { type: "boolean" },
|
|
fingerprintPrevious: { type: "string", required: false },
|
|
fingerprintAt: { type: "string", required: false, description: "ISO timestamp of the stored record" },
|
|
fingerprintAge: { type: "number", required: false, description: "Age in milliseconds" },
|
|
fingerprintExpired: { type: "boolean" },
|
|
},
|
|
context: {
|
|
fingerprint: { type: "string" },
|
|
fingerprintChanged: { type: "boolean" },
|
|
fingerprintPrevious: { type: "string", required: false },
|
|
fingerprintAt: { type: "string", required: false },
|
|
fingerprintAge: { type: "number", required: false },
|
|
fingerprintExpired: { type: "boolean" },
|
|
},
|
|
example: {
|
|
data: { item: { guid: "https://example.com/post-1" } },
|
|
config: {
|
|
key: "latest-item",
|
|
jsonata: "data.item.guid ? data.item.guid : data.item.link",
|
|
},
|
|
},
|
|
};
|
|
|
|
export default fingerprint;
|