@budibase/worker 3.31.1 → 3.31.3

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/jest.config.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Config } from "jest"
2
- import * as fs from "fs"
3
2
 
4
3
  const config: Config = {
5
4
  globalSetup: "./../../globalSetup.ts",
@@ -16,13 +15,9 @@ const config: Config = {
16
15
  "@budibase/types": "<rootDir>/../types/src",
17
16
  "@budibase/shared-core": ["<rootDir>/../shared-core/src"],
18
17
  "@budibase/string-templates": ["<rootDir>/../string-templates/src"],
18
+ "@budibase/pro/(.*)": "<rootDir>/../pro/$1",
19
+ "@budibase/pro": "<rootDir>/../pro/src",
19
20
  },
20
21
  }
21
22
 
22
- // add pro sources if they exist
23
- if (fs.existsSync("../pro/src")) {
24
- config.moduleNameMapper!["@budibase/pro/(.*)"] = "<rootDir>/../pro/$1"
25
- config.moduleNameMapper!["@budibase/pro"] = "<rootDir>/../pro/src"
26
- }
27
-
28
23
  export default config
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.31.1",
4
+ "version": "3.31.3",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -37,7 +37,6 @@
37
37
  "@govtechsg/passport-openidconnect": "1.0.3",
38
38
  "@koa/router": "15.3.0",
39
39
  "@types/global-agent": "2.1.1",
40
- "aws-sdk": "2.1692.0",
41
40
  "bcrypt": "6.0.0",
42
41
  "bull": "4.10.1",
43
42
  "dd-trace": "5.63.0",
@@ -48,28 +47,19 @@
48
47
  "ical-generator": "4.1.0",
49
48
  "joi": "17.6.0",
50
49
  "jsonwebtoken": "9.0.2",
51
- "knex": "2.4.2",
52
50
  "koa": "2.15.4",
53
51
  "koa-body": "4.2.0",
54
52
  "koa-compress": "4.0.1",
55
- "koa-passport": "4.1.4",
56
53
  "koa-redis": "^4.0.1",
57
- "koa-send": "5.0.1",
58
54
  "koa-session": "5.13.1",
59
- "koa-static": "5.0.0",
60
55
  "koa-useragent": "^4.1.0",
61
56
  "lodash": "4.17.23",
62
57
  "marked": "^15.0.11",
63
58
  "node-fetch": "2.6.7",
64
59
  "nodemailer": "7.0.11",
65
- "passport-google-oauth": "2.0.0",
66
- "passport-local": "1.0.0",
67
60
  "pouchdb": "9.0.0",
68
- "pouchdb-all-dbs": "1.1.1",
69
61
  "scim-patch": "^0.8.1",
70
62
  "scim2-parse-filter": "^0.2.8",
71
- "server-destroy": "1.0.1",
72
- "undici": "^7.16.0",
73
63
  "uuid": "^8.3.2",
74
64
  "yaml": "^2.8.2"
75
65
  },
@@ -79,7 +69,6 @@
79
69
  "@types/maildev": "^0.0.7",
80
70
  "@types/node-fetch": "2.6.4",
81
71
  "@types/nodemailer": "^6.4.17",
82
- "@types/server-destroy": "1.0.1",
83
72
  "@types/supertest": "2.0.14",
84
73
  "@types/uuid": "8.3.4",
85
74
  "cheerio": "^1.0.0",
@@ -91,23 +80,5 @@
91
80
  "supertest": "6.3.3",
92
81
  "timekeeper": "2.2.0"
93
82
  },
94
- "resolutions": {
95
- "@budibase/pro": "npm:@budibase/pro@latest"
96
- },
97
- "nx": {
98
- "targets": {
99
- "dev": {
100
- "dependsOn": [
101
- {
102
- "comment": "Required for pro usage when submodule not loaded",
103
- "projects": [
104
- "@budibase/backend-core"
105
- ],
106
- "target": "build:oss"
107
- }
108
- ]
109
- }
110
- }
111
- },
112
- "gitHead": "9de72ce9dd1c290eb3d16fd22693192923f46f06"
83
+ "gitHead": "7f5947eadcf2e540ae7e9334ef8f160be64fea2e"
113
84
  }
@@ -514,9 +514,6 @@ export async function find(ctx: UserCtx<void, FindConfigResponse>) {
514
514
  case ConfigType.OIDC_LOGOS:
515
515
  await enrichOIDCLogos(config)
516
516
  break
517
- case ConfigType.AI:
518
- await pro.sdk.ai.enrichAIConfig(config)
519
- break
520
517
  }
521
518
 
522
519
  stripSecrets(config, ctx)
@@ -9,8 +9,13 @@ import {
9
9
  tenancy,
10
10
  users,
11
11
  } from "@budibase/backend-core"
