From 8bfb909280f769889e90dd6af44cbaa316c9cd8c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 26 Jun 2023 09:22:40 +0100 Subject: [PATCH 01/12] Fix scroll overflow in binding drawer --- .../builder/src/components/common/bindings/BindingPanel.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index c95988e90c..beb205fa81 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -469,10 +469,12 @@ display: flex; flex-direction: column; gap: var(--spacing-xl); + overflow: hidden; } .overlay-wrap { position: relative; flex: 1; + overflow: hidden; } .mode-overlay { position: absolute; From 5f407259dc3229ccd0521bd1f9ad402c6b18077c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 26 Jun 2023 10:39:38 +0100 Subject: [PATCH 02/12] Enable component nesting validation for new apps --- .../builder/src/builderStore/store/frontend.js | 17 +++++++++++++---- packages/client/manifest.json | 4 ++-- .../server/src/api/controllers/application.ts | 3 +++ packages/types/src/documents/app/app.ts | 5 +++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index d0414b5733..8af6a87787 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -61,6 +61,9 @@ const INITIAL_FRONTEND_STATE = { showNotificationAction: false, sidePanel: false, }, + features: { + componentValidation: false, + }, errors: [], hasAppPackage: false, libraries: null, @@ -145,6 +148,10 @@ export const getFrontendStore = () => { navigation: application.navigation || {}, usedPlugins: application.usedPlugins || [], hasLock, + features: { + ...INITIAL_FRONTEND_STATE.features, + ...application.features, + }, initialised: true, })) screenHistoryStore.reset() @@ -280,9 +287,12 @@ export const getFrontendStore = () => { } }, save: async screen => { - // Validate screen structure - // Temporarily disabled to accommodate migration issues - // store.actions.screens.validate(screen) + const state = get(store) + + // Validate screen structure if the app supports it + if (state.features?.componentValidation) { + store.actions.screens.validate(screen) + } // Check screen definition for any component settings which need updated store.actions.screens.enrichEmptySettings(screen) @@ -293,7 +303,6 @@ export const getFrontendStore = () => { const routesResponse = await API.fetchAppRoutes() // If plugins changed we need to fetch the latest app metadata - const state = get(store) let usedPlugins = state.usedPlugins if (savedScreen.pluginAdded) { const { application } = await API.fetchAppPackage(state.appId) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 76d7aff229..0a38cc315a 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2216,7 +2216,7 @@ "name": "Form", "icon": "Form", "hasChildren": true, - "illegalChildren": ["section", "form"], + "illegalChildren": ["section", "form", "formblock"], "actions": [ "ValidateForm", "ClearForm", @@ -2304,7 +2304,7 @@ "name": "Form Step", "icon": "AssetsAdded", "hasChildren": true, - "illegalChildren": ["section", "form", "form step"], + "illegalChildren": ["section", "form", "formstep", "formblock"], "styles": ["size"], "size": { "width": 400, diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index cf7e8e122b..53453b8538 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -308,6 +308,9 @@ async function performAppCreate(ctx: UserCtx) { customTheme: { buttonBorderRadius: "16px", }, + features: { + componentValidation: true, + }, } // If we used a template or imported an app there will be an existing doc. diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index c91d575714..258eaef297 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -20,6 +20,7 @@ export interface App extends Document { navigation?: AppNavigation automationErrors?: AppMetadataErrors icon?: AppIcon + features?: AppFeatures } export interface AppInstance { @@ -60,3 +61,7 @@ export interface AppIcon { name: string color: string } + +export interface AppFeatures { + componentValidation?: boolean +} From 2a2215f48514b35f11339d94f257f7eb406e5da3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 26 Jun 2023 10:50:54 +0100 Subject: [PATCH 03/12] Display users table as its own option at the very top of the datasource list --- .../DatasourceNavigator.svelte | 20 +++++++++++++++++-- .../TableNavigator/TableNavigator.svelte | 12 ++++------- .../data/datasource/bb_internal/index.svelte | 5 ++++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index c7723f2bc0..e2dd1b4cc3 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -12,8 +12,10 @@ customQueryText, } from "helpers/data/utils" import IntegrationIcon from "./IntegrationIcon.svelte" + import { TableNames } from "constants" let openDataSources = [] + $: enrichedDataSources = enrichDatasources( $datasources, $params, @@ -71,6 +73,13 @@ $goto(`./datasource/${datasource._id}`) } + const selectTable = tableId => { + tables.select(tableId) + if (!$isActive("./table/:tableId")) { + $goto(`./table/${tableId}`) + } + } + function closeNode(datasource) { openDataSources = openDataSources.filter(id => datasource._id !== id) } @@ -151,9 +160,16 @@ {#if $database?._id}
+ selectTable(TableNames.USERS)} + /> {#each enrichedDataSources as datasource, idx} 0} + border text={datasource.name} opened={datasource.open} selected={$isActive("./datasource") && datasource.selected} @@ -174,7 +190,7 @@ {#if datasource.open} - + {#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} b.name?.toLowerCase() ? 1 : -1 export let sourceId + export let selectTable $: sortedTables = $tables.list - .filter(table => table.sourceId === sourceId) + .filter( + table => table.sourceId === sourceId && table._id !== TableNames.USERS + ) .sort(alphabetical) - - const selectTable = tableId => { - tables.select(tableId) - if (!$isActive("./table/:tableId")) { - $goto(`./table/${tableId}`) - } - } {#if $database?._id} diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/index.svelte index 33f3b626a2..a2ee0cf19b 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/bb_internal/index.svelte @@ -6,12 +6,15 @@ import { goto } from "@roxi/routify" import { onMount } from "svelte" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" + import { TableNames } from "constants" let modal $: internalTablesBySourceId = $tables.list.filter( table => - table.type !== "external" && table.sourceId === BUDIBASE_INTERNAL_DB_ID + table.type !== "external" && + table.sourceId === BUDIBASE_INTERNAL_DB_ID && + table._id !== TableNames.USERS ) onMount(() => { From a3204ad17a0814f04400881b0fc97c8c0dd812e7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 26 Jun 2023 16:34:22 +0100 Subject: [PATCH 04/12] Update data section routing to properly handle datasource entities/table sync and handle having no data --- .../app/[application]/data/_layout.svelte | 11 +++- .../data/datasource/index.svelte | 6 ++- .../app/[application]/data/index.svelte | 16 +++--- .../app/[application]/data/table/index.svelte | 16 +++--- .../builder/src/stores/backend/datasources.js | 50 ++++++++++++++++--- 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/data/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/_layout.svelte index c0813fd2b8..a2db33306c 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/_layout.svelte @@ -2,8 +2,17 @@ import { Button, Layout } from "@budibase/bbui" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import Panel from "components/design/Panel.svelte" - import { isActive, goto } from "@roxi/routify" + import { isActive, goto, redirect } from "@roxi/routify" import BetaButton from "./_components/BetaButton.svelte" + import { datasources } from "stores/backend" + + $: { + // If we ever don't have any data other than the users table, prompt the + // user to add some + if (!$datasources.hasData) { + $redirect("./new") + } + } diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte index d3b962a226..347e1138d4 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte @@ -4,11 +4,13 @@ import { onMount } from "svelte" onMount(async () => { - const { list, selected } = $datasources + const { list, selected, hasData } = $datasources if (selected) { $redirect(`./${selected?._id}`) - } else { + } else if (hasData && list?.length) { $redirect(`./${list[0]._id}`) + } else { + $redirect("../new") } }) diff --git a/packages/builder/src/pages/builder/app/[application]/data/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/index.svelte index 47939f09b4..9ccc9bffcf 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/index.svelte @@ -1,17 +1,13 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/index.svelte index 6f12ae38ac..f513d8ceb9 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/index.svelte @@ -1,14 +1,16 @@ diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index 56c0a1b688..592437b629 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -1,9 +1,14 @@ import { writable, derived, get } from "svelte/store" -import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend" +import { + IntegrationTypes, + DEFAULT_BB_DATASOURCE_ID, + BUDIBASE_INTERNAL_DB_ID, +} from "constants/backend" import { queries, tables } from "./" import { API } from "api" import { DatasourceFeature } from "@budibase/types" import { notifications } from "@budibase/bbui" +import { TableNames } from "constants" export class ImportTableError extends Error { constructor(message) { @@ -24,13 +29,42 @@ export function createDatasourcesStore() { schemaError: null, }) - const derivedStore = derived(store, $store => ({ - ...$store, - selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId), - hasDefaultData: $store.list.some( - datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID - ), - })) + const derivedStore = derived([store, tables], ([$store, $tables]) => { + // Set the internal datasource entities from the table list, which we're + // able to keep updated unlike the egress generated definition of the + // internal datasource + let internalDS = $store.list?.find(ds => ds._id === BUDIBASE_INTERNAL_DB_ID) + let otherDS = $store.list?.find(ds => ds._id !== BUDIBASE_INTERNAL_DB_ID) + if (internalDS) { + internalDS = { + ...internalDS, + entities: $tables.list?.filter(table => { + return ( + table.sourceId === BUDIBASE_INTERNAL_DB_ID && + table._id !== TableNames.USERS + ) + }), + } + } + + // Build up enriched DS list + // Only add the internal DS if we have at least one non-users table + let list = [] + if (internalDS?.entities?.length) { + list.push(internalDS) + } + list = list.concat(otherDS || []) + + return { + ...$store, + list, + selected: list?.find(ds => ds._id === $store.selectedDatasourceId), + hasDefaultData: list?.some( + datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID + ), + hasData: !!internalDS?.entities?.length || list?.length > 1, + } + }) const fetch = async () => { const datasources = await API.getDatasources() From 9e3711a4c682cb4ec6f5ff4208f7c3e0a7f73b56 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 26 Jun 2023 18:14:12 +0100 Subject: [PATCH 05/12] Fix data check logic --- packages/builder/src/stores/backend/datasources.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index 963f206c94..d45d8c7095 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -61,7 +61,7 @@ export function createDatasourcesStore() { hasDefaultData: list?.some( datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID ), - hasData: !!internalDS?.entities?.length || list?.length > 1, + hasData: list?.length > 0, } }) From cfdd6bafb742b5e6f5fe5134f87616f0fc41c468 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 11:33:23 +0100 Subject: [PATCH 06/12] Rewrite how middlewares are ran inside sockets, and fix collaboration --- packages/server/src/websockets/grid.ts | 50 ++++++------ packages/server/src/websockets/middleware.ts | 85 ++++++++++++++++++++ packages/server/src/websockets/websocket.ts | 66 +++++---------- 3 files changed, 133 insertions(+), 68 deletions(-) create mode 100644 packages/server/src/websockets/middleware.ts diff --git a/packages/server/src/websockets/grid.ts b/packages/server/src/websockets/grid.ts index 2a797623cf..ffe26828bc 100644 --- a/packages/server/src/websockets/grid.ts +++ b/packages/server/src/websockets/grid.ts @@ -1,12 +1,15 @@ import authorized from "../middleware/authorized" +import currentApp from "../middleware/currentapp" import { BaseSocket } from "./websocket" -import { context, permissions } from "@budibase/backend-core" +import { auth, permissions } from "@budibase/backend-core" import http from "http" import Koa from "koa" import { getTableId } from "../api/controllers/row/utils" import { Row, Table } from "@budibase/types" import { Socket } from "socket.io" import { GridSocketEvent } from "@budibase/shared-core" +import { userAgent } from "koa-useragent" +import { createContext, runMiddlewares } from "./middleware" const { PermissionType, PermissionLevel } = permissions @@ -26,28 +29,27 @@ export default class GridSocket extends BaseSocket { return } - // Check if the user has permission to read this resource - const middleware = authorized( - PermissionType.TABLE, - PermissionLevel.READ - ) - const ctx = { - appId, + // Create context + const ctx = createContext(this.app, socket, { resourceId: tableId, - roleId: socket.data.roleId, - user: { _id: socket.data._id }, - isAuthenticated: socket.data.isAuthenticated, - request: { - url: "/fake", - }, - get: () => null, - throw: () => { - // If they don't have access, immediately disconnect them - socket.disconnect(true) - }, - } - await context.doInAppContext(appId, async () => { - await middleware(ctx, async () => { + appId, + }) + + // Construct full middleware chain to assess permissions + const middlewares = [ + userAgent, + auth.buildAuthMiddleware([], { + publicAllowed: true, + }), + currentApp, + authorized(PermissionType.TABLE, PermissionLevel.READ), + ] + + // Run all koa middlewares + try { + await runMiddlewares(ctx, middlewares, async () => { + // Middlewares are finished and we have permission + // Join room for this resource const room = `${appId}-${tableId}` await this.joinRoom(socket, room) @@ -55,7 +57,9 @@ export default class GridSocket extends BaseSocket { const sessions = await this.getRoomSessions(room) callback({ users: sessions }) }) - }) + } catch (error) { + socket.disconnect(true) + } } ) diff --git a/packages/server/src/websockets/middleware.ts b/packages/server/src/websockets/middleware.ts new file mode 100644 index 0000000000..2b6e7168af --- /dev/null +++ b/packages/server/src/websockets/middleware.ts @@ -0,0 +1,85 @@ +import { Socket } from "socket.io" +import Cookies from "cookies" +import http from "http" +import Koa from "koa" +import { Header } from "@budibase/backend-core" + +/** + * Constructs a fake Koa context to use for manually running middlewares in + * sockets + * @param app the Koa app + * @param socket the socket.io socket instance + * @param options additional metadata to populate the context with + */ +export const createContext = ( + app: Koa, + socket: Socket, + options?: WebsocketContextOptions +) => { + const res = new http.ServerResponse(socket.request) + const context: WebsocketContext = { + ...app.createContext(socket.request, res), + + // Additional overrides needed to make our middlewares work with this + // fake koa context + resourceId: options?.resourceId, + path: "/fake", + request: { + url: "/fake", + headers: { + [Header.APP_ID]: options?.appId, + }, + }, + cookies: new Cookies(socket.request, res), + get: (field: string) => socket.request.headers?.[field] as string, + throw: (...params: any[]) => { + // Throw has a bunch of different signatures, so we'll just stringify + // whatever params we get given + throw new Error( + ...(params?.join(" ") || "Unknown error in socket middleware") + ) + }, + + // Needed for koa-useragent middleware + headers: socket.request.headers, + header: socket.request.headers, + } + return context +} + +/** + * Runs a list of middlewares, nesting each callback inside each other mimic + * how the real middlewares run and ensuring that app or tenant context work + * as expected + * @param ctx the Koa context + * @param middlewares the array of middlewares to run + * @param callback a final callback for when all middlewares are completed + */ +export const runMiddlewares = async ( + ctx: any, + middlewares: any[], + callback: Function +) => { + if (!middlewares[0]) { + await callback() + } else { + await middlewares[0](ctx, async () => { + await runMiddlewares(ctx, middlewares.slice(1), callback) + }) + } +} + +export interface WebsocketContext extends Omit { + request: { + url: string + headers: { + [key: string]: string | undefined + } + } + cookies: Cookies +} + +export interface WebsocketContextOptions { + appId?: string + resourceId?: string +} diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts index 52351aea36..94636010fa 100644 --- a/packages/server/src/websockets/websocket.ts +++ b/packages/server/src/websockets/websocket.ts @@ -1,7 +1,6 @@ import { Server } from "socket.io" import http from "http" import Koa from "koa" -import Cookies from "cookies" import { userAgent } from "koa-useragent" import { auth, Header, redis } from "@budibase/backend-core" import { createAdapter } from "@socket.io/redis-adapter" @@ -10,14 +9,17 @@ import { getSocketPubSubClients } from "../utilities/redis" import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core" import { SocketSession } from "@budibase/types" import { v4 as uuid } from "uuid" +import { createContext, runMiddlewares } from "./middleware" const anonUser = () => ({ _id: uuid(), email: "user@mail.com", firstName: "Anonymous", + tenantId: "default", }) export class BaseSocket { + app: Koa io: Server path: string redisClient?: redis.Client @@ -28,6 +30,7 @@ export class BaseSocket { path: string = "/", additionalMiddlewares?: any[] ) { + this.app = app this.path = path this.io = new Server(server, { path, @@ -45,52 +48,25 @@ export class BaseSocket { // Apply middlewares this.io.use(async (socket, next) => { - // Build fake koa context - const res = new http.ServerResponse(socket.request) - const ctx: any = { - ...app.createContext(socket.request, res), + const ctx = createContext(this.app, socket) - // Additional overrides needed to make our middlewares work with this - // fake koa context - cookies: new Cookies(socket.request, res), - get: (field: string) => socket.request.headers[field], - throw: (code: number, message: string) => { - throw new Error(message) - }, - - // Needed for koa-useragent middleware - headers: socket.request.headers, - header: socket.request.headers, - - // We don't really care about the path since it will never contain - // an app ID - path: "/socket", - } - - // Run all koa middlewares try { - for (let [idx, middleware] of middlewares.entries()) { - await middleware(ctx, () => { - if (idx === middlewares.length - 1) { - // Middlewares are finished - // Extract some data from our enriched koa context to persist - // as metadata for the socket - const user = ctx.user?._id ? ctx.user : anonUser() - const { _id, email, firstName, lastName } = user - socket.data = { - _id, - email, - firstName, - lastName, - sessionId: socket.id, - connectedAt: Date.now(), - isAuthenticated: ctx.isAuthenticated, - roleId: ctx.roleId, - } - next() - } - }) - } + await runMiddlewares(ctx, middlewares, () => { + // Middlewares are finished + // Extract some data from our enriched koa context to persist + // as metadata for the socket + const user = ctx.user?._id ? ctx.user : anonUser() + const { _id, email, firstName, lastName } = user + socket.data = { + _id, + email, + firstName, + lastName, + sessionId: socket.id, + connectedAt: Date.now(), + } + next() + }) } catch (error: any) { next(error) } From 0d8d3a4851d731e4db868d265224b5b9f3685882 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 11:58:10 +0100 Subject: [PATCH 07/12] Handle showing errors inside grids --- .../src/components/grid/layout/Grid.svelte | 29 ++++++++++++++++++- .../src/components/grid/stores/rows.js | 15 +++++++++- packages/frontend-core/src/fetch/DataFetch.js | 5 ++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte index e94cf5da93..5207f293b1 100644 --- a/packages/frontend-core/src/components/grid/layout/Grid.svelte +++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte @@ -68,6 +68,7 @@ rowHeight, contentLines, gridFocused, + error, } = context // Keep config store up to date with props @@ -149,8 +150,15 @@
+ {:else if $error} +
+
There was a problem loading your grid
+
+ {$error} +
+
{/if} - {#if $loading} + {#if $loading && !$error}
@@ -273,6 +281,25 @@ opacity: 0.6; } + /* Error */ + .grid-error { + position: absolute; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + } + .grid-error-title { + font-size: 18px; + font-weight: 600; + } + .grid-error-subtitle { + font-size: 16px; + } + /* Disable checkbox animation anywhere in the grid data */ .grid-data-outer :global(.spectrum-Checkbox-box:before), .grid-data-outer :global(.spectrum-Checkbox-box:after), diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 1a98ef1848..d348d5caa5 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -14,6 +14,7 @@ export const createStores = () => { const rowChangeCache = writable({}) const inProgressChanges = writable({}) const hasNextPage = writable(false) + const error = writable(null) // Generate a lookup map to quick find a row by ID const rowLookupMap = derived( @@ -47,6 +48,7 @@ export const createStores = () => { rowChangeCache, inProgressChanges, hasNextPage, + error, } } @@ -68,6 +70,7 @@ export const deriveStores = context => { inProgressChanges, previousFocusedRowId, hasNextPage, + error, } = context const instanceLoaded = writable(false) const fetch = writable(null) @@ -122,7 +125,17 @@ export const deriveStores = context => { // Subscribe to changes of this fetch model unsubscribe = newFetch.subscribe(async $fetch => { - if ($fetch.loaded && !$fetch.loading) { + if ($fetch.error) { + // Present a helpful error to the user + let message = "An unknown error occurred" + if ($fetch.error.status === 403) { + message = "You don't have access to this data" + } else if ($fetch.error.message) { + message = $fetch.error.message + } + error.set(message) + } else if ($fetch.loaded && !$fetch.loading) { + error.set(null) hasNextPage.set($fetch.hasNextPage) const $instanceLoaded = get(instanceLoaded) const resetRows = $fetch.resetKey !== lastResetKey diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index b456753d4a..f9d7fd2624 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -57,6 +57,7 @@ export default class DataFetch { cursor: null, cursors: [], resetKey: Math.random(), + error: null, }) // Merge options with their default values @@ -252,6 +253,10 @@ export default class DataFetch { try { return await this.API.fetchTableDefinition(datasource.tableId) } catch (error) { + this.store.update(state => ({ + ...state, + error, + })) return null } } From 17e7a2d59ef76d26438a7f0ea9a1e9a22284f1bb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 12:03:51 +0100 Subject: [PATCH 08/12] Ensure tables and queries are kept in sync when datasource changes occur --- .../builder/src/stores/backend/datasources.js | 43 ++++++++----------- .../builder/src/stores/backend/queries.js | 8 ++++ packages/builder/src/stores/backend/tables.js | 8 ++++ 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index d45d8c7095..bce92daced 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -4,7 +4,7 @@ import { DEFAULT_BB_DATASOURCE_ID, BUDIBASE_INTERNAL_DB_ID, } from "constants/backend" -import { queries, tables } from "./" +import { tables, queries } from "./" import { API } from "api" import { DatasourceFeature } from "@budibase/types" import { TableNames } from "constants" @@ -33,7 +33,7 @@ export function createDatasourcesStore() { // able to keep updated unlike the egress generated definition of the // internal datasource let internalDS = $store.list?.find(ds => ds._id === BUDIBASE_INTERNAL_DB_ID) - let otherDS = $store.list?.find(ds => ds._id !== BUDIBASE_INTERNAL_DB_ID) + let otherDS = $store.list?.filter(ds => ds._id !== BUDIBASE_INTERNAL_DB_ID) if (internalDS) { internalDS = { ...internalDS, @@ -84,20 +84,14 @@ export function createDatasourcesStore() { const updateDatasource = response => { const { datasource, error } = response - store.update(state => { - const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) - const sources = state.list - if (currentIdx >= 0) { - sources.splice(currentIdx, 1, datasource) - } else { - sources.push(datasource) - } - return { - list: sources, - selectedDatasourceId: datasource._id, + if (error) { + store.update(state => ({ + ...state, schemaError: error, - } - }) + })) + } + replaceDatasource(datasource._id, datasource) + select(datasource._id) return datasource } @@ -165,18 +159,14 @@ export function createDatasourcesStore() { } const deleteDatasource = async datasource => { + if (!datasource?._id || !datasource?._rev) { + return + } await API.deleteDatasource({ - datasourceId: datasource?._id, - datasourceRev: datasource?._rev, + datasourceId: datasource._id, + datasourceRev: datasource._rev, }) - store.update(state => { - const sources = state.list.filter( - existing => existing._id !== datasource._id - ) - return { list: sources, selected: null } - }) - await queries.fetch() - await tables.fetch() + replaceDatasource(datasource._id, null) } const removeSchemaError = () => { @@ -185,7 +175,6 @@ export function createDatasourcesStore() { }) } - // Handles external updates of datasources const replaceDatasource = (datasourceId, datasource) => { if (!datasourceId) { return @@ -197,6 +186,8 @@ export function createDatasourcesStore() { ...state, list: state.list.filter(x => x._id !== datasourceId), })) + tables.removeDatasourceTables(datasourceId) + queries.removeDatasourceQueries(datasourceId) return } diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js index be7bcb8c5b..1ec23b300c 100644 --- a/packages/builder/src/stores/backend/queries.js +++ b/packages/builder/src/stores/backend/queries.js @@ -121,6 +121,13 @@ export function createQueriesStore() { return await save(datasourceId, newQuery) } + const removeDatasourceQueries = datasourceId => { + store.update(state => ({ + ...state, + list: state.list.filter(table => table.datasourceId !== datasourceId), + })) + } + return { subscribe: derivedStore.subscribe, fetch, @@ -131,6 +138,7 @@ export function createQueriesStore() { delete: deleteQuery, preview, duplicate, + removeDatasourceQueries, } } diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js index d79ed6f072..3f4a953bac 100644 --- a/packages/builder/src/stores/backend/tables.js +++ b/packages/builder/src/stores/backend/tables.js @@ -161,6 +161,13 @@ export function createTablesStore() { } } + const removeDatasourceTables = datasourceId => { + store.update(state => ({ + ...state, + list: state.list.filter(table => table.sourceId !== datasourceId), + })) + } + return { ...store, subscribe: derivedStore.subscribe, @@ -172,6 +179,7 @@ export function createTablesStore() { saveField, deleteField, replaceTable, + removeDatasourceTables, } } From a7c6f9a60a3012487fec047ed2fde82b3c589c35 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 12:11:58 +0100 Subject: [PATCH 09/12] Allow deleting external tables that you created --- packages/builder/src/stores/backend/tables.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js index 3f4a953bac..201a67824d 100644 --- a/packages/builder/src/stores/backend/tables.js +++ b/packages/builder/src/stores/backend/tables.js @@ -67,12 +67,12 @@ export function createTablesStore() { } const deleteTable = async table => { - if (!table?._id || !table?._rev) { + if (!table?._id) { return } await API.deleteTable({ tableId: table._id, - tableRev: table._rev, + tableRev: table._rev || "rev", }) replaceTable(table._id, null) } From 8587aef50624ffb6661ee4012036585320a4c797 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 14:25:09 +0100 Subject: [PATCH 10/12] Tidy --- packages/builder/src/stores/backend/datasources.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index bce92daced..16128d38fb 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -58,9 +58,7 @@ export function createDatasourcesStore() { ...$store, list, selected: list?.find(ds => ds._id === $store.selectedDatasourceId), - hasDefaultData: list?.some( - datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID - ), + hasDefaultData: list?.some(ds => ds._id === DEFAULT_BB_DATASOURCE_ID), hasData: list?.length > 0, } }) From f0c8cb82b2aa296b33dd8d18be0da468f61d21b1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 17:43:52 +0100 Subject: [PATCH 11/12] Remove unused tenantId --- packages/server/src/websockets/websocket.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts index 94636010fa..24cac3c37d 100644 --- a/packages/server/src/websockets/websocket.ts +++ b/packages/server/src/websockets/websocket.ts @@ -15,7 +15,6 @@ const anonUser = () => ({ _id: uuid(), email: "user@mail.com", firstName: "Anonymous", - tenantId: "default", }) export class BaseSocket { From ee74900abb9a624296cd5fbe001f75549109d95d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 27 Jun 2023 17:45:24 +0100 Subject: [PATCH 12/12] Update comments --- packages/server/src/websockets/middleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/websockets/middleware.ts b/packages/server/src/websockets/middleware.ts index 2b6e7168af..0a52dcbdeb 100644 --- a/packages/server/src/websockets/middleware.ts +++ b/packages/server/src/websockets/middleware.ts @@ -48,8 +48,8 @@ export const createContext = ( } /** - * Runs a list of middlewares, nesting each callback inside each other mimic - * how the real middlewares run and ensuring that app or tenant context work + * Runs a list of middlewares, nesting each callback inside each other to mimic + * how the real middlewares run and ensuring that app and tenant contexts work * as expected * @param ctx the Koa context * @param middlewares the array of middlewares to run