@budibase/server 2.5.9 → 2.5.10-alpha.1

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 (198) hide show
  1. package/builder/assets/index.24635afb.js +1794 -0
  2. package/builder/assets/index.4eae16b2.css +6 -0
  3. package/builder/index.html +2 -2
  4. package/dist/api/controllers/application.js +3 -4
  5. package/dist/api/controllers/automation.js +13 -7
  6. package/dist/api/controllers/datasource.js +1 -1
  7. package/dist/api/controllers/dev.js +1 -1
  8. package/dist/api/controllers/ops.js +40 -0
  9. package/dist/api/controllers/plugin/index.js +6 -37
  10. package/dist/api/controllers/query/index.js +2 -2
  11. package/dist/api/controllers/row/ExternalRequest.js +21 -14
  12. package/dist/api/controllers/row/internal.js +5 -2
  13. package/dist/api/controllers/row/utils.js +2 -2
  14. package/dist/api/controllers/table/index.js +2 -2
  15. package/dist/api/controllers/table/utils.js +9 -3
  16. package/dist/api/controllers/user.js +1 -83
  17. package/dist/api/controllers/view/exporters.js +3 -1
  18. package/dist/api/index.js +1 -2
  19. package/dist/api/routes/index.js +2 -2
  20. package/dist/api/routes/{cloud.js → ops.js} +19 -6
  21. package/dist/api/routes/user.js +0 -1
  22. package/dist/app.js +4 -13
  23. package/dist/automations/actions.js +32 -6
  24. package/dist/automations/index.js +3 -2
  25. package/dist/automations/steps/bash.js +6 -6
  26. package/dist/automations/steps/createRow.js +11 -11
  27. package/dist/automations/steps/delay.js +3 -3
  28. package/dist/automations/steps/deleteRow.js +8 -8
  29. package/dist/automations/steps/discord.js +8 -8
  30. package/dist/automations/steps/executeQuery.js +9 -9
  31. package/dist/automations/steps/executeScript.js +6 -6
  32. package/dist/automations/steps/filter.js +6 -6
  33. package/dist/automations/steps/integromat.js +10 -10
  34. package/dist/automations/steps/loop.js +9 -9
  35. package/dist/automations/steps/outgoingWebhook.js +10 -10
  36. package/dist/automations/steps/queryRows.js +14 -14
  37. package/dist/automations/steps/sendSmtpEmail.js +9 -9
  38. package/dist/automations/steps/serverLog.js +4 -4
  39. package/dist/automations/steps/slack.js +6 -6
  40. package/dist/automations/steps/updateRow.js +11 -11
  41. package/dist/automations/steps/zapier.js +9 -9
  42. package/dist/automations/triggerInfo/app.js +5 -5
  43. package/dist/automations/triggerInfo/cron.js +4 -4
  44. package/dist/automations/triggerInfo/rowDeleted.js +5 -5
  45. package/dist/automations/triggerInfo/rowSaved.js +7 -7
  46. package/dist/automations/triggerInfo/rowUpdated.js +7 -7
  47. package/dist/automations/triggerInfo/webhook.js +6 -6
  48. package/dist/db/utils.js +3 -2
  49. package/dist/environment.js +0 -1
  50. package/dist/events/docUpdates/index.js +17 -0
  51. package/dist/events/docUpdates/processors.js +18 -0
  52. package/dist/events/docUpdates/syncUsers.js +49 -0
  53. package/dist/events/index.js +3 -0
  54. package/dist/integrations/base/sqlTable.js +9 -2
  55. package/dist/integrations/index.js +3 -3
  56. package/dist/integrations/microsoftSqlServer.js +5 -2
  57. package/dist/integrations/mysql.js +5 -3
  58. package/dist/integrations/postgres.js +7 -5
  59. package/dist/integrations/redis.js +7 -0
  60. package/dist/integrations/rest.js +4 -0
  61. package/dist/migrations/functions/syncQuotas.js +2 -0
  62. package/dist/migrations/functions/usageQuotas/syncApps.js +1 -2
  63. package/dist/migrations/functions/usageQuotas/syncRows.js +1 -2
  64. package/dist/migrations/functions/usageQuotas/syncUsers.js +21 -0
  65. package/dist/sdk/app/applications/sync.js +117 -23
  66. package/dist/sdk/app/backups/exports.js +14 -38
  67. package/dist/sdk/index.js +2 -0
  68. package/dist/sdk/plugins/index.js +27 -0
  69. package/dist/sdk/plugins/plugins.js +53 -0
  70. package/dist/sdk/users/utils.js +21 -4
  71. package/dist/startup.js +31 -28
  72. package/dist/threads/automation.js +16 -5
  73. package/dist/tsconfig.build.tsbuildinfo +1 -1
  74. package/dist/utilities/csv.js +33 -0
  75. package/dist/utilities/fileSystem/plugin.js +33 -23
  76. package/dist/utilities/global.js +17 -12
  77. package/dist/utilities/rowProcessor/utils.js +4 -5
  78. package/dist/utilities/schema.js +5 -1
  79. package/dist/watch.js +2 -2
  80. package/dist/websockets/client.js +14 -0
  81. package/dist/websockets/grid.js +60 -0
  82. package/dist/websockets/index.js +17 -0
  83. package/dist/websockets/websocket.js +78 -0
  84. package/package.json +16 -16
  85. package/scripts/dev/manage.js +2 -0
  86. package/scripts/integrations/mssql/data/entrypoint.sh +1 -0
  87. package/scripts/integrations/mssql/data/setup.sql +17 -17
  88. package/scripts/integrations/mysql/init.sql +1 -1
  89. package/scripts/integrations/postgres/init.sql +1 -0
  90. package/src/api/controllers/application.ts +4 -4
  91. package/src/api/controllers/automation.ts +12 -6
  92. package/src/api/controllers/datasource.ts +15 -5
  93. package/src/api/controllers/dev.ts +2 -2
  94. package/src/api/controllers/ops.ts +32 -0
  95. package/src/api/controllers/plugin/index.ts +8 -45
  96. package/src/api/controllers/query/index.ts +2 -2
  97. package/src/api/controllers/row/ExternalRequest.ts +21 -12
  98. package/src/api/controllers/row/internal.ts +13 -11
  99. package/src/api/controllers/row/utils.ts +4 -4
  100. package/src/api/controllers/table/index.ts +2 -2
  101. package/src/api/controllers/table/utils.ts +10 -3
  102. package/src/api/controllers/user.ts +10 -96
  103. package/src/api/controllers/view/exporters.ts +3 -1
  104. package/src/api/index.ts +2 -4
  105. package/src/api/routes/index.ts +2 -2
  106. package/src/api/routes/ops.ts +30 -0
  107. package/src/api/routes/tests/automation.spec.js +7 -4
  108. package/src/api/routes/tests/user.spec.js +48 -37
  109. package/src/api/routes/user.ts +0 -5
  110. package/src/app.ts +4 -15
  111. package/src/automations/actions.ts +56 -24
  112. package/src/automations/index.ts +1 -1
  113. package/src/automations/steps/bash.ts +10 -7
  114. package/src/automations/steps/createRow.ts +15 -12
  115. package/src/automations/steps/delay.ts +6 -4
  116. package/src/automations/steps/deleteRow.ts +12 -9
  117. package/src/automations/steps/discord.ts +10 -8
  118. package/src/automations/steps/executeQuery.ts +13 -10
  119. package/src/automations/steps/executeScript.ts +10 -7
  120. package/src/automations/steps/filter.ts +8 -6
  121. package/src/automations/steps/integromat.ts +12 -10
  122. package/src/automations/steps/loop.ts +16 -10
  123. package/src/automations/steps/outgoingWebhook.ts +14 -11
  124. package/src/automations/steps/queryRows.ts +18 -15
  125. package/src/automations/steps/sendSmtpEmail.ts +11 -9
  126. package/src/automations/steps/serverLog.ts +6 -4
  127. package/src/automations/steps/slack.ts +8 -6
  128. package/src/automations/steps/updateRow.ts +15 -12
  129. package/src/automations/steps/zapier.ts +11 -9
  130. package/src/automations/tests/utilities/index.ts +2 -2
  131. package/src/automations/triggerInfo/app.ts +8 -5
  132. package/src/automations/triggerInfo/cron.ts +7 -4
  133. package/src/automations/triggerInfo/rowDeleted.ts +8 -5
  134. package/src/automations/triggerInfo/rowSaved.ts +10 -7
  135. package/src/automations/triggerInfo/rowUpdated.ts +10 -7
  136. package/src/automations/triggerInfo/webhook.ts +9 -6
  137. package/src/db/utils.ts +1 -0
  138. package/src/environment.ts +0 -1
  139. package/src/events/docUpdates/index.ts +1 -0
  140. package/src/events/docUpdates/processors.ts +14 -0
  141. package/src/events/docUpdates/syncUsers.ts +35 -0
  142. package/src/events/index.ts +1 -0
  143. package/src/integration-test/postgres.spec.ts +3 -1
  144. package/src/integrations/base/sqlTable.ts +9 -2
  145. package/src/integrations/index.ts +3 -3
  146. package/src/integrations/microsoftSqlServer.ts +5 -2
  147. package/src/integrations/mysql.ts +5 -3
  148. package/src/integrations/postgres.ts +7 -5
  149. package/src/integrations/redis.ts +8 -0
  150. package/src/integrations/rest.ts +3 -0
  151. package/src/migrations/functions/syncQuotas.ts +2 -0
  152. package/src/migrations/functions/usageQuotas/syncApps.ts +2 -3
  153. package/src/migrations/functions/usageQuotas/syncRows.ts +2 -3
  154. package/src/migrations/functions/usageQuotas/syncUsers.ts +9 -0
  155. package/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts +2 -2
  156. package/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts +26 -0
  157. package/src/migrations/index.ts +1 -0
  158. package/src/sdk/app/applications/sync.ts +129 -22
  159. package/src/sdk/app/applications/tests/sync.spec.ts +137 -0
  160. package/src/sdk/app/backups/exports.ts +17 -41
  161. package/src/sdk/index.ts +2 -0
  162. package/src/sdk/plugins/index.ts +5 -0
  163. package/src/sdk/plugins/plugins.ts +41 -0
  164. package/src/sdk/users/tests/utils.spec.ts +1 -32
  165. package/src/sdk/users/utils.ts +23 -5
  166. package/src/startup.ts +36 -34
  167. package/src/tests/jestEnv.ts +0 -1
  168. package/src/tests/jestSetup.ts +0 -1
  169. package/src/tests/utilities/TestConfiguration.ts +28 -0
  170. package/src/tests/utilities/structures.ts +25 -17
  171. package/src/threads/automation.ts +18 -6
  172. package/src/utilities/csv.ts +22 -0
  173. package/src/utilities/fileSystem/plugin.ts +13 -4
  174. package/src/utilities/global.ts +21 -16
  175. package/src/utilities/rowProcessor/utils.ts +9 -10
  176. package/src/utilities/schema.ts +8 -0
  177. package/src/utilities/tests/csv.spec.ts +33 -0
  178. package/src/watch.ts +2 -2
  179. package/src/websockets/client.ts +11 -0
  180. package/src/websockets/grid.ts +55 -0
  181. package/src/websockets/index.ts +14 -0
  182. package/src/websockets/websocket.ts +83 -0
  183. package/tsconfig.build.json +3 -5
  184. package/tsconfig.json +2 -1
  185. package/builder/assets/index.0b358332.js +0 -1817
  186. package/builder/assets/index.7f9a008b.css +0 -6
  187. package/dist/api/controllers/cloud.js +0 -130
  188. package/dist/elasticApm.js +0 -14
  189. package/dist/package.json +0 -180
  190. package/dist/websocket.js +0 -22
  191. package/scripts/likeCypress.ts +0 -35
  192. package/src/api/controllers/cloud.ts +0 -119
  193. package/src/api/routes/cloud.ts +0 -18
  194. package/src/api/routes/tests/cloud.spec.ts +0 -54
  195. package/src/elasticApm.ts +0 -10
  196. package/src/migrations/functions/tests/syncQuotas.spec.js +0 -26
  197. package/src/tests/logging.ts +0 -34
  198. package/src/websocket.ts +0 -26
