181 lines
8.0 KiB
JavaScript
181 lines
8.0 KiB
JavaScript
import urlJoin from "url-join";
|
|
import { parseTemplate } from "url-template";
|
|
import { fetchWithError, NetworkError, parseResponse, } from "../utils/fetchWithError.js";
|
|
import { stringifyQueryParams } from "../utils/stringifyQueryParams.js";
|
|
// constants
|
|
const SLASH = "/";
|
|
const pick = (value, keys) => Object.fromEntries(Object.entries(value).filter(([key]) => keys.includes(key)));
|
|
const omit = (value, keys) => Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
|
|
export class Agent {
|
|
#client;
|
|
#basePath;
|
|
#getBaseParams;
|
|
#getBaseUrl;
|
|
constructor({ client, path = "/", getUrlParams = () => ({}), getBaseUrl = () => client.baseUrl, }) {
|
|
this.#client = client;
|
|
this.#getBaseParams = getUrlParams;
|
|
this.#getBaseUrl = getBaseUrl;
|
|
this.#basePath = path;
|
|
}
|
|
request({ method, path = "", urlParamKeys = [], queryParamKeys = [], catchNotFound = false, keyTransform, payloadKey, returnResourceIdInLocationHeader, ignoredKeys, headers, }) {
|
|
return async (payload = {}, options) => {
|
|
const baseParams = this.#getBaseParams?.() ?? {};
|
|
// Filter query parameters by queryParamKeys
|
|
const queryParams = queryParamKeys.length > 0
|
|
? pick(payload, queryParamKeys)
|
|
: undefined;
|
|
// Add filtered payload parameters to base parameters
|
|
const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys];
|
|
const urlParams = { ...baseParams, ...pick(payload, allUrlParamKeys) };
|
|
if (!(payload instanceof FormData)) {
|
|
// Omit url parameters and query parameters from payload
|
|
const omittedKeys = ignoredKeys
|
|
? [...allUrlParamKeys, ...queryParamKeys].filter((key) => !ignoredKeys.includes(key))
|
|
: [...allUrlParamKeys, ...queryParamKeys];
|
|
payload = omit(payload, omittedKeys);
|
|
}
|
|
// Transform keys of both payload and queryParams
|
|
if (keyTransform) {
|
|
this.#transformKey(payload, keyTransform);
|
|
this.#transformKey(queryParams, keyTransform);
|
|
}
|
|
return this.#requestWithParams({
|
|
method,
|
|
path,
|
|
payload,
|
|
urlParams,
|
|
queryParams,
|
|
// catchNotFound precedence: global > local > default
|
|
catchNotFound,
|
|
...(this.#client.getGlobalRequestArgOptions() ?? options ?? {}),
|
|
payloadKey,
|
|
returnResourceIdInLocationHeader,
|
|
headers,
|
|
});
|
|
};
|
|
}
|
|
updateRequest({ method, path = "", urlParamKeys = [], queryParamKeys = [], catchNotFound = false, keyTransform, payloadKey, returnResourceIdInLocationHeader, headers, }) {
|
|
return async (query = {}, payload = {}) => {
|
|
const baseParams = this.#getBaseParams?.() ?? {};
|
|
// Filter query parameters by queryParamKeys
|
|
const queryParams = queryParamKeys
|
|
? pick(query, queryParamKeys)
|
|
: undefined;
|
|
// Add filtered query parameters to base parameters
|
|
const allUrlParamKeys = [...Object.keys(baseParams), ...urlParamKeys];
|
|
const urlParams = {
|
|
...baseParams,
|
|
...pick(query, allUrlParamKeys),
|
|
};
|
|
// Transform keys of queryParams
|
|
if (keyTransform) {
|
|
this.#transformKey(queryParams, keyTransform);
|
|
}
|
|
return this.#requestWithParams({
|
|
method,
|
|
path,
|
|
payload,
|
|
urlParams,
|
|
queryParams,
|
|
catchNotFound,
|
|
payloadKey,
|
|
returnResourceIdInLocationHeader,
|
|
headers,
|
|
});
|
|
};
|
|
}
|
|
async #requestWithParams({ method, path, payload, urlParams, queryParams, catchNotFound, payloadKey, returnResourceIdInLocationHeader, headers, }) {
|
|
const newPath = urlJoin(this.#basePath, path);
|
|
// Parse template and replace with values from urlParams
|
|
const pathTemplate = parseTemplate(newPath);
|
|
const parsedPath = pathTemplate.expand(urlParams);
|
|
const url = new URL(`${this.#getBaseUrl?.() ?? ""}${parsedPath}`);
|
|
const requestOptions = { ...this.#client.getRequestOptions() };
|
|
const requestHeaders = new Headers([
|
|
...new Headers(requestOptions.headers).entries(),
|
|
["authorization", `Bearer ${await this.#client.getAccessToken()}`],
|
|
["accept", "application/json, text/plain, */*"],
|
|
...new Headers(headers).entries(),
|
|
]);
|
|
const searchParams = {};
|
|
// Add payload parameters to search params if method is 'GET'.
|
|
if (method === "GET") {
|
|
Object.assign(searchParams, payload);
|
|
}
|
|
else if (requestHeaders.get("content-type") === "text/plain") {
|
|
// Pass the payload as a plain string if the content type is 'text/plain'.
|
|
requestOptions.body = payload;
|
|
}
|
|
else if (payload instanceof FormData) {
|
|
requestOptions.body = payload;
|
|
}
|
|
else {
|
|
// Otherwise assume it's JSON and stringify it.
|
|
requestOptions.body =
|
|
payloadKey && typeof payload[payloadKey] === "string"
|
|
? payload[payloadKey]
|
|
: JSON.stringify(payloadKey ? payload[payloadKey] : payload);
|
|
}
|
|
if (requestOptions.body &&
|
|
!requestHeaders.has("content-type") &&
|
|
!(payload instanceof FormData)) {
|
|
requestHeaders.set("content-type", "application/json");
|
|
}
|
|
if (queryParams) {
|
|
Object.assign(searchParams, queryParams);
|
|
}
|
|
url.search = stringifyQueryParams(searchParams);
|
|
try {
|
|
const res = await fetchWithError(url, {
|
|
...requestOptions,
|
|
headers: requestHeaders,
|
|
method,
|
|
});
|
|
// now we get the response of the http request
|
|
// if `resourceIdInLocationHeader` is true, we'll get the resourceId from the location header field
|
|
// todo: find a better way to find the id in path, maybe some kind of pattern matching
|
|
// for now, we simply split the last sub-path of the path returned in location header field
|
|
if (returnResourceIdInLocationHeader) {
|
|
const locationHeader = res.headers.get("location");
|
|
if (typeof locationHeader !== "string") {
|
|
throw new Error(`location header is not found in request: ${res.url}`);
|
|
}
|
|
const resourceId = locationHeader.split(SLASH).pop();
|
|
if (!resourceId) {
|
|
// throw an error to let users know the response is not expected
|
|
throw new Error(`resourceId is not found in Location header from request: ${res.url}`);
|
|
}
|
|
// return with format {[field]: string}
|
|
const { field } = returnResourceIdInLocationHeader;
|
|
return { [field]: resourceId };
|
|
}
|
|
if (Object.entries(headers || []).find(([key, value]) => key.toLowerCase() === "accept" &&
|
|
value === "application/octet-stream")) {
|
|
return await res.arrayBuffer();
|
|
}
|
|
return await parseResponse(res);
|
|
}
|
|
catch (err) {
|
|
if (err instanceof NetworkError &&
|
|
err.response.status === 404 &&
|
|
catchNotFound) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
#transformKey(payload, keyMapping) {
|
|
if (!payload) {
|
|
return;
|
|
}
|
|
Object.keys(keyMapping).some((key) => {
|
|
if (typeof payload[key] === "undefined") {
|
|
return false;
|
|
}
|
|
const newKey = keyMapping[key];
|
|
payload[newKey] = payload[key];
|
|
delete payload[key];
|
|
});
|
|
}
|
|
}
|