@budibase/server 2.5.9 → 2.5.10-alpha.0
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.24635afb.js +1794 -0
- package/builder/assets/index.4eae16b2.css +6 -0
- package/builder/index.html +2 -2
- package/dist/api/controllers/application.js +3 -4
- package/dist/api/controllers/automation.js +13 -7
- package/dist/api/controllers/datasource.js +1 -1
- package/dist/api/controllers/dev.js +1 -1
- package/dist/api/controllers/ops.js +40 -0
- package/dist/api/controllers/plugin/index.js +6 -37
- package/dist/api/controllers/query/index.js +2 -2
- package/dist/api/controllers/row/ExternalRequest.js +21 -14
- package/dist/api/controllers/table/utils.js +9 -3
- package/dist/api/controllers/user.js +1 -83
- package/dist/api/index.js +1 -2
- package/dist/api/routes/index.js +2 -2
- package/dist/api/routes/{cloud.js → ops.js} +19 -6
- package/dist/api/routes/user.js +0 -1
- package/dist/app.js +4 -13
- package/dist/automations/actions.js +32 -6
- package/dist/automations/index.js +3 -2
- package/dist/automations/steps/bash.js +6 -6
- package/dist/automations/steps/createRow.js +11 -11
- package/dist/automations/steps/delay.js +3 -3
- package/dist/automations/steps/deleteRow.js +8 -8
- package/dist/automations/steps/discord.js +8 -8
- package/dist/automations/steps/executeQuery.js +9 -9
- package/dist/automations/steps/executeScript.js +6 -6
- package/dist/automations/steps/filter.js +6 -6
- package/dist/automations/steps/integromat.js +10 -10
- package/dist/automations/steps/loop.js +9 -9
- package/dist/automations/steps/outgoingWebhook.js +10 -10
- package/dist/automations/steps/queryRows.js +14 -14
- package/dist/automations/steps/sendSmtpEmail.js +9 -9
- package/dist/automations/steps/serverLog.js +4 -4
- package/dist/automations/steps/slack.js +6 -6
- package/dist/automations/steps/updateRow.js +11 -11
- package/dist/automations/steps/zapier.js +9 -9
- package/dist/automations/triggerInfo/app.js +5 -5
- package/dist/automations/triggerInfo/cron.js +4 -4
- package/dist/automations/triggerInfo/rowDeleted.js +5 -5
- package/dist/automations/triggerInfo/rowSaved.js +7 -7
- package/dist/automations/triggerInfo/rowUpdated.js +7 -7
- package/dist/automations/triggerInfo/webhook.js +6 -6
- package/dist/db/utils.js +3 -2
- package/dist/environment.js +0 -1
- package/dist/events/docUpdates/index.js +17 -0
- package/dist/events/docUpdates/processors.js +18 -0
- package/dist/events/docUpdates/syncUsers.js +49 -0
- package/dist/events/index.js +3 -0
- package/dist/integrations/base/sqlTable.js +9 -2
- package/dist/integrations/index.js +3 -3
- package/dist/integrations/microsoftSqlServer.js +5 -2
- package/dist/integrations/mysql.js +5 -3
- package/dist/integrations/postgres.js +7 -5
- package/dist/integrations/redis.js +7 -0
- package/dist/integrations/rest.js +4 -0
- package/dist/migrations/functions/syncQuotas.js +2 -0
- package/dist/migrations/functions/usageQuotas/syncApps.js +1 -2
- package/dist/migrations/functions/usageQuotas/syncRows.js +1 -2
- package/dist/migrations/functions/usageQuotas/syncUsers.js +21 -0
- package/dist/sdk/app/applications/sync.js +117 -23
- package/dist/sdk/app/backups/exports.js +14 -38
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/plugins/index.js +27 -0
- package/dist/sdk/plugins/plugins.js +53 -0
- package/dist/sdk/users/utils.js +21 -4
- package/dist/startup.js +31 -28
- package/dist/threads/automation.js +16 -5
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utilities/fileSystem/plugin.js +33 -23
- package/dist/utilities/global.js +17 -12
- package/dist/utilities/rowProcessor/utils.js +4 -5
- package/dist/watch.js +2 -2
- package/dist/websockets/client.js +14 -0
- package/dist/websockets/grid.js +60 -0
- package/dist/websockets/index.js +17 -0
- package/dist/websockets/websocket.js +78 -0
- package/package.json +16 -16
- package/scripts/dev/manage.js +2 -0
- package/scripts/integrations/mssql/data/entrypoint.sh +1 -0
- package/scripts/integrations/mssql/data/setup.sql +17 -17
- package/scripts/integrations/mysql/init.sql +1 -1
- package/scripts/integrations/postgres/init.sql +1 -0
- package/src/api/controllers/application.ts +4 -4
- package/src/api/controllers/automation.ts +12 -6
- package/src/api/controllers/datasource.ts +15 -5
- package/src/api/controllers/dev.ts +2 -2
- package/src/api/controllers/ops.ts +32 -0
- package/src/api/controllers/plugin/index.ts +8 -45
- package/src/api/controllers/query/index.ts +2 -2
- package/src/api/controllers/row/ExternalRequest.ts +21 -12
- package/src/api/controllers/row/internal.ts +9 -10
- package/src/api/controllers/row/utils.ts +2 -2
- package/src/api/controllers/table/utils.ts +10 -3
- package/src/api/controllers/user.ts +10 -96
- package/src/api/index.ts +2 -4
- package/src/api/routes/index.ts +2 -2
- package/src/api/routes/ops.ts +30 -0
- package/src/api/routes/tests/automation.spec.js +7 -4
- package/src/api/routes/tests/user.spec.js +48 -37
- package/src/api/routes/user.ts +0 -5
- package/src/app.ts +4 -15
- package/src/automations/actions.ts +56 -24
- package/src/automations/index.ts +1 -1
- package/src/automations/steps/bash.ts +10 -7
- package/src/automations/steps/createRow.ts +15 -12
- package/src/automations/steps/delay.ts +6 -4
- package/src/automations/steps/deleteRow.ts +12 -9
- package/src/automations/steps/discord.ts +10 -8
- package/src/automations/steps/executeQuery.ts +13 -10
- package/src/automations/steps/executeScript.ts +10 -7
- package/src/automations/steps/filter.ts +8 -6
- package/src/automations/steps/integromat.ts +12 -10
- package/src/automations/steps/loop.ts +16 -10
- package/src/automations/steps/outgoingWebhook.ts +14 -11
- package/src/automations/steps/queryRows.ts +18 -15
- package/src/automations/steps/sendSmtpEmail.ts +11 -9
- package/src/automations/steps/serverLog.ts +6 -4
- package/src/automations/steps/slack.ts +8 -6
- package/src/automations/steps/updateRow.ts +15 -12
- package/src/automations/steps/zapier.ts +11 -9
- package/src/automations/tests/utilities/index.ts +2 -2
- package/src/automations/triggerInfo/app.ts +8 -5
- package/src/automations/triggerInfo/cron.ts +7 -4
- package/src/automations/triggerInfo/rowDeleted.ts +8 -5
- package/src/automations/triggerInfo/rowSaved.ts +10 -7
- package/src/automations/triggerInfo/rowUpdated.ts +10 -7
- package/src/automations/triggerInfo/webhook.ts +9 -6
- package/src/db/utils.ts +1 -0
- package/src/environment.ts +0 -1
- package/src/events/docUpdates/index.ts +1 -0
- package/src/events/docUpdates/processors.ts +14 -0
- package/src/events/docUpdates/syncUsers.ts +35 -0
- package/src/events/index.ts +1 -0
- package/src/integration-test/postgres.spec.ts +3 -1
- package/src/integrations/base/sqlTable.ts +9 -2
- package/src/integrations/index.ts +3 -3
- package/src/integrations/microsoftSqlServer.ts +5 -2
- package/src/integrations/mysql.ts +5 -3
- package/src/integrations/postgres.ts +7 -5
- package/src/integrations/redis.ts +8 -0
- package/src/integrations/rest.ts +3 -0
- package/src/migrations/functions/syncQuotas.ts +2 -0
- package/src/migrations/functions/usageQuotas/syncApps.ts +2 -3
- package/src/migrations/functions/usageQuotas/syncRows.ts +2 -3
- package/src/migrations/functions/usageQuotas/syncUsers.ts +9 -0
- package/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts +2 -2
- package/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts +26 -0
- package/src/migrations/index.ts +1 -0
- package/src/sdk/app/applications/sync.ts +129 -22
- package/src/sdk/app/applications/tests/sync.spec.ts +137 -0
- package/src/sdk/app/backups/exports.ts +17 -41
- package/src/sdk/index.ts +2 -0
- package/src/sdk/plugins/index.ts +5 -0
- package/src/sdk/plugins/plugins.ts +41 -0
- package/src/sdk/users/tests/utils.spec.ts +1 -32
- package/src/sdk/users/utils.ts +23 -5
- package/src/startup.ts +36 -34
- package/src/tests/jestEnv.ts +0 -1
- package/src/tests/jestSetup.ts +0 -1
- package/src/tests/utilities/TestConfiguration.ts +28 -0
- package/src/tests/utilities/structures.ts +25 -17
- package/src/threads/automation.ts +18 -6
- package/src/utilities/fileSystem/plugin.ts +13 -4
- package/src/utilities/global.ts +21 -16
- package/src/utilities/rowProcessor/utils.ts +9 -10
- package/src/watch.ts +2 -2
- package/src/websockets/client.ts +11 -0
- package/src/websockets/grid.ts +55 -0
- package/src/websockets/index.ts +14 -0
- package/src/websockets/websocket.ts +83 -0
- package/tsconfig.build.json +3 -5
- package/tsconfig.json +2 -1
- package/builder/assets/index.0b358332.js +0 -1817
- package/builder/assets/index.7f9a008b.css +0 -6
- package/dist/api/controllers/cloud.js +0 -130
- package/dist/elasticApm.js +0 -14
- package/dist/package.json +0 -180
- package/dist/websocket.js +0 -22
- package/scripts/likeCypress.ts +0 -35
- package/src/api/controllers/cloud.ts +0 -119
- package/src/api/routes/cloud.ts +0 -18
- package/src/api/routes/tests/cloud.spec.ts +0 -54
- package/src/elasticApm.ts +0 -10
- package/src/migrations/functions/tests/syncQuotas.spec.js +0 -26
- package/src/tests/logging.ts +0 -34
- package/src/websocket.ts +0 -26
|
@@ -1,6 +1,117 @@
|
|
|
1
1
|
import env from "../../../environment"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
db as dbCore,
|
|
4
|
+
context,
|
|
5
|
+
docUpdates,
|
|
6
|
+
constants,
|
|
7
|
+
logging,
|
|
8
|
+
roles,
|
|
9
|
+
} from "@budibase/backend-core"
|
|
10
|
+
import { User, ContextUser, UserGroup } from "@budibase/types"
|
|
11
|
+
import { sdk as proSdk } from "@budibase/pro"
|
|
3
12
|
import sdk from "../../"
|
|
13
|
+
import { getGlobalUsers, processUser } from "../../../utilities/global"
|
|
14
|
+
import { generateUserMetadataID, InternalTables } from "../../../db/utils"
|
|
15
|
+
|
|
16
|
+
type DeletedUser = { _id: string; deleted: boolean }
|
|
17
|
+
|
|
18
|
+
async function syncUsersToApp(
|
|
19
|
+
appId: string,
|
|
20
|
+
users: (User | DeletedUser)[],
|
|
21
|
+
groups: UserGroup[]
|
|
22
|
+
) {
|
|
23
|
+
if (!(await dbCore.dbExists(appId))) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
await context.doInAppContext(appId, async () => {
|
|
27
|
+
const db = context.getAppDB()
|
|
28
|
+
for (let user of users) {
|
|
29
|
+
let ctxUser = user as ContextUser
|
|
30
|
+
let deletedUser = false
|
|
31
|
+
const metadataId = generateUserMetadataID(user._id!)
|
|
32
|
+
if ((user as DeletedUser).deleted) {
|
|
33
|
+
deletedUser = true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// make sure role is correct
|
|
37
|
+
if (!deletedUser) {
|
|
38
|
+
ctxUser = await processUser(ctxUser, { appId, groups })
|
|
39
|
+
}
|
|
40
|
+
let roleId = ctxUser.roleId
|
|
41
|
+
if (roleId === roles.BUILTIN_ROLE_IDS.PUBLIC) {
|
|
42
|
+
roleId = undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let metadata
|
|
46
|
+
try {
|
|
47
|
+
metadata = await db.get(metadataId)
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
if (err.status !== 404) {
|
|
50
|
+
throw err
|
|
51
|
+
}
|
|
52
|
+
// no metadata and user is to be deleted, can skip
|
|
53
|
+
// no role - user isn't in app anyway
|
|
54
|
+
if (!roleId) {
|
|
55
|
+
continue
|
|
56
|
+
} else if (!deletedUser) {
|
|
57
|
+
// doesn't exist yet, creating it
|
|
58
|
+
metadata = {
|
|
59
|
+
tableId: InternalTables.USER_METADATA,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// the user doesn't exist, or doesn't have a role anymore
|
|
65
|
+
// get rid of their metadata
|
|
66
|
+
if (deletedUser || !roleId) {
|
|
67
|
+
await db.remove(metadata)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// assign the roleId for the metadata doc
|
|
72
|
+
if (roleId) {
|
|
73
|
+
metadata.roleId = roleId
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let combined = sdk.users.combineMetadataAndUser(ctxUser, metadata)
|
|
77
|
+
// if no combined returned, there are no updates to make
|
|
78
|
+
if (combined) {
|
|
79
|
+
await db.put(combined)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function syncUsersToAllApps(userIds: string[]) {
|
|
86
|
+
// list of users, if one has been deleted it will be undefined in array
|
|
87
|
+
const users = (await getGlobalUsers(userIds, {
|
|
88
|
+
noProcessing: true,
|
|
89
|
+
})) as User[]
|
|
90
|
+
const groups = await proSdk.groups.fetch()
|
|
91
|
+
const finalUsers: (User | DeletedUser)[] = []
|
|
92
|
+
for (let userId of userIds) {
|
|
93
|
+
const user = users.find(user => user._id === userId)
|
|
94
|
+
if (!user) {
|
|
95
|
+
finalUsers.push({ _id: userId, deleted: true })
|
|
96
|
+
} else {
|
|
97
|
+
finalUsers.push(user)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const devAppIds = await dbCore.getDevAppIDs()
|
|
101
|
+
let promises = []
|
|
102
|
+
for (let devAppId of devAppIds) {
|
|
103
|
+
const prodAppId = dbCore.getProdAppID(devAppId)
|
|
104
|
+
for (let appId of [prodAppId, devAppId]) {
|
|
105
|
+
promises.push(syncUsersToApp(appId, finalUsers, groups))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const resp = await Promise.allSettled(promises)
|
|
109
|
+
const failed = resp.filter(promise => promise.status === "rejected")
|
|
110
|
+
if (failed.length > 0) {
|
|
111
|
+
const reasons = failed.map(fail => (fail as PromiseRejectedResult).reason)
|
|
112
|
+
logging.logAlert("Failed to sync users to apps", reasons)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
4
115
|
|
|
5
116
|
export async function syncApp(
|
|
6
117
|
appId: string,
|
|
@@ -23,32 +134,28 @@ export async function syncApp(
|
|
|
23
134
|
// specific case, want to make sure setup is skipped
|
|
24
135
|
const prodDb = context.getProdAppDB({ skip_setup: true })
|
|
25
136
|
const exists = await prodDb.exists()
|
|
26
|
-
if (!exists) {
|
|
27
|
-
// the database doesn't exist. Don't replicate
|
|
28
|
-
return {
|
|
29
|
-
message: "App sync not required, app not deployed.",
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
137
|
|
|
33
|
-
const replication = new dbCore.Replication({
|
|
34
|
-
source: prodAppId,
|
|
35
|
-
target: appId,
|
|
36
|
-
})
|
|
37
138
|
let error
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
139
|
+
if (exists) {
|
|
140
|
+
const replication = new dbCore.Replication({
|
|
141
|
+
source: prodAppId,
|
|
142
|
+
target: appId,
|
|
143
|
+
})
|
|
144
|
+
try {
|
|
145
|
+
const replOpts = replication.appReplicateOpts()
|
|
146
|
+
if (opts?.automationOnly) {
|
|
147
|
+
replOpts.filter = (doc: any) =>
|
|
148
|
+
doc._id.startsWith(dbCore.DocumentType.AUTOMATION)
|
|
149
|
+
}
|
|
150
|
+
await replication.replicate(replOpts)
|
|
151
|
+
} catch (err) {
|
|
152
|
+
error = err
|
|
153
|
+
} finally {
|
|
154
|
+
await replication.close()
|
|
43
155
|
}
|
|
44
|
-
await replication.replicate(replOpts)
|
|
45
|
-
} catch (err) {
|
|
46
|
-
error = err
|
|
47
|
-
} finally {
|
|
48
|
-
await replication.close()
|
|
49
156
|
}
|
|
50
157
|
|
|
51
|
-
// sync the users
|
|
158
|
+
// sync the users - kept for safe keeping
|
|
52
159
|
await sdk.users.syncGlobalUsers()
|
|
53
160
|
|
|
54
161
|
if (error) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
|
2
|
+
import { events, context, roles, constants } from "@budibase/backend-core"
|
|
3
|
+
import { init } from "../../../../events"
|
|
4
|
+
import { rawUserMetadata } from "../../../users/utils"
|
|
5
|
+
import EventEmitter from "events"
|
|
6
|
+
import { UserGroup, UserMetadata, UserRoles, User } from "@budibase/types"
|
|
7
|
+
|
|
8
|
+
const config = new TestConfiguration()
|
|
9
|
+
let app, group: UserGroup, groupUser: User
|
|
10
|
+
const ROLE_ID = roles.BUILTIN_ROLE_IDS.BASIC
|
|
11
|
+
|
|
12
|
+
const emitter = new EventEmitter()
|
|
13
|
+
|
|
14
|
+
function updateCb(docId: string) {
|
|
15
|
+
const isGroup = docId.startsWith(constants.DocumentType.GROUP)
|
|
16
|
+
if (isGroup) {
|
|
17
|
+
emitter.emit("update-group")
|
|
18
|
+
} else {
|
|
19
|
+
emitter.emit("update-user")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init(updateCb)
|
|
24
|
+
|
|
25
|
+
function waitForUpdate(opts: { group?: boolean }) {
|
|
26
|
+
return new Promise<void>((resolve, reject) => {
|
|
27
|
+
const timeout = setTimeout(() => {
|
|
28
|
+
reject()
|
|
29
|
+
}, 5000)
|
|
30
|
+
const event = opts?.group ? "update-group" : "update-user"
|
|
31
|
+
emitter.on(event, () => {
|
|
32
|
+
clearTimeout(timeout)
|
|
33
|
+
resolve()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
app = await config.init("syncApp")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
async function createUser(email: string, roles: UserRoles, builder?: boolean) {
|
|
43
|
+
const user = await config.createUser({
|
|
44
|
+
email,
|
|
45
|
+
roles,
|
|
46
|
+
builder: builder || false,
|
|
47
|
+
admin: false,
|
|
48
|
+
})
|
|
49
|
+
await context.doInContext(config.appId!, async () => {
|
|
50
|
+
await events.user.created(user)
|
|
51
|
+
})
|
|
52
|
+
return user
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function removeUserRole(user: User) {
|
|
56
|
+
const final = await config.globalUser({
|
|
57
|
+
...user,
|
|
58
|
+
id: user._id,
|
|
59
|
+
roles: {},
|
|
60
|
+
builder: false,
|
|
61
|
+
admin: false,
|
|
62
|
+
})
|
|
63
|
+
await context.doInContext(config.appId!, async () => {
|
|
64
|
+
await events.user.updated(final)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function createGroupAndUser(email: string) {
|
|
69
|
+
groupUser = await config.createUser({
|
|
70
|
+
email,
|
|
71
|
+
roles: {},
|
|
72
|
+
builder: false,
|
|
73
|
+
admin: false,
|
|
74
|
+
})
|
|
75
|
+
group = await config.createGroup()
|
|
76
|
+
await config.addUserToGroup(group._id!, groupUser._id!)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function removeUserFromGroup() {
|
|
80
|
+
await config.removeUserFromGroup(group._id!, groupUser._id!)
|
|
81
|
+
return context.doInContext(config.appId!, async () => {
|
|
82
|
+
await events.user.updated(groupUser)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function getUserMetadata(): Promise<UserMetadata[]> {
|
|
87
|
+
return context.doInContext(config.appId!, async () => {
|
|
88
|
+
return await rawUserMetadata()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildRoles() {
|
|
93
|
+
return { [config.prodAppId!]: ROLE_ID }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("app user/group sync", () => {
|
|
97
|
+
const groupEmail = "test2@test.com",
|
|
98
|
+
normalEmail = "test@test.com"
|
|
99
|
+
async function checkEmail(
|
|
100
|
+
email: string,
|
|
101
|
+
opts?: { group?: boolean; notFound?: boolean }
|
|
102
|
+
) {
|
|
103
|
+
await waitForUpdate(opts || {})
|
|
104
|
+
const metadata = await getUserMetadata()
|
|
105
|
+
const found = metadata.find(data => data.email === email)
|
|
106
|
+
if (opts?.notFound) {
|
|
107
|
+
expect(found).toBeUndefined()
|
|
108
|
+
} else {
|
|
109
|
+
expect(found).toBeDefined()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("should be able to sync a new user, add then remove", async () => {
|
|
114
|
+
const user = await createUser(normalEmail, buildRoles())
|
|
115
|
+
await checkEmail(normalEmail)
|
|
116
|
+
await removeUserRole(user)
|
|
117
|
+
await checkEmail(normalEmail, { notFound: true })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should be able to sync a group", async () => {
|
|
121
|
+
await createGroupAndUser(groupEmail)
|
|
122
|
+
await checkEmail(groupEmail, { group: true })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("should be able to remove user from group", async () => {
|
|
126
|
+
if (!group) {
|
|
127
|
+
await createGroupAndUser(groupEmail)
|
|
128
|
+
}
|
|
129
|
+
await removeUserFromGroup()
|
|
130
|
+
await checkEmail(groupEmail, { notFound: true })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("should be able to handle builder users", async () => {
|
|
134
|
+
await createUser("test3@test.com", {}, true)
|
|
135
|
+
await checkEmail("test3@test.com")
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -3,6 +3,7 @@ import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
|
|
3
3
|
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
|
4
4
|
import { ObjectStoreBuckets } from "../../../constants"
|
|
5
5
|
import {
|
|
6
|
+
AUTOMATION_LOG_PREFIX,
|
|
6
7
|
LINK_USER_METADATA_PREFIX,
|
|
7
8
|
TABLE_ROW_PREFIX,
|
|
8
9
|
USER_METDATA_PREFIX,
|
|
@@ -20,11 +21,15 @@ const uuid = require("uuid/v4")
|
|
|
20
21
|
const tar = require("tar")
|
|
21
22
|
const MemoryStream = require("memorystream")
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
interface DBDumpOpts {
|
|
24
25
|
filter?: any
|
|
25
26
|
exportPath?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ExportOpts extends DBDumpOpts {
|
|
26
30
|
tar?: boolean
|
|
27
31
|
excludeRows?: boolean
|
|
32
|
+
excludeLogs?: boolean
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
|
@@ -49,7 +54,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
|
|
|
49
54
|
* a filter function or the name of the export.
|
|
50
55
|
* @return {*} either a readable stream or a string
|
|
51
56
|
*/
|
|
52
|
-
export async function exportDB(dbName: string, opts:
|
|
57
|
+
export async function exportDB(dbName: string, opts: DBDumpOpts = {}) {
|
|
53
58
|
const exportOpts = {
|
|
54
59
|
filter: opts?.filter,
|
|
55
60
|
batch_size: 1000,
|
|
@@ -76,11 +81,14 @@ export async function exportDB(dbName: string, opts: ExportOpts = {}) {
|
|
|
76
81
|
})
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function defineFilter(excludeRows?: boolean) {
|
|
84
|
+
function defineFilter(excludeRows?: boolean, excludeLogs?: boolean) {
|
|
80
85
|
const ids = [USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX]
|
|
81
86
|
if (excludeRows) {
|
|
82
87
|
ids.push(TABLE_ROW_PREFIX)
|
|
83
88
|
}
|
|
89
|
+
if (excludeLogs) {
|
|
90
|
+
ids.push(AUTOMATION_LOG_PREFIX)
|
|
91
|
+
}
|
|
84
92
|
return (doc: any) =>
|
|
85
93
|
!ids.map(key => doc._id.includes(key)).reduce((prev, curr) => prev || curr)
|
|
86
94
|
}
|
|
@@ -130,8 +138,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|
|
130
138
|
// enforce an export of app DB to the tmp path
|
|
131
139
|
const dbPath = join(tmpPath, DB_EXPORT_FILE)
|
|
132
140
|
await exportDB(appId, {
|
|
133
|
-
|
|
134
|
-
filter: defineFilter(config?.excludeRows),
|
|
141
|
+
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
|
135
142
|
exportPath: dbPath,
|
|
136
143
|
})
|
|
137
144
|
// if tar requested, return where the tarball is
|
|
@@ -148,41 +155,6 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
|
|
151
|
-
/**
|
|
152
|
-
* Export all apps + global DB (if supplied) to a single tarball, this includes
|
|
153
|
-
* the attachments for each app as well.
|
|
154
|
-
* @param {object[]} appMetadata The IDs and names of apps to export.
|
|
155
|
-
* @param {string} globalDbContents The contents of the global DB to export as well.
|
|
156
|
-
* @return {string} The path to the tarball.
|
|
157
|
-
*/
|
|
158
|
-
export async function exportMultipleApps(
|
|
159
|
-
appMetadata: { appId: string; name: string }[],
|
|
160
|
-
globalDbContents?: string
|
|
161
|
-
) {
|
|
162
|
-
const tmpPath = join(budibaseTempDir(), uuid())
|
|
163
|
-
fs.mkdirSync(tmpPath)
|
|
164
|
-
let exportPromises: Promise<void>[] = []
|
|
165
|
-
// export each app to a directory, then move it into the complete export
|
|
166
|
-
const exportAndMove = async (appId: string, appName: string) => {
|
|
167
|
-
const path = await exportApp(appId)
|
|
168
|
-
await fs.promises.rename(path, join(tmpPath, appName))
|
|
169
|
-
}
|
|
170
|
-
for (let metadata of appMetadata) {
|
|
171
|
-
exportPromises.push(exportAndMove(metadata.appId, metadata.name))
|
|
172
|
-
}
|
|
173
|
-
// wait for all exports to finish
|
|
174
|
-
await Promise.all(exportPromises)
|
|
175
|
-
// add the global DB contents
|
|
176
|
-
if (globalDbContents) {
|
|
177
|
-
fs.writeFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), globalDbContents)
|
|
178
|
-
}
|
|
179
|
-
const appNames = appMetadata.map(metadata => metadata.name)
|
|
180
|
-
const tarPath = tarFilesToTmp(tmpPath, [...appNames, GLOBAL_DB_EXPORT_FILE])
|
|
181
|
-
// clear up the tmp path now tarball generated
|
|
182
|
-
fs.rmSync(tmpPath, { recursive: true, force: true })
|
|
183
|
-
return tarPath
|
|
184
|
-
}
|
|
185
|
-
|
|
186
158
|
/**
|
|
187
159
|
* Streams a backup of the database state for an app
|
|
188
160
|
* @param {string} appId The ID of the app which is to be backed up.
|
|
@@ -190,6 +162,10 @@ export async function exportMultipleApps(
|
|
|
190
162
|
* @returns {*} a readable stream of the backup which is written in real time
|
|
191
163
|
*/
|
|
192
164
|
export async function streamExportApp(appId: string, excludeRows: boolean) {
|
|
193
|
-
const tmpPath = await exportApp(appId, {
|
|
165
|
+
const tmpPath = await exportApp(appId, {
|
|
166
|
+
excludeRows,
|
|
167
|
+
excludeLogs: true,
|
|
168
|
+
tar: true,
|
|
169
|
+
})
|
|
194
170
|
return streamFile(tmpPath)
|
|
195
171
|
}
|
package/src/sdk/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { default as datasources } from "./app/datasources"
|
|
|
6
6
|
import { default as queries } from "./app/queries"
|
|
7
7
|
import { default as rows } from "./app/rows"
|
|
8
8
|
import { default as users } from "./users"
|
|
9
|
+
import { default as plugins } from "./plugins"
|
|
9
10
|
|
|
10
11
|
const sdk = {
|
|
11
12
|
backups,
|
|
@@ -16,6 +17,7 @@ const sdk = {
|
|
|
16
17
|
users,
|
|
17
18
|
datasources,
|
|
18
19
|
queries,
|
|
20
|
+
plugins,
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// default export for TS
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FileType, Plugin, PluginSource, PluginType } from "@budibase/types"
|
|
2
|
+
import {
|
|
3
|
+
db as dbCore,
|
|
4
|
+
objectStore,
|
|
5
|
+
plugins as pluginCore,
|
|
6
|
+
tenancy,
|
|
7
|
+
} from "@budibase/backend-core"
|
|
8
|
+
import { fileUpload } from "../../api/controllers/plugin/file"
|
|
9
|
+
import env from "../../environment"
|
|
10
|
+
import { clientAppSocket } from "../../websockets"
|
|
11
|
+
import { sdk as pro } from "@budibase/pro"
|
|
12
|
+
|
|
13
|
+
export async function fetch(type?: PluginType) {
|
|
14
|
+
const db = tenancy.getGlobalDB()
|
|
15
|
+
const response = await db.allDocs(
|
|
16
|
+
dbCore.getPluginParams(null, {
|
|
17
|
+
include_docs: true,
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
let plugins = response.rows.map((row: any) => row.doc) as Plugin[]
|
|
21
|
+
plugins = objectStore.enrichPluginURLs(plugins)
|
|
22
|
+
if (type) {
|
|
23
|
+
return plugins.filter((plugin: Plugin) => plugin.schema?.type === type)
|
|
24
|
+
} else {
|
|
25
|
+
return plugins
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function processUploaded(plugin: FileType, source?: PluginSource) {
|
|
30
|
+
const { metadata, directory } = await fileUpload(plugin)
|
|
31
|
+
pluginCore.validate(metadata?.schema)
|
|
32
|
+
|
|
33
|
+
// Only allow components in cloud
|
|
34
|
+
if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) {
|
|
35
|
+
throw new Error("Only component plugins are supported outside of self-host")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const doc = await pro.plugins.storePlugin(metadata, directory, source)
|
|
39
|
+
clientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash })
|
|
40
|
+
return doc
|
|
41
|
+
}
|
|
@@ -121,38 +121,7 @@ describe("syncGlobalUsers", () => {
|
|
|
121
121
|
await syncGlobalUsers()
|
|
122
122
|
|
|
123
123
|
const metadata = await rawUserMetadata()
|
|
124
|
-
expect(metadata).toHaveLength(
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("app users are removed when app is removed from user group", async () => {
|
|
130
|
-
await config.doInTenant(async () => {
|
|
131
|
-
const group = await proSdk.groups.save(structures.userGroups.userGroup())
|
|
132
|
-
const user1 = await config.createUser({ admin: false, builder: false })
|
|
133
|
-
const user2 = await config.createUser({ admin: false, builder: false })
|
|
134
|
-
await proSdk.groups.updateGroupApps(group.id, {
|
|
135
|
-
appsToAdd: [
|
|
136
|
-
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
await proSdk.groups.addUsers(group.id, [user1._id, user2._id])
|
|
140
|
-
|
|
141
|
-
await config.doInContext(config.appId, async () => {
|
|
142
|
-
await syncGlobalUsers()
|
|
143
|
-
expect(await rawUserMetadata()).toHaveLength(3)
|
|
144
|
-
|
|
145
|
-
await proSdk.groups.removeUsers(group.id, [user1._id])
|
|
146
|
-
await syncGlobalUsers()
|
|
147
|
-
|
|
148
|
-
const metadata = await rawUserMetadata()
|
|
149
|
-
expect(metadata).toHaveLength(2)
|
|
150
|
-
|
|
151
|
-
expect(metadata).not.toContainEqual(
|
|
152
|
-
expect.objectContaining({
|
|
153
|
-
_id: db.generateUserMetadataID(user1._id),
|
|
154
|
-
})
|
|
155
|
-
)
|
|
124
|
+
expect(metadata).toHaveLength(0)
|
|
156
125
|
})
|
|
157
126
|
})
|
|
158
127
|
})
|
package/src/sdk/users/utils.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getGlobalUsers } from "../../utilities/global"
|
|
2
2
|
import { context, roles as rolesCore } from "@budibase/backend-core"
|
|
3
3
|
import {
|
|
4
|
+
getGlobalIDFromUserMetadataID,
|
|
4
5
|
generateUserMetadataID,
|
|
5
6
|
getUserMetadataParams,
|
|
6
7
|
InternalTables,
|
|
7
8
|
} from "../../db/utils"
|
|
8
9
|
import { isEqual } from "lodash"
|
|
9
|
-
import { ContextUser, UserMetadata } from "@budibase/types"
|
|
10
|
+
import { ContextUser, UserMetadata, User } from "@budibase/types"
|
|
10
11
|
|
|
11
12
|
export function combineMetadataAndUser(
|
|
12
13
|
user: ContextUser,
|
|
@@ -37,6 +38,10 @@ export function combineMetadataAndUser(
|
|
|
37
38
|
if (found) {
|
|
38
39
|
newDoc._rev = found._rev
|
|
39
40
|
}
|
|
41
|
+
// clear fields that shouldn't be in metadata
|
|
42
|
+
delete newDoc.password
|
|
43
|
+
delete newDoc.forceResetPassword
|
|
44
|
+
delete newDoc.roles
|
|
40
45
|
if (found == null || !isEqual(newDoc, found)) {
|
|
41
46
|
return {
|
|
42
47
|
...found,
|
|
@@ -60,10 +65,9 @@ export async function rawUserMetadata() {
|
|
|
60
65
|
export async function syncGlobalUsers() {
|
|
61
66
|
// sync user metadata
|
|
62
67
|
const db = context.getAppDB()
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
])
|
|
68
|
+
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata()])
|
|
69
|
+
const users = resp[0] as User[]
|
|
70
|
+
const metadata = resp[1] as UserMetadata[]
|
|
67
71
|
const toWrite = []
|
|
68
72
|
for (let user of users) {
|
|
69
73
|
const combined = combineMetadataAndUser(user, metadata)
|
|
@@ -71,5 +75,19 @@ export async function syncGlobalUsers() {
|
|
|
71
75
|
toWrite.push(combined)
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
let foundEmails: string[] = []
|
|
79
|
+
for (let data of metadata) {
|
|
80
|
+
if (!data._id) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
const alreadyExisting = data.email && foundEmails.indexOf(data.email) !== -1
|
|
84
|
+
const globalId = getGlobalIDFromUserMetadataID(data._id)
|
|
85
|
+
if (!users.find(user => user._id === globalId) || alreadyExisting) {
|
|
86
|
+
toWrite.push({ ...data, _deleted: true })
|
|
87
|
+
}
|
|
88
|
+
if (data.email) {
|
|
89
|
+
foundEmails.push(data.email)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
74
92
|
await db.bulkDocs(toWrite)
|
|
75
93
|
}
|
package/src/startup.ts
CHANGED
|
@@ -10,19 +10,16 @@ import fs from "fs"
|
|
|
10
10
|
import { watch } from "./watch"
|
|
11
11
|
import * as automations from "./automations"
|
|
12
12
|
import * as fileSystem from "./utilities/fileSystem"
|
|
13
|
-
import eventEmitter from "./events"
|
|
13
|
+
import { default as eventEmitter, init as eventInit } from "./events"
|
|
14
14
|
import * as migrations from "./migrations"
|
|
15
15
|
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
|
-
const pino = require("koa-pino-logger")
|
|
20
19
|
|
|
21
20
|
let STARTUP_RAN = false
|
|
22
21
|
|
|
23
22
|
async function initRoutes(app: any) {
|
|
24
|
-
app.use(pino(logging.pinoSettings()))
|
|
25
|
-
|
|
26
23
|
if (!env.isTest()) {
|
|
27
24
|
const plugin = await bullboard.init()
|
|
28
25
|
app.use(plugin)
|
|
@@ -48,8 +45,10 @@ async function initPro() {
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
function shutdown(server?: any) {
|
|
51
|
-
server
|
|
52
|
-
|
|
48
|
+
if (server) {
|
|
49
|
+
server.close()
|
|
50
|
+
server.destroy()
|
|
51
|
+
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
export async function startup(app?: any, server?: any) {
|
|
@@ -64,6 +63,7 @@ export async function startup(app?: any, server?: any) {
|
|
|
64
63
|
eventEmitter.emitPort(env.PORT)
|
|
65
64
|
fileSystem.init()
|
|
66
65
|
await redis.init()
|
|
66
|
+
eventInit()
|
|
67
67
|
|
|
68
68
|
// run migrations on startup if not done via http
|
|
69
69
|
// not recommended in a clustered environment
|
|
@@ -72,11 +72,39 @@ export async function startup(app?: any, server?: any) {
|
|
|
72
72
|
await migrations.migrate()
|
|
73
73
|
} catch (e) {
|
|
74
74
|
logging.logAlert("Error performing migrations. Exiting.", e)
|
|
75
|
-
shutdown()
|
|
75
|
+
shutdown(server)
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// monitor plugin directory if required
|
|
80
|
+
if (
|
|
81
|
+
env.SELF_HOSTED &&
|
|
82
|
+
!env.MULTI_TENANCY &&
|
|
83
|
+
env.PLUGINS_DIR &&
|
|
84
|
+
fs.existsSync(env.PLUGINS_DIR)
|
|
85
|
+
) {
|
|
86
|
+
watch()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// check for version updates
|
|
90
|
+
await installation.checkInstallVersion()
|
|
91
|
+
|
|
92
|
+
// get the references to the queue promises, don't await as
|
|
93
|
+
// they will never end, unless the processing stops
|
|
94
|
+
let queuePromises = []
|
|
95
|
+
// configure events to use the pro audit log write
|
|
96
|
+
// can't integrate directly into backend-core due to cyclic issues
|
|
97
|
+
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
|
|
98
|
+
queuePromises.push(automations.init())
|
|
99
|
+
queuePromises.push(initPro())
|
|
100
|
+
if (app) {
|
|
101
|
+
// bring routes online as final step once everything ready
|
|
102
|
+
await initRoutes(app)
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
// check and create admin user if required
|
|
106
|
+
// this must be run after the api has been initialised due to
|
|
107
|
+
// the app user sync
|
|
80
108
|
if (
|
|
81
109
|
env.SELF_HOSTED &&
|
|
82
110
|
!env.MULTI_TENANCY &&
|
|
@@ -103,34 +131,8 @@ export async function startup(app?: any, server?: any) {
|
|
|
103
131
|
)
|
|
104
132
|
} catch (e) {
|
|
105
133
|
logging.logAlert("Error creating initial admin user. Exiting.", e)
|
|
106
|
-
shutdown()
|
|
134
|
+
shutdown(server)
|
|
107
135
|
}
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
|
-
|
|
111
|
-
// monitor plugin directory if required
|
|
112
|
-
if (
|
|
113
|
-
env.SELF_HOSTED &&
|
|
114
|
-
!env.MULTI_TENANCY &&
|
|
115
|
-
env.PLUGINS_DIR &&
|
|
116
|
-
fs.existsSync(env.PLUGINS_DIR)
|
|
117
|
-
) {
|
|
118
|
-
watch()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// check for version updates
|
|
122
|
-
await installation.checkInstallVersion()
|
|
123
|
-
|
|
124
|
-
// get the references to the queue promises, don't await as
|
|
125
|
-
// they will never end, unless the processing stops
|
|
126
|
-
let queuePromises = []
|
|
127
|
-
// configure events to use the pro audit log write
|
|
128
|
-
// can't integrate directly into backend-core due to cyclic issues
|
|
129
|
-
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
|
|
130
|
-
queuePromises.push(automations.init())
|
|
131
|
-
queuePromises.push(initPro())
|
|
132
|
-
if (app) {
|
|
133
|
-
// bring routes online as final step once everything ready
|
|
134
|
-
await initRoutes(app)
|
|
135
|
-
}
|
|
136
138
|
}
|