diff --git a/packages/client/src/api.js b/packages/client/src/api.js deleted file mode 100644 index 5b37dd06a8..0000000000 --- a/packages/client/src/api.js +++ /dev/null @@ -1,137 +0,0 @@ -import { createAPIClient, Constants } from "@budibase/frontend-core" -import { notificationStore } from "./stores" -import { FieldTypes } from "./constants" - -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 => { - const { status, method, url, message, handled } = error || {} - - // Log any errors that we haven't manually handled - if (!handled) { - console.error("Unhandled error from API client", error) - return - } - - // Notify all errors - if (message) { - // Don't notify if the URL contains the word analytics as it may be - // blocked by browser extensions - if (!url?.includes("analytics")) { - notificationStore.actions.error(message) - } - } - - // Log all errors to console - console.warn(`[Client] HTTP ${status} on ${method}:${url}\n\t${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], Constants.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 -} diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js new file mode 100644 index 0000000000..469f01f6f5 --- /dev/null +++ b/packages/client/src/api/api.js @@ -0,0 +1,40 @@ +import { createAPIClient } from "@budibase/frontend-core" +import { notificationStore } from "../stores" + +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 => { + const { status, method, url, message, handled } = error || {} + + // Log any errors that we haven't manually handled + if (!handled) { + console.error("Unhandled error from API client", error) + return + } + + // Notify all errors + if (message) { + // Don't notify if the URL contains the word analytics as it may be + // blocked by browser extensions + if (!url?.includes("analytics")) { + notificationStore.actions.error(message) + } + } + + // Log all errors to console + console.warn(`[Client] HTTP ${status} on ${method}:${url}\n\t${message}`) + }, +}) diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js new file mode 100644 index 0000000000..5eb6b2b6f4 --- /dev/null +++ b/packages/client/src/api/index.js @@ -0,0 +1,9 @@ +import { API } from "./api.js" +import { patchAPI } from "./patches.js" + +// Certain endpoints which return rows need patched so that they transform +// and enrich the row docs, so that they can be correctly handled by the +// client library +patchAPI(API) + +export { API } diff --git a/packages/client/src/api/patches.js b/packages/client/src/api/patches.js new file mode 100644 index 0000000000..faad9c81ec --- /dev/null +++ b/packages/client/src/api/patches.js @@ -0,0 +1,107 @@ +import { Constants } from "@budibase/frontend-core" +import { FieldTypes } from "../constants" + +export const patchAPI = API => { + /** + * 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 + } + + // Enrich rows so they properly handle client bindings + const fetchSelf = API.fetchSelf + API.fetchSelf = async () => { + const user = await fetchSelf() + 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], Constants.TableNames.USERS))[0] + } + } else { + return null + } + } + const fetchRelationshipData = API.fetchRelationshipData + API.fetchRelationshipData = async params => { + const tableId = params?.tableId + const rows = await fetchRelationshipData(params) + return await enrichRows(rows, tableId) + } + const fetchTableData = API.fetchTableData + API.fetchTableData = async tableId => { + const rows = await fetchTableData(tableId) + return await enrichRows(rows, tableId) + } + const searchTable = API.searchTable + API.searchTable = async params => { + const tableId = params?.tableId + const output = await searchTable(params) + return { + ...output, + rows: await enrichRows(output?.rows, tableId), + } + } + const fetchViewData = API.fetchViewData + API.fetchViewData = async params => { + const tableId = params?.tableId + const rows = await fetchViewData(params) + return await enrichRows(rows, tableId) + } + + // Wipe any HBS formulae from table definitions, as these interfere with + // handlebars enrichment + const fetchTableDefinition = API.fetchTableDefinition + API.fetchTableDefinition = async tableId => { + const definition = await fetchTableDefinition(tableId) + Object.keys(definition?.schema || {}).forEach(field => { + if (definition.schema[field]?.type === "formula") { + delete definition.schema[field].formula + } + }) + return definition + } +} diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index fac285b253..4851b2cc02 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -1,4 +1,4 @@ -import { API } from "./api.js" +import { API } from "api" import { authStore, notificationStore, diff --git a/packages/client/src/stores/app.js b/packages/client/src/stores/app.js index 31aa5d0b00..a28a4cd9eb 100644 --- a/packages/client/src/stores/app.js +++ b/packages/client/src/stores/app.js @@ -1,4 +1,4 @@ -import { API } from "../api" +import { API } from "api" import { get, writable } from "svelte/store" const createAppStore = () => { diff --git a/packages/client/src/stores/auth.js b/packages/client/src/stores/auth.js index 58e5e3a096..39f11319cf 100644 --- a/packages/client/src/stores/auth.js +++ b/packages/client/src/stores/auth.js @@ -1,4 +1,4 @@ -import { API } from "../api" +import { API } from "api" import { writable } from "svelte/store" const createAuthStore = () => { diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index e6a0cf3e41..719909b538 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -1,7 +1,7 @@ import { writable, derived, get } from "svelte/store" import Manifest from "manifest.json" import { findComponentById, findComponentPathById } from "../utils/components" -import { API } from "../api" +import { API } from "api" const dispatchEvent = (type, data = {}) => { window.parent.postMessage({ type, data }) diff --git a/packages/client/src/stores/dataSource.js b/packages/client/src/stores/dataSource.js index 5b7e38054d..d5ad0cb594 100644 --- a/packages/client/src/stores/dataSource.js +++ b/packages/client/src/stores/dataSource.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import { API } from "../api" +import { API } from "api" import { FieldTypes } from "../constants" import { routeStore } from "./routes" diff --git a/packages/client/src/stores/routes.js b/packages/client/src/stores/routes.js index b4274197ae..69cd42d5f5 100644 --- a/packages/client/src/stores/routes.js +++ b/packages/client/src/stores/routes.js @@ -1,6 +1,6 @@ import { get, writable } from "svelte/store" import { push } from "svelte-spa-router" -import { API } from "../api" +import { API } from "api" import { peekStore } from "./peek" import { builderStore } from "./builder" diff --git a/packages/client/src/utils/schema.js b/packages/client/src/utils/schema.js index 5cedef3aed..fba25e384a 100644 --- a/packages/client/src/utils/schema.js +++ b/packages/client/src/utils/schema.js @@ -1,4 +1,4 @@ -import { API } from "../api.js" +import { API } from "api" import { JSONUtils } from "@budibase/frontend-core" import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch.js" import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch.js" diff --git a/packages/frontend-core/src/api/index.js b/packages/frontend-core/src/api/index.js index 469dbfa0d4..2c1df6ffaf 100644 --- a/packages/frontend-core/src/api/index.js +++ b/packages/frontend-core/src/api/index.js @@ -25,7 +25,6 @@ import { buildViewEndpoints } from "./views" const defaultAPIClientConfig = { attachHeaders: null, onError: null, - patches: null, } /** @@ -185,7 +184,7 @@ export const createAPIClient = config => { } // Attach all endpoints - API = { + return { ...API, ...buildAnalyticsEndpoints(API), ...buildAppEndpoints(API), @@ -210,18 +209,4 @@ export const createAPIClient = config => { ...buildUserEndpoints(API), ...buildViewEndpoints(API), } - - // Assign any patches - const patches = Object.entries(config.patches || {}) - if (patches.length) { - patches.forEach(([method, fn]) => { - const baseFn = API[method] - API[method] = async (...params) => { - const output = await baseFn(...params) - return await fn({ params, output }) - } - }) - } - - return API }