@budibase/server 2.6.19-alpha.26 → 2.6.19-alpha.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builder/assets/{index.ffb6a106.css → index.87500798.css} +1 -1
- package/builder/assets/{index.47cc7efc.js → index.dc879886.js} +294 -293
- package/builder/index.html +2 -2
- package/dist/automation.js +301 -273
- package/dist/automation.js.map +4 -4
- package/dist/index.js +452 -339
- package/dist/index.js.map +4 -4
- package/dist/query.js +24 -6
- package/dist/query.js.map +4 -4
- package/package.json +9 -8
- package/src/api/controllers/application.ts +2 -1
- package/src/api/controllers/datasource.ts +4 -0
- package/src/api/controllers/row/index.ts +15 -22
- package/src/api/controllers/row/utils.ts +12 -0
- package/src/api/controllers/table/index.ts +3 -0
- package/src/api/controllers/view/index.ts +5 -5
- package/src/app.ts +0 -1
- package/src/middleware/builder.ts +4 -5
- package/src/startup.ts +2 -0
- package/src/utilities/redis.ts +19 -2
- package/src/websockets/builder.ts +69 -0
- package/src/websockets/grid.ts +25 -7
- package/src/websockets/index.ts +5 -2
- package/src/websockets/websocket.ts +20 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/server",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "2.6.19-alpha.
|
|
4
|
+
"version": "2.6.19-alpha.27",
|
|
5
5
|
"description": "Budibase Web Server",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"license": "GPL-3.0",
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@apidevtools/swagger-parser": "10.0.3",
|
|
49
|
-
"@budibase/backend-core": "2.6.19-alpha.
|
|
50
|
-
"@budibase/client": "2.6.19-alpha.
|
|
51
|
-
"@budibase/pro": "2.6.19-alpha.
|
|
52
|
-
"@budibase/shared-core": "2.6.19-alpha.
|
|
53
|
-
"@budibase/string-templates": "2.6.19-alpha.
|
|
54
|
-
"@budibase/types": "2.6.19-alpha.
|
|
49
|
+
"@budibase/backend-core": "2.6.19-alpha.27",
|
|
50
|
+
"@budibase/client": "2.6.19-alpha.27",
|
|
51
|
+
"@budibase/pro": "2.6.19-alpha.27",
|
|
52
|
+
"@budibase/shared-core": "2.6.19-alpha.27",
|
|
53
|
+
"@budibase/string-templates": "2.6.19-alpha.27",
|
|
54
|
+
"@budibase/types": "2.6.19-alpha.27",
|
|
55
55
|
"@bull-board/api": "3.7.0",
|
|
56
56
|
"@bull-board/koa": "3.9.4",
|
|
57
57
|
"@elastic/elasticsearch": "7.10.0",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@koa/router": "8.0.8",
|
|
60
60
|
"@sendgrid/mail": "7.1.1",
|
|
61
61
|
"@sentry/node": "6.17.7",
|
|
62
|
+
"@socket.io/redis-adapter": "^8.2.1",
|
|
62
63
|
"airtable": "0.10.1",
|
|
63
64
|
"arangojs": "7.2.0",
|
|
64
65
|
"aws-sdk": "2.1030.0",
|
|
@@ -194,5 +195,5 @@
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
},
|
|
197
|
-
"gitHead": "
|
|
198
|
+
"gitHead": "84334ce650f8782d49ca9c7b5a7186e5232ad91a"
|
|
198
199
|
}
|
|
@@ -29,7 +29,7 @@ import { USERS_TABLE_SCHEMA } from "../../constants"
|
|
|
29
29
|
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
|
30
30
|
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
|
31
31
|
import { stringToReadStream, isQsTrue } from "../../utilities"
|
|
32
|
-
import { getLocksById } from "../../utilities/redis"
|
|
32
|
+
import { getLocksById, doesUserHaveLock } from "../../utilities/redis"
|
|
33
33
|
import {
|
|
34
34
|
updateClientLibrary,
|
|
35
35
|
backupClientLibrary,
|
|
@@ -227,6 +227,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
|
|
227
227
|
screens,
|
|
228
228
|
layouts,
|
|
229
229
|
clientLibPath,
|
|
230
|
+
hasLock: await doesUserHaveLock(application.appId, ctx.user),
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
DatasourcePlus,
|
|
27
27
|
} from "@budibase/types"
|
|
28
28
|
import sdk from "../../sdk"
|
|
29
|
+
import { builderSocket } from "../../websockets"
|
|
29
30
|
|
|
30
31
|
function getErrorTables(errors: any, errorType: string) {
|
|
31
32
|
return Object.entries(errors)
|
|
@@ -296,6 +297,7 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
|
|
296
297
|
ctx.body = {
|
|
297
298
|
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
|
298
299
|
}
|
|
300
|
+
builderSocket.emitDatasourceUpdate(ctx, datasource)
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
export async function save(
|
|
@@ -338,6 +340,7 @@ export async function save(
|
|
|
338
340
|
response.error = schemaError
|
|
339
341
|
}
|
|
340
342
|
ctx.body = response
|
|
343
|
+
builderSocket.emitDatasourceUpdate(ctx, datasource)
|
|
341
344
|
}
|
|
342
345
|
|
|
343
346
|
async function destroyInternalTablesBySourceId(datasourceId: string) {
|
|
@@ -397,6 +400,7 @@ export async function destroy(ctx: UserCtx) {
|
|
|
397
400
|
|
|
398
401
|
ctx.message = `Datasource deleted.`
|
|
399
402
|
ctx.status = 200
|
|
403
|
+
builderSocket.emitDatasourceDeletion(ctx, datasourceId)
|
|
400
404
|
}
|
|
401
405
|
|
|
402
406
|
export async function find(ctx: UserCtx) {
|
|
@@ -4,6 +4,7 @@ import * as external from "./external"
|
|
|
4
4
|
import { isExternalTable } from "../../../integrations/utils"
|
|
5
5
|
import { Ctx } from "@budibase/types"
|
|
6
6
|
import * as utils from "./utils"
|
|
7
|
+
import { gridSocket } from "../../../websockets"
|
|
7
8
|
|
|
8
9
|
function pickApi(tableId: any) {
|
|
9
10
|
if (isExternalTable(tableId)) {
|
|
@@ -12,21 +13,9 @@ function pickApi(tableId: any) {
|
|
|
12
13
|
return internal
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function getTableId(ctx: any) {
|
|
16
|
-
if (ctx.request.body && ctx.request.body.tableId) {
|
|
17
|
-
return ctx.request.body.tableId
|
|
18
|
-
}
|
|
19
|
-
if (ctx.params && ctx.params.tableId) {
|
|
20
|
-
return ctx.params.tableId
|
|
21
|
-
}
|
|
22
|
-
if (ctx.params && ctx.params.viewName) {
|
|
23
|
-
return ctx.params.viewName
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
16
|
export async function patch(ctx: any): Promise<any> {
|
|
28
17
|
const appId = ctx.appId
|
|
29
|
-
const tableId = getTableId(ctx)
|
|
18
|
+
const tableId = utils.getTableId(ctx)
|
|
30
19
|
const body = ctx.request.body
|
|
31
20
|
// if it doesn't have an _id then its save
|
|
32
21
|
if (body && !body._id) {
|
|
@@ -47,6 +36,7 @@ export async function patch(ctx: any): Promise<any> {
|
|
|
47
36
|
ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
|
|
48
37
|
ctx.message = `${table.name} updated successfully.`
|
|
49
38
|
ctx.body = row
|
|
39
|
+
gridSocket?.emitRowUpdate(ctx, row)
|
|
50
40
|
} catch (err) {
|
|
51
41
|
ctx.throw(400, err)
|
|
52
42
|
}
|
|
@@ -54,7 +44,7 @@ export async function patch(ctx: any): Promise<any> {
|
|
|
54
44
|
|
|
55
45
|
export const save = async (ctx: any) => {
|
|
56
46
|
const appId = ctx.appId
|
|
57
|
-
const tableId = getTableId(ctx)
|
|
47
|
+
const tableId = utils.getTableId(ctx)
|
|
58
48
|
const body = ctx.request.body
|
|
59
49
|
// if it has an ID already then its a patch
|
|
60
50
|
if (body && body._id) {
|
|
@@ -69,23 +59,24 @@ export const save = async (ctx: any) => {
|
|
|
69
59
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
|
70
60
|
ctx.message = `${table.name} saved successfully`
|
|
71
61
|
ctx.body = row
|
|
62
|
+
gridSocket?.emitRowUpdate(ctx, row)
|
|
72
63
|
}
|
|
73
64
|
export async function fetchView(ctx: any) {
|
|
74
|
-
const tableId = getTableId(ctx)
|
|
65
|
+
const tableId = utils.getTableId(ctx)
|
|
75
66
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx), {
|
|
76
67
|
datasourceId: tableId,
|
|
77
68
|
})
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
export async function fetch(ctx: any) {
|
|
81
|
-
const tableId = getTableId(ctx)
|
|
72
|
+
const tableId = utils.getTableId(ctx)
|
|
82
73
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx), {
|
|
83
74
|
datasourceId: tableId,
|
|
84
75
|
})
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
export async function find(ctx: any) {
|
|
88
|
-
const tableId = getTableId(ctx)
|
|
79
|
+
const tableId = utils.getTableId(ctx)
|
|
89
80
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), {
|
|
90
81
|
datasourceId: tableId,
|
|
91
82
|
})
|
|
@@ -94,7 +85,7 @@ export async function find(ctx: any) {
|
|
|
94
85
|
export async function destroy(ctx: any) {
|
|
95
86
|
const appId = ctx.appId
|
|
96
87
|
const inputs = ctx.request.body
|
|
97
|
-
const tableId = getTableId(ctx)
|
|
88
|
+
const tableId = utils.getTableId(ctx)
|
|
98
89
|
let response, row
|
|
99
90
|
if (inputs.rows) {
|
|
100
91
|
let { rows } = await quotas.addQuery(
|
|
@@ -107,6 +98,7 @@ export async function destroy(ctx: any) {
|
|
|
107
98
|
response = rows
|
|
108
99
|
for (let row of rows) {
|
|
109
100
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
|
101
|
+
gridSocket?.emitRowDeletion(ctx, row._id)
|
|
110
102
|
}
|
|
111
103
|
} else {
|
|
112
104
|
let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), {
|
|
@@ -116,6 +108,7 @@ export async function destroy(ctx: any) {
|
|
|
116
108
|
response = resp.response
|
|
117
109
|
row = resp.row
|
|
118
110
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
|
111
|
+
gridSocket?.emitRowDeletion(ctx, row._id)
|
|
119
112
|
}
|
|
120
113
|
ctx.status = 200
|
|
121
114
|
// for automations include the row that was deleted
|
|
@@ -124,7 +117,7 @@ export async function destroy(ctx: any) {
|
|
|
124
117
|
}
|
|
125
118
|
|
|
126
119
|
export async function search(ctx: any) {
|
|
127
|
-
const tableId = getTableId(ctx)
|
|
120
|
+
const tableId = utils.getTableId(ctx)
|
|
128
121
|
ctx.status = 200
|
|
129
122
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx), {
|
|
130
123
|
datasourceId: tableId,
|
|
@@ -132,7 +125,7 @@ export async function search(ctx: any) {
|
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
export async function validate(ctx: Ctx) {
|
|
135
|
-
const tableId = getTableId(ctx)
|
|
128
|
+
const tableId = utils.getTableId(ctx)
|
|
136
129
|
// external tables are hard to validate currently
|
|
137
130
|
if (isExternalTable(tableId)) {
|
|
138
131
|
ctx.body = { valid: true }
|
|
@@ -145,7 +138,7 @@ export async function validate(ctx: Ctx) {
|
|
|
145
138
|
}
|
|
146
139
|
|
|
147
140
|
export async function fetchEnrichedRow(ctx: any) {
|
|
148
|
-
const tableId = getTableId(ctx)
|
|
141
|
+
const tableId = utils.getTableId(ctx)
|
|
149
142
|
ctx.body = await quotas.addQuery(
|
|
150
143
|
() => pickApi(tableId).fetchEnrichedRow(ctx),
|
|
151
144
|
{
|
|
@@ -155,7 +148,7 @@ export async function fetchEnrichedRow(ctx: any) {
|
|
|
155
148
|
}
|
|
156
149
|
|
|
157
150
|
export const exportRows = async (ctx: any) => {
|
|
158
|
-
const tableId = getTableId(ctx)
|
|
151
|
+
const tableId = utils.getTableId(ctx)
|
|
159
152
|
ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx), {
|
|
160
153
|
datasourceId: tableId,
|
|
161
154
|
})
|
|
@@ -154,3 +154,15 @@ export function cleanExportRows(
|
|
|
154
154
|
|
|
155
155
|
return cleanRows
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
export function getTableId(ctx: any) {
|
|
159
|
+
if (ctx.request.body && ctx.request.body.tableId) {
|
|
160
|
+
return ctx.request.body.tableId
|
|
161
|
+
}
|
|
162
|
+
if (ctx.params && ctx.params.tableId) {
|
|
163
|
+
return ctx.params.tableId
|
|
164
|
+
}
|
|
165
|
+
if (ctx.params && ctx.params.viewName) {
|
|
166
|
+
return ctx.params.viewName
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -11,6 +11,7 @@ import { context, events } from "@budibase/backend-core"
|
|
|
11
11
|
import { Table, UserCtx } from "@budibase/types"
|
|
12
12
|
import sdk from "../../../sdk"
|
|
13
13
|
import { jsonFromCsvString } from "../../../utilities/csv"
|
|
14
|
+
import { builderSocket } from "../../../websockets"
|
|
14
15
|
|
|
15
16
|
function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
|
|
16
17
|
if (table && !tableId) {
|
|
@@ -77,6 +78,7 @@ export async function save(ctx: UserCtx) {
|
|
|
77
78
|
ctx.eventEmitter &&
|
|
78
79
|
ctx.eventEmitter.emitTable(`table:save`, appId, savedTable)
|
|
79
80
|
ctx.body = savedTable
|
|
81
|
+
builderSocket.emitTableUpdate(ctx, savedTable)
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export async function destroy(ctx: UserCtx) {
|
|
@@ -89,6 +91,7 @@ export async function destroy(ctx: UserCtx) {
|
|
|
89
91
|
ctx.status = 200
|
|
90
92
|
ctx.table = deletedTable
|
|
91
93
|
ctx.body = { message: `Table ${tableId} deleted.` }
|
|
94
|
+
builderSocket.emitTableDeletion(ctx, tableId)
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
export async function bulkImport(ctx: UserCtx) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
View,
|
|
17
17
|
} from "@budibase/types"
|
|
18
18
|
import { cleanExportRows } from "../row/utils"
|
|
19
|
+
import { builderSocket } from "../../../websockets"
|
|
19
20
|
|
|
20
21
|
const { cloneDeep, isEqual } = require("lodash")
|
|
21
22
|
|
|
@@ -48,7 +49,7 @@ export async function save(ctx: Ctx) {
|
|
|
48
49
|
if (!view.meta.schema) {
|
|
49
50
|
view.meta.schema = table.schema
|
|
50
51
|
}
|
|
51
|
-
table.views[viewName] = view.meta
|
|
52
|
+
table.views[viewName] = { ...view.meta, name: viewName }
|
|
52
53
|
if (originalName) {
|
|
53
54
|
delete table.views[originalName]
|
|
54
55
|
existingTable.views[viewName] = existingTable.views[originalName]
|
|
@@ -56,10 +57,8 @@ export async function save(ctx: Ctx) {
|
|
|
56
57
|
await db.put(table)
|
|
57
58
|
await handleViewEvents(existingTable.views[viewName], table.views[viewName])
|
|
58
59
|
|
|
59
|
-
ctx.body =
|
|
60
|
-
|
|
61
|
-
name: viewToSave.name,
|
|
62
|
-
}
|
|
60
|
+
ctx.body = table.views[viewName]
|
|
61
|
+
builderSocket.emitTableUpdate(ctx, table)
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export async function calculationEvents(existingView: View, newView: View) {
|
|
@@ -128,6 +127,7 @@ export async function destroy(ctx: Ctx) {
|
|
|
128
127
|
await events.view.deleted(view)
|
|
129
128
|
|
|
130
129
|
ctx.body = view
|
|
130
|
+
builderSocket.emitTableUpdate(ctx, table)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export async function exportView(ctx: Ctx) {
|
package/src/app.ts
CHANGED
|
@@ -35,12 +35,11 @@ async function checkDevAppLocks(ctx: BBContext) {
|
|
|
35
35
|
if (!appId || !appId.startsWith(APP_DEV_PREFIX)) {
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
|
-
if (!(await doesUserHaveLock(appId, ctx.user))) {
|
|
39
|
-
ctx.throw(400, "User does not hold app lock.")
|
|
40
|
-
}
|
|
41
38
|
|
|
42
|
-
//
|
|
43
|
-
await
|
|
39
|
+
// If this user already owns the lock, then update it
|
|
40
|
+
if (await doesUserHaveLock(appId, ctx.user)) {
|
|
41
|
+
await updateLock(appId, ctx.user)
|
|
42
|
+
}
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
async function updateAppUpdatedAt(ctx: BBContext) {
|
package/src/startup.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as bullboard from "./automations/bullboard"
|
|
|
16
16
|
import * as pro from "@budibase/pro"
|
|
17
17
|
import * as api from "./api"
|
|
18
18
|
import sdk from "./sdk"
|
|
19
|
+
import { initialise as initialiseWebsockets } from "./websockets"
|
|
19
20
|
|
|
20
21
|
let STARTUP_RAN = false
|
|
21
22
|
|
|
@@ -64,6 +65,7 @@ export async function startup(app?: any, server?: any) {
|
|
|
64
65
|
fileSystem.init()
|
|
65
66
|
await redis.init()
|
|
66
67
|
eventInit()
|
|
68
|
+
initialiseWebsockets(app, server)
|
|
67
69
|
|
|
68
70
|
// run migrations on startup if not done via http
|
|
69
71
|
// not recommended in a clustered environment
|
package/src/utilities/redis.ts
CHANGED
|
@@ -4,23 +4,33 @@ import { ContextUser } from "@budibase/types"
|
|
|
4
4
|
|
|
5
5
|
const APP_DEV_LOCK_SECONDS = 600
|
|
6
6
|
const AUTOMATION_TEST_FLAG_SECONDS = 60
|
|
7
|
-
let devAppClient: any, debounceClient: any, flagClient: any
|
|
7
|
+
let devAppClient: any, debounceClient: any, flagClient: any, socketClient: any
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// We need to maintain a duplicate client for socket.io pub/sub
|
|
10
|
+
let socketSubClient: any
|
|
11
|
+
|
|
12
|
+
// We init this as we want to keep the connection open all the time
|
|
10
13
|
// reduces the performance hit
|
|
11
14
|
export async function init() {
|
|
12
15
|
devAppClient = new redis.Client(redis.utils.Databases.DEV_LOCKS)
|
|
13
16
|
debounceClient = new redis.Client(redis.utils.Databases.DEBOUNCE)
|
|
14
17
|
flagClient = new redis.Client(redis.utils.Databases.FLAGS)
|
|
18
|
+
socketClient = new redis.Client(redis.utils.Databases.SOCKET_IO)
|
|
15
19
|
await devAppClient.init()
|
|
16
20
|
await debounceClient.init()
|
|
17
21
|
await flagClient.init()
|
|
22
|
+
await socketClient.init()
|
|
23
|
+
|
|
24
|
+
// Duplicate the socket client for pub/sub
|
|
25
|
+
socketSubClient = socketClient.getClient().duplicate()
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export async function shutdown() {
|
|
21
29
|
if (devAppClient) await devAppClient.finish()
|
|
22
30
|
if (debounceClient) await debounceClient.finish()
|
|
23
31
|
if (flagClient) await flagClient.finish()
|
|
32
|
+
if (socketClient) await socketClient.finish()
|
|
33
|
+
if (socketSubClient) socketSubClient.disconnect()
|
|
24
34
|
// shutdown core clients
|
|
25
35
|
await redis.clients.shutdown()
|
|
26
36
|
console.log("Redis shutdown")
|
|
@@ -86,3 +96,10 @@ export async function checkTestFlag(id: string) {
|
|
|
86
96
|
export async function clearTestFlag(id: string) {
|
|
87
97
|
await devAppClient.delete(id)
|
|
88
98
|
}
|
|
99
|
+
|
|
100
|
+
export function getSocketPubSubClients() {
|
|
101
|
+
return {
|
|
102
|
+
pub: socketClient.getClient(),
|
|
103
|
+
sub: socketSubClient,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import authorized from "../middleware/authorized"
|
|
2
|
+
import Socket from "./websocket"
|
|
3
|
+
import { permissions } from "@budibase/backend-core"
|
|
4
|
+
import http from "http"
|
|
5
|
+
import Koa from "koa"
|
|
6
|
+
import { Datasource, Table } from "@budibase/types"
|
|
7
|
+
import { gridSocket } from "./index"
|
|
8
|
+
import { clearLock } from "../utilities/redis"
|
|
9
|
+
|
|
10
|
+
export default class BuilderSocket extends Socket {
|
|
11
|
+
constructor(app: Koa, server: http.Server) {
|
|
12
|
+
super(app, server, "/socket/builder", [authorized(permissions.BUILDER)])
|
|
13
|
+
|
|
14
|
+
this.io.on("connection", socket => {
|
|
15
|
+
// Join a room for this app
|
|
16
|
+
const user = socket.data.user
|
|
17
|
+
const appId = socket.data.appId
|
|
18
|
+
socket.join(appId)
|
|
19
|
+
socket.to(appId).emit("user-update", user)
|
|
20
|
+
|
|
21
|
+
// Initial identification of connected spreadsheet
|
|
22
|
+
socket.on("get-users", async (payload, callback) => {
|
|
23
|
+
const sockets = await this.io.in(appId).fetchSockets()
|
|
24
|
+
callback({
|
|
25
|
+
users: sockets.map(socket => socket.data.user),
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Disconnection cleanup
|
|
30
|
+
socket.on("disconnect", async () => {
|
|
31
|
+
socket.to(appId).emit("user-disconnect", user)
|
|
32
|
+
|
|
33
|
+
// Remove app lock from this user if they have no other connections
|
|
34
|
+
try {
|
|
35
|
+
const sockets = await this.io.in(appId).fetchSockets()
|
|
36
|
+
const hasOtherConnection = sockets.some(socket => {
|
|
37
|
+
const { _id, sessionId } = socket.data.user
|
|
38
|
+
return _id === user._id && sessionId !== user.sessionId
|
|
39
|
+
})
|
|
40
|
+
if (!hasOtherConnection) {
|
|
41
|
+
await clearLock(appId, user)
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// This is fine, just means this user didn't hold the lock
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
emitTableUpdate(ctx: any, table: Table) {
|
|
51
|
+
this.io.in(ctx.appId).emit("table-change", { id: table._id, table })
|
|
52
|
+
gridSocket.emitTableUpdate(table)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
emitTableDeletion(ctx: any, id: string) {
|
|
56
|
+
this.io.in(ctx.appId).emit("table-change", { id, table: null })
|
|
57
|
+
gridSocket.emitTableDeletion(id)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
emitDatasourceUpdate(ctx: any, datasource: Datasource) {
|
|
61
|
+
this.io
|
|
62
|
+
.in(ctx.appId)
|
|
63
|
+
.emit("datasource-change", { id: datasource._id, datasource })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
emitDatasourceDeletion(ctx: any, id: string) {
|
|
67
|
+
this.io.in(ctx.appId).emit("datasource-change", { id, datasource: null })
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/websockets/grid.ts
CHANGED
|
@@ -3,6 +3,8 @@ import Socket from "./websocket"
|
|
|
3
3
|
import { permissions } from "@budibase/backend-core"
|
|
4
4
|
import http from "http"
|
|
5
5
|
import Koa from "koa"
|
|
6
|
+
import { getTableId } from "../api/controllers/row/utils"
|
|
7
|
+
import { Row, Table } from "@budibase/types"
|
|
6
8
|
|
|
7
9
|
export default class GridSocket extends Socket {
|
|
8
10
|
constructor(app: Koa, server: http.Server) {
|
|
@@ -10,7 +12,6 @@ export default class GridSocket extends Socket {
|
|
|
10
12
|
|
|
11
13
|
this.io.on("connection", socket => {
|
|
12
14
|
const user = socket.data.user
|
|
13
|
-
console.log(`Spreadsheet user connected: ${user?.id}`)
|
|
14
15
|
|
|
15
16
|
// Socket state
|
|
16
17
|
let currentRoom: string
|
|
@@ -19,37 +20,54 @@ export default class GridSocket extends Socket {
|
|
|
19
20
|
socket.on("select-table", async (tableId, callback) => {
|
|
20
21
|
// Leave current room
|
|
21
22
|
if (currentRoom) {
|
|
22
|
-
socket.to(currentRoom).emit("user-disconnect",
|
|
23
|
+
socket.to(currentRoom).emit("user-disconnect", user)
|
|
23
24
|
socket.leave(currentRoom)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Join new room
|
|
27
28
|
currentRoom = tableId
|
|
28
29
|
socket.join(currentRoom)
|
|
29
|
-
socket.to(currentRoom).emit("user-update",
|
|
30
|
+
socket.to(currentRoom).emit("user-update", user)
|
|
30
31
|
|
|
31
32
|
// Reply with all users in current room
|
|
32
33
|
const sockets = await this.io.in(currentRoom).fetchSockets()
|
|
33
34
|
callback({
|
|
34
35
|
users: sockets.map(socket => socket.data.user),
|
|
35
|
-
id: user.id,
|
|
36
36
|
})
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
// Handle users selecting a new cell
|
|
40
40
|
socket.on("select-cell", cellId => {
|
|
41
|
-
socket.data.user.
|
|
41
|
+
socket.data.user.focusedCellId = cellId
|
|
42
42
|
if (currentRoom) {
|
|
43
|
-
socket.to(currentRoom).emit("user-update",
|
|
43
|
+
socket.to(currentRoom).emit("user-update", user)
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
// Disconnection cleanup
|
|
48
48
|
socket.on("disconnect", () => {
|
|
49
49
|
if (currentRoom) {
|
|
50
|
-
socket.to(currentRoom).emit("user-disconnect",
|
|
50
|
+
socket.to(currentRoom).emit("user-disconnect", user)
|
|
51
51
|
}
|
|
52
52
|
})
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
emitRowUpdate(ctx: any, row: Row) {
|
|
57
|
+
const tableId = getTableId(ctx)
|
|
58
|
+
this.io.in(tableId).emit("row-change", { id: row._id, row })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
emitRowDeletion(ctx: any, id: string) {
|
|
62
|
+
const tableId = getTableId(ctx)
|
|
63
|
+
this.io.in(tableId).emit("row-change", { id, row: null })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
emitTableUpdate(table: Table) {
|
|
67
|
+
this.io.in(table._id!).emit("table-change", { id: table._id, table })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
emitTableDeletion(id: string) {
|
|
71
|
+
this.io.in(id).emit("table-change", { id, table: null })
|
|
72
|
+
}
|
|
55
73
|
}
|
package/src/websockets/index.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import http from "http"
|
|
2
2
|
import Koa from "koa"
|
|
3
|
-
import GridSocket from "./grid"
|
|
4
3
|
import ClientAppSocket from "./client"
|
|
4
|
+
import GridSocket from "./grid"
|
|
5
|
+
import BuilderSocket from "./builder"
|
|
5
6
|
|
|
6
7
|
let clientAppSocket: ClientAppSocket
|
|
7
8
|
let gridSocket: GridSocket
|
|
9
|
+
let builderSocket: BuilderSocket
|
|
8
10
|
|
|
9
11
|
export const initialise = (app: Koa, server: http.Server) => {
|
|
10
12
|
clientAppSocket = new ClientAppSocket(app, server)
|
|
11
13
|
gridSocket = new GridSocket(app, server)
|
|
14
|
+
builderSocket = new BuilderSocket(app, server)
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
export { clientAppSocket, gridSocket }
|
|
17
|
+
export { clientAppSocket, gridSocket, builderSocket }
|
|
@@ -5,6 +5,9 @@ import Cookies from "cookies"
|
|
|
5
5
|
import { userAgent } from "koa-useragent"
|
|
6
6
|
import { auth } from "@budibase/backend-core"
|
|
7
7
|
import currentApp from "../middleware/currentapp"
|
|
8
|
+
import { createAdapter } from "@socket.io/redis-adapter"
|
|
9
|
+
import { getSocketPubSubClients } from "../utilities/redis"
|
|
10
|
+
import uuid from "uuid"
|
|
8
11
|
|
|
9
12
|
export default class Socket {
|
|
10
13
|
io: Server
|
|
@@ -12,7 +15,7 @@ export default class Socket {
|
|
|
12
15
|
constructor(
|
|
13
16
|
app: Koa,
|
|
14
17
|
server: http.Server,
|
|
15
|
-
path: string,
|
|
18
|
+
path: string = "/",
|
|
16
19
|
additionalMiddlewares?: any[]
|
|
17
20
|
) {
|
|
18
21
|
this.io = new Server(server, {
|
|
@@ -59,13 +62,21 @@ export default class Socket {
|
|
|
59
62
|
for (let [idx, middleware] of middlewares.entries()) {
|
|
60
63
|
await middleware(ctx, () => {
|
|
61
64
|
if (idx === middlewares.length - 1) {
|
|
62
|
-
// Middlewares are finished
|
|
65
|
+
// Middlewares are finished
|
|
63
66
|
// Extract some data from our enriched koa context to persist
|
|
64
67
|
// as metadata for the socket
|
|
68
|
+
// Add user info, including a deterministic color and label
|
|
69
|
+
const { _id, email, firstName, lastName } = ctx.user
|
|
65
70
|
socket.data.user = {
|
|
66
|
-
|
|
67
|
-
email
|
|
71
|
+
_id,
|
|
72
|
+
email,
|
|
73
|
+
firstName,
|
|
74
|
+
lastName,
|
|
75
|
+
sessionId: uuid.v4(),
|
|
68
76
|
}
|
|
77
|
+
|
|
78
|
+
// Add app ID to help split sockets into rooms
|
|
79
|
+
socket.data.appId = ctx.appId
|
|
69
80
|
next()
|
|
70
81
|
}
|
|
71
82
|
})
|
|
@@ -74,6 +85,11 @@ export default class Socket {
|
|
|
74
85
|
next(error)
|
|
75
86
|
}
|
|
76
87
|
})
|
|
88
|
+
|
|
89
|
+
// Instantiate redis adapter
|
|
90
|
+
const { pub, sub } = getSocketPubSubClients()
|
|
91
|
+
const opts = { key: `socket.io-${path}` }
|
|
92
|
+
this.io.adapter(createAdapter(pub, sub, opts))
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
// Emit an event to all sockets
|