@stackone/ai
Version:
Tools for agents to perform actions on your SaaS
208 lines (206 loc) • 6.65 kB
JavaScript
import { ParameterLocation } from "../types.js";
import { StackOneAPIError } from "../utils/errors.js";
//#region src/modules/requestBuilder.ts
var ParameterSerializationError = class extends Error {
constructor(message) {
super(message);
this.name = "ParameterSerializationError";
}
};
/**
* Builds and executes HTTP requests
*/
var RequestBuilder = class {
method;
url;
bodyType;
params;
headers;
constructor(config, headers = {}) {
this.method = config.method;
this.url = config.url;
this.bodyType = config.bodyType;
this.params = config.params;
this.headers = { ...headers };
}
/**
* Set headers for the request
*/
setHeaders(headers) {
this.headers = {
...this.headers,
...headers
};
return this;
}
/**
* Get the current headers
*/
getHeaders() {
return { ...this.headers };
}
/**
* Prepare headers for the API request
*/
prepareHeaders() {
return {
"User-Agent": "stackone-ai-node",
...this.headers
};
}
/**
* Prepare URL and parameters for the API request
*/
prepareRequestParams(params) {
let url = this.url;
const bodyParams = {};
const queryParams = {};
for (const [key, value] of Object.entries(params)) {
const paramConfig = this.params.find((p) => p.name === key);
const paramLocation = paramConfig?.location;
switch (paramLocation) {
case ParameterLocation.PATH:
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
break;
case ParameterLocation.QUERY:
queryParams[key] = value;
break;
case ParameterLocation.HEADER:
this.headers[key] = String(value);
break;
case ParameterLocation.BODY:
bodyParams[key] = value;
break;
default:
bodyParams[key] = value;
break;
}
}
return [
url,
bodyParams,
queryParams
];
}
/**
* Build the fetch options for the request
*/
buildFetchOptions(bodyParams) {
const headers = this.prepareHeaders();
const fetchOptions = {
method: this.method,
headers
};
if (Object.keys(bodyParams).length > 0) switch (this.bodyType) {
case "json":
fetchOptions.headers = {
...fetchOptions.headers,
"Content-Type": "application/json"
};
fetchOptions.body = JSON.stringify(bodyParams);
break;
case "form": {
fetchOptions.headers = {
...fetchOptions.headers,
"Content-Type": "application/x-www-form-urlencoded"
};
const formBody = new URLSearchParams();
for (const [key, value] of Object.entries(bodyParams)) formBody.append(key, String(value));
fetchOptions.body = formBody.toString();
break;
}
case "multipart-form": {
const formData = new FormData();
for (const [key, value] of Object.entries(bodyParams)) formData.append(key, String(value));
fetchOptions.body = formData;
break;
}
}
return fetchOptions;
}
/**
* Validates parameter keys to prevent injection attacks
*/
validateParameterKey(key) {
if (!/^[a-zA-Z0-9_.-]+$/.test(key)) throw new ParameterSerializationError(`Invalid parameter key: ${key}`);
}
/**
* Safely serializes values to strings with special type handling
*/
serializeValue(value) {
if (value instanceof Date) return value.toISOString();
if (value instanceof RegExp) return value.toString();
if (typeof value === "function") throw new ParameterSerializationError("Functions cannot be serialized as parameters");
if (value === null || value === void 0) return "";
return String(value);
}
/**
* Serialize an object into deep object query parameters with security protections
* Converts {filter: {updated_after: "2020-01-01", job_id: "123"}}
* to filter[updated_after]=2020-01-01&filter[job_id]=123
*/
serializeDeepObject(obj, prefix, depth = 0, visited = /* @__PURE__ */ new WeakSet(), options = {}) {
const maxDepth = options.maxDepth ?? 10;
const strictValidation = options.strictValidation ?? true;
const params = [];
if (depth > maxDepth) throw new ParameterSerializationError(`Maximum nesting depth (${maxDepth}) exceeded for parameter serialization`);
if (obj === null || obj === void 0) return params;
if (typeof obj === "object" && !Array.isArray(obj)) {
if (visited.has(obj)) throw new ParameterSerializationError("Circular reference detected in parameter object");
visited.add(obj);
try {
for (const [key, value] of Object.entries(obj)) {
if (strictValidation) this.validateParameterKey(key);
const nestedKey = `${prefix}[${key}]`;
if (value !== null && value !== void 0) if (this.shouldUseDeepObjectSerialization(key, value)) params.push(...this.serializeDeepObject(value, nestedKey, depth + 1, visited, options));
else params.push([nestedKey, this.serializeValue(value)]);
}
} finally {
visited.delete(obj);
}
} else params.push([prefix, this.serializeValue(obj)]);
return params;
}
/**
* Check if a parameter should use deep object serialization
* Applies to all plain object parameters (excludes special types and arrays)
*/
shouldUseDeepObjectSerialization(_key, value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && typeof value !== "function";
}
/**
* Builds all query parameters with optimized batching
*/
buildQueryParameters(queryParams) {
const allParams = [];
for (const [key, value] of Object.entries(queryParams)) if (this.shouldUseDeepObjectSerialization(key, value)) allParams.push(...this.serializeDeepObject(value, key));
else allParams.push([key, this.serializeValue(value)]);
return allParams;
}
/**
* Execute the request
*/
async execute(params, options) {
const [url, bodyParams, queryParams] = this.prepareRequestParams(params);
const urlWithQuery = new URL(url);
const serializedParams = this.buildQueryParameters(queryParams);
for (const [paramKey, paramValue] of serializedParams) urlWithQuery.searchParams.append(paramKey, paramValue);
const fetchOptions = this.buildFetchOptions(bodyParams);
if (options?.dryRun) return {
url: urlWithQuery.toString(),
method: this.method,
headers: fetchOptions.headers,
body: fetchOptions.body instanceof FormData ? "[FormData]" : fetchOptions.body,
mappedParams: params
};
const response = await fetch(urlWithQuery.toString(), fetchOptions);
if (!response.ok) {
const responseBody = await response.json().catch(() => null);
throw new StackOneAPIError(`API request failed with status ${response.status} for ${url}`, response.status, responseBody, bodyParams);
}
return await response.json();
}
};
//#endregion
export { RequestBuilder };
//# sourceMappingURL=requestBuilder.js.map