Handle table, query and datasource events using builder socket

This commit is contained in:
Andrew Kingston 2023-05-22 15:59:44 +01:00
parent 3794d8e204
commit cfa07a68ae
13 changed files with 177 additions and 73 deletions

View File

@ -1,21 +1,34 @@
import { createWebsocket } from "@budibase/frontend-core"
import { userStore } from "builderStore"
import { datasources, tables } from "stores/backend"
export const createBuilderWebsocket = () => {
const socket = createWebsocket("/socket/builder")
// Connection events
socket.on("connect", () => {
socket.emit("get-users", null, response => {
userStore.actions.init(response.users)
})
})
socket.on("user-update", userStore.actions.updateUser)
socket.on("user-disconnect", userStore.actions.removeUser)
socket.on("connect_error", err => {
console.log("Failed to connect to builder websocket:", err.message)
})
// User events
socket.on("user-update", userStore.actions.updateUser)
socket.on("user-disconnect", userStore.actions.removeUser)
// Table events
socket.on("table-change", ({ id, table }) => {
tables.replaceTable(id, table)
})
// Table events
socket.on("datasource-change", ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
return {
...socket,
disconnect: () => {

View File

@ -35,7 +35,6 @@
allowAddRows={!isUsersTable}
allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
on:updatetable={e => tables.updateTable(e.detail)}
showAvatars={false}
>
<svelte:fragment slot="controls">

View File

@ -1,4 +1,4 @@
import { writable, derived } from "svelte/store"
import { writable, derived, get } from "svelte/store"
import { queries, tables } from "./"
import { API } from "api"
@ -89,6 +89,39 @@ export function createDatasourcesStore() {
})
}
// Handles external updates of tables
const replaceDatasource = (datasourceId, datasource) => {
if (!datasourceId) {
return
}
// Handle deletion
if (!datasource) {
store.update(state => ({
...state,
list: state.list.filter(x => x._id !== datasourceId),
}))
return
}
// Add new datasource
const index = get(store).list.findIndex(x => x._id === datasource._id)
if (index === -1) {
store.update(state => ({
...state,
list: [...state.list, datasource],
}))
}
// Update existing datasource
else if (datasource) {
store.update(state => {
state.list[index] = datasource
return state
})
}
}
return {
subscribe: derivedStore.subscribe,
fetch,
@ -98,6 +131,7 @@ export function createDatasourcesStore() {
save,
delete: deleteDatasource,
removeSchemaError,
replaceDatasource,
}
}

View File

@ -22,18 +22,6 @@ export function createTablesStore() {
}))
}
const fetchTable = async tableId => {
const table = await API.fetchTableDefinition(tableId)
store.update(state => {
const indexToUpdate = state.list.findIndex(t => t._id === table._id)
state.list[indexToUpdate] = table
return {
...state,
}
})
}
const select = tableId => {
store.update(state => ({
...state,
@ -74,20 +62,23 @@ export function createTablesStore() {
}
const savedTable = await API.saveTable(updatedTable)
await fetch()
replaceTable(table._id, savedTable)
if (table.type === "external") {
await datasources.fetch()
}
await select(savedTable._id)
select(savedTable._id)
return savedTable
}
const deleteTable = async table => {
if (!table?._id || !table?._rev) {
return
}
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
tableId: table._id,
tableRev: table._rev,
})
await fetch()
replaceTable(table._id, null)
}
const saveField = async ({
@ -135,35 +126,56 @@ export function createTablesStore() {
await save(draft)
}
const updateTable = table => {
const index = get(store).list.findIndex(x => x._id === table._id)
if (index === -1) {
// Handles external updates of tables
const replaceTable = (tableId, table) => {
if (!tableId) {
return
}
// This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things.
store.update(state => {
state.list[index] = {
...table,
type: state.list[index].type,
}
return state
})
// Handle deletion
if (!table) {
store.update(state => ({
...state,
list: state.list.filter(x => x._id !== tableId),
}))
return
}
// Add new table
const index = get(store).list.findIndex(x => x._id === table._id)
if (index === -1) {
store.update(state => ({
...state,
list: [...state.list, table],
}))
}
// Update existing table
else if (table) {
// This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things.
store.update(state => {
state.list[index] = {
...table,
type: state.list[index].type,
}
return state
})
}
}
return {
...store,
subscribe: derivedStore.subscribe,
fetch,
fetchTable,
init: fetch,
select,
save,
delete: deleteTable,
saveField,
deleteField,
updateTable,
replaceTable,
}
}

View File

@ -1,4 +1,4 @@
import { writable, get, derived } from "svelte/store"
import { writable, derived } from "svelte/store"
import { tables } from "./"
import { API } from "api"
@ -27,21 +27,31 @@ export function createViewsStore() {
const deleteView = async view => {
await API.deleteView(view)
await tables.fetch()
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table) {
delete table.views[view.name]
}
return { ...state }
})
}
const save = async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(table => table._id === view.tableId)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table) {
if (view.originalName) {
delete table.views[view.originalName]
}
table.views[view.name] = savedView
}
return { ...state }
})
}
return {

View File

@ -2,7 +2,7 @@ import { get } from "svelte/store"
import { createWebsocket } from "../../../utils"
export const createGridWebsocket = context => {
const { rows, tableId, users, focusedCellId } = context
const { rows, tableId, users, focusedCellId, table } = context
const socket = createWebsocket("/socket/grid")
const connectToTable = tableId => {
@ -16,23 +16,36 @@ export const createGridWebsocket = context => {
})
}
// Event handlers
// Connection events
socket.on("connect", () => {
connectToTable(get(tableId))
})
socket.on("row-update", data => {
if (data.id) {
rows.actions.replaceRow(data.id, data.row)
}
socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message)
})
// User events
socket.on("user-update", user => {
users.actions.updateUser(user)
})
socket.on("user-disconnect", user => {
users.actions.removeUser(user)
})
socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message)
// Row events
socket.on("row-change", data => {
if (data.id) {
rows.actions.replaceRow(data.id, data.row)
}
})
// Table events
socket.on("table-change", data => {
// Only update table if one exists. If the table was deleted then we don't
// want to know - let the builder navigate away
if (data.table) {
table.set(data.table)
}
})
// Change websocket connection when table changes

