budibase/packages/frontend-core/src/api/index.js

246 lines
6.8 KiB
JavaScript

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),
}
}