Merge pull request #11039 from Budibase/cheeks-fixes

Collaboration and data section fixes
This commit is contained in:
Andrew Kingston 2023-06-27 19:58:22 +01:00 committed by GitHub
commit 33e6c1e534
21 changed files with 333 additions and 142 deletions

View File

@ -61,6 +61,9 @@ const INITIAL_FRONTEND_STATE = {
showNotificationAction: false, showNotificationAction: false,
sidePanel: false, sidePanel: false,
}, },
features: {
componentValidation: false,
},
errors: [], errors: [],
hasAppPackage: false, hasAppPackage: false,
libraries: null, libraries: null,
@ -148,6 +151,10 @@ export const getFrontendStore = () => {
navigation: application.navigation || {}, navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [], usedPlugins: application.usedPlugins || [],
hasLock, hasLock,
features: {
...INITIAL_FRONTEND_STATE.features,
...application.features,
},
initialised: true, initialised: true,
})) }))
screenHistoryStore.reset() screenHistoryStore.reset()
@ -283,9 +290,12 @@ export const getFrontendStore = () => {
} }
}, },
save: async screen => { save: async screen => {
// Validate screen structure const state = get(store)
// Temporarily disabled to accommodate migration issues
// store.actions.screens.validate(screen) // 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 // Check screen definition for any component settings which need updated
store.actions.screens.enrichEmptySettings(screen) store.actions.screens.enrichEmptySettings(screen)
@ -296,7 +306,6 @@ export const getFrontendStore = () => {
const routesResponse = await API.fetchAppRoutes() const routesResponse = await API.fetchAppRoutes()
// If plugins changed we need to fetch the latest app metadata // If plugins changed we need to fetch the latest app metadata
const state = get(store)
let usedPlugins = state.usedPlugins let usedPlugins = state.usedPlugins
if (savedScreen.pluginAdded) { if (savedScreen.pluginAdded) {
const { application } = await API.fetchAppPackage(state.appId) const { application } = await API.fetchAppPackage(state.appId)

View File

@ -12,8 +12,10 @@
customQueryText, customQueryText,
} from "helpers/data/utils" } from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte" import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants"
let openDataSources = [] let openDataSources = []
$: enrichedDataSources = enrichDatasources( $: enrichedDataSources = enrichDatasources(
$datasources, $datasources,
$params, $params,
@ -71,6 +73,13 @@
$goto(`./datasource/${datasource._id}`) $goto(`./datasource/${datasource._id}`)
} }
const selectTable = tableId => {
tables.select(tableId)
if (!$isActive("./table/:tableId")) {
$goto(`./table/${tableId}`)
}
}
function closeNode(datasource) { function closeNode(datasource) {
openDataSources = openDataSources.filter(id => datasource._id !== id) openDataSources = openDataSources.filter(id => datasource._id !== id)
} }
@ -151,9 +160,16 @@
{#if $database?._id} {#if $database?._id}
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
<NavItem
icon="UserGroup"
text="Users"
selected={$isActive("./table/:tableId") &&
$tables.selected?._id === TableNames.USERS}
on:click={() => selectTable(TableNames.USERS)}
/>
{#each enrichedDataSources as datasource, idx} {#each enrichedDataSources as datasource, idx}
<NavItem <NavItem
border={idx > 0} border
text={datasource.name} text={datasource.name}
opened={datasource.open} opened={datasource.open}
selected={$isActive("./datasource") && datasource.selected} selected={$isActive("./datasource") && datasource.selected}
@ -174,7 +190,7 @@
</NavItem> </NavItem>
{#if datasource.open} {#if datasource.open}
<TableNavigator sourceId={datasource._id} /> <TableNavigator sourceId={datasource._id} {selectTable} />
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} {#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
<NavItem <NavItem
indentLevel={1} indentLevel={1}

View File

@ -10,17 +10,13 @@
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1 a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
export let sourceId export let sourceId
export let selectTable
$: sortedTables = $tables.list $: sortedTables = $tables.list
.filter(table => table.sourceId === sourceId) .filter(
table => table.sourceId === sourceId && table._id !== TableNames.USERS
)
.sort(alphabetical) .sort(alphabetical)
const selectTable = tableId => {
tables.select(tableId)
if (!$isActive("./table/:tableId")) {
$goto(`./table/${tableId}`)
}
}
</script> </script>
{#if $database?._id} {#if $database?._id}

View File

@ -469,10 +469,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-xl); gap: var(--spacing-xl);
overflow: hidden;
} }
.overlay-wrap { .overlay-wrap {
position: relative; position: relative;
flex: 1; flex: 1;
overflow: hidden;
} }
.mode-overlay { .mode-overlay {
position: absolute; position: absolute;

View File

@ -2,8 +2,17 @@
import { Button, Layout } from "@budibase/bbui" import { Button, Layout } from "@budibase/bbui"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import Panel from "components/design/Panel.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 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")
}
}
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->

View File

@ -6,12 +6,15 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { TableNames } from "constants"
let modal let modal
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && table.sourceId === BUDIBASE_INTERNAL_DB_ID table.type !== "external" &&
table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
table._id !== TableNames.USERS
) )
onMount(() => { onMount(() => {

View File

@ -4,11 +4,13 @@
import { onMount } from "svelte" import { onMount } from "svelte"
onMount(async () => { onMount(async () => {
const { list, selected } = $datasources const { list, selected, hasData } = $datasources
if (selected) { if (selected) {
$redirect(`./${selected?._id}`) $redirect(`./${selected?._id}`)
} else { } else if (hasData && list?.length) {
$redirect(`./${list[0]._id}`) $redirect(`./${list[0]._id}`)
} else {
$redirect("../new")
} }
}) })
</script> </script>

View File

@ -1,17 +1,13 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { onMount } from "svelte" import { TableNames } from "constants"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
$: hasData = $: {
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length > if ($datasources.hasData) {
1 || $datasources.list.length > 1 $redirect(`./table/${TableNames.USERS}`)
onMount(() => {
if (!hasData) {
$redirect("./new")
} else { } else {
$redirect("./table") $redirect("./new")
} }
}) }
</script> </script>

View File

@ -1,14 +1,16 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { TableNames } from "constants"
onMount(async () => { onMount(() => {
const { list, selected } = $tables if ($tables.selected) {
if (selected) { $redirect(`./${$tables.selected._id}`)
$redirect(`./${selected?._id}`) } else if ($datasources.hasData) {
} else if (list?.length) { $redirect(`./${TableNames.USERS}`)
$redirect(`./${list[0]._id}`) } else {
$redirect("../new")
} }
}) })
</script> </script>

View File

@ -1,8 +1,13 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend" import {
import { queries, tables } from "./" IntegrationTypes,
DEFAULT_BB_DATASOURCE_ID,
BUDIBASE_INTERNAL_DB_ID,
} from "constants/backend"
import { tables, queries } from "./"
import { API } from "api" import { API } from "api"
import { DatasourceFeature } from "@budibase/types" import { DatasourceFeature } from "@budibase/types"
import { TableNames } from "constants"
export class ImportTableError extends Error { export class ImportTableError extends Error {
constructor(message) { constructor(message) {
@ -23,13 +28,40 @@ export function createDatasourcesStore() {
schemaError: null, schemaError: null,
}) })
const derivedStore = derived(store, $store => ({ const derivedStore = derived([store, tables], ([$store, $tables]) => {
...$store, // Set the internal datasource entities from the table list, which we're
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId), // able to keep updated unlike the egress generated definition of the
hasDefaultData: $store.list.some( // internal datasource
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID let internalDS = $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,
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(ds => ds._id === DEFAULT_BB_DATASOURCE_ID),
hasData: list?.length > 0,
}
})
const fetch = async () => { const fetch = async () => {
const datasources = await API.getDatasources() const datasources = await API.getDatasources()
@ -50,20 +82,14 @@ export function createDatasourcesStore() {
const updateDatasource = response => { const updateDatasource = response => {
const { datasource, error } = response const { datasource, error } = response
store.update(state => { if (error) {
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) store.update(state => ({
const sources = state.list ...state,
if (currentIdx >= 0) {
sources.splice(currentIdx, 1, datasource)
} else {
sources.push(datasource)
}
return {
list: sources,
selectedDatasourceId: datasource._id,
schemaError: error, schemaError: error,
} }))
}) }
replaceDatasource(datasource._id, datasource)
select(datasource._id)
return datasource return datasource
} }
@ -134,18 +160,14 @@ export function createDatasourcesStore() {
} }
const deleteDatasource = async datasource => { const deleteDatasource = async datasource => {
if (!datasource?._id || !datasource?._rev) {
return
}
await API.deleteDatasource({ await API.deleteDatasource({
datasourceId: datasource?._id, datasourceId: datasource._id,
datasourceRev: datasource?._rev, datasourceRev: datasource._rev,
}) })
store.update(state => { replaceDatasource(datasource._id, null)
const sources = state.list.filter(
existing => existing._id !== datasource._id
)
return { list: sources, selected: null }
})
await queries.fetch()
await tables.fetch()
} }
const removeSchemaError = () => { const removeSchemaError = () => {
@ -154,7 +176,6 @@ export function createDatasourcesStore() {
}) })
} }
// Handles external updates of datasources
const replaceDatasource = (datasourceId, datasource) => { const replaceDatasource = (datasourceId, datasource) => {
if (!datasourceId) { if (!datasourceId) {
return return
@ -166,6 +187,8 @@ export function createDatasourcesStore() {
...state, ...state,
list: state.list.filter(x => x._id !== datasourceId), list: state.list.filter(x => x._id !== datasourceId),
})) }))
tables.removeDatasourceTables(datasourceId)
queries.removeDatasourceQueries(datasourceId)
return return
} }

View File

@ -121,6 +121,13 @@ export function createQueriesStore() {
return await save(datasourceId, newQuery) return await save(datasourceId, newQuery)
} }
const removeDatasourceQueries = datasourceId => {
store.update(state => ({
...state,
list: state.list.filter(table => table.datasourceId !== datasourceId),
}))
}
return { return {
subscribe: derivedStore.subscribe, subscribe: derivedStore.subscribe,
fetch, fetch,
@ -131,6 +138,7 @@ export function createQueriesStore() {
delete: deleteQuery, delete: deleteQuery,
preview, preview,
duplicate, duplicate,
removeDatasourceQueries,
} }
} }

View File

@ -67,12 +67,12 @@ export function createTablesStore() {
} }
const deleteTable = async table => { const deleteTable = async table => {
if (!table?._id || !table?._rev) { if (!table?._id) {
return return
} }
await API.deleteTable({ await API.deleteTable({
tableId: table._id, tableId: table._id,
tableRev: table._rev, tableRev: table._rev || "rev",
}) })
replaceTable(table._id, null) replaceTable(table._id, null)
} }
@ -161,6 +161,13 @@ export function createTablesStore() {
} }
} }
const removeDatasourceTables = datasourceId => {
store.update(state => ({
...state,
list: state.list.filter(table => table.sourceId !== datasourceId),
}))
}
return { return {
...store, ...store,
subscribe: derivedStore.subscribe, subscribe: derivedStore.subscribe,
@ -172,6 +179,7 @@ export function createTablesStore() {
saveField, saveField,
deleteField, deleteField,
replaceTable, replaceTable,
removeDatasourceTables,
} }
} }

View File

@ -2216,7 +2216,7 @@
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
"hasChildren": true, "hasChildren": true,
"illegalChildren": ["section", "form"], "illegalChildren": ["section", "form", "formblock"],
"actions": [ "actions": [
"ValidateForm", "ValidateForm",
"ClearForm", "ClearForm",
@ -2304,7 +2304,7 @@
"name": "Form Step", "name": "Form Step",
"icon": "AssetsAdded", "icon": "AssetsAdded",
"hasChildren": true, "hasChildren": true,
"illegalChildren": ["section", "form", "form step"], "illegalChildren": ["section", "form", "formstep", "formblock"],
"styles": ["size"], "styles": ["size"],
"size": { "size": {
"width": 400, "width": 400,

View File

@ -68,6 +68,7 @@
rowHeight, rowHeight,
contentLines, contentLines,
gridFocused, gridFocused,
error,
} = context } = context
// Keep config store up to date with props // Keep config store up to date with props
@ -149,8 +150,15 @@
</div> </div>
</div> </div>
</div> </div>
{:else if $error}
<div class="grid-error">
<div class="grid-error-title">There was a problem loading your grid</div>
<div class="grid-error-subtitle">
{$error}
</div>
</div>
{/if} {/if}
{#if $loading} {#if $loading && !$error}
<div in:fade|local={{ duration: 130 }} class="grid-loading"> <div in:fade|local={{ duration: 130 }} class="grid-loading">
<ProgressCircle /> <ProgressCircle />
</div> </div>
@ -273,6 +281,25 @@
opacity: 0.6; 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 */ /* Disable checkbox animation anywhere in the grid data */
.grid-data-outer :global(.spectrum-Checkbox-box:before), .grid-data-outer :global(.spectrum-Checkbox-box:before),
.grid-data-outer :global(.spectrum-Checkbox-box:after), .grid-data-outer :global(.spectrum-Checkbox-box:after),

View File

@ -14,6 +14,7 @@ export const createStores = () => {
const rowChangeCache = writable({}) const rowChangeCache = writable({})
const inProgressChanges = writable({}) const inProgressChanges = writable({})
const hasNextPage = writable(false) const hasNextPage = writable(false)
const error = writable(null)
// Generate a lookup map to quick find a row by ID // Generate a lookup map to quick find a row by ID
const rowLookupMap = derived( const rowLookupMap = derived(
@ -47,6 +48,7 @@ export const createStores = () => {
rowChangeCache, rowChangeCache,
inProgressChanges, inProgressChanges,
hasNextPage, hasNextPage,
error,
} }
} }
@ -68,6 +70,7 @@ export const deriveStores = context => {
inProgressChanges, inProgressChanges,
previousFocusedRowId, previousFocusedRowId,
hasNextPage, hasNextPage,
error,
} = context } = context
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
const fetch = writable(null) const fetch = writable(null)
@ -122,7 +125,17 @@ export const deriveStores = context => {
// Subscribe to changes of this fetch model // Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe(async $fetch => { 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) hasNextPage.set($fetch.hasNextPage)
const $instanceLoaded = get(instanceLoaded) const $instanceLoaded = get(instanceLoaded)
const resetRows = $fetch.resetKey !== lastResetKey const resetRows = $fetch.resetKey !== lastResetKey

View File

@ -57,6 +57,7 @@ export default class DataFetch {
cursor: null, cursor: null,
cursors: [], cursors: [],
resetKey: Math.random(), resetKey: Math.random(),
error: null,
}) })
// Merge options with their default values // Merge options with their default values
@ -252,6 +253,10 @@ export default class DataFetch {
try { try {
return await this.API.fetchTableDefinition(datasource.tableId) return await this.API.fetchTableDefinition(datasource.tableId)
} catch (error) { } catch (error) {
this.store.update(state => ({
...state,
error,
}))
return null return null
} }
} }

