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" |
export const buildAnalyticsEndpoints = API => ({ |
||||
|
/** |
||||
/** |
* Notifies that an end user client app has been loaded. |
||||
* Notifies that an end user client app has been loaded. |
*/ |
||||
*/ |
pingEndUser: async () => { |
||||
export const pingEndUser = async () => { |
return await API.post({ |
||||
return await API.post({ |
url: `/api/analytics/ping`, |
||||
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" |
export const buildAppEndpoints = API => ({ |
||||
|
/** |
||||
/** |
* Fetches screen definition for an app. |
||||
* Fetches screen definition for an app. |
*/ |
||||
*/ |
fetchAppPackage: async appId => { |
||||
export const fetchAppPackage = async appId => { |
return await API.get({ |
||||
return await API.get({ |
url: `/api/applications/${appId}/appPackage`, |
||||
url: `/api/applications/${appId}/appPackage`, |
}) |
||||
}) |
}, |
||||
} |
}) |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
import API from "./api" |
export const buildAttachmentEndpoints = API => ({ |
||||
|
/** |
||||
/** |
* Uploads an attachment to the server. |
||||
* Uploads an attachment to the server. |
*/ |
||||
*/ |
uploadAttachment: async ({ data, tableId }) => { |
||||
export const uploadAttachment = async (data, tableId = "") => { |
return await API.post({ |
||||
return await API.post({ |
url: `/api/attachments/${tableId}/upload`, |
||||
url: `/api/attachments/${tableId}/upload`, |
body: data, |
||||
body: data, |
json: false, |
||||
json: false, |
}) |
||||
}) |
}, |
||||
} |
}) |
||||
|
|||||
@ -1,36 +1,27 @@ |
|||||
import API from "./api" |
export const buildAuthEndpoints = API => ({ |
||||
import { enrichRows } from "./rows" |
/** |
||||
import { TableNames } from "../constants" |
* Performs a log in request. |
||||
|
*/ |
||||
/** |
logIn: async ({ email, password }) => { |
||||
* Performs a log in request. |
if (!email) { |
||||
*/ |
return API.error("Please enter your email") |
||||
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] |
|
||||
} |
} |
||||
} else { |
if (!password) { |
||||
return null |
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" |
export const buildAutomationEndpoints = API => ({ |
||||
import API from "./api" |
/** |
||||
|
* Executes an automation. Must have "App Action" trigger. |
||||
/** |
*/ |
||||
* Executes an automation. Must have "App Action" trigger. |
triggerAutomation: async ({ automationId, fields }) => { |
||||
*/ |
return await API.post({ |
||||
export const triggerAutomation = async (automationId, fields) => { |
url: `/api/automations/${automationId}/trigger`, |
||||
const res = await API.post({ |
body: { fields }, |
||||
url: `/api/automations/${automationId}/trigger`, |
}) |
||||
body: { fields }, |
}, |
||||
}) |
}) |
||||
res.error |
|
||||
? notificationStore.actions.error("An error has occurred") |
|
||||
: notificationStore.actions.success("Automation triggered") |
|
||||
return res |
|
||||
} |
|
||||
|
|||||
@ -1,11 +1,188 @@ |
|||||
export * from "./rows" |
import { ApiVersion } from "../constants" |
||||
export * from "./auth" |
import { buildAnalyticsEndpoints } from "./analytics" |
||||
export * from "./tables" |
import { buildAppEndpoints } from "./app" |
||||
export * from "./attachments" |
import { buildAttachmentEndpoints } from "./attachments" |
||||
export * from "./views" |
import { buildAuthEndpoints } from "./auth" |
||||
export * from "./relationships" |
import { buildAutomationEndpoints } from "./automations" |
||||
export * from "./routes" |
import { buildQueryEndpoints } from "./queries" |
||||
export * from "./queries" |
import { buildRelationshipEndpoints } from "./relationships" |
||||
export * from "./app" |
import { buildRouteEndpoints } from "./routes" |
||||
export * from "./automations" |
import { buildRowEndpoints } from "./rows" |
||||
export * from "./analytics" |
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" |
export const buildQueryEndpoints = API => ({ |
||||
import API from "./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. |
* Fetches the definition of an external query. |
||||
*/ |
*/ |
||||
export const executeQuery = async ({ queryId, pagination, parameters }) => { |
fetchQueryDefinition: async queryId => { |
||||
const query = await fetchQueryDefinition(queryId) |
return await API.get({ |
||||
if (query?.datasourceId == null) { |
url: `/api/queries/${queryId}`, |
||||
notificationStore.actions.error("That query couldn't be found") |
cache: true, |
||||
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 }) |
|
||||
} |
|
||||
|
|||||
@ -1,14 +1,12 @@ |
|||||
import API from "./api" |
export const buildRelationshipEndpoints = API => ({ |
||||
import { enrichRows } from "./rows" |
/** |
||||
|
* Fetches related rows for a certain field of a certain row. |
||||
/** |
*/ |
||||
* Fetches related rows for a certain field of a certain row. |
fetchRelationshipData: async ({ tableId, rowId, fieldName }) => { |
||||
*/ |
if (!tableId || !rowId || !fieldName) { |
||||
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => { |
return [] |
||||
if (!tableId || !rowId || !fieldName) { |
} |
||||
return [] |
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) |
||||
} |
return response[fieldName] || [] |
||||
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) |
}, |
||||
const rows = response[fieldName] || [] |
}) |
||||
return await enrichRows(rows, tableId) |
|
||||
} |
|
||||
|
|||||
@ -1,10 +1,10 @@ |
|||||
import API from "./api" |
export const buildRouteEndpoints = API => ({ |
||||
|
/** |
||||
/** |
* Fetches available routes for the client app. |
||||
* Fetches available routes for the client app. |
*/ |
||||
*/ |
fetchClientAppRoutes: async () => { |
||||
export const fetchRoutes = async () => { |
return await API.get({ |
||||
return await API.get({ |
url: `/api/routing/client`, |
||||
url: `/api/routing/client`, |
}) |
||||
}) |
}, |
||||
} |
}) |
||||
|
|||||
@ -1,155 +1,43 @@ |
|||||
import { notificationStore, dataSourceStore } from "stores" |
export const buildRowEndpoints = API => ({ |
||||
import API from "./api" |
/** |
||||
import { fetchTableDefinition } from "./tables" |
* Fetches data about a certain row in a table. |
||||
import { FieldTypes } from "../constants" |
*/ |
||||
|
fetchRow: async ({ tableId, rowId }) => { |
||||
/** |
if (!tableId || !rowId) { |
||||
* Fetches data about a certain row in a table. |
return null |
||||
*/ |
} |
||||
export const fetchRow = async ({ tableId, rowId }) => { |
const row = await API.get({ |
||||
if (!tableId || !rowId) { |
url: `/api/${tableId}/rows/${rowId}`, |
||||
return |
}) |
||||
} |
return (await API.enrichRows([row], tableId))[0] |
||||
const row = await API.get({ |
}, |
||||
url: `/api/${tableId}/rows/${rowId}`, |
|
||||
}) |
/** |
||||
return (await enrichRows([row], tableId))[0] |
* Creates a row in a table. |
||||
} |
*/ |
||||
|
saveRow: async row => { |
||||
/** |
if (!row?.tableId) { |
||||
* Creates a row in a table. |
return |
||||
*/ |
} |
||||
export const saveRow = async row => { |
return await API.post({ |
||||
if (!row?.tableId) { |
url: `/api/${row.tableId}/rows`, |
||||
return |
body: row, |
||||
} |
}) |
||||
const res = await API.post({ |
}, |
||||
url: `/api/${row.tableId}/rows`, |
|
||||
body: row, |
/** |
||||
}) |
* Deletes a row from a table. |
||||
res.error |
*/ |
||||
? notificationStore.actions.error("An error has occurred") |
deleteRow: async ({ tableId, rowId, revId }) => { |
||||
: notificationStore.actions.success("Row saved") |
if (!tableId || !rowId || !revId) { |
||||
|
return |
||||
// 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 |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
} |
return await API.delete({ |
||||
return rows |
url: `/api/${tableId}/rows`, |
||||
} |
body: { |
||||
|
_id: rowId, |
||||
|
_rev: revId, |
||||
|
}, |
||||
|
}) |
||||
|
}, |
||||
|
}) |
||||
|
|||||
@ -1,63 +1,51 @@ |
|||||
import API from "./api" |
export const buildTableEndpoints = API => ({ |
||||
import { enrichRows } from "./rows" |
/** |
||||
|
* 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. |
* Fetches all rows from a table. |
||||
* Since definitions cannot change at runtime, the result is cached. |
*/ |
||||
*/ |
fetchTableData: async tableId => { |
||||
export const fetchTableDefinition = async tableId => { |
return await API.get({ url: `/api/${tableId}/rows` }) |
||||
const res = await API.get({ url: `/api/tables/${tableId}`, cache: true }) |
}, |
||||
|
|
||||
// Wipe any HBS formulae, as these interfere with handlebars enrichment
|
/** |
||||
Object.keys(res?.schema || {}).forEach(field => { |
* Searches a table using Lucene. |
||||
if (res.schema[field]?.type === "formula") { |
*/ |
||||
delete res.schema[field].formula |
searchTable: async ({ |
||||
|
tableId, |
||||
|
query, |
||||
|
bookmark, |
||||
|
limit, |
||||
|
sort, |
||||
|
sortOrder, |
||||
|
sortType, |
||||
|
paginate, |
||||
|
}) => { |
||||
|
if (!tableId || !query) { |
||||
|
return { |
||||
|
rows: [], |
||||
|
} |
||||
} |
} |
||||
}) |
return await API.post({ |
||||
|
url: `/api/${tableId}/search`, |
||||
return res |
body: { |
||||
} |
query, |
||||
|
bookmark, |
||||
/** |
limit, |
||||
* Fetches all rows from a table. |
sort, |
||||
*/ |
sortOrder, |
||||
export const fetchTableData = async tableId => { |
sortType, |
||||
const rows = await API.get({ url: `/api/${tableId}/rows` }) |
paginate, |
||||
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), |
|
||||
} |
|
||||
} |
|
||||
|
|||||
@ -1,30 +1,19 @@ |
|||||
import API from "./api" |
export const buildViewEndpoints = API => ({ |
||||
import { enrichRows } from "./rows" |
/** |
||||
|
* Fetches all rows in a view. |
||||
/** |
*/ |
||||
* Fetches all rows in a view. |
fetchViewData: async ({ name, field, groupBy, calculation }) => { |
||||
*/ |
const params = new URLSearchParams() |
||||
export const fetchViewData = async ({ |
if (calculation) { |
||||
name, |
params.set("field", field) |
||||
field, |
params.set("calculation", calculation) |
||||
groupBy, |
} |
||||
calculation, |
if (groupBy) { |
||||
tableId, |
params.set("group", groupBy ? "true" : "false") |
||||
}) => { |
} |
||||
const params = new URLSearchParams() |
const QUERY_VIEW_URL = field |
||||
|
? `/api/views/${name}?${params}` |
||||
if (calculation) { |
: `/api/views/${name}` |
||||
params.set("field", field) |
return await API.get({ url: QUERY_VIEW_URL }) |
||||
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) |
|
||||
} |
|
||||
|
|||||
@ -1,13 +1,16 @@ |
|||||
import FieldFetch from "./FieldFetch.js" |
import FieldFetch from "./FieldFetch.js" |
||||
import { fetchTableDefinition } from "../api" |
|
||||
import { getJSONArrayDatasourceSchema } from "../utils/json" |
import { getJSONArrayDatasourceSchema } from "../utils/json" |
||||
|
|
||||
export default class JSONArrayFetch extends FieldFetch { |
export default class JSONArrayFetch extends FieldFetch { |
||||
static async getDefinition(datasource) { |
static async getDefinition(datasource) { |
||||
// JSON arrays need their table definitions fetched.
|
// JSON arrays need their table definitions fetched.
|
||||
// We can then extract their schema as a subset of the table schema.
|
// We can then extract their schema as a subset of the table schema.
|
||||
const table = await fetchTableDefinition(datasource.tableId) |
try { |
||||
const schema = getJSONArrayDatasourceSchema(table?.schema, datasource) |
const table = await this.API.fetchTableDefinition(datasource.tableId) |
||||
return { schema } |
const schema = getJSONArrayDatasourceSchema(table?.schema, datasource) |
||||
|
return { schema } |
||||
|
} catch (error) { |
||||
|
return null |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,16 +1,17 @@ |
|||||
import DataFetch from "./DataFetch.js" |
import DataFetch from "./DataFetch.js" |
||||
import { fetchRelationshipData } from "../api" |
|
||||
|
|
||||
export default class RelationshipFetch extends DataFetch { |
export default class RelationshipFetch extends DataFetch { |
||||
async getData() { |
async getData() { |
||||
const { datasource } = this.options |
const { datasource } = this.options |
||||
const res = await fetchRelationshipData({ |
try { |
||||
rowId: datasource?.rowId, |
const res = await this.API.fetchRelationshipData({ |
||||
tableId: datasource?.rowTableId, |
rowId: datasource?.rowId, |
||||
fieldName: datasource?.fieldName, |
tableId: datasource?.rowTableId, |
||||
}) |
fieldName: datasource?.fieldName, |
||||
return { |
}) |
||||
rows: res || [], |
return { rows: res || [] } |
||||
|
} catch (error) { |
||||
|
return { rows: [] } |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue