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