@budibase/server 2.7.24 → 2.7.25-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.d9b46807.css → index.36462e95.css} +1 -1
- package/builder/assets/{index.1066b334.js → index.91f1d954.js} +347 -347
- package/builder/index.html +2 -2
- package/dist/automation.js +421 -180
- package/dist/automation.js.map +3 -3
- package/dist/index.js +532 -283
- package/dist/index.js.map +3 -3
- package/dist/query.js +317 -142
- package/dist/query.js.map +3 -3
- package/package.json +11 -11
- package/src/api/controllers/backup.ts +22 -8
- package/src/api/controllers/datasource.ts +41 -24
- package/src/api/controllers/role.ts +5 -5
- package/src/api/controllers/routing.ts +3 -3
- package/src/api/routes/backup.ts +1 -1
- package/src/api/routes/tests/backup.spec.ts +18 -2
- package/src/automations/steps/sendSmtpEmail.ts +55 -4
- package/src/automations/tests/sendSmtpEmail.spec.ts +74 -0
- package/src/events/docUpdates/syncUsers.ts +4 -0
- package/src/integrations/googlesheets.ts +35 -18
- package/src/integrations/mongodb.ts +4 -2
- package/src/integrations/postgres.ts +21 -3
- package/src/middleware/currentapp.ts +1 -1
- package/src/sdk/app/backups/exports.ts +33 -5
- package/src/sdk/app/backups/imports.ts +21 -1
- package/src/sdk/app/datasources/datasources.ts +1 -0
- package/src/utilities/workerRequests.ts +20 -9
- package/src/automations/tests/sendSmtpEmail.spec.js +0 -71
|
@@ -20,7 +20,7 @@ import Sql from "./base/sql"
|
|
|
20
20
|
import { PostgresColumn } from "./base/types"
|
|
21
21
|
import { escapeDangerousCharacters } from "../utilities"
|
|
22
22
|
|
|
23
|
-
import { Client, types } from "pg"
|
|
23
|
+
import { Client, ClientConfig, types } from "pg"
|
|
24
24
|
|
|
25
25
|
// Return "date" and "timestamp" types as plain strings.
|
|
26
26
|
// This lets us reference the original stored timezone.
|
|
@@ -42,6 +42,8 @@ interface PostgresConfig {
|
|
|
42
42
|
schema: string
|
|
43
43
|
ssl?: boolean
|
|
44
44
|
ca?: string
|
|
45
|
+
clientKey?: string
|
|
46
|
+
clientCert?: string
|
|
45
47
|
rejectUnauthorized?: boolean
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -98,6 +100,19 @@ const SCHEMA: Integration = {
|
|
|
98
100
|
required: false,
|
|
99
101
|
},
|
|
100
102
|
ca: {
|
|
103
|
+
display: "Server CA",
|
|
104
|
+
type: DatasourceFieldType.LONGFORM,
|
|
105
|
+
default: false,
|
|
106
|
+
required: false,
|
|
107
|
+
},
|
|
108
|
+
clientKey: {
|
|
109
|
+
display: "Client key",
|
|
110
|
+
type: DatasourceFieldType.LONGFORM,
|
|
111
|
+
default: false,
|
|
112
|
+
required: false,
|
|
113
|
+
},
|
|
114
|
+
clientCert: {
|
|
115
|
+
display: "Client cert",
|
|
101
116
|
type: DatasourceFieldType.LONGFORM,
|
|
102
117
|
default: false,
|
|
103
118
|
required: false,
|
|
@@ -144,12 +159,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|
|
144
159
|
super(SqlClient.POSTGRES)
|
|
145
160
|
this.config = config
|
|
146
161
|
|
|
147
|
-
let newConfig = {
|
|
162
|
+
let newConfig: ClientConfig = {
|
|
148
163
|
...this.config,
|
|
149
164
|
ssl: this.config.ssl
|
|
150
165
|
? {
|
|
151
166
|
rejectUnauthorized: this.config.rejectUnauthorized,
|
|
152
167
|
ca: this.config.ca,
|
|
168
|
+
key: this.config.clientKey,
|
|
169
|
+
cert: this.config.clientCert,
|
|
153
170
|
}
|
|
154
171
|
: undefined,
|
|
155
172
|
}
|
|
@@ -322,7 +339,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|
|
322
339
|
await this.openConnection()
|
|
323
340
|
const columnsResponse: { rows: PostgresColumn[] } =
|
|
324
341
|
await this.client.query(this.COLUMNS_SQL)
|
|
325
|
-
|
|
342
|
+
const names = columnsResponse.rows.map(row => row.table_name)
|
|
343
|
+
return [...new Set(names)]
|
|
326
344
|
} finally {
|
|
327
345
|
await this.closeConnection()
|
|
328
346
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db as dbCore, objectStore } from "@budibase/backend-core"
|
|
1
|
+
import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
|
|
2
2
|
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
|
3
3
|
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
|
4
4
|
import { ObjectStoreBuckets } from "../../../constants"
|
|
@@ -18,7 +18,8 @@ import { join } from "path"
|
|
|
18
18
|
import env from "../../../environment"
|
|
19
19
|
|
|
20
20
|
const uuid = require("uuid/v4")
|
|
21
|
-
|
|
21
|
+
import tar from "tar"
|
|
22
|
+
|
|
22
23
|
const MemoryStream = require("memorystream")
|
|
23
24
|
|
|
24
25
|
interface DBDumpOpts {
|
|
@@ -30,16 +31,18 @@ interface ExportOpts extends DBDumpOpts {
|
|
|
30
31
|
tar?: boolean
|
|
31
32
|
excludeRows?: boolean
|
|
32
33
|
excludeLogs?: boolean
|
|
34
|
+
encryptPassword?: string
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
|
36
|
-
const
|
|
38
|
+
const fileName = `${uuid()}.tar.gz`
|
|
39
|
+
const exportFile = join(budibaseTempDir(), fileName)
|
|
37
40
|
tar.create(
|
|
38
41
|
{
|
|
39
42
|
sync: true,
|
|
40
43
|
gzip: true,
|
|
41
44
|
file: exportFile,
|
|
42
|
-
|
|
45
|
+
noDirRecurse: false,
|
|
43
46
|
cwd: tmpDir,
|
|
44
47
|
},
|
|
45
48
|
files
|
|
@@ -124,6 +127,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|
|
124
127
|
)
|
|
125
128
|
}
|
|
126
129
|
}
|
|
130
|
+
|
|
127
131
|
const downloadedPath = join(tmpPath, appPath)
|
|
128
132
|
if (fs.existsSync(downloadedPath)) {
|
|
129
133
|
const allFiles = fs.readdirSync(downloadedPath)
|
|
@@ -141,12 +145,27 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|
|
141
145
|
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
|
142
146
|
exportPath: dbPath,
|
|
143
147
|
})
|
|
148
|
+
|
|
149
|
+
if (config?.encryptPassword) {
|
|
150
|
+
for (let file of fs.readdirSync(tmpPath)) {
|
|
151
|
+
const path = join(tmpPath, file)
|
|
152
|
+
|
|
153
|
+
await encryption.encryptFile(
|
|
154
|
+
{ dir: tmpPath, filename: file },
|
|
155
|
+
config.encryptPassword
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
fs.rmSync(path)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
// if tar requested, return where the tarball is
|
|
145
163
|
if (config?.tar) {
|
|
146
164
|
// now the tmpPath contains both the DB export and attachments, tar this
|
|
147
165
|
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
|
148
166
|
// cleanup the tmp export files as tarball returned
|
|
149
167
|
fs.rmSync(tmpPath, { recursive: true, force: true })
|
|
168
|
+
|
|
150
169
|
return tarPath
|
|
151
170
|
}
|
|
152
171
|
// tar not requested, turn the directory where export is
|
|
@@ -161,11 +180,20 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|
|
161
180
|
* @param {boolean} excludeRows Flag to state whether the export should include data.
|
|
162
181
|
* @returns {*} a readable stream of the backup which is written in real time
|
|
163
182
|
*/
|
|
164
|
-
export async function streamExportApp(
|
|
183
|
+
export async function streamExportApp({
|
|
184
|
+
appId,
|
|
185
|
+
excludeRows,
|
|
186
|
+
encryptPassword,
|
|
187
|
+
}: {
|
|
188
|
+
appId: string
|
|
189
|
+
excludeRows: boolean
|
|
190
|
+
encryptPassword?: string
|
|
191
|
+
}) {
|
|
165
192
|
const tmpPath = await exportApp(appId, {
|
|
166
193
|
excludeRows,
|
|
167
194
|
excludeLogs: true,
|
|
168
195
|
tar: true,
|
|
196
|
+
encryptPassword,
|
|
169
197
|
})
|
|
170
198
|
return streamFile(tmpPath)
|
|
171
199
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db as dbCore, objectStore } from "@budibase/backend-core"
|
|
1
|
+
import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
|
|
2
2
|
import { Database, Row } from "@budibase/types"
|
|
3
3
|
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
|
4
4
|
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
|
@@ -20,6 +20,7 @@ type TemplateType = {
|
|
|
20
20
|
file?: {
|
|
21
21
|
type: string
|
|
22
22
|
path: string
|
|
23
|
+
password?: string
|
|
23
24
|
}
|
|
24
25
|
key?: string
|
|
25
26
|
}
|
|
@@ -123,6 +124,22 @@ export function untarFile(file: { path: string }) {
|
|
|
123
124
|
return tmpPath
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
async function decryptFiles(path: string, password: string) {
|
|
128
|
+
try {
|
|
129
|
+
for (let file of fs.readdirSync(path)) {
|
|
130
|
+
const inputPath = join(path, file)
|
|
131
|
+
const outputPath = inputPath.replace(/\.enc$/, "")
|
|
132
|
+
await encryption.decryptFile(inputPath, outputPath, password)
|
|
133
|
+
fs.rmSync(inputPath)
|
|
134
|
+
}
|
|
135
|
+
} catch (err: any) {
|
|
136
|
+
if (err.message === "incorrect header check") {
|
|
137
|
+
throw new Error("File cannot be imported")
|
|
138
|
+
}
|
|
139
|
+
throw err
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
export function getGlobalDBFile(tmpPath: string) {
|
|
127
144
|
return fs.readFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), "utf8")
|
|
128
145
|
}
|
|
@@ -143,6 +160,9 @@ export async function importApp(
|
|
|
143
160
|
template.file && fs.lstatSync(template.file.path).isDirectory()
|
|
144
161
|
if (template.file && (isTar || isDirectory)) {
|
|
145
162
|
const tmpPath = isTar ? untarFile(template.file) : template.file.path
|
|
163
|
+
if (isTar && template.file.password) {
|
|
164
|
+
await decryptFiles(tmpPath, template.file.password)
|
|
165
|
+
}
|
|
146
166
|
const contents = fs.readdirSync(tmpPath)
|
|
147
167
|
// have to handle object import
|
|
148
168
|
if (contents.length) {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
env as coreEnv,
|
|
10
10
|
} from "@budibase/backend-core"
|
|
11
11
|
import { updateAppRole } from "./global"
|
|
12
|
-
import { BBContext, User } from "@budibase/types"
|
|
12
|
+
import { BBContext, User, EmailInvite } from "@budibase/types"
|
|
13
13
|
|
|
14
14
|
export function request(ctx?: BBContext, request?: any) {
|
|
15
15
|
if (!request.headers) {
|
|
@@ -65,15 +65,25 @@ async function checkResponse(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// have to pass in the tenant ID as this could be coming from an automation
|
|
68
|
-
export async function sendSmtpEmail(
|
|
69
|
-
to
|
|
70
|
-
from
|
|
71
|
-
subject
|
|
72
|
-
contents
|
|
73
|
-
cc
|
|
74
|
-
bcc
|
|
68
|
+
export async function sendSmtpEmail({
|
|
69
|
+
to,
|
|
70
|
+
from,
|
|
71
|
+
subject,
|
|
72
|
+
contents,
|
|
73
|
+
cc,
|
|
74
|
+
bcc,
|
|
75
|
+
automation,
|
|
76
|
+
invite,
|
|
77
|
+
}: {
|
|
78
|
+
to: string
|
|
79
|
+
from: string
|
|
80
|
+
subject: string
|
|
81
|
+
contents: string
|
|
82
|
+
cc: string
|
|
83
|
+
bcc: string
|
|
75
84
|
automation: boolean
|
|
76
|
-
|
|
85
|
+
invite?: EmailInvite
|
|
86
|
+
}) {
|
|
77
87
|
// tenant ID will be set in header
|
|
78
88
|
const response = await fetch(
|
|
79
89
|
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
|
|
@@ -88,6 +98,7 @@ export async function sendSmtpEmail(
|
|
|
88
98
|
bcc,
|
|
89
99
|
purpose: "custom",
|
|
90
100
|
automation,
|
|
101
|
+
invite,
|
|
91
102
|
},
|
|
92
103
|
})
|
|
93
104
|
)
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
function generateResponse(to, from) {
|
|
3
|
-
return {
|
|
4
|
-
"success": true,
|
|
5
|
-
"response": {
|
|
6
|
-
"accepted": [
|
|
7
|
-
to
|
|
8
|
-
],
|
|
9
|
-
"envelope": {
|
|
10
|
-
"from": from,
|
|
11
|
-
"to": [
|
|
12
|
-
to
|
|
13
|
-
]
|
|
14
|
-
},
|
|
15
|
-
"message": `Email sent to ${to}.`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const mockFetch = jest.fn(() => ({
|
|
22
|
-
headers: {
|
|
23
|
-
raw: () => {
|
|
24
|
-
return { "content-type": ["application/json"] }
|
|
25
|
-
},
|
|
26
|
-
get: () => ["application/json"],
|
|
27
|
-
},
|
|
28
|
-
json: jest.fn(() => response),
|
|
29
|
-
status: 200,
|
|
30
|
-
text: jest.fn(),
|
|
31
|
-
}))
|
|
32
|
-
jest.mock("node-fetch", () => mockFetch)
|
|
33
|
-
const setup = require("./utilities")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
describe("test the outgoing webhook action", () => {
|
|
37
|
-
let inputs
|
|
38
|
-
let config = setup.getConfig()
|
|
39
|
-
beforeAll(async () => {
|
|
40
|
-
await config.init()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
afterAll(setup.afterAll)
|
|
44
|
-
|
|
45
|
-
it("should be able to run the action", async () => {
|
|
46
|
-
inputs = {
|
|
47
|
-
to: "user1@test.com",
|
|
48
|
-
from: "admin@test.com",
|
|
49
|
-
subject: "hello",
|
|
50
|
-
contents: "testing",
|
|
51
|
-
}
|
|
52
|
-
let resp = generateResponse(inputs.to, inputs.from)
|
|
53
|
-
mockFetch.mockImplementationOnce(() => ({
|
|
54
|
-
headers: {
|
|
55
|
-
raw: () => {
|
|
56
|
-
return { "content-type": ["application/json"] }
|
|
57
|
-
},
|
|
58
|
-
get: () => ["application/json"],
|
|
59
|
-
},
|
|
60
|
-
json: jest.fn(() => resp),
|
|
61
|
-
status: 200,
|
|
62
|
-
text: jest.fn(),
|
|
63
|
-
}))
|
|
64
|
-
const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
|
|
65
|
-
expect(res.response).toEqual(resp)
|
|
66
|
-
expect(res.success).toEqual(true)
|
|
67
|
-
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|