@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.
Files changed (156) hide show
  1. package/builder/assets/index.5c1a6913.js +1776 -0
  2. package/builder/assets/index.c0265b74.css +6 -0
  3. package/builder/index.html +2 -2
  4. package/dist/api/controllers/automation.js +13 -7
  5. package/dist/api/controllers/ops.js +40 -0
  6. package/dist/api/controllers/plugin/index.js +6 -37
  7. package/dist/api/controllers/table/utils.js +2 -1
  8. package/dist/api/controllers/user.js +1 -83
  9. package/dist/api/routes/index.js +2 -0
  10. package/dist/api/routes/ops.js +52 -0
  11. package/dist/api/routes/user.js +0 -1
  12. package/dist/app.js +4 -13
  13. package/dist/automations/actions.js +32 -6
  14. package/dist/automations/index.js +3 -2
  15. package/dist/automations/steps/bash.js +6 -6
  16. package/dist/automations/steps/createRow.js +11 -11
  17. package/dist/automations/steps/delay.js +3 -3
  18. package/dist/automations/steps/deleteRow.js +8 -8
  19. package/dist/automations/steps/discord.js +8 -8
  20. package/dist/automations/steps/executeQuery.js +9 -9
  21. package/dist/automations/steps/executeScript.js +6 -6
  22. package/dist/automations/steps/filter.js +6 -6
  23. package/dist/automations/steps/integromat.js +10 -10
  24. package/dist/automations/steps/loop.js +9 -9
  25. package/dist/automations/steps/outgoingWebhook.js +10 -10
  26. package/dist/automations/steps/queryRows.js +14 -14
  27. package/dist/automations/steps/sendSmtpEmail.js +9 -9
  28. package/dist/automations/steps/serverLog.js +4 -4
  29. package/dist/automations/steps/slack.js +6 -6
  30. package/dist/automations/steps/updateRow.js +11 -11
  31. package/dist/automations/steps/zapier.js +9 -9
  32. package/dist/automations/triggerInfo/app.js +5 -5
  33. package/dist/automations/triggerInfo/cron.js +4 -4
  34. package/dist/automations/triggerInfo/rowDeleted.js +5 -5
  35. package/dist/automations/triggerInfo/rowSaved.js +7 -7
  36. package/dist/automations/triggerInfo/rowUpdated.js +7 -7
  37. package/dist/automations/triggerInfo/webhook.js +6 -6
  38. package/dist/environment.js +0 -1
  39. package/dist/events/docUpdates/index.js +17 -0
  40. package/dist/events/docUpdates/processors.js +18 -0
  41. package/dist/events/docUpdates/syncUsers.js +49 -0
  42. package/dist/events/index.js +3 -0
  43. package/dist/integrations/index.js +3 -3
  44. package/dist/integrations/microsoftSqlServer.js +5 -2
  45. package/dist/integrations/mysql.js +5 -3
  46. package/dist/integrations/postgres.js +7 -5
  47. package/dist/integrations/redis.js +7 -0
  48. package/dist/integrations/rest.js +4 -0
  49. package/dist/migrations/functions/usageQuotas/syncApps.js +1 -1
  50. package/dist/migrations/functions/usageQuotas/syncRows.js +1 -2
  51. package/dist/package.json +15 -15
  52. package/dist/sdk/app/applications/sync.js +117 -23
  53. package/dist/sdk/index.js +2 -0
  54. package/dist/sdk/plugins/index.js +27 -0
  55. package/dist/sdk/plugins/plugins.js +53 -0
  56. package/dist/sdk/users/utils.js +21 -4
  57. package/dist/startup.js +31 -28
  58. package/dist/threads/automation.js +16 -5
  59. package/dist/tsconfig.build.tsbuildinfo +1 -1
  60. package/dist/utilities/fileSystem/plugin.js +33 -23
  61. package/dist/utilities/global.js +17 -12
  62. package/dist/watch.js +2 -2
  63. package/dist/websockets/client.js +14 -0
  64. package/dist/websockets/grid.js +60 -0
  65. package/dist/websockets/index.js +17 -0
  66. package/dist/websockets/websocket.js +78 -0
  67. package/jest.config.ts +3 -3
  68. package/nodemon.json +7 -3
  69. package/package.json +16 -16
  70. package/scripts/dev/manage.js +2 -0
  71. package/scripts/integrations/mssql/data/entrypoint.sh +1 -0
  72. package/scripts/integrations/mssql/data/setup.sql +17 -17
  73. package/scripts/integrations/mysql/init.sql +1 -1
  74. package/scripts/integrations/postgres/init.sql +1 -0
  75. package/src/api/controllers/automation.ts +12 -6
  76. package/src/api/controllers/ops.ts +32 -0
  77. package/src/api/controllers/plugin/index.ts +8 -45
  78. package/src/api/controllers/row/internal.ts +9 -10
  79. package/src/api/controllers/row/utils.ts +2 -2
  80. package/src/api/controllers/table/utils.ts +2 -1
  81. package/src/api/controllers/user.ts +10 -96
  82. package/src/api/routes/index.ts +2 -0
  83. package/src/api/routes/ops.ts +30 -0
  84. package/src/api/routes/tests/automation.spec.js +7 -4
  85. package/src/api/routes/tests/user.spec.js +48 -37
  86. package/src/api/routes/user.ts +0 -5
  87. package/src/app.ts +4 -15
  88. package/src/automations/actions.ts +56 -24
  89. package/src/automations/index.ts +1 -1
  90. package/src/automations/steps/bash.ts +10 -7
  91. package/src/automations/steps/createRow.ts +15 -12
  92. package/src/automations/steps/delay.ts +6 -4
  93. package/src/automations/steps/deleteRow.ts +12 -9
  94. package/src/automations/steps/discord.ts +10 -8
  95. package/src/automations/steps/executeQuery.ts +13 -10
  96. package/src/automations/steps/executeScript.ts +10 -7
  97. package/src/automations/steps/filter.ts +8 -6
  98. package/src/automations/steps/integromat.ts +12 -10
  99. package/src/automations/steps/loop.ts +16 -10
  100. package/src/automations/steps/outgoingWebhook.ts +14 -11
  101. package/src/automations/steps/queryRows.ts +18 -15
  102. package/src/automations/steps/sendSmtpEmail.ts +11 -9
  103. package/src/automations/steps/serverLog.ts +6 -4
  104. package/src/automations/steps/slack.ts +8 -6
  105. package/src/automations/steps/updateRow.ts +15 -12
  106. package/src/automations/steps/zapier.ts +11 -9
  107. package/src/automations/tests/utilities/index.ts +2 -2
  108. package/src/automations/triggerInfo/app.ts +8 -5
  109. package/src/automations/triggerInfo/cron.ts +7 -4
  110. package/src/automations/triggerInfo/rowDeleted.ts +8 -5
  111. package/src/automations/triggerInfo/rowSaved.ts +10 -7
  112. package/src/automations/triggerInfo/rowUpdated.ts +10 -7
  113. package/src/automations/triggerInfo/webhook.ts +9 -6
  114. package/src/environment.ts +0 -1
  115. package/src/events/docUpdates/index.ts +1 -0
  116. package/src/events/docUpdates/processors.ts +14 -0
  117. package/src/events/docUpdates/syncUsers.ts +35 -0
  118. package/src/events/index.ts +1 -0
  119. package/src/integrations/index.ts +3 -3
  120. package/src/integrations/microsoftSqlServer.ts +5 -2
  121. package/src/integrations/mysql.ts +5 -3
  122. package/src/integrations/postgres.ts +7 -5
  123. package/src/integrations/redis.ts +8 -0
  124. package/src/integrations/rest.ts +3 -0
  125. package/src/migrations/functions/usageQuotas/syncApps.ts +1 -1
  126. package/src/migrations/functions/usageQuotas/syncRows.ts +2 -3
  127. package/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts +2 -2
  128. package/src/sdk/app/applications/sync.ts +129 -22
  129. package/src/sdk/app/applications/tests/sync.spec.ts +137 -0
  130. package/src/sdk/index.ts +2 -0
  131. package/src/sdk/plugins/index.ts +5 -0
  132. package/src/sdk/plugins/plugins.ts +41 -0
  133. package/src/sdk/users/tests/utils.spec.ts +1 -32
  134. package/src/sdk/users/utils.ts +23 -5
  135. package/src/startup.ts +36 -34
  136. package/src/tests/jestEnv.ts +0 -1
  137. package/src/tests/jestSetup.ts +0 -1
  138. package/src/tests/utilities/TestConfiguration.ts +28 -0
  139. package/src/tests/utilities/structures.ts +25 -17
  140. package/src/threads/automation.ts +18 -6
  141. package/src/utilities/fileSystem/plugin.ts +13 -4
  142. package/src/utilities/global.ts +21 -16
  143. package/src/watch.ts +2 -2
  144. package/src/websockets/client.ts +11 -0
  145. package/src/websockets/grid.ts +55 -0
  146. package/src/websockets/index.ts +14 -0
  147. package/src/websockets/websocket.ts +83 -0
  148. package/tsconfig.json +1 -7
  149. package/builder/assets/index.7f9a008b.css +0 -6
  150. package/builder/assets/index.f493a2b3.js +0 -1817
  151. package/dist/elasticApm.js +0 -14
  152. package/dist/websocket.js +0 -22
  153. package/scripts/likeCypress.ts +0 -35
  154. package/src/elasticApm.ts +0 -10
  155. package/src/tests/logging.ts +0 -34
  156. 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: "string",
