@budibase/worker 3.9.5 → 3.10.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/package.json +2 -2
- package/src/api/controllers/global/configs.ts +25 -25
- package/src/api/controllers/global/self.ts +0 -1
- package/src/api/controllers/global/tests/configs.spec.ts +77 -32
- package/src/api/controllers/global/users.ts +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/routes/global/configs.ts +10 -10
- package/src/api/routes/global/tests/configs.spec.ts +1 -4
- package/src/tests/api/configs.ts +33 -5
- package/src/tests/structures/configs.ts +0 -18
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.10.1",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -120,5 +120,5 @@
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
|
-
"gitHead": "
|
|
123
|
+
"gitHead": "1a95ba0eed16547cffc223832e87e5b8818a23eb"
|
|
124
124
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
events,
|
|
10
10
|
objectStore,
|
|
11
11
|
tenancy,
|
|
12
|
+
BadRequestError,
|
|
12
13
|
} from "@budibase/backend-core"
|
|
13
14
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
14
15
|
import {
|
|
@@ -31,14 +32,12 @@ import {
|
|
|
31
32
|
OIDCConfigs,
|
|
32
33
|
OIDCLogosConfig,
|
|
33
34
|
PASSWORD_REPLACEMENT,
|
|
34
|
-
QuotaUsageType,
|
|
35
35
|
SaveConfigRequest,
|
|
36
36
|
SaveConfigResponse,
|
|
37
37
|
SettingsBrandingConfig,
|
|
38
38
|
SettingsInnerConfig,
|
|
39
39
|
SSOConfig,
|
|
40
40
|
SSOConfigType,
|
|
41
|
-
StaticQuotaName,
|
|
42
41
|
UploadConfigFileResponse,
|
|
43
42
|
UserCtx,
|
|
44
43
|
} from "@budibase/types"
|
|
@@ -52,7 +51,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
|
|
|
52
51
|
fns.push(events.email.SMTPCreated)
|
|
53
52
|
} else if (isAIConfig(config)) {
|
|
54
53
|
fns.push(() => events.ai.AIConfigCreated)
|
|
55
|
-
fns.push(() => pro.quotas.addCustomAIConfig())
|
|
56
54
|
} else if (isGoogleConfig(config)) {
|
|
57
55
|
fns.push(() => events.auth.SSOCreated(ConfigType.GOOGLE))
|
|
58
56
|
if (config.config.activated) {
|
|
@@ -91,12 +89,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
|
|
|
91
89
|
fns.push(events.email.SMTPUpdated)
|
|
92
90
|
} else if (isAIConfig(config)) {
|
|
93
91
|
fns.push(() => events.ai.AIConfigUpdated)
|
|
94
|
-
if (
|
|
95
|
-
Object.keys(existing.config).length > Object.keys(config.config).length
|
|
96
|
-
) {
|
|
97
|
-
fns.push(() => pro.quotas.removeCustomAIConfig())
|
|
98
|
-
}
|
|
99
|
-
fns.push(() => pro.quotas.addCustomAIConfig())
|
|
100
92
|
} else if (isGoogleConfig(config)) {
|
|
101
93
|
fns.push(() => events.auth.SSOUpdated(ConfigType.GOOGLE))
|
|
102
94
|
if (!existing.config.activated && config.config.activated) {
|
|
@@ -219,14 +211,30 @@ async function verifyOIDCConfig(config: OIDCConfigs) {
|
|
|
219
211
|
await verifySSOConfig(ConfigType.OIDC, config.configs[0])
|
|
220
212
|
}
|
|
221
213
|
|
|
222
|
-
export async function
|
|
223
|
-
|
|
224
|
-
existingConfig:
|
|
214
|
+
export async function processAIConfig(
|
|
215
|
+
newConfig: AIInnerConfig,
|
|
216
|
+
existingConfig: AIInnerConfig
|
|
225
217
|
) {
|
|
226
218
|
// ensure that the redacted API keys are not overwritten in the DB
|
|
227
|
-
for (const
|
|
228
|
-
if (
|
|
229
|
-
|
|
219
|
+
for (const key in existingConfig) {
|
|
220
|
+
if (newConfig[key]?.apiKey === PASSWORD_REPLACEMENT) {
|
|
221
|
+
newConfig[key].apiKey = existingConfig[key].apiKey
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let numBudibaseAI = 0
|
|
226
|
+
for (const config of Object.values(newConfig)) {
|
|
227
|
+
if (config.provider === "BudibaseAI") {
|
|
228
|
+
numBudibaseAI++
|
|
229
|
+
if (numBudibaseAI > 1) {
|
|
230
|
+
throw new BadRequestError("Only one Budibase AI provider is allowed")
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
if (!config.apiKey) {
|
|
234
|
+
throw new BadRequestError(
|
|
235
|
+
`API key is required for provider ${config.provider}`
|
|
236
|
+
)
|
|
237
|
+
}
|
|
230
238
|
}
|
|
231
239
|
}
|
|
232
240
|
}
|
|
@@ -246,7 +254,6 @@ export async function save(
|
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
try {
|
|
249
|
-
// verify the configuration
|
|
250
257
|
switch (type) {
|
|
251
258
|
case ConfigType.SMTP:
|
|
252
259
|
await email.verifyConfig(config)
|
|
@@ -262,7 +269,7 @@ export async function save(
|
|
|
262
269
|
break
|
|
263
270
|
case ConfigType.AI:
|
|
264
271
|
if (existingConfig) {
|
|
265
|
-
await
|
|
272
|
+
await processAIConfig(config, existingConfig.config)
|
|
266
273
|
}
|
|
267
274
|
break
|
|
268
275
|
}
|
|
@@ -354,7 +361,7 @@ export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
|
|
354
361
|
if (scopedConfig) {
|
|
355
362
|
await handleConfigType(type, scopedConfig)
|
|
356
363
|
} else if (type === ConfigType.AI) {
|
|
357
|
-
scopedConfig = { config: {} }
|
|
364
|
+
scopedConfig = { type: ConfigType.AI, config: {} }
|
|
358
365
|
await handleAIConfig(scopedConfig)
|
|
359
366
|
} else {
|
|
360
367
|
// If no config found and not AI type, just return an empty body
|
|
@@ -560,13 +567,6 @@ export async function destroy(ctx: UserCtx<void, DeleteConfigResponse>) {
|
|
|
560
567
|
try {
|
|
561
568
|
await db.remove(id, rev)
|
|
562
569
|
await cache.destroy(cache.CacheKey.CHECKLIST)
|
|
563
|
-
if (id === configs.generateConfigID(ConfigType.AI)) {
|
|
564
|
-
await pro.quotas.set(
|
|
565
|
-
StaticQuotaName.AI_CUSTOM_CONFIGS,
|
|
566
|
-
QuotaUsageType.STATIC,
|
|
567
|
-
0
|
|
568
|
-
)
|
|
569
|
-
}
|
|
570
570
|
ctx.body = { message: "Config deleted successfully" }
|
|
571
571
|
} catch (err: any) {
|
|
572
572
|
ctx.throw(err.status, err)
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { TestConfiguration } from "../../../../tests"
|
|
2
|
+
import { AIConfig, ConfigType } from "@budibase/types"
|
|
3
|
+
import { configs, context } from "@budibase/backend-core"
|
|
4
|
+
|
|
5
|
+
const BASE_CONFIG: AIConfig = {
|
|
6
|
+
type: ConfigType.AI,
|
|
7
|
+
config: {
|
|
8
|
+
ai: {
|
|
9
|
+
provider: "OpenAI",
|
|
10
|
+
isDefault: false,
|
|
11
|
+
name: "Test",
|
|
12
|
+
active: true,
|
|
13
|
+
defaultModel: "gpt4",
|
|
14
|
+
apiKey: "myapikey",
|
|
15
|
+
baseUrl: "https://api.example.com",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
describe("Global configs controller", () => {
|
|
6
21
|
const config = new TestConfiguration()
|
|
@@ -13,40 +28,70 @@ describe("Global configs controller", () => {
|
|
|
13
28
|
await config.afterAll()
|
|
14
29
|
})
|
|
15
30
|
|
|
16
|
-
it("
|
|
17
|
-
|
|
18
|
-
await config.api.configs.saveConfig(data)
|
|
31
|
+
it("should strip secrets when pulling AI config", async () => {
|
|
32
|
+
await config.api.configs.saveConfig(BASE_CONFIG)
|
|
19
33
|
const response = await config.api.configs.getAIConfig()
|
|
20
|
-
expect(response.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
expect(response.config.ai.apiKey).toEqual("--secret-value--")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should not update existing secrets when updating an existing AI Config", async () => {
|
|
38
|
+
await config.api.configs.saveConfig(BASE_CONFIG)
|
|
39
|
+
|
|
40
|
+
const savedConfig = await config.api.configs.getAIConfig()
|
|
41
|
+
delete savedConfig._id
|
|
42
|
+
delete savedConfig._rev
|
|
43
|
+
delete savedConfig.createdAt
|
|
44
|
+
delete savedConfig.updatedAt
|
|
45
|
+
|
|
46
|
+
await config.api.configs.saveConfig(savedConfig)
|
|
47
|
+
|
|
48
|
+
await context.doInTenant(config.tenantId, async () => {
|
|
49
|
+
const aiConfig = await configs.getAIConfig()
|
|
50
|
+
expect(aiConfig!.config.ai.apiKey).toEqual(BASE_CONFIG.config.ai.apiKey)
|
|
30
51
|
})
|
|
31
52
|
})
|
|
32
53
|
|
|
33
|
-
it("
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
active: true,
|
|
44
|
-
defaultModel: "gpt4",
|
|
54
|
+
it("should allow BudibaseAI to save without an apiKey", async () => {
|
|
55
|
+
await config.api.configs.saveConfig({
|
|
56
|
+
type: ConfigType.AI,
|
|
57
|
+
config: {
|
|
58
|
+
ai: {
|
|
59
|
+
name: "Budibase AI",
|
|
60
|
+
active: true,
|
|
61
|
+
provider: "BudibaseAI",
|
|
62
|
+
isDefault: true,
|
|
63
|
+
},
|
|
45
64
|
},
|
|
46
|
-
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const aiConfig = await config.api.configs.getAIConfig()
|
|
68
|
+
expect(aiConfig.config.ai).toEqual({
|
|
69
|
+
name: "Budibase AI",
|
|
70
|
+
provider: "BudibaseAI",
|
|
71
|
+
active: true,
|
|
72
|
+
isDefault: true,
|
|
73
|
+
})
|
|
74
|
+
})
|
|
47
75
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
it("should not allow OpenAI to save without an apiKey", async () => {
|
|
77
|
+
await config.api.configs.saveConfig(
|
|
78
|
+
{
|
|
79
|
+
type: ConfigType.AI,
|
|
80
|
+
config: {
|
|
81
|
+
ai: {
|
|
82
|
+
name: "OpenAI",
|
|
83
|
+
active: true,
|
|
84
|
+
provider: "OpenAI",
|
|
85
|
+
isDefault: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
status: 400,
|
|
91
|
+
body: {
|
|
92
|
+
message: /API key is required for provider OpenAI/,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
)
|
|
51
96
|
})
|
|
52
97
|
})
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DeleteInviteUsersRequest,
|
|
17
17
|
DeleteInviteUsersResponse,
|
|
18
18
|
DeleteUserResponse,
|
|
19
|
+
ErrorCode,
|
|
19
20
|
FetchUsersResponse,
|
|
20
21
|
FindUserResponse,
|
|
21
22
|
GetUserInvitesResponse,
|
|
@@ -42,7 +43,6 @@ import {
|
|
|
42
43
|
import {
|
|
43
44
|
users,
|
|
44
45
|
cache,
|
|
45
|
-
ErrorCode,
|
|
46
46
|
events,
|
|
47
47
|
platform,
|
|
48
48
|
tenancy,
|
package/src/api/index.ts
CHANGED
|
@@ -68,16 +68,16 @@ function scimValidation() {
|
|
|
68
68
|
function aiValidation() {
|
|
69
69
|
// prettier-ignore
|
|
70
70
|
return Joi.object().pattern(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
Joi.string(),
|
|
72
|
+
Joi.object({
|
|
73
|
+
provider: Joi.string().required(),
|
|
74
|
+
isDefault: Joi.boolean().required(),
|
|
75
|
+
name: Joi.string().required(),
|
|
76
|
+
active: Joi.boolean().required(),
|
|
77
|
+
baseUrl: Joi.string().optional().allow("", null),
|
|
78
|
+
apiKey: Joi.string().optional(),
|
|
79
|
+
defaultModel: Joi.string().optional(),
|
|
80
|
+
}).required()
|
|
81
81
|
)
|
|
82
82
|
}
|
|
83
83
|
|
package/src/tests/api/configs.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIConfig,
|
|
3
|
+
SaveConfigRequest,
|
|
4
|
+
SaveConfigResponse,
|
|
5
|
+
} from "@budibase/types"
|
|
1
6
|
import { TestAPI } from "./base"
|
|
2
7
|
|
|
3
8
|
export class ConfigAPI extends TestAPI {
|
|
@@ -17,21 +22,44 @@ export class ConfigAPI extends TestAPI {
|
|
|
17
22
|
.expect("Content-Type", /json/)
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
getAIConfig = () => {
|
|
21
|
-
|
|
25
|
+
getAIConfig = async () => {
|
|
26
|
+
const resp = await this.request
|
|
22
27
|
.get(`/api/global/configs/ai`)
|
|
23
28
|
.set(this.config.defaultHeaders())
|
|
24
29
|
.expect(200)
|
|
25
30
|
.expect("Content-Type", /json/)
|
|
31
|
+
return resp.body as AIConfig
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
saveConfig = (
|
|
29
|
-
|
|
34
|
+
saveConfig = async (
|
|
35
|
+
data: SaveConfigRequest,
|
|
36
|
+
expectations?: { status?: number; body?: Record<string, string | RegExp> }
|
|
37
|
+
) => {
|
|
38
|
+
const { status = 200, body } = expectations || {}
|
|
39
|
+
|
|
40
|
+
const resp = await this.request
|
|
30
41
|
.post(`/api/global/configs`)
|
|
31
42
|
.send(data)
|
|
32
43
|
.set(this.config.defaultHeaders())
|
|
33
|
-
.expect(200)
|
|
34
44
|
.expect("Content-Type", /json/)
|
|
45
|
+
|
|
46
|
+
if (resp.status !== status) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Expected status ${status}, got ${resp.status}: ${resp.text}`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (body) {
|
|
53
|
+
for (const [key, value] of Object.entries(body)) {
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
expect(resp.body[key]).toEqual(value)
|
|
56
|
+
} else if (value instanceof RegExp) {
|
|
57
|
+
expect(resp.body[key]).toMatch(value)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resp.body as SaveConfigResponse
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
OIDCCallback = (configId: string, preAuthRes: any) => {
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
SMTPConfig,
|
|
6
6
|
GoogleConfig,
|
|
7
7
|
OIDCConfig,
|
|
8
|
-
AIConfig,
|
|
9
8
|
} from "@budibase/types"
|
|
10
9
|
|
|
11
10
|
export function oidc(conf?: any): OIDCConfig {
|
|
@@ -82,20 +81,3 @@ export function settings(conf?: any): SettingsConfig {
|
|
|
82
81
|
},
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
export function ai(): AIConfig {
|
|
87
|
-
return {
|
|
88
|
-
type: ConfigType.AI,
|
|
89
|
-
config: {
|
|
90
|
-
ai: {
|
|
91
|
-
provider: "OpenAI",
|
|
92
|
-
isDefault: false,
|
|
93
|
-
name: "Test",
|
|
94
|
-
active: true,
|
|
95
|
-
defaultModel: "gpt4",
|
|
96
|
-
apiKey: "myapikey",
|
|
97
|
-
baseUrl: "https://api.example.com",
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
}
|