Improve typing of core API client functions
This commit is contained in:
parent
f02797416a
commit
fdc6e53495
|
@ -4,7 +4,7 @@
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "./src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "0.0.0",
|
"@budibase/bbui": "0.0.0",
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import {
|
||||||
|
HTTPMethod,
|
||||||
|
APICallParams,
|
||||||
|
APIClientConfig,
|
||||||
|
APIClient,
|
||||||
|
APICallConfig,
|
||||||
|
} from "../types"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { Header } from "@budibase/shared-core"
|
import { Header } from "@budibase/shared-core"
|
||||||
import { ApiVersion } from "../constants"
|
import { ApiVersion } from "../constants"
|
||||||
|
@ -45,55 +52,21 @@ import { buildRowActionEndpoints } from "./rowActions"
|
||||||
*/
|
*/
|
||||||
export const APISessionID = Helpers.uuid()
|
export const APISessionID = Helpers.uuid()
|
||||||
|
|
||||||
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,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function can be passed to be called when an API call returns info about a migration running for a specific app
|
|
||||||
*/
|
|
||||||
onMigrationDetected: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an API client with the provided configuration.
|
* Constructs an API client with the provided configuration.
|
||||||
* @param config the API client configuration
|
|
||||||
* @return {object} the API client
|
|
||||||
*/
|
*/
|
||||||
export const createAPIClient = config => {
|
export const createAPIClient = (config: APIClientConfig = {}) => {
|
||||||
config = {
|
let cache: Record<string, any> = {}
|
||||||
...defaultAPIClientConfig,
|
|
||||||
...config,
|
|
||||||
}
|
|
||||||
let cache = {}
|
|
||||||
|
|
||||||
// Generates an error object from an API response
|
// Generates an error object from an API response
|
||||||
const makeErrorFromResponse = async (
|
const makeErrorFromResponse = async (
|
||||||
response,
|
response: Response,
|
||||||
method,
|
method: HTTPMethod,
|
||||||
suppressErrors = false
|
suppressErrors = false
|
||||||
) => {
|
) => {
|
||||||
// Try to read a message from the error
|
// Try to read a message from the error
|
||||||
let message = response.statusText
|
let message = response.statusText
|
||||||
let json = null
|
let json: any = null
|
||||||
try {
|
try {
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
if (json?.message) {
|
if (json?.message) {
|
||||||
|
@ -116,29 +89,24 @@ export const createAPIClient = config => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates an error object from a string
|
// Generates an error object from a string
|
||||||
const makeError = (message, request) => {
|
const makeError = (message: string, url?: string, method?: HTTPMethod) => {
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
json: null,
|
json: null,
|
||||||
status: 400,
|
status: 400,
|
||||||
url: request?.url,
|
url: url,
|
||||||
method: request?.method,
|
method: method,
|
||||||
handled: true,
|
handled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performs an API call to the server.
|
// Performs an API call to the server.
|
||||||
const makeApiCall = async ({
|
const makeApiCall = async <T>(callConfig: APICallConfig): Promise<T> => {
|
||||||
method,
|
let { json, method, external, body, url, parseResponse, suppressErrors } =
|
||||||
url,
|
callConfig
|
||||||
body,
|
|
||||||
json = true,
|
|
||||||
external = false,
|
|
||||||
parseResponse,
|
|
||||||
suppressErrors = false,
|
|
||||||
}) => {
|
|
||||||
// Ensure we don't do JSON processing if sending a GET request
|
// Ensure we don't do JSON processing if sending a GET request
|
||||||
json = json && method !== "GET"
|
json = json && method !== HTTPMethod.GET
|
||||||
|
|
||||||
// Build headers
|
// Build headers
|
||||||
let headers = { Accept: "application/json" }
|
let headers = { Accept: "application/json" }
|
||||||
|
@ -159,12 +127,12 @@ export const createAPIClient = config => {
|
||||||
try {
|
try {
|
||||||
requestBody = JSON.stringify(body)
|
requestBody = JSON.stringify(body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw makeError("Invalid JSON body", { url, method })
|
throw makeError("Invalid JSON body", url, method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make request
|
// Make request
|
||||||
let response
|
let response: Response
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
|
@ -174,7 +142,7 @@ export const createAPIClient = config => {
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
delete cache[url]
|
delete cache[url]
|
||||||
throw makeError("Failed to send request", { url, method })
|
throw makeError("Failed to send request", url, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle response
|
// Handle response
|
||||||
|
@ -182,13 +150,13 @@ export const createAPIClient = config => {
|
||||||
handleMigrations(response)
|
handleMigrations(response)
|
||||||
try {
|
try {
|
||||||
if (parseResponse) {
|
if (parseResponse) {
|
||||||
return await parseResponse(response)
|
return await parseResponse<T>(response)
|
||||||
} else {
|
} else {
|
||||||
return await response.json()
|
return (await response.json()) as T
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
delete cache[url]
|
delete cache[url]
|
||||||
return null
|
throw `Failed to parse response: ${error}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete cache[url]
|
delete cache[url]
|
||||||
|
@ -196,7 +164,7 @@ export const createAPIClient = config => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMigrations = response => {
|
const handleMigrations = (response: Response) => {
|
||||||
if (!config.onMigrationDetected) {
|
if (!config.onMigrationDetected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -210,48 +178,55 @@ export const createAPIClient = config => {
|
||||||
// Performs an API call to the server and caches the response.
|
// Performs an API call to the server and caches the response.
|
||||||
// Future invocation for this URL will return the cached result instead of
|
// Future invocation for this URL will return the cached result instead of
|
||||||
// hitting the server again.
|
// hitting the server again.
|
||||||
const makeCachedApiCall = async params => {
|
const makeCachedApiCall = async <T>(
|
||||||
const identifier = params.url
|
callConfig: APICallConfig
|
||||||
if (!identifier) {
|
): Promise<T> => {
|
||||||
return null
|
const identifier = callConfig.url
|
||||||
}
|
|
||||||
if (!cache[identifier]) {
|
if (!cache[identifier]) {
|
||||||
cache[identifier] = makeApiCall(params)
|
cache[identifier] = makeApiCall(callConfig)
|
||||||
cache[identifier] = await cache[identifier]
|
cache[identifier] = await cache[identifier]
|
||||||
}
|
}
|
||||||
return await cache[identifier]
|
return (await cache[identifier]) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs an API call function for a particular HTTP method
|
// Constructs an API call function for a particular HTTP method
|
||||||
const requestApiCall = method => async params => {
|
const requestApiCall =
|
||||||
try {
|
(method: HTTPMethod) =>
|
||||||
let { url, cache = false, external = false } = params
|
async <T>(params: APICallParams): Promise<T> => {
|
||||||
if (!external) {
|
try {
|
||||||
url = `/${url}`.replace("//", "/")
|
let callConfig: APICallConfig = {
|
||||||
}
|
json: true,
|
||||||
|
external: false,
|
||||||
|
suppressErrors: false,
|
||||||
|
cache: false,
|
||||||
|
method,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
let { url, cache, external } = callConfig
|
||||||
|
if (!external) {
|
||||||
|
callConfig.url = `/${url}`.replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
// Cache the request if possible and desired
|
// Cache the request if possible and desired
|
||||||
const cacheRequest = cache && config?.enableCaching
|
const cacheRequest = cache && config?.enableCaching
|
||||||
const handler = cacheRequest ? makeCachedApiCall : makeApiCall
|
const handler = cacheRequest ? makeCachedApiCall : makeApiCall
|
||||||
|
return await handler<T>(callConfig)
|
||||||
const enrichedParams = { ...params, method, url }
|
} catch (error) {
|
||||||
return await handler(enrichedParams)
|
if (config?.onError) {
|
||||||
} catch (error) {
|
config.onError(error)
|
||||||
if (config?.onError) {
|
}
|
||||||
config.onError(error)
|
throw error
|
||||||
}
|
}
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build the underlying core API methods
|
// Build the underlying core API methods
|
||||||
let API = {
|
let API: APIClient = {
|
||||||
post: requestApiCall("POST"),
|
post: requestApiCall(HTTPMethod.POST),
|
||||||
get: requestApiCall("GET"),
|
get: requestApiCall(HTTPMethod.GET),
|
||||||
patch: requestApiCall("PATCH"),
|
patch: requestApiCall(HTTPMethod.PATCH),
|
||||||
delete: requestApiCall("DELETE"),
|
delete: requestApiCall(HTTPMethod.DELETE),
|
||||||
put: requestApiCall("PUT"),
|
put: requestApiCall(HTTPMethod.PUT),
|
||||||
error: message => {
|
error: (message: string) => {
|
||||||
throw makeError(message)
|
throw makeError(message)
|
||||||
},
|
},
|
||||||
invalidateCache: () => {
|
invalidateCache: () => {
|
||||||
|
@ -260,9 +235,9 @@ export const createAPIClient = config => {
|
||||||
|
|
||||||
// Generic utility to extract the current app ID. Assumes that any client
|
// Generic utility to extract the current app ID. Assumes that any client
|
||||||
// that exists in an app context will be attaching our app ID header.
|
// that exists in an app context will be attaching our app ID header.
|
||||||
getAppID: () => {
|
getAppID: (): string => {
|
||||||
let headers = {}
|
let headers = {}
|
||||||
config?.attachHeaders(headers)
|
config?.attachHeaders?.(headers)
|
||||||
return headers?.[Header.APP_ID]
|
return headers?.[Header.APP_ID]
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
export enum HTTPMethod {
|
||||||
|
POST = "POST",
|
||||||
|
PATCH = "PATCH",
|
||||||
|
GET = "GET",
|
||||||
|
PUT = "PUT",
|
||||||
|
DELETE = "DELETE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APIClientConfig = {
|
||||||
|
enableCaching?: boolean
|
||||||
|
attachHeaders?: Function
|
||||||
|
onError?: Function
|
||||||
|
onMigrationDetected?: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APICallConfig = {
|
||||||
|
method: HTTPMethod
|
||||||
|
url: string
|
||||||
|
json: boolean
|
||||||
|
external: boolean
|
||||||
|
suppressErrors: boolean
|
||||||
|
cache: boolean
|
||||||
|
body?: any
|
||||||
|
parseResponse?: <T>(response: Response) => Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APICallParams = Pick<APICallConfig, "url"> & Partial<APICallConfig>
|
||||||
|
|
||||||
|
export type APIClient = {
|
||||||
|
post: <T>(params: APICallParams) => Promise<T>
|
||||||
|
get: <T>(params: APICallParams) => Promise<T>
|
||||||
|
put: <T>(params: APICallParams) => Promise<T>
|
||||||
|
delete: <T>(params: APICallParams) => Promise<T>
|
||||||
|
patch: <T>(params: APICallParams) => Promise<T>
|
||||||
|
[key: string]: any
|
||||||
|
}
|
Loading…
Reference in New Issue