12
- import { groups as proGroups } from "@budibase/pro"
13
- import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core"
12
+ import { db as proDb } from "@budibase/pro"
13
+ import {
14
+ BpmStatusKey,
15
+ BpmStatusValue,
16
+ dataFilters,
17
+ utils,
18
+ } from "@budibase/shared-core"
14
19
  import {
15
20
  AcceptUserInviteRequest,
16
21
  AcceptUserInviteResponse,
@@ -50,6 +55,7 @@ import {
50
55
  UnsavedUser,
51
56
  UpdateInviteResponse,
52
57
  User,
58
+ UserGroup,
53
59
  UserCtx,
54
60
  UserIdentifier,
55
61
  } from "@budibase/types"
@@ -370,12 +376,66 @@ const searchWorkspaceUsers = async (
370
376
  return { data: [], hasNextPage: false }
371
377
  }
372
378
 
379
+ const prodWorkspaceId = db.getProdWorkspaceID(workspaceId)
373
380
  const limit = body.limit ?? DEFAULT_USER_LIMIT
374
381
  const query = body.query
375
- const filtered: User[] = []
376
- let cursor = body.bookmark
377
- let nextPage: string | undefined
378
- const groupAccessCache = new Map<string, boolean>()
382
+ const globalDb = context.getGlobalDB()
383
+
384
+ const [workspaceUsers, globalPermissionUsers, workspaceGroups] =
385
+ await Promise.all([
386
+ db.queryGlobalView<User>(
387
+ db.ViewName.USER_BY_WORKSPACE,
388
+ db.getUsersByWorkspaceParams(prodWorkspaceId, {
389
+ include_docs: true,
390
+ }),
391
+ undefined,
392
+ { arrayResponse: true }
393
+ ) as Promise<User[]>,
394
+ globalDb
395
+ .find<User>({
396
+ selector: {
397
+ _id: {
398
+ $regex: `^${db.DocumentType.USER}${db.SEPARATOR}`,
399
+ },
400
+ $or: [{ "admin.global": true }, { "builder.global": true }],
401
+ },
402
+ })
403
+ .then(response => response.docs),
404
+ globalDb
405
+ .allDocs<UserGroup>(
406
+ db.getDocParams(db.DocumentType.GROUP, null, {
407
+ include_docs: true,
408
+ })
409
+ )
410
+ .then(
411
+ response =>
412
+ response.rows
413
+ .map(row => row.doc)
414
+ .filter(group => !!group?.roles?.[prodWorkspaceId]) as UserGroup[]
415
+ ),
416
+ ])
417
+
418
+ const workspaceGroupIds = workspaceGroups.map(group => group._id!)
419
+ const workspaceGroupIdSet = new Set(workspaceGroupIds)
420
+
421
+ let groupUsers: User[] = []
422
+ if (workspaceGroupIds.length) {
423
+ const usersByGroup = await Promise.all(
424
+ workspaceGroupIds.map(groupId => proDb.groups.getGroupUsers(groupId))
425
+ )
426
+ const groupUserIds = [
427
+ ...new Set(
428
+ usersByGroup
429
+ .flat()
430
+ .map(user => user._id)
431
+ .filter(Boolean)
432
+ ),
433
+ ] as string[]
434
+
435
+ if (groupUserIds.length) {
436
+ groupUsers = await userSdk.db.bulkGet(groupUserIds)
437
+ }
438
+ }
379
439
 
380
440
  const getBookmarkValue = (user: User) => {
381
441
  if (query?.string?.email && user.email) {
@@ -384,73 +444,54 @@ const searchWorkspaceUsers = async (
384
444
  return user._id
385
445
  }
386
446
 
387
- const hydrateGroupAccess = async (usersToCheck: User[]) => {
388
- const missingGroupIds = new Set<string>()
389
- for (const user of usersToCheck) {
390
- for (const groupId of user.userGroups || []) {
391
- if (!groupAccessCache.has(groupId)) {
392
- missingGroupIds.add(groupId)
393
- }
394
- }
395
- }
396
-
397
- if (!missingGroupIds.size) {
398
- return
399
- }
400
-
401
- const groupIdList = [...missingGroupIds]
402
- const groups = await proGroups.getBulk(groupIdList, { enriched: false })
403
- groups.forEach((group, index) => {
404
- const groupId = group?._id || groupIdList[index]
405
- const hasAccess = !!group?.roles?.[workspaceId]
406
- groupAccessCache.set(groupId, hasAccess)
407
- })
408
- }
409
-
410
447
  const hasWorkspaceAccess = (user: User) => {
411
- if (users.isAdminOrBuilder(user, workspaceId)) {
448
+ if (users.isAdminOrBuilder(user, prodWorkspaceId)) {
412
449
  return true
413
450
  }
414
- if (user.roles?.[workspaceId]) {
451
+ if (user.roles?.[prodWorkspaceId]) {
415
452
  return true
416
453
  }
417
454
  return (user.userGroups || []).some(groupId =>
418
- groupAccessCache.get(groupId)
455
+ workspaceGroupIdSet.has(groupId)
419
456
  )
420
457
  }
421
458
 
422
- while (filtered.length <= limit) {
423
- const page = await userSdk.core.paginatedUsers({
424
- bookmark: cursor,
425
- query,
426
- limit,
427
- })
428
-
429
- if (!page.data?.length) {
430
- break
459
+ const dedupedUsers = new Map<string, User>()
460
+ for (const user of [
461
+ ...workspaceUsers,
462
+ ...globalPermissionUsers,
463
+ ...groupUsers,
464
+ ]) {
465
+ if (user?._id) {
466
+ dedupedUsers.set(user._id, user)
431
467
  }
468
+ }
432
469
 
433
- await hydrateGroupAccess(page.data)
470
+ let filtered = [...dedupedUsers.values()].filter(hasWorkspaceAccess)
434
471
 
435
- for (const user of page.data) {
436
- if (!hasWorkspaceAccess(user)) {
437
- continue
438
- }
439
- filtered.push(user)
440
- if (filtered.length === limit + 1) {
441
- nextPage = getBookmarkValue(user)
442
- break
443
- }
444
- }
472
+ if (query) {
473
+ filtered = dataFilters.search(filtered, { query }).rows
474
+ }
445
475
 
446
- if (filtered.length === limit + 1 || !page.hasNextPage) {
447
- break
476
+ filtered.sort((userA, userB) => {
477
+ if (query?.string?.email) {
478
+ const emailA = userA.email?.toLowerCase() || ""
479
+ const emailB = userB.email?.toLowerCase() || ""
480
+ if (emailA !== emailB) {
481
+ return emailA.localeCompare(emailB)
482
+ }
448
483
  }
449
- cursor = page.nextPage
450
- }
484
+ return (userA._id || "").localeCompare(userB._id || "")
485
+ })
451
486
 
452
- const hasNextPage = filtered.length > limit
453
- const data = hasNextPage ? filtered.slice(0, limit) : filtered
487
+ const bookmark = body.bookmark
488
+ const pagedUsers = bookmark
489
+ ? filtered.filter(user => (getBookmarkValue(user) || "") >= bookmark)
490
+ : filtered
491
+ const pageData = pagedUsers.slice(0, limit + 1)
492
+ const hasNextPage = pageData.length > limit
493
+ const data = hasNextPage ? pageData.slice(0, limit) : pageData
494
+ const nextPage = hasNextPage ? getBookmarkValue(pageData[limit]) : undefined
454
495
 
455
496
  for (let user of data) {
456
497
  if (user) {
@@ -2,6 +2,7 @@ import { InviteUsersResponse, OIDCUser, User } from "@budibase/types"
2
2
 
3
3
  import { accounts as _accounts, events, tenancy } from "@budibase/backend-core"
4
4
  import { mocks as featureMocks } from "@budibase/backend-core/tests"
5
+ import { sdk as proSdk } from "@budibase/pro"
5
6
  import * as userSdk from "../../../../sdk/users"
6
7
  import { TestConfiguration, mocks, structures } from "../../../../tests"
7
8
 
@@ -919,6 +920,49 @@ describe("/api/global/users", () => {
919
920
  expect(response.body.data.length).toBe(0)
920
921
  })
921
922
 
923
+ it("should include users with workspace access via groups", async () => {
924
+ const workspaceId = "app_workspace_filter_group"
925
+ const email = structures.users.newEmail()
926
+ featureMocks.licenses.useUnlimited()
927
+ const group = await config.doInTenant(() =>
928
+ proSdk.groups.save({
929
+ ...structures.groups.UserGroup(),
930
+ roles: { [workspaceId]: "BASIC" },
931
+ })
932
+ )
933
+
934
+ await config.createUser({
935
+ email,
936
+ userGroups: [group.id],
937
+ })
938
+
939
+ const response = await config.api.users.searchUsers({
940
+ workspaceId,
941
+ query: { string: { email } },
942
+ })
943
+
944
+ expect(response.body.data.length).toBe(1)
945
+ expect(response.body.data[0].email).toBe(email)
946
+ })
947
+
948
+ it("should include global admins in workspace search", async () => {
949
+ const workspaceId = "app_workspace_filter_global_admin"
950
+ const email = structures.users.newEmail()
951
+ await config.createUser({
952
+ email,
953
+ admin: { global: true },
954
+ builder: { global: true },
955
+ })
956
+
957
+ const response = await config.api.users.searchUsers({
958
+ workspaceId,
959
+ query: { string: { email } },
960
+ })
961
+
962
+ expect(response.body.data.length).toBe(1)
963
+ expect(response.body.data[0].email).toBe(email)
964
+ })
965
+
922
966
  it("should return no users when workspaceId is empty", async () => {
923
967
  const email = structures.users.newEmail()
924
968
  await config.createUser({ email })
@@ -101,9 +101,6 @@ const environment = {
101
101
  PASSWORD_RESET_RATE_IP_WINDOW_SECONDS:
102
102
  parseIntSafe(process.env.PASSWORD_RESET_RATE_IP_WINDOW_SECONDS) || 900,
103
103
 
104
- // Budibase AI
105
- BUDIBASE_AI_API_KEY: process.env.BUDIBASE_AI_API_KEY,
106
- BUDIBASE_AI_DEFAULT_MODEL: process.env.BUDIBASE_AI_DEFAULT_MODEL,
107
104
  _set(key: any, value: any) {
108
105
  process.env[key] = value
109
106
  // @ts-ignore