@budibase/server 2.5.5 → 2.5.6-alpha.10
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.5c1a6913.js +1776 -0
- package/builder/assets/index.c0265b74.css +6 -0
- package/builder/index.html +2 -2
- package/dist/api/controllers/automation.js +13 -7
- package/dist/api/controllers/ops.js +40 -0
- package/dist/api/controllers/plugin/index.js +6 -37
- package/dist/api/controllers/table/utils.js +2 -1
- package/dist/api/controllers/user.js +1 -83
- package/dist/api/routes/index.js +2 -0
- package/dist/api/routes/ops.js +52 -0
- 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/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/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/usageQuotas/syncApps.js +1 -1
- package/dist/migrations/functions/usageQuotas/syncRows.js +1 -2
- package/dist/package.json +15 -15
- package/dist/sdk/app/applications/sync.js +117 -23
- 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/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/jest.config.ts +3 -3
- package/nodemon.json +7 -3
- 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/automation.ts +12 -6
- package/src/api/controllers/ops.ts +32 -0
- package/src/api/controllers/plugin/index.ts +8 -45
- 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 +2 -1
- package/src/api/controllers/user.ts +10 -96
- package/src/api/routes/index.ts +2 -0
- 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/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/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/usageQuotas/syncApps.ts +1 -1
- package/src/migrations/functions/usageQuotas/syncRows.ts +2 -3
- package/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts +2 -2
- package/src/sdk/app/applications/sync.ts +129 -22
- package/src/sdk/app/applications/tests/sync.spec.ts +137 -0
- 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/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.json +1 -7
- package/builder/assets/index.7f9a008b.css +0 -6
- package/builder/assets/index.f493a2b3.js +0 -1817
- package/dist/elasticApm.js +0 -14
- package/dist/websocket.js +0 -22
- package/scripts/likeCypress.ts +0 -35
- package/src/elasticApm.ts +0 -10
- package/src/tests/logging.ts +0 -34
- package/src/websocket.ts +0 -26
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AutomationCustomIOType,
|
|
3
|
+
AutomationIOType,
|
|
4
|
+
AutomationStepType,
|
|
2
5
|
AutomationTriggerSchema,
|
|
3
6
|
AutomationTriggerStepId,
|
|
4
7
|
} from "@budibase/types"
|
|
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
|
|
|
15
18
|
inputs: {
|
|
16
19
|
properties: {
|
|
17
20
|
tableId: {
|
|
18
|
-
type:
|
|
19
|
-
customType:
|
|
21
|
+
type: AutomationIOType.STRING,
|
|
22
|
+
customType: AutomationCustomIOType.TABLE,
|
|
20
23
|
title: "Table",
|
|
21
24
|
},
|
|
22
25
|
},
|
|
@@ -25,21 +28,21 @@ export const definition: AutomationTriggerSchema = {
|
|
|
25
28
|
outputs: {
|
|
26
29
|
properties: {
|
|
27
30
|
row: {
|
|
28
|
-
type:
|
|
29
|
-
customType:
|
|
31
|
+
type: AutomationIOType.OBJECT,
|
|
32
|
+
customType: AutomationCustomIOType.ROW,
|
|
30
33
|
description: "The row that was updated",
|
|
31
34
|
},
|
|
32
35
|
id: {
|
|
33
|
-
type:
|
|
36
|
+
type: AutomationIOType.STRING,
|
|
34
37
|
description: "Row ID - can be used for updating",
|
|
35
38
|
},
|
|
36
39
|
revision: {
|
|
37
|
-
type:
|
|
40
|
+
type: AutomationIOType.STRING,
|
|
38
41
|
description: "Revision of row",
|
|
39
42
|
},
|
|
40
43
|
},
|
|
41
44
|
required: ["row", "id"],
|
|
42
45
|
},
|
|
43
46
|
},
|
|
44
|
-
type:
|
|
47
|
+
type: AutomationStepType.TRIGGER,
|
|
45
48
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AutomationCustomIOType,
|
|
3
|
+
AutomationIOType,
|
|
4
|
+
AutomationStepType,
|
|
2
5
|
AutomationTriggerSchema,
|
|
3
6
|
AutomationTriggerStepId,
|
|
4
7
|
} from "@budibase/types"
|
|
@@ -15,13 +18,13 @@ export const definition: AutomationTriggerSchema = {
|
|
|
15
18
|
inputs: {
|
|
16
19
|
properties: {
|
|
17
20
|
schemaUrl: {
|
|
18
|
-
type:
|
|
19
|
-
customType:
|
|
21
|
+
type: AutomationIOType.STRING,
|
|
22
|
+
customType: AutomationCustomIOType.WEBHOOK_URL,
|
|
20
23
|
title: "Schema URL",
|
|
21
24
|
},
|
|
22
25
|
triggerUrl: {
|
|
23
|
-
type:
|
|
24
|
-
customType:
|
|
26
|
+
type: AutomationIOType.STRING,
|
|
27
|
+
customType: AutomationCustomIOType.WEBHOOK_URL,
|
|
25
28
|
title: "Trigger URL",
|
|
26
29
|
},
|
|
27
30
|
},
|
|
@@ -30,12 +33,12 @@ export const definition: AutomationTriggerSchema = {
|
|
|
30
33
|
outputs: {
|
|
31
34
|
properties: {
|
|
32
35
|
body: {
|
|
33
|
-
type:
|
|
36
|
+
type: AutomationIOType.OBJECT,
|
|
34
37
|
description: "Body of the request which hit the webhook",
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
required: ["body"],
|
|
38
41
|
},
|
|
39
42
|
},
|
|
40
|
-
type:
|
|
43
|
+
type: AutomationStepType.TRIGGER,
|
|
41
44
|
}
|
package/src/environment.ts
CHANGED
|
@@ -62,7 +62,6 @@ const environment = {
|
|
|
62
62
|
// minor
|
|
63
63
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
|
64
64
|
LOGGER: process.env.LOGGER,
|
|
65
|
-
LOG_LEVEL: process.env.LOG_LEVEL,
|
|
66
65
|
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
|
67
66
|
AUTOMATION_MAX_ITERATIONS:
|
|
68
67
|
parseIntSafe(process.env.AUTOMATION_MAX_ITERATIONS) || 200,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./processors"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import userGroupProcessor from "./syncUsers"
|
|
2
|
+
import { docUpdates } from "@budibase/backend-core"
|
|
3
|
+
|
|
4
|
+
export type UpdateCallback = (docId: string) => void
|
|
5
|
+
let started = false
|
|
6
|
+
|
|
7
|
+
export function init(updateCb?: UpdateCallback) {
|
|
8
|
+
if (started) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
const processors = [userGroupProcessor(updateCb)]
|
|
12
|
+
docUpdates.init(processors)
|
|
13
|
+
started = true
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { constants, logging } from "@budibase/backend-core"
|
|
2
|
+
import { sdk as proSdk } from "@budibase/pro"
|
|
3
|
+
import { DocUpdateEvent, UserGroupSyncEvents } from "@budibase/types"
|
|
4
|
+
import { syncUsersToAllApps } from "../../sdk/app/applications/sync"
|
|
5
|
+
import { UpdateCallback } from "./processors"
|
|
6
|
+
|
|
7
|
+
export default function process(updateCb?: UpdateCallback) {
|
|
8
|
+
const processor = async (update: DocUpdateEvent) => {
|
|
9
|
+
try {
|
|
10
|
+
const docId = update.id
|
|
11
|
+
const isGroup = docId.startsWith(constants.DocumentType.GROUP)
|
|
12
|
+
let userIds: string[]
|
|
13
|
+
if (isGroup) {
|
|
14
|
+
const group = await proSdk.groups.get(docId)
|
|
15
|
+
userIds = group.users?.map(user => user._id) || []
|
|
16
|
+
} else {
|
|
17
|
+
userIds = [docId]
|
|
18
|
+
}
|
|
19
|
+
if (userIds.length > 0) {
|
|
20
|
+
await syncUsersToAllApps(userIds)
|
|
21
|
+
}
|
|
22
|
+
if (updateCb) {
|
|
23
|
+
updateCb(docId)
|
|
24
|
+
}
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
// if something not found - no changes to perform
|
|
27
|
+
if (err?.status === 404) {
|
|
28
|
+
return
|
|
29
|
+
} else {
|
|
30
|
+
logging.logAlert("Failed to perform user/group app sync", err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { events: UserGroupSyncEvents, processor }
|
|
35
|
+
}
|
package/src/events/index.ts
CHANGED
|
@@ -14,11 +14,11 @@ import firebase from "./firebase"
|
|
|
14
14
|
import redis from "./redis"
|
|
15
15
|
import snowflake from "./snowflake"
|
|
16
16
|
import oracle from "./oracle"
|
|
17
|
-
import { getPlugins } from "../api/controllers/plugin"
|
|
18
17
|
import { SourceName, Integration, PluginType } from "@budibase/types"
|
|
19
18
|
import { getDatasourcePlugin } from "../utilities/fileSystem"
|
|
20
19
|
import env from "../environment"
|
|
21
20
|
import { cloneDeep } from "lodash"
|
|
21
|
+
import sdk from "../sdk"
|
|
22
22
|
|
|
23
23
|
const DEFINITIONS: { [key: string]: Integration } = {
|
|
24
24
|
[SourceName.POSTGRES]: postgres.schema,
|
|
@@ -79,7 +79,7 @@ export async function getDefinition(source: SourceName): Promise<Integration> {
|
|
|
79
79
|
export async function getDefinitions() {
|
|
80
80
|
const pluginSchemas: { [key: string]: Integration } = {}
|
|
81
81
|
if (env.SELF_HOSTED) {
|
|
82
|
-
const plugins = await
|
|
82
|
+
const plugins = await sdk.plugins.fetch(PluginType.DATASOURCE)
|
|
83
83
|
// extract the actual schema from each custom
|
|
84
84
|
for (let plugin of plugins) {
|
|
85
85
|
const sourceId = plugin.name
|
|
@@ -103,7 +103,7 @@ export async function getIntegration(integration: string) {
|
|
|
103
103
|
return INTEGRATIONS[integration]
|
|
104
104
|
}
|
|
105
105
|
if (env.SELF_HOSTED) {
|
|
106
|
-
const plugins = await
|
|
106
|
+
const plugins = await sdk.plugins.fetch(PluginType.DATASOURCE)
|
|
107
107
|
for (let plugin of plugins) {
|
|
108
108
|
if (plugin.name === integration) {
|
|
109
109
|
// need to use commonJS require due to its dynamic runtime nature
|
|
@@ -243,11 +243,14 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|
|
243
243
|
if (typeof name !== "string") {
|
|
244
244
|
continue
|
|
245
245
|
}
|
|
246
|
+
const hasDefault = def.COLUMN_DEFAULT
|
|
247
|
+
const isAuto = !!autoColumns.find(col => col === name)
|
|
248
|
+
const required = !!requiredColumns.find(col => col === name)
|
|
246
249
|
schema[name] = {
|
|
247
|
-
autocolumn:
|
|
250
|
+
autocolumn: isAuto,
|
|
248
251
|
name: name,
|
|
249
252
|
constraints: {
|
|
250
|
-
presence:
|
|
253
|
+
presence: required && !isAuto && !hasDefault,
|
|
251
254
|
},
|
|
252
255
|
...convertSqlType(def.DATA_TYPE),
|
|
253
256
|
externalType: def.DATA_TYPE,
|
|
@@ -229,13 +229,15 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|
|
229
229
|
if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) {
|
|
230
230
|
primaryKeys.push(columnName)
|
|
231
231
|
}
|
|
232
|
-
const
|
|
233
|
-
presence: column.Null !== "YES",
|
|
234
|
-
}
|
|
232
|
+
const hasDefault = column.Default != null
|
|
235
233
|
const isAuto: boolean =
|
|
236
234
|
typeof column.Extra === "string" &&
|
|
237
235
|
(column.Extra === "auto_increment" ||
|
|
238
236
|
column.Extra.toLowerCase().includes("generated"))
|
|
237
|
+
const required = column.Null !== "YES"
|
|
238
|
+
const constraints = {
|
|
239
|
+
presence: required && !isAuto && !hasDefault,
|
|
240
|
+
}
|
|
239
241
|
schema[columnName] = {
|
|
240
242
|
name: columnName,
|
|
241
243
|
autocolumn: isAuto,
|
|
@@ -262,15 +262,17 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|
|
262
262
|
column.identity_start ||
|
|
263
263
|
column.identity_increment
|
|
264
264
|
)
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
const hasDefault =
|
|
265
|
+
const hasDefault = column.column_default != null
|
|
266
|
+
const hasNextVal =
|
|
269
267
|
typeof column.column_default === "string" &&
|
|
270
268
|
column.column_default.startsWith("nextval")
|
|
271
269
|
const isGenerated =
|
|
272
270
|
column.is_generated && column.is_generated !== "NEVER"
|
|
273
|
-
const isAuto: boolean =
|
|
271
|
+
const isAuto: boolean = hasNextVal || identity || isGenerated
|
|
272
|
+
const required = column.is_nullable === "NO"
|
|
273
|
+
const constraints = {
|
|
274
|
+
presence: required && !hasDefault && !isGenerated,
|
|
275
|
+
}
|
|
274
276
|
tables[tableName].schema[columnName] = {
|
|
275
277
|
autocolumn: isAuto,
|
|
276
278
|
name: columnName,
|
|
@@ -6,6 +6,7 @@ interface RedisConfig {
|
|
|
6
6
|
port: number
|
|
7
7
|
username: string
|
|
8
8
|
password?: string
|
|
9
|
+
db?: number
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const SCHEMA: Integration = {
|
|
@@ -32,6 +33,12 @@ const SCHEMA: Integration = {
|
|
|
32
33
|
type: "password",
|
|
33
34
|
required: false,
|
|
34
35
|
},
|
|
36
|
+
db: {
|
|
37
|
+
type: "number",
|
|
38
|
+
required: false,
|
|
39
|
+
display: "DB",
|
|
40
|
+
default: 0,
|
|
41
|
+
},
|
|
35
42
|
},
|
|
36
43
|
query: {
|
|
37
44
|
create: {
|
|
@@ -88,6 +95,7 @@ class RedisIntegration {
|
|
|
88
95
|
port: this.config.port,
|
|
89
96
|
username: this.config.username,
|
|
90
97
|
password: this.config.password,
|
|
98
|
+
db: this.config.db,
|
|
91
99
|
})
|
|
92
100
|
}
|
|
93
101
|
|
package/src/integrations/rest.ts
CHANGED
|
@@ -151,6 +151,9 @@ class RestIntegration implements IntegrationBase {
|
|
|
151
151
|
data = data[keys[0]]
|
|
152
152
|
}
|
|
153
153
|
raw = rawXml
|
|
154
|
+
} else if (contentType.includes("application/pdf")) {
|
|
155
|
+
data = await response.arrayBuffer() // Save PDF as ArrayBuffer
|
|
156
|
+
raw = Buffer.from(data)
|
|
154
157
|
} else {
|
|
155
158
|
data = await response.text()
|
|
156
159
|
raw = data
|
|
@@ -9,6 +9,6 @@ export const run = async () => {
|
|
|
9
9
|
|
|
10
10
|
// sync app count
|
|
11
11
|
const tenantId = tenancy.getTenantId()
|
|
12
|
-
console.log(`
|
|
12
|
+
console.log(`Syncing app count: ${appCount}`)
|
|
13
13
|
await quotas.setUsage(appCount, StaticQuotaName.APPS, QuotaUsageType.STATIC)
|
|
14
14
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { db as dbCore } from "@budibase/backend-core"
|
|
2
2
|
import { getUniqueRows } from "../../../utilities/usageQuota/rows"
|
|
3
3
|
import { quotas } from "@budibase/pro"
|
|
4
4
|
import { StaticQuotaName, QuotaUsageType, App } from "@budibase/types"
|
|
@@ -18,8 +18,7 @@ export const run = async () => {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
// sync row count
|
|
21
|
-
|
|
22
|
-
console.log(`[Tenant: ${tenantId}] Syncing row count: ${rowCount}`)
|
|
21
|
+
console.log(`Syncing row count: ${rowCount}`)
|
|
23
22
|
await quotas.setUsagePerApp(
|
|
24
23
|
counts,
|
|
25
24
|
StaticQuotaName.ROWS,
|
|
@@ -24,7 +24,7 @@ describe("syncRows", () => {
|
|
|
24
24
|
|
|
25
25
|
// app 1
|
|
26
26
|
const app1 = config.app
|
|
27
|
-
await context.doInAppContext(app1
|
|
27
|
+
await context.doInAppContext(app1!.appId, async () => {
|
|
28
28
|
await config.createTable()
|
|
29
29
|
await config.createRow()
|
|
30
30
|
})
|
|
@@ -43,7 +43,7 @@ describe("syncRows", () => {
|
|
|
43
43
|
usageDoc = await quotas.getQuotaUsage()
|
|
44
44
|
expect(usageDoc.usageQuota.rows).toEqual(3)
|
|
45
45
|
expect(
|
|
46
|
-
usageDoc.apps?.[dbCore.getProdAppID(app1
|
|
46
|
+
usageDoc.apps?.[dbCore.getProdAppID(app1!.appId)].usageQuota.rows
|
|
47
47
|
).toEqual(1)
|
|
48
48
|
expect(
|
|
49
49
|
usageDoc.apps?.[dbCore.getProdAppID(app2.appId)].usageQuota.rows
|
|
@@ -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
|
+
})
|
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
|