forked from tsai/budibase
35 changed files with 755 additions and 653 deletions
@ -0,0 +1,120 @@ |
|||
import { createAPIClient } from "@budibase/frontend-core" |
|||
import { notificationStore } from "./stores" |
|||
import { FieldTypes } from "./constants" |
|||
import { TableNames } from "@budibase/frontend-core/src/constants.js" |
|||
|
|||
export const API = createAPIClient({ |
|||
// Attach client specific headers
|
|||
attachHeaders: headers => { |
|||
// Attach app ID header
|
|||
headers["x-budibase-app-id"] = window["##BUDIBASE_APP_ID##"] |
|||
|
|||
// Attach client header if not inside the builder preview
|
|||
if (!window["##BUDIBASE_IN_BUILDER##"]) { |
|||
headers["x-budibase-type"] = "client" |
|||
} |
|||
}, |
|||
|
|||
// Show an error notification for all API failures.
|
|||
// We could also log these to sentry.
|
|||
// Or we could check error.status and redirect to login on a 403 etc.
|
|||
onError: error => { |
|||
notificationStore.actions.error(error.message) |
|||
}, |
|||
|
|||
// Patch certain endpoints with functionality specific to client apps
|
|||
patches: { |
|||
// Enrich rows so they properly handle client bindings
|
|||
fetchSelf: async ({ output }) => { |
|||
const user = output |
|||
if (user && user._id) { |
|||
if (user.roleId === "PUBLIC") { |
|||
// Don't try to enrich a public user as it will 403
|
|||
return user |
|||
} else { |
|||
return (await enrichRows([user], TableNames.USERS))[0] |
|||
} |
|||
} else { |
|||
return null |
|||
} |
|||
}, |
|||
fetchRelationshipData: async ({ params, output }) => { |
|||
const tableId = params[0]?.tableId |
|||
return await enrichRows(output, tableId) |
|||
}, |
|||
fetchTableData: async ({ params, output }) => { |
|||
const tableId = params[0] |
|||
return await enrichRows(output, tableId) |
|||
}, |
|||
searchTable: async ({ params, output }) => { |
|||
const tableId = params[0]?.tableId |
|||
return { |
|||
...output, |
|||
rows: await enrichRows(output?.rows, tableId), |
|||
} |
|||
}, |
|||
fetchViewData: async ({ params, output }) => { |
|||
const tableId = params[0]?.tableId |
|||
return await enrichRows(output, tableId) |
|||
}, |
|||
|
|||
// Wipe any HBS formulae from table definitions, as these interfere with
|
|||
// handlebars enrichment
|
|||
fetchTableDefinition: async ({ output }) => { |
|||
Object.keys(output?.schema || {}).forEach(field => { |
|||
if (output.schema[field]?.type === "formula") { |
|||
delete output.schema[field].formula |
|||
} |
|||
}) |
|||
return output |
|||
}, |
|||
}, |
|||
}) |
|||
|
|||
/** |
|||
* Enriches rows which contain certain field types so that they can |
|||
* be properly displayed. |
|||
* The ability to create these bindings has been removed, but they will still |
|||
* exist in client apps to support backwards compatibility. |
|||
*/ |
|||
const enrichRows = async (rows, tableId) => { |
|||
if (!Array.isArray(rows)) { |
|||
return [] |
|||
} |
|||
if (rows.length) { |
|||
const tables = {} |
|||
for (let row of rows) { |
|||
// Fall back to passed in tableId if row doesn't have it specified
|
|||
let rowTableId = row.tableId || tableId |
|||
let table = tables[rowTableId] |
|||
if (!table) { |
|||
// Fetch table schema so we can check column types
|
|||
table = await API.fetchTableDefinition(rowTableId) |
|||
tables[rowTableId] = table |
|||
} |
|||
const schema = table?.schema |
|||
if (schema) { |
|||
const keys = Object.keys(schema) |
|||
for (let key of keys) { |
|||
const type = schema[key].type |
|||
if (type === FieldTypes.LINK && Array.isArray(row[key])) { |
|||
// Enrich row a string join of relationship fields
|
|||
row[`${key}_text`] = |
|||
row[key] |
|||
?.map(option => option?.primaryDisplay) |
|||
.filter(option => !!option) |
|||
.join(", ") || "" |
|||
} else if (type === "attachment") { |
|||
// Enrich row with the first image URL for any attachment fields
|
|||
let url = null |
|||
if (Array.isArray(row[key]) && row[key][0] != null) { |
|||
url = row[key][0].url |
|||
} |
|||
row[`${key}_first`] = url |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return rows |
|||
} |
|||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@ -1,10 +1,10 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Notifies that an end user client app has been loaded. |
|||
*/ |
|||
export const pingEndUser = async () => { |
|||
return await API.post({ |
|||
url: `/api/analytics/ping`, |
|||
}) |
|||
} |
|||
export const buildAnalyticsEndpoints = API => ({ |
|||
/** |
|||
* Notifies that an end user client app has been loaded. |
|||
*/ |
|||
pingEndUser: async () => { |
|||
return await API.post({ |
|||
url: `/api/analytics/ping`, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,145 +0,0 @@ |
|||
import { ApiVersion } from "../constants" |
|||
|
|||
const defaultAPIClientConfig = { |
|||
attachHeaders: null, |
|||
onError: null, |
|||
} |
|||
|
|||
export const createAPIClient = config => { |
|||
config = { |
|||
...defaultAPIClientConfig, |
|||
...config, |
|||
} |
|||
|
|||
/** |
|||
* API cache for cached request responses. |
|||
*/ |
|||
let cache = {} |
|||
|
|||
/** |
|||
* Handler for API errors. |
|||
*/ |
|||
const makeErrorFromResponse = async response => { |
|||
// Try to read a message from the error
|
|||
let message |
|||
try { |
|||
const json = await response.json() |
|||
if (json?.error) { |
|||
message = json.error |
|||
} |
|||
} catch (error) { |
|||
// Do nothing
|
|||
} |
|||
console.log("building error from", response) |
|||
return { |
|||
message, |
|||
status: response.status, |
|||
} |
|||
} |
|||
|
|||
const makeError = message => { |
|||
return { |
|||
message, |
|||
status: 400, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Performs an API call to the server. |
|||
* App ID header is always correctly set. |
|||
*/ |
|||
const makeApiCall = async ({ |
|||
method, |
|||
url, |
|||
body, |
|||
json = true, |
|||
external = false, |
|||
}) => { |
|||
// 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") |
|||
} |
|||
} |
|||
|
|||
// Make request
|
|||
let response |
|||
try { |
|||
response = await fetch(url, { |
|||
method, |
|||
headers, |
|||
body: requestBody, |
|||
credentials: "same-origin", |
|||
}) |
|||
} catch (error) { |
|||
throw makeError("Failed to send request") |
|||
} |
|||
|
|||
// Handle response
|
|||
if (response.status >= 200 && response.status < 400) { |
|||
try { |
|||
return await response.json() |
|||
} catch (error) { |
|||
return null |
|||
} |
|||
} else { |
|||
const error = await makeErrorFromResponse(response) |
|||
if (config?.onError) { |
|||
config.onError(error) |
|||
} |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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 => { |
|||
let { url, cache = false, external = false } = params |
|||
if (!external) { |
|||
url = `/${url}`.replace("//", "/") |
|||
} |
|||
const enrichedParams = { ...params, method, url } |
|||
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) |
|||
} |
|||
|
|||
return { |
|||
post: requestApiCall("POST"), |
|||
get: requestApiCall("GET"), |
|||
patch: requestApiCall("PATCH"), |
|||
delete: requestApiCall("DELETE"), |
|||
error: message => throw makeError(message), |
|||
} |
|||
} |
|||
@ -1,10 +1,10 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Fetches screen definition for an app. |
|||
*/ |
|||
export const fetchAppPackage = async appId => { |
|||
return await API.get({ |
|||
url: `/api/applications/${appId}/appPackage`, |
|||
}) |
|||
} |
|||
export const buildAppEndpoints = API => ({ |
|||
/** |
|||
* Fetches screen definition for an app. |
|||
*/ |
|||
fetchAppPackage: async appId => { |
|||
return await API.get({ |
|||
url: `/api/applications/${appId}/appPackage`, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,12 +1,12 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Uploads an attachment to the server. |
|||
*/ |
|||
export const uploadAttachment = async (data, tableId = "") => { |
|||
return await API.post({ |
|||
url: `/api/attachments/${tableId}/upload`, |
|||
body: data, |
|||
json: false, |
|||
}) |
|||
} |
|||
export const buildAttachmentEndpoints = API => ({ |
|||
/** |
|||
* Uploads an attachment to the server. |
|||
*/ |
|||
uploadAttachment: async ({ data, tableId }) => { |
|||
return await API.post({ |
|||
url: `/api/attachments/${tableId}/upload`, |
|||
body: data, |
|||
json: false, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,36 +1,27 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
import { TableNames } from "../constants" |
|||
|
|||
/** |
|||
* Performs a log in request. |
|||
*/ |
|||
export const logIn = async ({ email, password }) => { |
|||
if (!email) { |
|||
return API.error("Please enter your email") |
|||
} |
|||
if (!password) { |
|||
return API.error("Please enter your password") |
|||
} |
|||
return await API.post({ |
|||
url: "/api/global/auth", |
|||
body: { username: email, password }, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Fetches the currently logged in user object |
|||
*/ |
|||
export const fetchSelf = async () => { |
|||
const user = await API.get({ url: "/api/self" }) |
|||
if (user && user._id) { |
|||
if (user.roleId === "PUBLIC") { |
|||
// Don't try to enrich a public user as it will 403
|
|||
return user |
|||
} else { |
|||
return (await enrichRows([user], TableNames.USERS))[0] |
|||
export const buildAuthEndpoints = API => ({ |
|||
/** |
|||
* Performs a log in request. |
|||
*/ |
|||
logIn: async ({ email, password }) => { |
|||
if (!email) { |
|||
return API.error("Please enter your email") |
|||
} |
|||
} else { |
|||
return null |
|||
} |
|||
} |
|||
if (!password) { |
|||
return API.error("Please enter your password") |
|||
} |
|||
return await API.post({ |
|||
url: "/api/global/auth", |
|||
body: { |
|||
username: email, |
|||
password, |
|||
}, |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Fetches the currently logged in user object |
|||
*/ |
|||
fetchSelf: async () => { |
|||
return await API.get({ url: "/api/self" }) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,16 +1,11 @@ |
|||
import { notificationStore } from "stores/notification" |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Executes an automation. Must have "App Action" trigger. |
|||
*/ |
|||
export const triggerAutomation = async (automationId, fields) => { |
|||
const res = await API.post({ |
|||
url: `/api/automations/${automationId}/trigger`, |
|||
body: { fields }, |
|||
}) |
|||
res.error |
|||
? notificationStore.actions.error("An error has occurred") |
|||
: notificationStore.actions.success("Automation triggered") |
|||
return res |
|||
} |
|||
export const buildAutomationEndpoints = API => ({ |
|||
/** |
|||
* Executes an automation. Must have "App Action" trigger. |
|||
*/ |
|||
triggerAutomation: async ({ automationId, fields }) => { |
|||
return await API.post({ |
|||
url: `/api/automations/${automationId}/trigger`, |
|||
body: { fields }, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,11 +1,188 @@ |
|||
export * from "./rows" |
|||
export * from "./auth" |
|||
export * from "./tables" |
|||
export * from "./attachments" |
|||
export * from "./views" |
|||
export * from "./relationships" |
|||
export * from "./routes" |
|||
export * from "./queries" |
|||
export * from "./app" |
|||
export * from "./automations" |
|||
export * from "./analytics" |
|||
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 { buildQueryEndpoints } from "./queries" |
|||
import { buildRelationshipEndpoints } from "./relationships" |
|||
import { buildRouteEndpoints } from "./routes" |
|||
import { buildRowEndpoints } from "./rows" |
|||
import { buildTableEndpoints } from "./tables" |
|||
import { buildViewEndpoints } from "./views" |
|||
|
|||
const defaultAPIClientConfig = { |
|||
attachHeaders: null, |
|||
onError: null, |
|||
patches: 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, |
|||
} |
|||
|
|||
/** |
|||
* Handler for API errors. |
|||
*/ |
|||
const makeErrorFromResponse = async response => { |
|||
// Try to read a message from the error
|
|||
let message |
|||
try { |
|||
const json = await response.json() |
|||
if (json?.error) { |
|||
message = json.error |
|||
} |
|||
} catch (error) { |
|||
// Do nothing
|
|||
} |
|||
console.log("building error from", response) |
|||
return { |
|||
message, |
|||
status: response.status, |
|||
} |
|||
} |
|||
|
|||
const makeError = message => { |
|||
return { |
|||
message, |
|||
status: 400, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Performs an API call to the server. |
|||
* App ID header is always correctly set. |
|||
*/ |
|||
const makeApiCall = async ({ |
|||
method, |
|||
url, |
|||
body, |
|||
json = true, |
|||
external = false, |
|||
}) => { |
|||
// 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") |
|||
} |
|||
} |
|||
|
|||
// Make request
|
|||
let response |
|||
try { |
|||
response = await fetch(url, { |
|||
method, |
|||
headers, |
|||
body: requestBody, |
|||
credentials: "same-origin", |
|||
}) |
|||
} catch (error) { |
|||
throw makeError("Failed to send request") |
|||
} |
|||
|
|||
// Handle response
|
|||
if (response.status >= 200 && response.status < 400) { |
|||
try { |
|||
return await response.json() |
|||
} catch (error) { |
|||
return null |
|||
} |
|||
} else { |
|||
const error = await makeErrorFromResponse(response) |
|||
if (config?.onError) { |
|||
config.onError(error) |
|||
} |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
*/ |
|||
let cache = {} |
|||
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 => { |
|||
let { url, cache = false, external = false } = params |
|||
if (!external) { |
|||
url = `/${url}`.replace("//", "/") |
|||
} |
|||
const enrichedParams = { ...params, method, url } |
|||
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) |
|||
} |
|||
|
|||
// Build the underlying core API methods
|
|||
let API = { |
|||
post: requestApiCall("POST"), |
|||
get: requestApiCall("GET"), |
|||
patch: requestApiCall("PATCH"), |
|||
delete: requestApiCall("DELETE"), |
|||
error: message => throw makeError(message), |
|||
} |
|||
|
|||
// Attach all other endpoints
|
|||
API = { |
|||
...API, |
|||
...buildAnalyticsEndpoints(API), |
|||
...buildAppEndpoints(API), |
|||
...buildAttachmentEndpoints(API), |
|||
...buildAuthEndpoints(API), |
|||
...buildAutomationEndpoints(API), |
|||
...buildQueryEndpoints(API), |
|||
...buildRelationshipEndpoints(API), |
|||
...buildRouteEndpoints(API), |
|||
...buildRowEndpoints(API), |
|||
...buildTableEndpoints(API), |
|||
...buildViewEndpoints(API), |
|||
} |
|||
|
|||
// Assign any patches
|
|||
const patches = Object.entries(config.patches || {}) |
|||
if (patches.length) { |
|||
patches.forEach(([method, fn]) => { |
|||
API[method] = async (...params) => { |
|||
const output = await API[method](...params) |
|||
return await fn({ params, output }) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
return API |
|||
} |
|||
|
|||
@ -1,34 +1,24 @@ |
|||
import { notificationStore, dataSourceStore } from "stores" |
|||
import API from "./api" |
|||
export const buildQueryEndpoints = API => ({ |
|||
/** |
|||
* Executes a query against an external data connector. |
|||
*/ |
|||
executeQuery: async ({ queryId, pagination, parameters }) => { |
|||
return await API.post({ |
|||
url: `/api/v2/queries/${queryId}`, |
|||
body: { |
|||
parameters, |
|||
pagination, |
|||
}, |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Executes a query against an external data connector. |
|||
*/ |
|||
export const executeQuery = async ({ queryId, pagination, parameters }) => { |
|||
const query = await fetchQueryDefinition(queryId) |
|||
if (query?.datasourceId == null) { |
|||
notificationStore.actions.error("That query couldn't be found") |
|||
return |
|||
} |
|||
const res = await API.post({ |
|||
url: `/api/v2/queries/${queryId}`, |
|||
body: { |
|||
parameters, |
|||
pagination, |
|||
}, |
|||
}) |
|||
if (res.error) { |
|||
notificationStore.actions.error("An error has occurred") |
|||
} else if (!query.readable) { |
|||
notificationStore.actions.success("Query executed successfully") |
|||
await dataSourceStore.actions.invalidateDataSource(query.datasourceId) |
|||
} |
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Fetches the definition of an external query. |
|||
*/ |
|||
export const fetchQueryDefinition = async queryId => { |
|||
return await API.get({ url: `/api/queries/${queryId}`, cache: true }) |
|||
} |
|||
/** |
|||
* Fetches the definition of an external query. |
|||
*/ |
|||
fetchQueryDefinition: async queryId => { |
|||
return await API.get({ |
|||
url: `/api/queries/${queryId}`, |
|||
cache: true, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,14 +1,12 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
|
|||
/** |
|||
* Fetches related rows for a certain field of a certain row. |
|||
*/ |
|||
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => { |
|||
if (!tableId || !rowId || !fieldName) { |
|||
return [] |
|||
} |
|||
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) |
|||
const rows = response[fieldName] || [] |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
export const buildRelationshipEndpoints = API => ({ |
|||
/** |
|||
* Fetches related rows for a certain field of a certain row. |
|||
*/ |
|||
fetchRelationshipData: async ({ tableId, rowId, fieldName }) => { |
|||
if (!tableId || !rowId || !fieldName) { |
|||
return [] |
|||
} |
|||
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) |
|||
return response[fieldName] || [] |
|||
}, |
|||
}) |
|||
|
|||
@ -1,10 +1,10 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Fetches available routes for the client app. |
|||
*/ |
|||
export const fetchRoutes = async () => { |
|||
return await API.get({ |
|||
url: `/api/routing/client`, |
|||
}) |
|||
} |
|||
export const buildRouteEndpoints = API => ({ |
|||
/** |
|||
* Fetches available routes for the client app. |
|||
*/ |
|||
fetchClientAppRoutes: async () => { |
|||
return await API.get({ |
|||
url: `/api/routing/client`, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,155 +1,43 @@ |
|||
import { notificationStore, dataSourceStore } from "stores" |
|||
import API from "./api" |
|||
import { fetchTableDefinition } from "./tables" |
|||
import { FieldTypes } from "../constants" |
|||
|
|||
/** |
|||
* Fetches data about a certain row in a table. |
|||
*/ |
|||
export const fetchRow = async ({ tableId, rowId }) => { |
|||
if (!tableId || !rowId) { |
|||
return |
|||
} |
|||
const row = await API.get({ |
|||
url: `/api/${tableId}/rows/${rowId}`, |
|||
}) |
|||
return (await enrichRows([row], tableId))[0] |
|||
} |
|||
|
|||
/** |
|||
* Creates a row in a table. |
|||
*/ |
|||
export const saveRow = async row => { |
|||
if (!row?.tableId) { |
|||
return |
|||
} |
|||
const res = await API.post({ |
|||
url: `/api/${row.tableId}/rows`, |
|||
body: row, |
|||
}) |
|||
res.error |
|||
? notificationStore.actions.error("An error has occurred") |
|||
: notificationStore.actions.success("Row saved") |
|||
|
|||
// Refresh related datasources
|
|||
await dataSourceStore.actions.invalidateDataSource(row.tableId) |
|||
|
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Updates a row in a table. |
|||
*/ |
|||
export const updateRow = async row => { |
|||
if (!row?.tableId || !row?._id) { |
|||
return |
|||
} |
|||
const res = await API.patch({ |
|||
url: `/api/${row.tableId}/rows`, |
|||
body: row, |
|||
}) |
|||
res.error |
|||
? notificationStore.actions.error("An error has occurred") |
|||
: notificationStore.actions.success("Row updated") |
|||
|
|||
// Refresh related datasources
|
|||
await dataSourceStore.actions.invalidateDataSource(row.tableId) |
|||
|
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Deletes a row from a table. |
|||
*/ |
|||
export const deleteRow = async ({ tableId, rowId, revId }) => { |
|||
if (!tableId || !rowId || !revId) { |
|||
return |
|||
} |
|||
const res = await API.del({ |
|||
url: `/api/${tableId}/rows`, |
|||
body: { |
|||
_id: rowId, |
|||
_rev: revId, |
|||
}, |
|||
}) |
|||
res.error |
|||
? notificationStore.actions.error("An error has occurred") |
|||
: notificationStore.actions.success("Row deleted") |
|||
|
|||
// Refresh related datasources
|
|||
await dataSourceStore.actions.invalidateDataSource(tableId) |
|||
|
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Deletes many rows from a table. |
|||
*/ |
|||
export const deleteRows = async ({ tableId, rows }) => { |
|||
if (!tableId || !rows) { |
|||
return |
|||
} |
|||
const res = await API.del({ |
|||
url: `/api/${tableId}/rows`, |
|||
body: { |
|||
rows, |
|||
}, |
|||
}) |
|||
res.error |
|||
? notificationStore.actions.error("An error has occurred") |
|||
: notificationStore.actions.success(`${rows.length} row(s) deleted`) |
|||
|
|||
// Refresh related datasources
|
|||
await dataSourceStore.actions.invalidateDataSource(tableId) |
|||
|
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Enriches rows which contain certain field types so that they can |
|||
* be properly displayed. |
|||
* The ability to create these bindings has been removed, but they will still |
|||
* exist in client apps to support backwards compatibility. |
|||
*/ |
|||
export const enrichRows = async (rows, tableId) => { |
|||
if (!Array.isArray(rows)) { |
|||
return [] |
|||
} |
|||
if (rows.length) { |
|||
// map of tables, incase a row being loaded is not from the same table
|
|||
const tables = {} |
|||
for (let row of rows) { |
|||
// fallback to passed in tableId if row doesn't have it specified
|
|||
let rowTableId = row.tableId || tableId |
|||
let table = tables[rowTableId] |
|||
if (!table) { |
|||
// Fetch table schema so we can check column types
|
|||
table = await fetchTableDefinition(rowTableId) |
|||
tables[rowTableId] = table |
|||
} |
|||
const schema = table?.schema |
|||
if (schema) { |
|||
const keys = Object.keys(schema) |
|||
for (let key of keys) { |
|||
const type = schema[key].type |
|||
if (type === FieldTypes.LINK && Array.isArray(row[key])) { |
|||
// Enrich row a string join of relationship fields
|
|||
row[`${key}_text`] = |
|||
row[key] |
|||
?.map(option => option?.primaryDisplay) |
|||
.filter(option => !!option) |
|||
.join(", ") || "" |
|||
} else if (type === "attachment") { |
|||
// Enrich row with the first image URL for any attachment fields
|
|||
let url = null |
|||
if (Array.isArray(row[key]) && row[key][0] != null) { |
|||
url = row[key][0].url |
|||
} |
|||
row[`${key}_first`] = url |
|||
} |
|||
} |
|||
} |
|||
export const buildRowEndpoints = API => ({ |
|||
/** |
|||
* Fetches data about a certain row in a table. |
|||
*/ |
|||
fetchRow: async ({ tableId, rowId }) => { |
|||
if (!tableId || !rowId) { |
|||
return null |
|||
} |
|||
const row = await API.get({ |
|||
url: `/api/${tableId}/rows/${rowId}`, |
|||
}) |
|||
return (await API.enrichRows([row], tableId))[0] |
|||
}, |
|||
|
|||
/** |
|||
* Creates a row in a table. |
|||
*/ |
|||
saveRow: async row => { |
|||
if (!row?.tableId) { |
|||
return |
|||
} |
|||
return await API.post({ |
|||
url: `/api/${row.tableId}/rows`, |
|||
body: row, |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Deletes a row from a table. |
|||
*/ |
|||
deleteRow: async ({ tableId, rowId, revId }) => { |
|||
if (!tableId || !rowId || !revId) { |
|||
return |
|||
} |
|||
} |
|||
return rows |
|||
} |
|||
return await API.delete({ |
|||
url: `/api/${tableId}/rows`, |
|||
body: { |
|||
_id: rowId, |
|||
_rev: revId, |
|||
}, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,63 +1,51 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
export const buildTableEndpoints = API => ({ |
|||
/** |
|||
* Fetches a table definition. |
|||
* Since definitions cannot change at runtime, the result is cached. |
|||
*/ |
|||
fetchTableDefinition: async tableId => { |
|||
return await API.get({ |
|||
url: `/api/tables/${tableId}`, |
|||
cache: true, |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Fetches a table definition. |
|||
* Since definitions cannot change at runtime, the result is cached. |
|||
*/ |
|||
export const fetchTableDefinition = async tableId => { |
|||
const res = await API.get({ url: `/api/tables/${tableId}`, cache: true }) |
|||
/** |
|||
* Fetches all rows from a table. |
|||
*/ |
|||
fetchTableData: async tableId => { |
|||
return await API.get({ url: `/api/${tableId}/rows` }) |
|||
}, |
|||
|
|||
// Wipe any HBS formulae, as these interfere with handlebars enrichment
|
|||
Object.keys(res?.schema || {}).forEach(field => { |
|||
if (res.schema[field]?.type === "formula") { |
|||
delete res.schema[field].formula |
|||
/** |
|||
* Searches a table using Lucene. |
|||
*/ |
|||
searchTable: async ({ |
|||
tableId, |
|||
query, |
|||
bookmark, |
|||
limit, |
|||
sort, |
|||
sortOrder, |
|||
sortType, |
|||
paginate, |
|||
}) => { |
|||
if (!tableId || !query) { |
|||
return { |
|||
rows: [], |
|||
} |
|||
} |
|||
}) |
|||
|
|||
return res |
|||
} |
|||
|
|||
/** |
|||
* Fetches all rows from a table. |
|||
*/ |
|||
export const fetchTableData = async tableId => { |
|||
const rows = await API.get({ url: `/api/${tableId}/rows` }) |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
|
|||
/** |
|||
* Searches a table using Lucene. |
|||
*/ |
|||
export const searchTable = async ({ |
|||
tableId, |
|||
query, |
|||
bookmark, |
|||
limit, |
|||
sort, |
|||
sortOrder, |
|||
sortType, |
|||
paginate, |
|||
}) => { |
|||
if (!tableId || !query) { |
|||
return { |
|||
rows: [], |
|||
} |
|||
} |
|||
const res = await API.post({ |
|||
url: `/api/${tableId}/search`, |
|||
body: { |
|||
query, |
|||
bookmark, |
|||
limit, |
|||
sort, |
|||
sortOrder, |
|||
sortType, |
|||
paginate, |
|||
}, |
|||
}) |
|||
return { |
|||
...res, |
|||
rows: await enrichRows(res?.rows, tableId), |
|||
} |
|||
} |
|||
return await API.post({ |
|||
url: `/api/${tableId}/search`, |
|||
body: { |
|||
query, |
|||
bookmark, |
|||
limit, |
|||
sort, |
|||
sortOrder, |
|||
sortType, |
|||
paginate, |
|||
}, |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,30 +1,19 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
|
|||
/** |
|||
* Fetches all rows in a view. |
|||
*/ |
|||
export const fetchViewData = async ({ |
|||
name, |
|||
field, |
|||
groupBy, |
|||
calculation, |
|||
tableId, |
|||
}) => { |
|||
const params = new URLSearchParams() |
|||
|
|||
if (calculation) { |
|||
params.set("field", field) |
|||
params.set("calculation", calculation) |
|||
} |
|||
if (groupBy) { |
|||
params.set("group", groupBy ? "true" : "false") |
|||
} |
|||
|
|||
const QUERY_VIEW_URL = field |
|||
? `/api/views/${name}?${params}` |
|||
: `/api/views/${name}` |
|||
|
|||
const rows = await API.get({ url: QUERY_VIEW_URL }) |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
export const buildViewEndpoints = API => ({ |
|||
/** |
|||
* Fetches all rows in a view. |
|||
*/ |
|||
fetchViewData: async ({ name, field, groupBy, calculation }) => { |
|||
const params = new URLSearchParams() |
|||
if (calculation) { |
|||
params.set("field", field) |
|||
params.set("calculation", calculation) |
|||
} |
|||
if (groupBy) { |
|||
params.set("group", groupBy ? "true" : "false") |
|||
} |
|||
const QUERY_VIEW_URL = field |
|||
? `/api/views/${name}?${params}` |
|||
: `/api/views/${name}` |
|||
return await API.get({ url: QUERY_VIEW_URL }) |
|||
}, |
|||
}) |
|||
|
|||
@ -1,13 +1,16 @@ |
|||
import FieldFetch from "./FieldFetch.js" |
|||
import { fetchTableDefinition } from "../api" |
|||
import { getJSONArrayDatasourceSchema } from "../utils/json" |
|||
|
|||
export default class JSONArrayFetch extends FieldFetch { |
|||
static async getDefinition(datasource) { |
|||
// JSON arrays need their table definitions fetched.
|
|||
// We can then extract their schema as a subset of the table schema.
|
|||
const table = await fetchTableDefinition(datasource.tableId) |
|||
const schema = getJSONArrayDatasourceSchema(table?.schema, datasource) |
|||
return { schema } |
|||
try { |
|||
const table = await this.API.fetchTableDefinition(datasource.tableId) |
|||
const schema = getJSONArrayDatasourceSchema(table?.schema, datasource) |
|||
return { schema } |
|||
} catch (error) { |
|||
return null |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,16 +1,17 @@ |
|||
import DataFetch from "./DataFetch.js" |
|||
import { fetchRelationshipData } from "../api" |
|||
|
|||
export default class RelationshipFetch extends DataFetch { |
|||
async getData() { |
|||
const { datasource } = this.options |
|||
const res = await fetchRelationshipData({ |
|||
rowId: datasource?.rowId, |
|||
tableId: datasource?.rowTableId, |
|||
fieldName: datasource?.fieldName, |
|||
}) |
|||
return { |
|||
rows: res || [], |
|||
try { |
|||
const res = await this.API.fetchRelationshipData({ |
|||
rowId: datasource?.rowId, |
|||
tableId: datasource?.rowTableId, |
|||
fieldName: datasource?.fieldName, |
|||
}) |
|||
return { rows: res || [] } |
|||
} catch (error) { |
|||
return { rows: [] } |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue