@budibase/worker 2.6.23 → 2.6.24-alpha.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/.dockerignore +4 -7
- package/Dockerfile +11 -4
- package/jest.config.ts +6 -5
- package/nodemon.json +7 -3
- package/package.json +11 -10
- package/src/api/controllers/global/configs.ts +29 -11
- package/src/api/controllers/global/users.ts +17 -16
- package/src/api/routes/global/tests/groups.spec.ts +176 -0
- package/src/tests/api/groups.ts +32 -2
- package/src/tests/structures/groups.ts +19 -4
- package/src/utilities/redis.ts +1 -1
- package/tsconfig.build.json +11 -6
- package/tsconfig.json +1 -12
package/.dockerignore
CHANGED
package/Dockerfile
CHANGED
|
@@ -7,13 +7,20 @@ LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-ho
|
|
|
7
7
|
|
|
8
8
|
WORKDIR /app
|
|
9
9
|
|
|
10
|
-
# copy files and install dependencies
|
|
11
|
-
COPY . ./
|
|
12
10
|
# handle node-gyp
|
|
13
|
-
RUN apk add --no-cache --virtual .gyp python3 make g++
|
|
14
|
-
&& yarn && apk del .gyp
|
|
11
|
+
RUN apk add --no-cache --virtual .gyp python3 make g++
|
|
15
12
|
RUN yarn global add pm2
|
|
16
13
|
|
|
14
|
+
|
|
15
|
+
COPY package.json .
|
|
16
|
+
RUN yarn install --frozen-lockfile --production=true
|
|
17
|
+
# Remove unneeded data from file system to reduce image size
|
|
18
|
+
RUN apk del .gyp \
|
|
19
|
+
&& yarn cache clean
|
|
20
|
+
|
|
21
|
+
COPY dist/ dist/
|
|
22
|
+
COPY docker_run.sh .
|
|
23
|
+
|
|
17
24
|
EXPOSE 4001
|
|
18
25
|
|
|
19
26
|
# have to add node environment production after install
|
package/jest.config.ts
CHANGED
|
@@ -16,15 +16,16 @@ const config: Config.InitialOptions = {
|
|
|
16
16
|
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
|
|
17
17
|
"@budibase/backend-core": "<rootDir>/../backend-core/src",
|
|
18
18
|
"@budibase/types": "<rootDir>/../types/src",
|
|
19
|
+
"@budibase/shared-core": ["<rootDir>/../shared-core/src"],
|
|
19
20
|
},
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
// add pro sources if they exist
|
|
23
|
-
if (fs.existsSync("
|
|
24
|
-
config.moduleNameMapper["@budibase/pro/(.*)"] =
|
|
25
|
-
"<rootDir
|
|
26
|
-
config.moduleNameMapper["@budibase/pro"] =
|
|
27
|
-
"<rootDir
|
|
24
|
+
if (fs.existsSync("../pro/packages")) {
|
|
25
|
+
config.moduleNameMapper!["@budibase/pro/(.*)"] =
|
|
26
|
+
"<rootDir>/../pro/packages/pro/$1"
|
|
27
|
+
config.moduleNameMapper!["@budibase/pro"] =
|
|
28
|
+
"<rootDir>/../pro/packages/pro/src"
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export default config
|
package/nodemon.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"watch": ["src", "../backend-core", "
|
|
2
|
+
"watch": ["src", "../backend-core", "../pro/packages/pro"],
|
|
3
3
|
"ext": "js,ts,json",
|
|
4
|
-
"ignore": [
|
|
5
|
-
|
|
4
|
+
"ignore": [
|
|
5
|
+
"src/**/*.spec.ts",
|
|
6
|
+
"src/**/*.spec.js",
|
|
7
|
+
"../backend-core/dist/**/*"
|
|
8
|
+
],
|
|
9
|
+
"exec": "yarn build && node dist/index.js"
|
|
6
10
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "2.6.
|
|
4
|
+
"version": "2.6.24-alpha.0",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"prebuild": "rimraf dist/",
|
|
16
|
-
"build": "
|
|
17
|
-
"
|
|
16
|
+
"build": "node ../../scripts/build.js",
|
|
17
|
+
"check:types": "tsc -p tsconfig.build.json --noEmit",
|
|
18
18
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
|
19
19
|
"run:docker": "node dist/index.js",
|
|
20
20
|
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
|
|
21
21
|
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
|
22
|
-
"
|
|
22
|
+
"predocker": "yarn build --configuration=production",
|
|
23
|
+
"build:docker": "yarn predocker && docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
|
|
23
24
|
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
|
24
|
-
"dev:builder": "npm run dev:stack:init && nodemon",
|
|
25
|
+
"dev:builder": "npm run dev:stack:init && rimraf dist/ && nodemon",
|
|
25
26
|
"dev:built": "yarn run dev:stack:init && yarn run run:docker",
|
|
26
27
|
"test": "bash scripts/test.sh",
|
|
27
28
|
"test:watch": "jest --watch",
|
|
@@ -37,10 +38,10 @@
|
|
|
37
38
|
"author": "Budibase",
|
|
38
39
|
"license": "GPL-3.0",
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@budibase/backend-core": "
|
|
41
|
-
"@budibase/pro": "2.6.
|
|
42
|
-
"@budibase/string-templates": "
|
|
43
|
-
"@budibase/types": "
|
|
41
|
+
"@budibase/backend-core": "2.6.24-alpha.0",
|
|
42
|
+
"@budibase/pro": "2.6.24-alpha.0",
|
|
43
|
+
"@budibase/string-templates": "2.6.24-alpha.0",
|
|
44
|
+
"@budibase/types": "2.6.24-alpha.0",
|
|
44
45
|
"@koa/router": "8.0.8",
|
|
45
46
|
"@sentry/node": "6.17.7",
|
|
46
47
|
"@techpass/passport-openidconnect": "0.3.2",
|
|
@@ -102,5 +103,5 @@
|
|
|
102
103
|
"typescript": "4.7.3",
|
|
103
104
|
"update-dotenv": "1.1.1"
|
|
104
105
|
},
|
|
105
|
-
"gitHead": "
|
|
106
|
+
"gitHead": "71a9558d12c55e7d9ef87b82995c62497662acf8"
|
|
106
107
|
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
tenancy,
|
|
12
12
|
} from "@budibase/backend-core"
|
|
13
13
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
14
|
-
import { getLicensedConfig } from "../../../utilities/configs"
|
|
15
14
|
import {
|
|
16
15
|
Config,
|
|
17
16
|
ConfigType,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
isSettingsConfig,
|
|
25
24
|
isSMTPConfig,
|
|
26
25
|
OIDCConfigs,
|
|
26
|
+
SettingsBrandingConfig,
|
|
27
27
|
SettingsInnerConfig,
|
|
28
28
|
SSOConfig,
|
|
29
29
|
SSOConfigType,
|
|
@@ -143,13 +143,29 @@ async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
|
|
|
143
143
|
return !!Object.values(ssoConfigs).find(c => c?.activated)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
async function verifySettingsConfig(
|
|
146
|
+
async function verifySettingsConfig(
|
|
147
|
+
config: SettingsInnerConfig & SettingsBrandingConfig,
|
|
148
|
+
existingConfig?: SettingsInnerConfig & SettingsBrandingConfig
|
|
149
|
+
) {
|
|
147
150
|
if (config.isSSOEnforced) {
|
|
148
151
|
const valid = await hasActivatedConfig()
|
|
149
152
|
if (!valid) {
|
|
150
153
|
throw new Error("Cannot enforce SSO without an activated configuration")
|
|
151
154
|
}
|
|
152
155
|
}
|
|
156
|
+
|
|
157
|
+
// always preserve file attributes
|
|
158
|
+
// these should be set via upload instead
|
|
159
|
+
// only allow for deletion by checking empty string to bypass this behaviour
|
|
160
|
+
|
|
161
|
+
if (existingConfig && config.logoUrl !== "") {
|
|
162
|
+
config.logoUrl = existingConfig.logoUrl
|
|
163
|
+
config.logoUrlEtag = existingConfig.logoUrlEtag
|
|
164
|
+
}
|
|
165
|
+
if (existingConfig && config.faviconUrl !== "") {
|
|
166
|
+
config.faviconUrl = existingConfig.faviconUrl
|
|
167
|
+
config.faviconUrlEtag = existingConfig.faviconUrlEtag
|
|
168
|
+
}
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
|
|
@@ -199,7 +215,7 @@ export async function save(ctx: UserCtx<Config>) {
|
|
|
199
215
|
await email.verifyConfig(config)
|
|
200
216
|
break
|
|
201
217
|
case ConfigType.SETTINGS:
|
|
202
|
-
await verifySettingsConfig(config)
|
|
218
|
+
await verifySettingsConfig(config, existingConfig?.config)
|
|
203
219
|
break
|
|
204
220
|
case ConfigType.GOOGLE:
|
|
205
221
|
await verifyGoogleConfig(config)
|
|
@@ -321,14 +337,15 @@ export async function publicSettings(
|
|
|
321
337
|
)
|
|
322
338
|
}
|
|
323
339
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
// enrich the favicon url - empty url means deleted
|
|
341
|
+
const faviconUrl =
|
|
342
|
+
branding.faviconUrl && branding.faviconUrl !== ""
|
|
343
|
+
? objectStore.getGlobalFileUrl(
|
|
344
|
+
"settings",
|
|
345
|
+
"faviconUrl",
|
|
346
|
+
branding.faviconUrlEtag
|
|
347
|
+
)
|
|
348
|
+
: undefined
|
|
332
349
|
|
|
333
350
|
// google
|
|
334
351
|
const googleConfig = await configs.getGoogleConfig()
|
|
@@ -353,6 +370,7 @@ export async function publicSettings(
|
|
|
353
370
|
config: {
|
|
354
371
|
...config,
|
|
355
372
|
...branding,
|
|
373
|
+
...{ faviconUrl },
|
|
356
374
|
google,
|
|
357
375
|
googleDatasourceConfigured,
|
|
358
376
|
oidc,
|
|
@@ -69,9 +69,11 @@ const bulkCreate = async (users: User[], groupIds: string[]) => {
|
|
|
69
69
|
return await userSdk.bulkCreate(users, groupIds)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export const bulkUpdate = async (
|
|
72
|
+
export const bulkUpdate = async (
|
|
73
|
+
ctx: Ctx<BulkUserRequest, BulkUserResponse>
|
|
74
|
+
) => {
|
|
73
75
|
const currentUserId = ctx.user._id
|
|
74
|
-
const input = ctx.request.body
|
|
76
|
+
const input = ctx.request.body
|
|
75
77
|
let created, deleted
|
|
76
78
|
try {
|
|
77
79
|
if (input.create) {
|
|
@@ -83,7 +85,7 @@ export const bulkUpdate = async (ctx: any) => {
|
|
|
83
85
|
} catch (err: any) {
|
|
84
86
|
ctx.throw(err.status || 400, err?.message || err)
|
|
85
87
|
}
|
|
86
|
-
ctx.body = { created, deleted }
|
|
88
|
+
ctx.body = { created, deleted }
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
const parseBooleanParam = (param: any) => {
|
|
@@ -184,15 +186,15 @@ export const destroy = async (ctx: any) => {
|
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
export const getAppUsers = async (ctx:
|
|
188
|
-
const body = ctx.request.body
|
|
189
|
+
export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
|
190
|
+
const body = ctx.request.body
|
|
189
191
|
const users = await userSdk.getUsersByAppAccess(body?.appId)
|
|
190
192
|
|
|
191
193
|
ctx.body = { data: users }
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
export const search = async (ctx:
|
|
195
|
-
const body = ctx.request.body
|
|
196
|
+
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
|
197
|
+
const body = ctx.request.body
|
|
196
198
|
|
|
197
199
|
if (body.paginated === false) {
|
|
198
200
|
await getAppUsers(ctx)
|
|
@@ -238,8 +240,8 @@ export const tenantUserLookup = async (ctx: any) => {
|
|
|
238
240
|
/*
|
|
239
241
|
Encapsulate the app user onboarding flows here.
|
|
240
242
|
*/
|
|
241
|
-
export const onboardUsers = async (ctx:
|
|
242
|
-
const request = ctx.request.body
|
|
243
|
+
export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
|
244
|
+
const request = ctx.request.body
|
|
243
245
|
const isBulkCreate = "create" in request
|
|
244
246
|
|
|
245
247
|
const emailConfigured = await isEmailConfigured()
|
|
@@ -255,7 +257,7 @@ export const onboardUsers = async (ctx: any) => {
|
|
|
255
257
|
} else if (emailConfigured) {
|
|
256
258
|
onboardingResponse = await inviteMultiple(ctx)
|
|
257
259
|
} else if (!emailConfigured) {
|
|
258
|
-
const inviteRequest = ctx.request.body
|
|
260
|
+
const inviteRequest = ctx.request.body
|
|
259
261
|
|
|
260
262
|
let createdPasswords: any = {}
|
|
261
263
|
|
|
@@ -295,10 +297,10 @@ export const onboardUsers = async (ctx: any) => {
|
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
export const invite = async (ctx:
|
|
299
|
-
const request = ctx.request.body
|
|
300
|
+
export const invite = async (ctx: Ctx<InviteUserRequest>) => {
|
|
301
|
+
const request = ctx.request.body
|
|
300
302
|
|
|
301
|
-
let multiRequest = [request]
|
|
303
|
+
let multiRequest = [request]
|
|
302
304
|
const response = await userSdk.invite(multiRequest)
|
|
303
305
|
|
|
304
306
|
// explicitly throw for single user invite
|
|
@@ -318,8 +320,8 @@ export const invite = async (ctx: any) => {
|
|
|
318
320
|
}
|
|
319
321
|
}
|
|
320
322
|
|
|
321
|
-
export const inviteMultiple = async (ctx:
|
|
322
|
-
const request = ctx.request.body
|
|
323
|
+
export const inviteMultiple = async (ctx: Ctx<InviteUsersRequest>) => {
|
|
324
|
+
const request = ctx.request.body
|
|
323
325
|
ctx.body = await userSdk.invite(request)
|
|
324
326
|
}
|
|
325
327
|
|
|
@@ -424,7 +426,6 @@ export const inviteAccept = async (
|
|
|
424
426
|
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
|
|
425
427
|
// explicitly re-throw limit exceeded errors
|
|
426
428
|
ctx.throw(400, err)
|
|
427
|
-
return
|
|
428
429
|
}
|
|
429
430
|
console.warn("Error inviting user", err)
|
|
430
431
|
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { events } from "@budibase/backend-core"
|
|
2
|
+
import { generator } from "@budibase/backend-core/tests"
|
|
2
3
|
import { structures, TestConfiguration, mocks } from "../../../../tests"
|
|
4
|
+
import { UserGroup } from "@budibase/types"
|
|
5
|
+
|
|
6
|
+
mocks.licenses.useGroups()
|
|
3
7
|
|
|
4
8
|
describe("/api/global/groups", () => {
|
|
5
9
|
const config = new TestConfiguration()
|
|
@@ -13,6 +17,7 @@ describe("/api/global/groups", () => {
|
|
|
13
17
|
})
|
|
14
18
|
|
|
15
19
|
beforeEach(async () => {
|
|
20
|
+
jest.resetAllMocks()
|
|
16
21
|
mocks.licenses.useGroups()
|
|
17
22
|
})
|
|
18
23
|
|
|
@@ -24,6 +29,63 @@ describe("/api/global/groups", () => {
|
|
|
24
29
|
expect(events.group.updated).not.toBeCalled()
|
|
25
30
|
expect(events.group.permissionsEdited).not.toBeCalled()
|
|
26
31
|
})
|
|
32
|
+
|
|
33
|
+
it("should not allow undefined names", async () => {
|
|
34
|
+
const group = { ...structures.groups.UserGroup(), name: undefined } as any
|
|
35
|
+
const response = await config.api.groups.saveGroup(group, { expect: 400 })
|
|
36
|
+
expect(JSON.parse(response.text).message).toEqual(
|
|
37
|
+
'Invalid body - "name" is required'
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should not allow empty names", async () => {
|
|
42
|
+
const group = { ...structures.groups.UserGroup(), name: "" }
|
|
43
|
+
const response = await config.api.groups.saveGroup(group, { expect: 400 })
|
|
44
|
+
expect(JSON.parse(response.text).message).toEqual(
|
|
45
|
+
'Invalid body - "name" is not allowed to be empty'
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should not allow whitespace names", async () => {
|
|
50
|
+
const group = { ...structures.groups.UserGroup(), name: " " }
|
|
51
|
+
const response = await config.api.groups.saveGroup(group, { expect: 400 })
|
|
52
|
+
expect(JSON.parse(response.text).message).toEqual(
|
|
53
|
+
'Invalid body - "name" is not allowed to be empty'
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("should trim names", async () => {
|
|
58
|
+
const group = { ...structures.groups.UserGroup(), name: " group name " }
|
|
59
|
+
await config.api.groups.saveGroup(group)
|
|
60
|
+
expect(events.group.created).toBeCalledWith(
|
|
61
|
+
expect.objectContaining({ name: "group name" })
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe("name max length", () => {
|
|
66
|
+
const maxLength = 50
|
|
67
|
+
|
|
68
|
+
it(`should allow names shorter than ${maxLength} characters`, async () => {
|
|
69
|
+
const group = {
|
|
70
|
+
...structures.groups.UserGroup(),
|
|
71
|
+
name: structures.generator.word({ length: maxLength }),
|
|
72
|
+
}
|
|
73
|
+
await config.api.groups.saveGroup(group, { expect: 200 })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it(`should not allow names longer than ${maxLength} characters`, async () => {
|
|
77
|
+
const group = {
|
|
78
|
+
...structures.groups.UserGroup(),
|
|
79
|
+
name: structures.generator.word({ length: maxLength + 1 }),
|
|
80
|
+
}
|
|
81
|
+
const response = await config.api.groups.saveGroup(group, {
|
|
82
|
+
expect: 400,
|
|
83
|
+
})
|
|
84
|
+
expect(JSON.parse(response.text).message).toEqual(
|
|
85
|
+
'Invalid body - "name" length must be less than or equal to 50 characters long'
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
27
89
|
})
|
|
28
90
|
|
|
29
91
|
describe("update", () => {
|
|
@@ -55,4 +117,118 @@ describe("/api/global/groups", () => {
|
|
|
55
117
|
})
|
|
56
118
|
})
|
|
57
119
|
})
|
|
120
|
+
|
|
121
|
+
describe("find users", () => {
|
|
122
|
+
describe("without users", () => {
|
|
123
|
+
let group: UserGroup
|
|
124
|
+
beforeAll(async () => {
|
|
125
|
+
group = structures.groups.UserGroup()
|
|
126
|
+
await config.api.groups.saveGroup(group)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should return empty", async () => {
|
|
130
|
+
const result = await config.api.groups.searchUsers(group._id!)
|
|
131
|
+
expect(result.body).toEqual({
|
|
132
|
+
users: [],
|
|
133
|
+
bookmark: undefined,
|
|
134
|
+
hasNextPage: false,
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("existing users", () => {
|
|
140
|
+
let groupId: string
|
|
141
|
+
let users: { _id: string; email: string }[] = []
|
|
142
|
+
|
|
143
|
+
beforeAll(async () => {
|
|
144
|
+
groupId = (
|
|
145
|
+
await config.api.groups.saveGroup(structures.groups.UserGroup())
|
|
146
|
+
).body._id
|
|
147
|
+
|
|
148
|
+
await Promise.all(
|
|
149
|
+
Array.from({ length: 30 }).map(async (_, i) => {
|
|
150
|
+
const email = `user${i}@${generator.domain()}`
|
|
151
|
+
const user = await config.api.users.saveUser({
|
|
152
|
+
...structures.users.user(),
|
|
153
|
+
email,
|
|
154
|
+
})
|
|
155
|
+
users.push({ _id: user.body._id, email })
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
users = users.sort((a, b) => a._id.localeCompare(b._id))
|
|
159
|
+
await config.api.groups.updateGroupUsers(groupId, {
|
|
160
|
+
add: users.map(u => u._id),
|
|
161
|
+
remove: [],
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("pagination", () => {
|
|
166
|
+
it("should return first page", async () => {
|
|
167
|
+
const result = await config.api.groups.searchUsers(groupId)
|
|
168
|
+
expect(result.body).toEqual({
|
|
169
|
+
users: users.slice(0, 10),
|
|
170
|
+
bookmark: users[10]._id,
|
|
171
|
+
hasNextPage: true,
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("given a bookmark, should return skip items", async () => {
|
|
176
|
+
const result = await config.api.groups.searchUsers(groupId, {
|
|
177
|
+
bookmark: users[7]._id,
|
|
178
|
+
})
|
|
179
|
+
expect(result.body).toEqual({
|
|
180
|
+
users: users.slice(7, 17),
|
|
181
|
+
bookmark: users[17]._id,
|
|
182
|
+
hasNextPage: true,
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("bookmarking the last page, should return last page info", async () => {
|
|
187
|
+
const result = await config.api.groups.searchUsers(groupId, {
|
|
188
|
+
bookmark: users[20]._id,
|
|
189
|
+
})
|
|
190
|
+
expect(result.body).toEqual({
|
|
191
|
+
users: users.slice(20),
|
|
192
|
+
bookmark: undefined,
|
|
193
|
+
hasNextPage: false,
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe("search by email", () => {
|
|
199
|
+
it('should be able to search "starting" by email', async () => {
|
|
200
|
+
const result = await config.api.groups.searchUsers(groupId, {
|
|
201
|
+
emailSearch: `user1`,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const matchedUsers = users
|
|
205
|
+
.filter(u => u.email.startsWith("user1"))
|
|
206
|
+
.sort((a, b) => a.email.localeCompare(b.email))
|
|
207
|
+
|
|
208
|
+
expect(result.body).toEqual({
|
|
209
|
+
users: matchedUsers.slice(0, 10),
|
|
210
|
+
bookmark: matchedUsers[10].email,
|
|
211
|
+
hasNextPage: true,
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("should be able to bookmark when searching by email", async () => {
|
|
216
|
+
const matchedUsers = users
|
|
217
|
+
.filter(u => u.email.startsWith("user1"))
|
|
218
|
+
.sort((a, b) => a.email.localeCompare(b.email))
|
|
219
|
+
|
|
220
|
+
const result = await config.api.groups.searchUsers(groupId, {
|
|
221
|
+
emailSearch: `user1`,
|
|
222
|
+
bookmark: matchedUsers[4].email,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(result.body).toEqual({
|
|
226
|
+
users: matchedUsers.slice(4),
|
|
227
|
+
bookmark: undefined,
|
|
228
|
+
hasNextPage: false,
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
|
58
234
|
})
|
package/src/tests/api/groups.ts
CHANGED
|
@@ -7,13 +7,13 @@ export class GroupsAPI extends TestAPI {
|
|
|
7
7
|
super(config)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
saveGroup = (group: UserGroup) => {
|
|
10
|
+
saveGroup = (group: UserGroup, { expect } = { expect: 200 }) => {
|
|
11
11
|
return this.request
|
|
12
12
|
.post(`/api/global/groups`)
|
|
13
13
|
.send(group)
|
|
14
14
|
.set(this.config.defaultHeaders())
|
|
15
15
|
.expect("Content-Type", /json/)
|
|
16
|
-
.expect(
|
|
16
|
+
.expect(expect)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
deleteGroup = (id: string, rev: string) => {
|
|
@@ -23,4 +23,34 @@ export class GroupsAPI extends TestAPI {
|
|
|
23
23
|
.expect("Content-Type", /json/)
|
|
24
24
|
.expect(200)
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
searchUsers = (
|
|
28
|
+
id: string,
|
|
29
|
+
params?: { bookmark?: string; emailSearch?: string }
|
|
30
|
+
) => {
|
|
31
|
+
let url = `/api/global/groups/${id}/users?`
|
|
32
|
+
if (params?.bookmark) {
|
|
33
|
+
url += `bookmark=${params.bookmark}&`
|
|
34
|
+
}
|
|
35
|
+
if (params?.emailSearch) {
|
|
36
|
+
url += `emailSearch=${params.emailSearch}&`
|
|
37
|
+
}
|
|
38
|
+
return this.request
|
|
39
|
+
.get(url)
|
|
40
|
+
.set(this.config.defaultHeaders())
|
|
41
|
+
.expect("Content-Type", /json/)
|
|
42
|
+
.expect(200)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
updateGroupUsers = (
|
|
46
|
+
id: string,
|
|
47
|
+
body: { add: string[]; remove: string[] }
|
|
48
|
+
) => {
|
|
49
|
+
return this.request
|
|
50
|
+
.post(`/api/global/groups/${id}/users`)
|
|
51
|
+
.send(body)
|
|
52
|
+
.set(this.config.defaultHeaders())
|
|
53
|
+
.expect("Content-Type", /json/)
|
|
54
|
+
.expect(200)
|
|
55
|
+
}
|
|
26
56
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
import { generator } from "@budibase/backend-core/tests"
|
|
2
|
+
import { db } from "@budibase/backend-core"
|
|
3
|
+
import { UserGroupRoles } from "@budibase/types"
|
|
4
|
+
|
|
1
5
|
export const UserGroup = () => {
|
|
6
|
+
const appsCount = generator.integer({ min: 0, max: 3 })
|
|
7
|
+
const roles = Array.from({ length: appsCount }).reduce(
|
|
8
|
+
(p: UserGroupRoles, v) => {
|
|
9
|
+
return {
|
|
10
|
+
...p,
|
|
11
|
+
[db.generateAppID()]: generator.pickone(["ADMIN", "POWER", "BASIC"]),
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{}
|
|
15
|
+
)
|
|
16
|
+
|
|
2
17
|
let group = {
|
|
3
18
|
apps: [],
|
|
4
|
-
color:
|
|
5
|
-
icon:
|
|
6
|
-
name:
|
|
7
|
-
roles:
|
|
19
|
+
color: generator.color(),
|
|
20
|
+
icon: generator.word(),
|
|
21
|
+
name: generator.word(),
|
|
22
|
+
roles: roles,
|
|
8
23
|
users: [],
|
|
9
24
|
}
|
|
10
25
|
return group
|
package/src/utilities/redis.ts
CHANGED
package/tsconfig.build.json
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
"esModuleInterop": true,
|
|
9
9
|
"resolveJsonModule": true,
|
|
10
10
|
"incremental": true,
|
|
11
|
-
"types": [
|
|
11
|
+
"types": ["node", "jest"],
|
|
12
12
|
"outDir": "dist",
|
|
13
|
-
"skipLibCheck": true
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"paths": {
|
|
15
|
+
"@budibase/types": ["../types/src"],
|
|
16
|
+
"@budibase/backend-core": ["../backend-core/src"],
|
|
17
|
+
"@budibase/backend-core/*": ["../backend-core/*"],
|
|
18
|
+
"@budibase/shared-core": ["../shared-core/src"],
|
|
19
|
+
"@budibase/pro": ["../pro/packages/pro/src"]
|
|
20
|
+
}
|
|
14
21
|
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src/**/*"
|
|
17
|
-
],
|
|
22
|
+
"include": ["src/**/*"],
|
|
18
23
|
"exclude": [
|
|
19
24
|
"node_modules",
|
|
20
25
|
"dist",
|
|
@@ -22,4 +27,4 @@
|
|
|
22
27
|
"**/*.spec.js",
|
|
23
28
|
"**/*.spec.ts"
|
|
24
29
|
]
|
|
25
|
-
}
|
|
30
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -4,23 +4,12 @@
|
|
|
4
4
|
"composite": true,
|
|
5
5
|
"declaration": true,
|
|
6
6
|
"sourceMap": true,
|
|
7
|
-
"baseUrl": "."
|
|
8
|
-
"paths": {
|
|
9
|
-
"@budibase/types": ["../types/src"],
|
|
10
|
-
"@budibase/backend-core": ["../backend-core/src"],
|
|
11
|
-
"@budibase/backend-core/*": ["../backend-core/*"],
|
|
12
|
-
"@budibase/pro": ["../../../budibase-pro/packages/pro/src"]
|
|
13
|
-
}
|
|
7
|
+
"baseUrl": "."
|
|
14
8
|
},
|
|
15
9
|
"ts-node": {
|
|
16
10
|
"require": ["tsconfig-paths/register"],
|
|
17
11
|
"swc": true
|
|
18
12
|
},
|
|
19
|
-
"references": [
|
|
20
|
-
{ "path": "../types" },
|
|
21
|
-
{ "path": "../backend-core" },
|
|
22
|
-
{ "path": "../../../budibase-pro/packages/pro" }
|
|
23
|
-
],
|
|
24
13
|
"include": ["src/**/*"],
|
|
25
14
|
"exclude": ["dist"]
|
|
26
15
|
}
|