Add optional stronger typing of requests and responses to ensure conformity of flattened function params

This commit is contained in:
Andrew Kingston 2024-12-02 09:49:12 +00:00
parent 6d50f70258
commit 417040f2d5
No known key found for this signature in database
4 changed files with 171 additions and 114 deletions

View File

@ -1,92 +0,0 @@
export const buildDatasourceEndpoints = API => ({
/**
* Gets a list of datasources.
*/
getDatasources: async () => {
return await API.get({
url: "/api/datasources",
})
},
/**
* Prompts the server to build the schema for a datasource.
* @param datasourceId the datasource ID to build the schema for
* @param tablesFilter list of specific table names to be build the schema
*/
buildDatasourceSchema: async ({ datasourceId, tablesFilter }) => {
return await API.post({
url: `/api/datasources/${datasourceId}/schema`,
body: {
tablesFilter,
},
})
},
/**
* Creates a datasource
* @param datasource the datasource to create
* @param fetchSchema whether to fetch the schema or not
* @param tablesFilter a list of tables to actually fetch rather than simply
* all that are accessible.
*/
createDatasource: async ({ datasource, fetchSchema, tablesFilter }) => {
return await API.post({
url: "/api/datasources",
body: {
datasource,
fetchSchema,
tablesFilter,
},
})
},
/**
* Updates a datasource
* @param datasource the datasource to update
*/
updateDatasource: async datasource => {
return await API.put({
url: `/api/datasources/${datasource._id}`,
body: datasource,
})
},
/**
* Deletes a datasource.
* @param datasourceId the ID of the ddtasource to delete
* @param datasourceRev the rev of the datasource to delete
*/
deleteDatasource: async ({ datasourceId, datasourceRev }) => {
return await API.delete({
url: `/api/datasources/${datasourceId}/${datasourceRev}`,
})
},
/**
* Validate a datasource configuration
* @param datasource the datasource configuration to validate
*/
validateDatasource: async datasource => {
return await API.post({
url: `/api/datasources/verify`,
body: { datasource },
})
},
/**
* Fetch table names available within the datasource, for filtering out undesired tables
* @param datasource the datasource configuration to use for fetching tables
*/
fetchInfoForDatasource: async datasource => {
return await API.post({
url: `/api/datasources/info`,
body: { datasource },
})
},
fetchExternalSchema: async datasourceId => {
return await API.get({
url: `/api/datasources/${datasourceId}/schema/external`,
})
},
})

View File

@ -0,0 +1,125 @@
import {
BuildSchemaFromSourceRequest,
BuildSchemaFromSourceResponse,
CreateDatasourceRequest,
CreateDatasourceResponse,
Datasource,
FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse,
UpdateDatasourceRequest,
UpdateDatasourceResponse,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
} from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface DatasourceEndpoints {
getDatasources: () => Promise<Datasource[]>
buildDatasourceSchema: (
datasourceId: string,
tablesFilter?: BuildSchemaFromSourceRequest["tablesFilter"]
) => Promise<BuildSchemaFromSourceResponse>
createDatasource: (
request: CreateDatasourceRequest
) => Promise<CreateDatasourceResponse>
updateDatasource: (
datasource: Datasource
) => Promise<UpdateDatasourceResponse>
deleteDatasource: (id: string, rev: string) => Promise<void>
validateDatasource: (
datasource: Datasource
) => Promise<VerifyDatasourceResponse>
fetchInfoForDatasource: (
datasource: Datasource
) => Promise<FetchDatasourceInfoResponse>
fetchExternalSchema: (datasourceId: string) => Promise<{ schema: string }>
}
export const buildDatasourceEndpoints = (
API: BaseAPIClient
): DatasourceEndpoints => ({
/**
* Gets a list of datasources.
*/
getDatasources: async () => {
return await API.get({
url: "/api/datasources",
})
},
/**
* Prompts the server to build the schema for a datasource.
*/
buildDatasourceSchema: async (datasourceId, tablesFilter?) => {
return await API.post<
BuildSchemaFromSourceRequest,
BuildSchemaFromSourceResponse
>({
url: `/api/datasources/${datasourceId}/schema`,
body: {
tablesFilter,
},
})
},
/**
* Creates a datasource
*/
createDatasource: async request => {
return await API.post({
url: "/api/datasources",
body: request,
})
},
/**
* Updates a datasource
*/
updateDatasource: async datasource => {
return await API.put<UpdateDatasourceRequest, UpdateDatasourceResponse>({
url: `/api/datasources/${datasource._id}`,
body: datasource,
})
},
/**
* Deletes a datasource.
*/
deleteDatasource: async (id: string, rev: string) => {
return await API.delete({
url: `/api/datasources/${id}/${rev}`,
})
},
/**
* Validate a datasource configuration
*/
validateDatasource: async (datasource: Datasource) => {
return await API.post<VerifyDatasourceRequest, VerifyDatasourceResponse>({
url: `/api/datasources/verify`,
body: { datasource },
})
},
/**
* Fetch table names available within the datasource, for filtering out undesired tables
*/
fetchInfoForDatasource: async (datasource: Datasource) => {
return await API.post<
FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse
>({
url: `/api/datasources/info`,
body: { datasource },
})
},
/**
* Fetches the external schema of a datasource
*/
fetchExternalSchema: async (datasourceId: string) => {
return await API.get({
url: `/api/datasources/${datasourceId}/schema/external`,
})
},
})

View File