View File

@ -308,6 +308,9 @@ async function performAppCreate(ctx: UserCtx) {
customTheme: { customTheme: {
buttonBorderRadius: "16px", buttonBorderRadius: "16px",
}, },
features: {
componentValidation: true,
},
} }
// If we used a template or imported an app there will be an existing doc. // If we used a template or imported an app there will be an existing doc.

View File

@ -1,12 +1,15 @@
import authorized from "../middleware/authorized" import authorized from "../middleware/authorized"
import currentApp from "../middleware/currentapp"
import { BaseSocket } from "./websocket" import { BaseSocket } from "./websocket"
import { context, permissions } from "@budibase/backend-core" import { auth, permissions } from "@budibase/backend-core"
import http from "http" import http from "http"
import Koa from "koa" import Koa from "koa"
import { getTableId } from "../api/controllers/row/utils" import { getTableId } from "../api/controllers/row/utils"
import { Row, Table } from "@budibase/types" import { Row, Table } from "@budibase/types"
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { GridSocketEvent } from "@budibase/shared-core" import { GridSocketEvent } from "@budibase/shared-core"
import { userAgent } from "koa-useragent"
import { createContext, runMiddlewares } from "./middleware"
const { PermissionType, PermissionLevel } = permissions const { PermissionType, PermissionLevel } = permissions
@ -26,28 +29,27 @@ export default class GridSocket extends BaseSocket {
return return
} }
// Check if the user has permission to read this resource // Create context
const middleware = authorized( const ctx = createContext(this.app, socket, {
PermissionType.TABLE,
PermissionLevel.READ
)
const ctx = {
appId,
resourceId: tableId, resourceId: tableId,
roleId: socket.data.roleId, appId,
user: { _id: socket.data._id }, })
isAuthenticated: socket.data.isAuthenticated,
request: { // Construct full middleware chain to assess permissions
url: "/fake", const middlewares = [
}, userAgent,
get: () => null, auth.buildAuthMiddleware([], {
throw: () => { publicAllowed: true,
// If they don't have access, immediately disconnect them }),
socket.disconnect(true) currentApp,
}, authorized(PermissionType.TABLE, PermissionLevel.READ),
} ]
await context.doInAppContext(appId, async () => {
await middleware(ctx, async () => { // 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}` const room = `${appId}-${tableId}`
await this.joinRoom(socket, room) await this.joinRoom(socket, room)
@ -55,7 +57,9 @@ export default class GridSocket extends BaseSocket {
const sessions = await this.getRoomSessions(room) const sessions = await this.getRoomSessions(room)
callback({ users: sessions }) callback({ users: sessions })
}) })
}) } catch (error) {
socket.disconnect(true)
}
} }
) )

