feat(scripts): add fetch-http script for HTTP requests
Introduce a new fetch-http script that facilitates making HTTP requests with configurable methods, headers, and body. The script includes validation for input parameters and logs the request and response details. This addition enhances the server's capability to interact with external APIs and services.
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
||||||
|
|
||||||
|
function ensureDataObject(ctx) {
|
||||||
|
if (ctx.data == null || typeof ctx.data !== "object" || Array.isArray(ctx.data)) {
|
||||||
|
ctx.data = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMethod(ctx) {
|
||||||
|
const raw = ctx.config?.method ?? ctx.data?.method ?? "GET";
|
||||||
|
if (typeof raw !== "string" || raw.length === 0) {
|
||||||
|
throw new Error("fetch-http: method must be a non-empty string");
|
||||||
|
}
|
||||||
|
const method = raw.toUpperCase();
|
||||||
|
if (!HTTP_METHODS.includes(method)) {
|
||||||
|
throw new Error(`fetch-http: unsupported method "${raw}"`);
|
||||||
|
}
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeaders(value, label) {
|
||||||
|
if (value == null) return {};
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
throw new Error(`fetch-http: ${label} must be an object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
|
const headers = {};
|
||||||
|
for (const [key, val] of Object.entries(value)) {
|
||||||
|
if (val == null) continue;
|
||||||
|
if (typeof val === "string") {
|
||||||
|
headers[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof val === "number" || typeof val === "boolean") {
|
||||||
|
headers[key] = String(val);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Secret wrappers are unwrapped by $axios; do not String() them.
|
||||||
|
if (typeof val === "object") {
|
||||||
|
headers[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`fetch-http: header "${key}" must be a string`);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHeaders(ctx) {
|
||||||
|
return {
|
||||||
|
...normalizeHeaders(ctx.config?.headers, "config.headers"),
|
||||||
|
...normalizeHeaders(ctx.data?.headers, "data.headers"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBody(ctx) {
|
||||||
|
if (ctx.config != null && typeof ctx.config === "object" && "body" in ctx.config) {
|
||||||
|
return { present: true, value: ctx.config.body };
|
||||||
|
}
|
||||||
|
if (ctx.data != null && typeof ctx.data === "object" && "body" in ctx.data) {
|
||||||
|
return { present: true, value: ctx.data.body };
|
||||||
|
}
|
||||||
|
return { present: false, value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseSize(data) {
|
||||||
|
if (data == null) return 0;
|
||||||
|
if (typeof data === "string") return data.length;
|
||||||
|
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) return data.length;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data).length;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchHttp(ctx) {
|
||||||
|
const url = ctx.config?.url;
|
||||||
|
if (typeof url !== "string" || url.length === 0) {
|
||||||
|
throw new Error("fetch-http: ctx.config.url is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = resolveMethod(ctx);
|
||||||
|
const headers = resolveHeaders(ctx);
|
||||||
|
const body = resolveBody(ctx);
|
||||||
|
|
||||||
|
ensureDataObject(ctx);
|
||||||
|
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
|
const request = { url, method, headers };
|
||||||
|
if (body.present && method !== "HEAD") {
|
||||||
|
request.data = body.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info({ url, method }, "fetch-http: fetching");
|
||||||
|
const response = await $axios.request(request);
|
||||||
|
log.info(
|
||||||
|
{ status: response.status, length: responseSize(response.data) },
|
||||||
|
"fetch-http: fetch complete",
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.data.httpResponse = response.data;
|
||||||
|
ctx.data.httpStatus = response.status;
|
||||||
|
log.info(
|
||||||
|
{ status: ctx.data.httpStatus, length: responseSize(ctx.data.httpResponse) },
|
||||||
|
"fetch-http: saved httpResponse",
|
||||||
|
);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchHttp.meta = {
|
||||||
|
description: "Fetch a URL and store the response body as-is (JSON stays an object)",
|
||||||
|
config: {
|
||||||
|
url: { type: "string", required: true, description: "Request URL" },
|
||||||
|
method: {
|
||||||
|
type: "string",
|
||||||
|
default: "GET",
|
||||||
|
description: "HTTP method (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS)",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
required: false,
|
||||||
|
description: "Request headers (string values; Secret values are unwrapped)",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: "any",
|
||||||
|
required: false,
|
||||||
|
description: "Request body for POST/PUT/PATCH (object, string, or buffer)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
method: { type: "string", required: false, description: "Fallback when config.method is omitted" },
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
required: false,
|
||||||
|
description: "Merged over config.headers (e.g. Authorization from a secret)",
|
||||||
|
},
|
||||||
|
body: { type: "any", required: false, description: "Fallback when config.body is omitted" },
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
httpResponse: { type: "any", description: "Response body as returned by the server" },
|
||||||
|
httpStatus: { type: "number", description: "HTTP status code" },
|
||||||
|
},
|
||||||
|
example: {
|
||||||
|
data: {},
|
||||||
|
config: {
|
||||||
|
url: "https://httpbin.org/post",
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
body: { hello: "world" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchHttp;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import jsonata from "jsonata";
|
import jsonata from "jsonata";
|
||||||
|
|
||||||
function jsonataFn(ctx) {
|
async function jsonataFn(ctx) {
|
||||||
log.info({ctx}, "jsonata: context")
|
log.info({ctx}, "jsonata: context")
|
||||||
log.info("jsonata: evaluating expression %s", ctx.config.expression);
|
log.info("jsonata: evaluating expression %s", ctx.config.expression);
|
||||||
const expression = jsonata(ctx.config.expression);
|
const expression = jsonata(ctx.config.expression);
|
||||||
const result = expression.evaluate(ctx.data);
|
const result = await expression.evaluate(ctx.data);
|
||||||
log.info({result}, "jsonata: expression result");
|
log.info({result}, "jsonata: expression result");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ sendEmail.meta = {
|
|||||||
type: "string",
|
type: "string",
|
||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
"Well-known provider shortcut (e.g. gmail, outlook365, sendgrid). Alternative to host.",
|
"Nodemailer well-known service ID (e.g. Gmail, Outlook365, SendGrid). Sets host, port, and TLS. See https://nodemailer.com/smtp/well-known-services",
|
||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -430,9 +430,7 @@ sendEmail.meta = {
|
|||||||
text: "This is a plain-text test message.",
|
text: "This is a plain-text test message.",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
host: "smtp.gmail.com",
|
service: "Gmail",
|
||||||
port: 587,
|
|
||||||
secure: false,
|
|
||||||
user: "smtp-login@gmail.com",
|
user: "smtp-login@gmail.com",
|
||||||
from: "notifications@example.com",
|
from: "notifications@example.com",
|
||||||
passwordSecret: "gmail_app_password",
|
passwordSecret: "gmail_app_password",
|
||||||
|
|||||||
Reference in New Issue
Block a user