@ -103,7 +103,9 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
} }
// Performs an API call to the server. // Performs an API call to the server.
const makeApiCall = async <T>(callConfig: APICallConfig): Promise<T> => { const makeApiCall = async <RequestT, ResponseT>(
callConfig: APICallConfig<RequestT, ResponseT>
): Promise<ResponseT> => {
let { json, method, external, body, url, parseResponse, suppressErrors } = let { json, method, external, body, url, parseResponse, suppressErrors } =
callConfig callConfig
@ -124,7 +126,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
} }
// Build request body // Build request body
let requestBody = body let requestBody: RequestT | string = body
if (json) { if (json) {
try { try {
requestBody = JSON.stringify(body) requestBody = JSON.stringify(body)
@ -139,7 +141,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
response = await fetch(url, { response = await fetch(url, {
method, method,
headers, headers,
body: requestBody, body: requestBody as any,
credentials: "same-origin", credentials: "same-origin",
}) })
} catch (error) { } catch (error) {
@ -152,9 +154,9 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
handleMigrations(response) handleMigrations(response)
try { try {
if (parseResponse) { if (parseResponse) {
return await parseResponse<T>(response) return await parseResponse(response)
} else { } else {
return (await response.json()) as T return (await response.json()) as ResponseT
} }
} catch (error) { } catch (error) {
delete cache[url] delete cache[url]
@ -180,28 +182,31 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
// 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 <T>( const makeCachedApiCall = async <RequestT = void, ResponseT = void>(
callConfig: APICallConfig callConfig: APICallConfig<RequestT, ResponseT>
): Promise<T> => { ): Promise<ResponseT> => {
const identifier = callConfig.url const identifier = callConfig.url
if (!cache[identifier]) { if (!cache[identifier]) {
cache[identifier] = makeApiCall(callConfig) cache[identifier] = makeApiCall(callConfig)
cache[identifier] = await cache[identifier] cache[identifier] = await cache[identifier]
} }
return (await cache[identifier]) as T return (await cache[identifier]) as ResponseT
} }
// Constructs an API call function for a particular HTTP method // Constructs an API call function for a particular HTTP method
const requestApiCall = const requestApiCall =
(method: HTTPMethod) => (method: HTTPMethod) =>
async <T>(params: APICallParams): Promise<T> => { async <RequestT = void, ResponseT = void>(
params: APICallParams<RequestT, ResponseT>
): Promise<ResponseT> => {
try { try {
let callConfig: APICallConfig = { let callConfig: APICallConfig<RequestT, ResponseT> = {
json: true, json: true,
external: false, external: false,
suppressErrors: false, suppressErrors: false,
cache: false, cache: false,
method, method,
body: params.body,
...params, ...params,
} }
let { url, cache, external } = callConfig let { url, cache, external } = callConfig
@ -212,7 +217,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
// 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) return await handler(callConfig)
} catch (error) { } catch (error) {
if (config?.onError) { if (config?.onError) {
config.onError(error) config.onError(error)

View File

@ -7,6 +7,7 @@ import { AuthEndpoints } from "./auth"
import { AutomationEndpoints } from "./automations" import { AutomationEndpoints } from "./automations"
import { BackupEndpoints } from "./backups" import { BackupEndpoints } from "./backups"
import { ConfigEndpoints } from "./configs" import { ConfigEndpoints } from "./configs"
import { DatasourceEndpoints } from "./datasources"
export enum HTTPMethod { export enum HTTPMethod {
POST = "POST", POST = "POST",
@ -25,25 +26,42 @@ export type APIClientConfig = {
onMigrationDetected?: (migration: string) => void onMigrationDetected?: (migration: string) => void
} }
export type APICallConfig = { export type APICallConfig<RequestT = null, ResponseT = void> = {
method: HTTPMethod method: HTTPMethod
url: string url: string
body: RequestT
json: boolean json: boolean
external: boolean external: boolean
suppressErrors: boolean suppressErrors: boolean
cache: boolean cache: boolean
body?: any parseResponse?: (response: Response) => Promise<ResponseT> | ResponseT
parseResponse?: <T>(response: Response) => Promise<T> | T
} }
export type APICallParams = Pick<APICallConfig, "url"> & Partial<APICallConfig> export type APICallParams<
RequestT = null,
ResponseT = void
> = RequestT extends null
? Pick<APICallConfig<RequestT, ResponseT>, "url"> &
Partial<APICallConfig<RequestT, ResponseT>>
: Pick<APICallConfig<RequestT, ResponseT>, "url" | "body"> &
Partial<APICallConfig<RequestT, ResponseT>>
export type BaseAPIClient = { export type BaseAPIClient = {
post: <T>(params: APICallParams) => Promise<T> post: <RequestT = null, ResponseT = void>(
get: <T>(params: APICallParams) => Promise<T> params: APICallParams<RequestT, ResponseT>
put: <T>(params: APICallParams) => Promise<T> ) => Promise<ResponseT>
delete: <T>(params: APICallParams) => Promise<T> get: <ResponseT = void>(
patch: <T>(params: APICallParams) => Promise<T> params: APICallParams<undefined | null, ResponseT>
) => Promise<ResponseT>
put: <RequestT = null, ResponseT = void>(
params: APICallParams<RequestT, ResponseT>
) => Promise<ResponseT>
delete: <RequestT = null, ResponseT = void>(
params: APICallParams<RequestT, ResponseT>
) => Promise<ResponseT>
patch: <RequestT = null, ResponseT = void>(
params: APICallParams<RequestT, ResponseT>
) => Promise<ResponseT>
error: (message: string) => void error: (message: string) => void
invalidateCache: () => void invalidateCache: () => void
getAppID: () => string getAppID: () => string
@ -58,4 +76,5 @@ export type APIClient = BaseAPIClient &
AuthEndpoints & AuthEndpoints &
AutomationEndpoints & AutomationEndpoints &
BackupEndpoints & BackupEndpoints &
ConfigEndpoints & { [key: string]: any } ConfigEndpoints &
DatasourceEndpoints & { [key: string]: any }