View File

@ -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 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
* @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<Koa.Context, "request"> {
request: {
url: string
headers: {
[key: string]: string | undefined
}
}
cookies: Cookies
}
export interface WebsocketContextOptions {
appId?: string
resourceId?: string
}

View File

@ -1,7 +1,6 @@
import { Server } from "socket.io" import { Server } from "socket.io"
import http from "http" import http from "http"
import Koa from "koa" import Koa from "koa"
import Cookies from "cookies"
import { userAgent } from "koa-useragent" import { userAgent } from "koa-useragent"
import { auth, Header, redis } from "@budibase/backend-core" import { auth, Header, redis } from "@budibase/backend-core"
import { createAdapter } from "@socket.io/redis-adapter" import { createAdapter } from "@socket.io/redis-adapter"
@ -10,6 +9,7 @@ import { getSocketPubSubClients } from "../utilities/redis"
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core" import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
import { SocketSession } from "@budibase/types" import { SocketSession } from "@budibase/types"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { createContext, runMiddlewares } from "./middleware"
const anonUser = () => ({ const anonUser = () => ({
_id: uuid(), _id: uuid(),
@ -18,6 +18,7 @@ const anonUser = () => ({
}) })
export class BaseSocket { export class BaseSocket {
app: Koa
io: Server io: Server
path: string path: string
redisClient?: redis.Client redisClient?: redis.Client
@ -28,6 +29,7 @@ export class BaseSocket {
path: string = "/", path: string = "/",
additionalMiddlewares?: any[] additionalMiddlewares?: any[]
) { ) {
this.app = app
this.path = path this.path = path
this.io = new Server(server, { this.io = new Server(server, {
path, path,
@ -45,52 +47,25 @@ export class BaseSocket {
// Apply middlewares // Apply middlewares
this.io.use(async (socket, next) => { this.io.use(async (socket, next) => {
// Build fake koa context const ctx = createContext(this.app, socket)
const res = new http.ServerResponse(socket.request)
const ctx: any = {
...app.createContext(socket.request, res),
// 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 { try {
for (let [idx, middleware] of middlewares.entries()) { await runMiddlewares(ctx, middlewares, () => {
await middleware(ctx, () => { // Middlewares are finished
if (idx === middlewares.length - 1) { // Extract some data from our enriched koa context to persist
// Middlewares are finished // as metadata for the socket
// Extract some data from our enriched koa context to persist const user = ctx.user?._id ? ctx.user : anonUser()
// as metadata for the socket const { _id, email, firstName, lastName } = user
const user = ctx.user?._id ? ctx.user : anonUser() socket.data = {
const { _id, email, firstName, lastName } = user _id,
socket.data = { email,
_id, firstName,
email, lastName,
firstName, sessionId: socket.id,
lastName, connectedAt: Date.now(),
sessionId: socket.id, }
connectedAt: Date.now(), next()
isAuthenticated: ctx.isAuthenticated, })
roleId: ctx.roleId,
}
next()
}
})
}
} catch (error: any) { } catch (error: any) {
next(error) next(error)
} }

View File

@ -20,6 +20,7 @@ export interface App extends Document {
navigation?: AppNavigation navigation?: AppNavigation
automationErrors?: AppMetadataErrors automationErrors?: AppMetadataErrors
icon?: AppIcon icon?: AppIcon
features?: AppFeatures
} }
export interface AppInstance { export interface AppInstance {
@ -60,3 +61,7 @@ export interface AppIcon {
name: string name: string
color: string color: string
} }
export interface AppFeatures {
componentValidation?: boolean
}