19
- customType: "table",
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: "object",
29
- customType: "row",
31
+ type: AutomationIOType.OBJECT,
32
+ customType: AutomationCustomIOType.ROW,
30
33
  description: "The row that was updated",
31
34
  },
32
35
  id: {
33
- type: "string",
36
+ type: AutomationIOType.STRING,
34
37
  description: "Row ID - can be used for updating",
35
38
  },
36
39
  revision: {
37
- type: "string",
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: "TRIGGER",
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: "string",
19
- customType: "webhookUrl",
21
+ type: AutomationIOType.STRING,
22
+ customType: AutomationCustomIOType.WEBHOOK_URL,
20
23
  title: "Schema URL",
21
24
  },
22
25
  triggerUrl: {
23
- type: "string",
24
- customType: "webhookUrl",
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: "object",
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: "TRIGGER",
43
+ type: AutomationStepType.TRIGGER,
41
44
  }
@@ -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
+ }
@@ -2,4 +2,5 @@ import BudibaseEmitter from "./BudibaseEmitter"
2
2
 
3
3
  const emitter = new BudibaseEmitter()
4
4
 
5
+ export { init } from "./docUpdates"
5
6
  export default emitter
@@ -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 getPlugins(PluginType.DATASOURCE)
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 getPlugins(PluginType.DATASOURCE)
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: !!autoColumns.find(col => col === name),
250
+ autocolumn: isAuto,
248
251
  name: name,
249
252
  constraints: {
250
- presence: requiredColumns.find(col => col === name),
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 constraints = {
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 constraints = {
266
- presence: column.is_nullable === "NO",
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 = hasDefault || identity || isGenerated
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
 
@@ -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(`[Tenant: ${tenantId}] Syncing app count: ${appCount}`)
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 { tenancy, db as dbCore } from "@budibase/backend-core"
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
- const tenantId = tenancy.getTenantId()
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.appId, async () => {
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.appId)].usageQuota.rows
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 { db as dbCore, context } from "@budibase/backend-core"
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
- try {
39
- const replOpts = replication.appReplicateOpts()
40
- if (opts?.automationOnly) {
41
- replOpts.filter = (doc: any) =>
42
- doc._id.startsWith(dbCore.DocumentType.AUTOMATION)
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
@@ -0,0 +1,5 @@
1
+ import * as plugins from "./plugins"
2
+
3
+ export default {
4
+ ...plugins,
5
+ }