NeahNew/node_modules/@keycloak/keycloak-admin-client/lib/resources/agent.js
2025-05-03 15:36:20 +02:00

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];
});
}
}