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

View File

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