import { ApiVersion } from "../constants" import { buildAnalyticsEndpoints } from "./analytics" import { buildAppEndpoints } from "./app" import { buildAttachmentEndpoints } from "./attachments" import { buildAuthEndpoints } from "./auth" import { buildAutomationEndpoints } from "./automations" import { buildConfigEndpoints } from "./configs" import { buildDatasourceEndpoints } from "./datasources" import { buildFlagEndpoints } from "./flags" import { buildHostingEndpoints } from "./hosting" import { buildLayoutEndpoints } from "./layouts" import { buildOtherEndpoints } from "./other" import { buildPermissionsEndpoints } from "./permissions" import { buildQueryEndpoints } from "./queries" import { buildRelationshipEndpoints } from "./relationships" import { buildRoleEndpoints } from "./roles" import { buildRouteEndpoints } from "./routes" import { buildRowEndpoints } from "./rows" import { buildScreenEndpoints } from "./screens" import { buildTableEndpoints } from "./tables" import { buildTemplateEndpoints } from "./templates" import { buildUserEndpoints } from "./user" import { buildSelfEndpoints } from "./self" import { buildViewEndpoints } from "./views" import { buildLicensingEndpoints } from "./licensing" const defaultAPIClientConfig = { /** * Certain definitions can't change at runtime for client apps, such as the * schema of tables. The endpoints that are cacheable can be cached by passing * in this flag. It's disabled by default to avoid bugs with stale data. */ enableCaching: false, /** * A function can be passed in to attach headers to all outgoing requests. * This function is passed in the headers object, which should be directly * mutated. No return value is required. */ attachHeaders: null, /** * A function can be passed in which will be invoked any time an API error * occurs. An error is defined as a status code >= 400. This function is * invoked before the actual JS error is thrown up the stack. */ onError: null, } /** * Constructs an API client with the provided configuration. * @param config the API client configuration * @return {object} the API client */ export const createAPIClient = config => { config = { ...defaultAPIClientConfig, ...config, } let cache = {} // Generates an error object from an API response const makeErrorFromResponse = async (response, method) => { // Try to read a message from the error let message = response.statusText let json = null try { json = await response.json() if (json?.message) { message = json.message } else if (json?.error) { message = json.error } } catch (error) { // Do nothing } return { message, json, status: response.status, url: response.url, method, handled: true, } } // Generates an error object from a string const makeError = (message, request) => { return { message, json: null, status: 400, url: request?.url, method: request?.method, handled: true, } } // Performs an API call to the server. const makeApiCall = async ({ method, url, body, json = true, external = false, parseResponse, }) => { // Ensure we don't do JSON processing if sending a GET request json = json && method !== "GET" // Build headers let headers = { Accept: "application/json" } if (!external) { headers["x-budibase-api-version"] = ApiVersion } if (json) { headers["Content-Type"] = "application/json" } if (config?.attachHeaders) { config.attachHeaders(headers) } // Build request body let requestBody = body if (json) { try { requestBody = JSON.stringify(body) } catch (error) { throw makeError("Invalid JSON body", { url, method }) } } // Make request let response try { response = await fetch(url, { method, headers, body: requestBody, credentials: "same-origin", }) } catch (error) { delete cache[url] throw makeError("Failed to send request", { url, method }) } // Handle response if (response.status >= 200 && response.status < 400) { try { if (parseResponse) { return await parseResponse(response) } else { return await response.json() } } catch (error) { delete cache[url] return null } } else { delete cache[url] throw await makeErrorFromResponse(response, method) } } // Performs an API call to the server and caches the response. // Future invocation for this URL will return the cached result instead of // hitting the server again. const makeCachedApiCall = async params => { const identifier = params.url if (!identifier) { return null } if (!cache[identifier]) { cache[identifier] = makeApiCall(params) cache[identifier] = await cache[identifier] } return await cache[identifier] } // Constructs an API call function for a particular HTTP method const requestApiCall = method => async params => { try { let { url, cache = false, external = false } = params if (!external) { url = `/${url}`.replace("//", "/") } // Cache the request if possible and desired const cacheRequest = cache && config?.enableCaching const handler = cacheRequest ? makeCachedApiCall : makeApiCall const enrichedParams = { ...params, method, url } return await handler(enrichedParams) } catch (error) { if (config?.onError) { config.onError(error) } throw error } } // Build the underlying core API methods let API = { post: requestApiCall("POST"), get: requestApiCall("GET"), patch: requestApiCall("PATCH"), delete: requestApiCall("DELETE"), put: requestApiCall("PUT"), error: message => { throw makeError(message) }, invalidateCache: () => { cache = {} }, } // Attach all endpoints return { ...API, ...buildAnalyticsEndpoints(API), ...buildAppEndpoints(API), ...buildAttachmentEndpoints(API), ...buildAuthEndpoints(API), ...buildAutomationEndpoints(API), ...buildConfigEndpoints(API), ...buildDatasourceEndpoints(API), ...buildFlagEndpoints(API), ...buildHostingEndpoints(API), ...buildLayoutEndpoints(API), ...buildOtherEndpoints(API), ...buildPermissionsEndpoints(API), ...buildQueryEndpoints(API), ...buildRelationshipEndpoints(API), ...buildRoleEndpoints(API), ...buildRouteEndpoints(API), ...buildRowEndpoints(API), ...buildScreenEndpoints(API), ...buildTableEndpoints(API), ...buildTemplateEndpoints(API), ...buildUserEndpoints(API), ...buildViewEndpoints(API), ...buildSelfEndpoints(API), ...buildLicensingEndpoints(API), } }