@budibase/worker 3.9.4 → 3.10.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.9.4",
4
+ "version": "3.10.0",
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": "e357c4e0cbfb4e0376cb8b692bf725263a5f7dc3"
123
+ "gitHead": "35c16326f7215671d405cd811b28e78f5be70d07"
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 verifyAIConfig(
223
- configToSave: AIInnerConfig,
224
- existingConfig: AIConfig
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 uuid in existingConfig.config) {
228
- if (configToSave[uuid]?.apiKey === PASSWORD_REPLACEMENT) {
229
- configToSave[uuid].apiKey = existingConfig.config[uuid].apiKey
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 verifyAIConfig(config, existingConfig)
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: {} } as AIConfig
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)
@@ -120,7 +120,6 @@ export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
120
120
  ? {
121
121
  provider: llmConfig.provider,
122
122
  model: llmConfig.model,
123
- measureUsage: llmConfig.measureUsage,
124
123
  }
125
124
  : undefined
126
125
 
@@ -1,6 +1,21 @@
1
- import { verifyAIConfig } from "../configs"
2
- import { TestConfiguration, structures } from "../../../../tests"
3
- import { AIInnerConfig } from "@budibase/types"
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("Should strip secrets when pulling AI config", async () => {
17
- const data = structures.configs.ai()
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.body.config).toEqual({
21
- ai: {
22
- active: true,
23
- apiKey: "--secret-value--",
24
- baseUrl: "https://api.example.com",
25
- defaultModel: "gpt4",
26
- isDefault: false,
27
- name: "Test",
28
- provider: "OpenAI",
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("Should not update existing secrets when updating an existing AI Config", async () => {
34
- const data = structures.configs.ai()
35
- await config.api.configs.saveConfig(data)
36
-
37
- const newConfig: AIInnerConfig = {
38
- ai: {
39
- provider: "OpenAI",
40
- isDefault: true,
41
- apiKey: "--secret-value--",
42
- name: "MyConfig",
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
- await verifyAIConfig(newConfig, data)
49
- // should be unchanged
50
- expect(newConfig.ai.apiKey).toEqual("myapikey")
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
@@ -130,6 +130,7 @@ const router: Router = new Router()
130
130
 
131
131
  router
132
132
  .use(middleware.errorHandling)
133
+ .use(middleware.featureFlagCookie)
133
134
  .use(
134
135
  compress({
135
136
  threshold: 2048,
@@ -68,16 +68,16 @@ function scimValidation() {
68
68
  function aiValidation() {
69
69
  // prettier-ignore
70
70
  return Joi.object().pattern(
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().required(),
79
- defaultModel: Joi.string().optional(),
80
- }).required()
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
 
@@ -28,10 +28,7 @@ describe("configs", () => {
28
28
  _rev,
29
29
  }
30
30
  const res = await config.api.configs.saveConfig(data)
31
- return {
32
- ...data,
33
- ...res.body,
34
- }
31
+ return { ...data, ...res }
35
32
  }
36
33
 
37
34
  const saveSettingsConfig = async (
@@ -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
- return this.request
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 = (data: any) => {
29
- return this.request
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
- }