@@ -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
@@ -2,6 +2,7 @@ import { runQuotaMigration } from "./usageQuotas"
2
2
  import * as syncApps from "./usageQuotas/syncApps"
3
3
  import * as syncRows from "./usageQuotas/syncRows"
4
4
  import * as syncPlugins from "./usageQuotas/syncPlugins"
5
+ import * as syncUsers from "./usageQuotas/syncUsers"
5
6
 
6
7
  /**
7
8
  * Synchronise quotas to the state of the db.
@@ -11,5 +12,6 @@ export const run = async () => {
11
12
  await syncApps.run()
12
13
  await syncRows.run()
13
14
  await syncPlugins.run()
15
+ await syncUsers.run()
14
16
  })
15
17
  }
@@ -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 { quotas } from "@budibase/pro"
3
3
  import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
4
4
 
@@ -8,7 +8,6 @@ export const run = async () => {
8
8
  const appCount = devApps ? devApps.length : 0
9
9
 
10
10
  // sync app count
11
- const tenantId = tenancy.getTenantId()
12
- console.log(`[Tenant: ${tenantId}] Syncing app count: ${appCount}`)
11
+ console.log(`Syncing app count: ${appCount}`)
13
12
  await quotas.setUsage(appCount, StaticQuotaName.APPS, QuotaUsageType.STATIC)
14
13
  }
@@ -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,
@@ -0,0 +1,9 @@
1
+ import { users } from "@budibase/backend-core"
2
+ import { quotas } from "@budibase/pro"
3
+ import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
4
+
5
+ export const run = async () => {
6
+ const userCount = await users.getUserCount()
7
+ console.log(`Syncing user count: ${userCount}`)
8
+ await quotas.setUsage(userCount, StaticQuotaName.USERS, QuotaUsageType.STATIC)
9
+ }
@@ -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
@@ -0,0 +1,26 @@
1
+ import TestConfig from "../../../../tests/utilities/TestConfiguration"
2
+ import * as syncUsers from "../syncUsers"
3
+ import { quotas } from "@budibase/pro"
4
+
5
+ describe("syncUsers", () => {
6
+ let config = new TestConfig(false)
7
+
8
+ beforeEach(async () => {
9
+ await config.init()
10
+ })
11
+
12
+ afterAll(config.end)
13
+
14
+ it("syncs users", async () => {
15
+ return config.doInContext(null, async () => {
16
+ await config.createUser()
17
+
18
+ await syncUsers.run()
19
+
20
+ const usageDoc = await quotas.getQuotaUsage()
21
+ // default + additional user
22
+ const userCount = 2
23
+ expect(usageDoc.usageQuota.users).toBe(userCount)
24
+ })
25
+ })
26
+ })
@@ -11,6 +11,7 @@ import env from "../environment"
11
11
  // migration functions
12
12
  import * as userEmailViewCasing from "./functions/userEmailViewCasing"
13
13
  import * as syncQuotas from "./functions/syncQuotas"
14
+ import * as syncUsers from "./functions/usageQuotas/syncUsers"
14
15
  import * as appUrls from "./functions/appUrls"
15
16
  import * as tableSettings from "./functions/tableSettings"
16
17
  import * as backfill from "./functions/backfill"
@@ -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
+ })
@@ -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
- type ExportOpts = {
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: ExportOpts = {}) {
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
- ...config,
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, { excludeRows, tar: true })
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,5 @@
1
+ import * as plugins from "./plugins"
2
+
3
+ export default {
4
+ ...plugins,
5
+ }