View File

@ -90,10 +90,6 @@ export const deriveStores = context => {
// Update local state
table.set(newTable)
// Broadcast event so that we can keep sync with external state
// (e.g. data section which maintains a list of table definitions)
dispatch("updatetable", newTable)
// Update server
await API.saveTable(newTable)
}

View File

@ -25,6 +25,7 @@ import {
DatasourcePlus,
} from "@budibase/types"
import sdk from "../../sdk"
import { builderSocket } from "../../websockets"
function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
@ -280,6 +281,7 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
ctx.body = {
datasource: await sdk.datasources.removeSecretSingle(datasource),
}
builderSocket.emitDatasourceUpdate(ctx, datasource)
}
export async function save(
@ -322,6 +324,7 @@ export async function save(
response.error = schemaError
}
ctx.body = response
builderSocket.emitDatasourceUpdate(ctx, datasource)
}
async function destroyInternalTablesBySourceId(datasourceId: string) {
@ -381,6 +384,7 @@ export async function destroy(ctx: UserCtx) {
ctx.message = `Datasource deleted.`
ctx.status = 200
builderSocket.emitDatasourceDeletion(ctx, datasourceId)
}
export async function find(ctx: UserCtx) {

View File

@ -11,6 +11,7 @@ import { context, events } from "@budibase/backend-core"
import { Table, UserCtx } from "@budibase/types"
import sdk from "../../../sdk"
import { jsonFromCsvString } from "../../../utilities/csv"
import { builderSocket } from "../../../websockets"
function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
if (table && !tableId) {
@ -77,6 +78,7 @@ export async function save(ctx: UserCtx) {
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:save`, appId, savedTable)
ctx.body = savedTable
builderSocket.emitTableUpdate(ctx, savedTable)
}
export async function destroy(ctx: UserCtx) {
@ -89,6 +91,7 @@ export async function destroy(ctx: UserCtx) {
ctx.status = 200
ctx.table = deletedTable
ctx.body = { message: `Table ${tableId} deleted.` }
builderSocket.emitTableDeletion(ctx, tableId)
}
export async function bulkImport(ctx: UserCtx) {

View File

@ -16,6 +16,7 @@ import {
View,
} from "@budibase/types"
import { cleanExportRows } from "../row/utils"
import { builderSocket } from "../../../websockets"
const { cloneDeep, isEqual } = require("lodash")
@ -48,7 +49,7 @@ export async function save(ctx: Ctx) {
if (!view.meta.schema) {
view.meta.schema = table.schema
}
table.views[viewName] = view.meta
table.views[viewName] = { ...view.meta, name: viewName }
if (originalName) {
delete table.views[originalName]
existingTable.views[viewName] = existingTable.views[originalName]
@ -56,10 +57,8 @@ export async function save(ctx: Ctx) {
await db.put(table)
await handleViewEvents(existingTable.views[viewName], table.views[viewName])
ctx.body = {
...table.views[viewToSave.name],
name: viewToSave.name,
}
ctx.body = table.views[viewName]
builderSocket.emitTableUpdate(ctx, table)
}
export async function calculationEvents(existingView: View, newView: View) {
@ -128,6 +127,7 @@ export async function destroy(ctx: Ctx) {
await events.view.deleted(view)
ctx.body = view
builderSocket.emitTableUpdate(ctx, table)
}
export async function exportView(ctx: Ctx) {

View File

@ -3,7 +3,8 @@ import Socket from "./websocket"
import { permissions } from "@budibase/backend-core"
import http from "http"
import Koa from "koa"
import { Table } from "@budibase/types"
import { Datasource, Table } from "@budibase/types"
import { gridSocket } from "./index"
export default class BuilderSocket extends Socket {
constructor(app: Koa, server: http.Server) {
@ -22,7 +23,6 @@ export default class BuilderSocket extends Socket {
const sockets = await this.io.in(appId).fetchSockets()
callback({
users: sockets.map(socket => socket.data.user),
id: user.id,
})
})
@ -34,10 +34,22 @@ export default class BuilderSocket extends Socket {
}
emitTableUpdate(ctx: any, table: Table) {
this.io.in(ctx.appId).emit("table-update", { id: table._id, table })
this.io.in(ctx.appId).emit("table-change", { id: table._id, table })
gridSocket.emitTableUpdate(table)
}
emitTableDeletion(ctx: any, id: string) {
this.io.in(ctx.appId).emit("table-update", { id, table: null })
this.io.in(ctx.appId).emit("table-change", { id, table: null })
gridSocket.emitTableDeletion(id)
}
emitDatasourceUpdate(ctx: any, datasource: Datasource) {
this.io
.in(ctx.appId)
.emit("datasource-change", { id: datasource._id, datasource })
}
emitDatasourceDeletion(ctx: any, id: string) {
this.io.in(ctx.appId).emit("datasource-change", { id, datasource: null })
}
}

View File

@ -4,7 +4,7 @@ import { permissions } from "@budibase/backend-core"
import http from "http"
import Koa from "koa"
import { getTableId } from "../api/controllers/row/utils"
import { Row } from "@budibase/types"
import { Row, Table } from "@budibase/types"
export default class GridSocket extends Socket {
constructor(app: Koa, server: http.Server) {
@ -56,11 +56,19 @@ export default class GridSocket extends Socket {
emitRowUpdate(ctx: any, row: Row) {
const tableId = getTableId(ctx)
this.io.in(tableId).emit("row-update", { id: row._id, row })
this.io.in(tableId).emit("row-change", { id: row._id, row })
}
emitRowDeletion(ctx: any, id: string) {
const tableId = getTableId(ctx)
this.io.in(tableId).emit("row-update", { id, row: null })
this.io.in(tableId).emit("row-change", { id, row: null })
}
emitTableUpdate(table: Table) {
this.io.in(table._id!).emit("table-change", { id: table._id, table })
}
emitTableDeletion(id: string) {
this.io.in(id).emit("table-change", { id, table: null })
}
}

View File

@ -14,4 +14,4 @@ export const initialise = (app: Koa, server: http.Server) => {
builderSocket = new BuilderSocket(app, server)
}
export { clientAppSocket, gridSocket }
export { clientAppSocket, gridSocket, builderSocket }