diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index fc35575ec6..9d1131ed7f 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -30,8 +30,8 @@ jobs: with: root-reserve-mb: 35000 swap-size-mb: 1024 - remove-android: 'true' - remove-dotnet: 'true' + remove-android: "true" + remove-dotnet: "true" - name: Checkout repo and submodules uses: actions/checkout@v3 if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' @@ -121,7 +121,7 @@ jobs: name: codecov-umbrella verbose: true - test-services: + test-worker: runs-on: ubuntu-latest steps: - name: Checkout repo and submodules @@ -143,12 +143,48 @@ jobs: node-version: 18.x cache: "yarn" - run: yarn --frozen-lockfile - - name: Test worker and server + - name: Test worker run: | if ${{ env.USE_NX_AFFECTED }}; then - yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }} + yarn test --scope=@budibase/worker --since=${{ env.NX_BASE_BRANCH }} else - yarn test --scope=@budibase/worker --scope=@budibase/server + yarn test --scope=@budibase/worker + fi + + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos + name: codecov-umbrella + verbose: true + + test-server: + runs-on: ubuntu-latest + steps: + - name: Checkout repo and submodules + uses: actions/checkout@v3 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' + with: + submodules: true + token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} + fetch-depth: 0 + - name: Checkout repo only + uses: actions/checkout@v3 + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' + with: + fetch-depth: 0 + + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: "yarn" + - run: yarn --frozen-lockfile + - name: Test server + run: | + if ${{ env.USE_NX_AFFECTED }}; then + yarn test --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }} + else + yarn test --scope=@budibase/server fi - uses: codecov/codecov-action@v3 @@ -259,4 +295,4 @@ jobs: process.exit(1); } else { console.log('All good, the submodule had been merged and setup correctly!') - } \ No newline at end of file + } diff --git a/.github/workflows/close-featurebranch.yml b/.github/workflows/close-featurebranch.yml new file mode 100644 index 0000000000..0ec3b43598 --- /dev/null +++ b/.github/workflows/close-featurebranch.yml @@ -0,0 +1,21 @@ +name: close-featurebranch + +on: + pull_request: + types: [closed] + branches: + - develop + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: passeidireto/trigger-external-workflow-action@main + env: + PAYLOAD_BRANCH: ${{ github.head_ref }} + PAYLOAD_PR_NUMBER: ${{ github.ref }} + with: + repository: budibase/budibase-deploys + event: featurebranch-qa-close + github_pat: ${{ secrets.GH_ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml index 9057d32c4c..f06707ab2b 100644 --- a/.github/workflows/deploy-featurebranch.yml +++ b/.github/workflows/deploy-featurebranch.yml @@ -12,7 +12,8 @@ jobs: - uses: actions/checkout@v3 - uses: passeidireto/trigger-external-workflow-action@main env: - BRANCH: ${{ github.head_ref }} + PAYLOAD_BRANCH: ${{ github.head_ref }} + PAYLOAD_PR_NUMBER: ${{ github.ref }} with: repository: budibase/budibase-deploys event: featurebranch-qa-deploy diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 8cda3a9342..f87d561db9 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -2,7 +2,7 @@ name: Close stale issues and PRs # https://github.com/actions/stale on: workflow_dispatch: schedule: - - cron: '30 1 * * *' # 1:30 every morning + - cron: '*/30 * * * *' # Every 30 mins jobs: stale: diff --git a/.prettierignore b/.prettierignore index a73fed4890..64607d74ab 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,5 @@ packages/backend-core/coverage packages/server/client packages/server/src/definitions/openapi.ts packages/builder/.routify -packages/sdk/sdk \ No newline at end of file +packages/sdk/sdk +packages/pro/coverage \ No newline at end of file diff --git a/hosting/proxy/nginx.prod.conf b/hosting/proxy/nginx.prod.conf index 9ce6b54053..365765ccbb 100644 --- a/hosting/proxy/nginx.prod.conf +++ b/hosting/proxy/nginx.prod.conf @@ -55,7 +55,7 @@ http { set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_object "object-src 'none'"; set $csp_base_uri "base-uri 'self'"; - set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; + set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_frame "frame-src 'self' https:"; set $csp_img "img-src http: https: data: blob:"; diff --git a/lerna.json b/lerna.json index b3eeaad3f5..84d7bc0ec1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.10.15", + "version": "2.10.16-alpha.19", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 6df4105e25..e5b6554fca 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@esbuild-plugins/tsconfig-paths": "^0.1.2", "@nx/js": "16.4.3", "@rollup/plugin-json": "^4.0.2", - "@typescript-eslint/parser": "5.45.0", + "@typescript-eslint/parser": "6.7.2", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", @@ -21,8 +21,8 @@ "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", "rollup-plugin-replace": "^2.2.0", - "svelte": "^3.38.2", - "typescript": "4.7.3", + "svelte": "3.49.0", + "typescript": "5.2.2", "@babel/core": "^7.22.5", "@babel/eslint-parser": "^7.22.5", "@babel/preset-env": "^7.22.5", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 739469b49a..b1dced660c 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -33,17 +33,14 @@ "bull": "4.10.1", "correlation-id": "4.0.0", "dotenv": "16.0.1", - "emitter-listener": "1.1.2", "ioredis": "5.3.2", "joi": "17.6.0", "jsonwebtoken": "9.0.0", "koa-passport": "4.1.4", "koa-pino-logger": "4.0.0", "lodash": "4.17.21", - "lodash.isarguments": "3.1.0", "node-fetch": "2.6.7", "passport-google-oauth": "2.0.0", - "passport-jwt": "4.0.0", "passport-local": "1.0.0", "passport-oauth2-refresh": "^2.1.0", "pino": "8.11.0", @@ -59,14 +56,13 @@ "uuid": "8.3.2" }, "devDependencies": { - "@jest/test-sequencer": "29.6.2", "@shopify/jest-koa-mocks": "5.1.1", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", + "@types/cookies": "0.7.8", "@types/jest": "29.5.3", - "@types/koa": "2.13.4", "@types/lodash": "4.14.180", "@types/node": "18.17.0", "@types/node-fetch": "2.6.4", @@ -80,14 +76,10 @@ "jest": "29.6.2", "jest-environment-node": "29.6.2", "jest-serial-runner": "1.2.1", - "koa": "2.13.4", - "nodemon": "2.0.16", "pino-pretty": "10.0.0", "pouchdb-adapter-memory": "7.2.2", "timekeeper": "2.2.0", - "ts-node": "10.8.1", - "tsconfig-paths": "4.0.0", - "typescript": "4.7.3" + "typescript": "5.2.2" }, "nx": { "targets": { diff --git a/packages/backend-core/src/cache/tests/user.spec.ts b/packages/backend-core/src/cache/tests/user.spec.ts new file mode 100644 index 0000000000..80e5bc3063 --- /dev/null +++ b/packages/backend-core/src/cache/tests/user.spec.ts @@ -0,0 +1,145 @@ +import { User } from "@budibase/types" +import { generator, structures } from "../../../tests" +import { DBTestConfiguration } from "../../../tests/extra" +import { getUsers } from "../user" +import { getGlobalDB } from "../../context" +import _ from "lodash" + +import * as redis from "../../redis/init" +import { UserDB } from "../../users" + +const config = new DBTestConfiguration() + +describe("user cache", () => { + describe("getUsers", () => { + const users: User[] = [] + beforeAll(async () => { + const userCount = 10 + const userIds = generator.arrayOf(() => generator.guid(), { + min: userCount, + max: userCount, + }) + + await config.doInTenant(async () => { + const db = getGlobalDB() + for (const userId of userIds) { + const user = structures.users.user({ _id: userId }) + await db.put(user) + users.push(user) + } + }) + }) + + beforeEach(async () => { + jest.clearAllMocks() + + const redisClient = await redis.getUserClient() + await redisClient.clear() + }) + + it("when no user is in cache, all of them are retrieved from db", async () => { + const usersToRequest = _.sampleSize(users, 5) + + const userIdsToRequest = usersToRequest.map(x => x._id!) + + jest.spyOn(UserDB, "bulkGet") + + const results = await config.doInTenant(() => getUsers(userIdsToRequest)) + + expect(results.users).toHaveLength(5) + expect(results).toEqual({ + users: usersToRequest.map(u => ({ + ...u, + budibaseAccess: true, + _rev: expect.any(String), + })), + }) + + expect(UserDB.bulkGet).toBeCalledTimes(1) + expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest) + }) + + it("on a second all, all of them are retrieved from cache", async () => { + const usersToRequest = _.sampleSize(users, 5) + + const userIdsToRequest = usersToRequest.map(x => x._id!) + + jest.spyOn(UserDB, "bulkGet") + + await config.doInTenant(() => getUsers(userIdsToRequest)) + const resultsFromCache = await config.doInTenant(() => + getUsers(userIdsToRequest) + ) + + expect(resultsFromCache.users).toHaveLength(5) + expect(resultsFromCache).toEqual({ + users: expect.arrayContaining( + usersToRequest.map(u => ({ + ...u, + budibaseAccess: true, + _rev: expect.any(String), + })) + ), + }) + + expect(UserDB.bulkGet).toBeCalledTimes(1) + }) + + it("when some users are cached, only the missing ones are retrieved from db", async () => { + const usersToRequest = _.sampleSize(users, 5) + + const userIdsToRequest = usersToRequest.map(x => x._id!) + + jest.spyOn(UserDB, "bulkGet") + + await config.doInTenant(() => + getUsers([userIdsToRequest[0], userIdsToRequest[3]]) + ) + ;(UserDB.bulkGet as jest.Mock).mockClear() + + const results = await config.doInTenant(() => getUsers(userIdsToRequest)) + + expect(results.users).toHaveLength(5) + expect(results).toEqual({ + users: expect.arrayContaining( + usersToRequest.map(u => ({ + ...u, + budibaseAccess: true, + _rev: expect.any(String), + })) + ), + }) + + expect(UserDB.bulkGet).toBeCalledTimes(1) + expect(UserDB.bulkGet).toBeCalledWith([ + userIdsToRequest[1], + userIdsToRequest[2], + userIdsToRequest[4], + ]) + }) + + it("requesting existing and unexisting ids will return found ones", async () => { + const usersToRequest = _.sampleSize(users, 3) + const missingIds = [generator.guid(), generator.guid()] + + const userIdsToRequest = _.shuffle([ + ...missingIds, + ...usersToRequest.map(x => x._id!), + ]) + + const results = await config.doInTenant(() => getUsers(userIdsToRequest)) + + expect(results.users).toHaveLength(3) + expect(results).toEqual({ + users: expect.arrayContaining( + usersToRequest.map(u => ({ + ...u, + budibaseAccess: true, + _rev: expect.any(String), + })) + ), + notFoundIds: expect.arrayContaining(missingIds), + }) + }) + }) +}) diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index e2af78adfd..481d3691e4 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -6,6 +6,7 @@ import env from "../environment" import * as accounts from "../accounts" import { UserDB } from "../users" import { sdk } from "@budibase/shared-core" +import { User } from "@budibase/types" const EXPIRY_SECONDS = 3600 @@ -27,6 +28,35 @@ async function populateFromDB(userId: string, tenantId: string) { return user } +async function populateUsersFromDB( + userIds: string[] +): Promise<{ users: User[]; notFoundIds?: string[] }> { + const getUsersResponse = await UserDB.bulkGet(userIds) + + // Handle missed user ids + const notFoundIds = userIds.filter((uid, i) => !getUsersResponse[i]) + + const users = getUsersResponse.filter(x => x) + + await Promise.all( + users.map(async (user: any) => { + user.budibaseAccess = true + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const account = await accounts.getAccount(user.email) + if (account) { + user.account = account + user.accountPortalAccess = true + } + } + }) + ) + + if (notFoundIds.length) { + return { users, notFoundIds } + } + return { users } +} + /** * Get the requested user by id. * Use redis cache to first read the user. @@ -77,6 +107,36 @@ export async function getUser( return user } +/** + * Get the requested users by id. + * Use redis cache to first read the users. + * If not present fallback to loading the users directly and re-caching. + * @param {*} userIds the ids of the user to get + * @param {*} tenantId the tenant of the users to get + * @returns + */ +export async function getUsers( + userIds: string[] +): Promise<{ users: User[]; notFoundIds?: string[] }> { + const client = await redis.getUserClient() + // try cache + let usersFromCache = await client.bulkGet(userIds) + const missingUsersFromCache = userIds.filter(uid => !usersFromCache[uid]) + const users = Object.values(usersFromCache) + let notFoundIds + + if (missingUsersFromCache.length) { + const usersFromDb = await populateUsersFromDB(missingUsersFromCache) + + notFoundIds = usersFromDb.notFoundIds + for (const userToCache of usersFromDb.users) { + await client.store(userToCache._id!, userToCache, EXPIRY_SECONDS) + } + users.push(...usersFromDb.users) + } + return { users, notFoundIds: notFoundIds } +} + export async function invalidateUser(userId: string) { const client = await redis.getUserClient() await client.delete(userId) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts index 484a118cbd..c3ddf220e6 100644 --- a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -102,6 +102,7 @@ describe("sso", () => { // modified external id to match user format ssoUser._id = "us_" + details.userId + delete ssoUser.userId // new sso user won't have a password delete ssoUser.password diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 5056a5d549..e7755f275d 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -242,7 +242,7 @@ class RedisWrapper { } } - async bulkGet(keys: string[]) { + async bulkGet(keys: string[]) { const db = this._db if (keys.length === 0) { return {} @@ -250,7 +250,7 @@ class RedisWrapper { const prefixedKeys = keys.map(key => addDbPrefix(db, key)) let response = await this.getClient().mget(prefixedKeys) if (Array.isArray(response)) { - let final: any = {} + let final: Record = {} let count = 0 for (let result of response) { if (result) { diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index 13083534b1..539bbaef27 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -1,8 +1,9 @@ -import { PermissionType, PermissionLevel } from "@budibase/types" -export { PermissionType, PermissionLevel } from "@budibase/types" +import { PermissionLevel, PermissionType } from "@budibase/types" import flatten from "lodash/flatten" import cloneDeep from "lodash/fp/cloneDeep" +export { PermissionType, PermissionLevel } from "@budibase/types" + export type RoleHierarchy = { permissionId: string }[] @@ -78,6 +79,7 @@ export const BUILTIN_PERMISSIONS = { permissions: [ new Permission(PermissionType.QUERY, PermissionLevel.READ), new Permission(PermissionType.TABLE, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, WRITE: { @@ -88,6 +90,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, POWER: { @@ -99,6 +102,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, ADMIN: { @@ -111,6 +115,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, } diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index e87df2e9c9..24279e6b5c 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -215,21 +215,23 @@ async function getAllUserRoles(userRoleId?: string): Promise { return roles } +export async function getUserRoleIdHierarchy( + userRoleId?: string +): Promise { + const roles = await getUserRoleHierarchy(userRoleId) + return roles.map(role => role._id!) +} + /** * Returns an ordered array of the user's inherited role IDs, this can be used * to determine if a user can access something that requires a specific role. * @param {string} userRoleId The user's role ID, this can be found in their access token. - * @param {object} opts Various options, such as whether to only retrieve the IDs (default true). - * @returns {Promise} returns an ordered array of the roles, with the first being their + * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ -export async function getUserRoleHierarchy( - userRoleId?: string, - opts = { idOnly: true } -) { +export async function getUserRoleHierarchy(userRoleId?: string) { // special case, if they don't have a role then they are a public user - const roles = await getAllUserRoles(userRoleId) - return opts.idOnly ? roles.map(role => role._id) : roles + return getAllUserRoles(userRoleId) } // this function checks that the provided permissions are in an array format @@ -249,6 +251,11 @@ export function checkForRoleResourceArray( return rolePerms } +export async function getAllRoleIds(appId?: string) { + const roles = await getAllRoles(appId) + return roles.map(role => role._id) +} + /** * Given an app ID this will retrieve all of the roles that are currently within that app. * @return {Promise} An array of the role objects that were found. @@ -332,9 +339,7 @@ export class AccessController { } let roleIds = userRoleId ? this.userHierarchies[userRoleId] : null if (!roleIds && userRoleId) { - roleIds = (await getUserRoleHierarchy(userRoleId, { - idOnly: true, - })) as string[] + roleIds = await getUserRoleIdHierarchy(userRoleId) this.userHierarchies[userRoleId] = roleIds } diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 82da95983a..ac43fa1fdb 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -10,7 +10,7 @@ import { Event, TenantResolutionStrategy, } from "@budibase/types" -import { SetOption } from "cookies" +import type { SetOption } from "cookies" const jwt = require("jsonwebtoken") const APP_PREFIX = DocumentType.APP + SEPARATOR diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index 309f0fd159..758fd6bf9a 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -86,8 +86,8 @@ export const useAuditLogs = () => { return useFeature(Feature.AUDIT_LOGS) } -export const usePublicApiUserRoles = () => { - return useFeature(Feature.USER_ROLE_PUBLIC_API) +export const useExpandedPublicApi = () => { + return useFeature(Feature.EXPANDED_PUBLIC_API) } export const useScimIntegration = () => { diff --git a/packages/backend-core/tests/core/utilities/structures/shared.ts b/packages/backend-core/tests/core/utilities/structures/shared.ts deleted file mode 100644 index de0e19486c..0000000000 --- a/packages/backend-core/tests/core/utilities/structures/shared.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from "@budibase/types" -import { generator } from "./generator" -import { uuid } from "./common" - -export const newEmail = () => { - return `${uuid()}@test.com` -} - -export const user = (userProps?: any): User => { - return { - email: newEmail(), - password: "test", - roles: { app_test: "admin" }, - firstName: generator.first(), - lastName: generator.last(), - pictureUrl: "http://test.com", - ...userProps, - } -} diff --git a/packages/backend-core/tests/core/utilities/structures/sso.ts b/packages/backend-core/tests/core/utilities/structures/sso.ts index 4d13635f09..2e3af712a9 100644 --- a/packages/backend-core/tests/core/utilities/structures/sso.ts +++ b/packages/backend-core/tests/core/utilities/structures/sso.ts @@ -13,8 +13,7 @@ import { } from "@budibase/types" import { generator } from "./generator" import { email, uuid } from "./common" -import * as shared from "./shared" -import { user } from "./shared" +import * as users from "./users" import sample from "lodash/sample" export function OAuth(): OAuth2 { @@ -26,7 +25,7 @@ export function OAuth(): OAuth2 { export function authDetails(userDoc?: User): SSOAuthDetails { if (!userDoc) { - userDoc = user() + userDoc = users.user() } const userId = userDoc._id || uuid() @@ -52,7 +51,7 @@ export function providerType(): SSOProviderType { export function ssoProfile(user?: User): SSOProfile { if (!user) { - user = shared.user() + user = users.user() } return { id: user._id!, diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 0a4f2e8b54..66d23696e0 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -4,11 +4,32 @@ import { BuilderUser, SSOAuthDetails, SSOUser, + User, } from "@budibase/types" -import { user } from "./shared" import { authDetails } from "./sso" +import { uuid } from "./common" +import { generator } from "./generator" +import { tenant } from "." -export { user, newEmail } from "./shared" +export const newEmail = () => { + return `${uuid()}@test.com` +} + +export const user = (userProps?: Partial>): User => { + const userId = userProps?._id + return { + _id: userId, + userId, + email: newEmail(), + password: "test", + roles: { app_test: "admin" }, + firstName: generator.first(), + lastName: generator.last(), + pictureUrl: "http://test.com", + tenantId: tenant.id(), + ...userProps, + } +} export const adminUser = (userProps?: any): AdminUser => { return { @@ -31,7 +52,7 @@ export const adminOnlyUser = (userProps?: any): AdminOnlyUser => { } } -export const builderUser = (userProps?: any): BuilderUser => { +export const builderUser = (userProps?: Partial): BuilderUser => { return { ...user(userProps), builder: { diff --git a/packages/backend-core/tests/extra/DBTestConfiguration.ts b/packages/backend-core/tests/extra/DBTestConfiguration.ts index a2550a6e24..99a5bcba46 100644 --- a/packages/backend-core/tests/extra/DBTestConfiguration.ts +++ b/packages/backend-core/tests/extra/DBTestConfiguration.ts @@ -18,7 +18,7 @@ class DBTestConfiguration { // TENANCY - doInTenant(task: any) { + doInTenant(task: () => Promise) { return context.doInTenant(this.tenantId, () => { return task() }) diff --git a/packages/backend-core/tests/index.ts b/packages/backend-core/tests/index.ts index 50fc1dc431..cdbacc12d8 100644 --- a/packages/backend-core/tests/index.ts +++ b/packages/backend-core/tests/index.ts @@ -1 +1,2 @@ export * from "./core/utilities" +export * from "./extra" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 0b87960ab5..4791776c57 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -20,14 +20,12 @@ "@rollup/plugin-commonjs": "^16.0.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^11.2.1", - "cross-env": "^7.0.2", - "nollup": "^0.14.1", "postcss": "^8.2.9", "rollup": "^2.45.2", "rollup-plugin-postcss": "^4.0.0", "rollup-plugin-svelte": "^7.1.0", "rollup-plugin-terser": "^7.0.2", - "svelte": "^3.38.2" + "svelte": "3.49.0" }, "keywords": [ "svelte" @@ -82,7 +80,7 @@ "@spectrum-css/typography": "3.0.1", "@spectrum-css/underlay": "2.0.9", "@spectrum-css/vars": "3.0.1", - "dayjs": "^1.10.4", + "dayjs": "^1.10.8", "easymde": "^2.16.1", "svelte-flatpickr": "3.2.3", "svelte-portal": "^1.0.0", diff --git a/packages/bbui/src/Banner/Banner.svelte b/packages/bbui/src/Banner/Banner.svelte index 3810021a61..a04d469cc7 100644 --- a/packages/bbui/src/Banner/Banner.svelte +++ b/packages/bbui/src/Banner/Banner.svelte @@ -66,6 +66,10 @@ pointer-events: all; width: 100%; } + + .spectrum-Toast--neutral { + background-color: var(--grey-2); + } .spectrum-Button { border: 1px solid rgba(255, 255, 255, 0.2); } diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 8816da33c4..8d72dd0652 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -14,12 +14,12 @@ export let autocomplete = false export let sort = false export let autoWidth = false - export let fetchTerm = null - export let useFetch = false + export let searchTerm = null export let customPopoverHeight export let customPopoverOffsetBelow export let customPopoverMaxHeight export let open = false + export let loading const dispatch = createEventDispatcher() @@ -82,6 +82,7 @@ diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 9b90c1a865..aa06d5f748 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -2,7 +2,7 @@ import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css" - import { createEventDispatcher } from "svelte" + import { createEventDispatcher, onDestroy } from "svelte" import clickOutside from "../../Actions/click_outside" import Search from "./Search.svelte" import Icon from "../../Icon/Icon.svelte" @@ -10,6 +10,7 @@ import Popover from "../../Popover/Popover.svelte" import Tags from "../../Tags/Tags.svelte" import Tag from "../../Tags/Tag.svelte" + import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte" export let id = null export let disabled = false @@ -35,19 +36,20 @@ export let autoWidth = false export let autocomplete = false export let sort = false - export let fetchTerm = null - export let useFetch = false + export let searchTerm = null export let customPopoverHeight export let customPopoverOffsetBelow export let customPopoverMaxHeight export let align = "left" export let footer = null export let customAnchor = null + export let loading + const dispatch = createEventDispatcher() - let searchTerm = null let button let popover + let component $: sortedOptions = getSortedOptions(options, getOptionLabel, sort) $: filteredOptions = getFilteredOptions( @@ -82,7 +84,7 @@ } const getFilteredOptions = (options, term, getLabel) => { - if (autocomplete && term && !fetchTerm) { + if (autocomplete && term) { const lowerCaseTerm = term.toLowerCase() return options.filter(option => { return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) @@ -90,6 +92,20 @@ } return options } + + const onScroll = e => { + const scrollPxThreshold = 100 + const scrollPositionFromBottom = + e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop + if (scrollPositionFromBottom < scrollPxThreshold) { + dispatch("loadMore") + } + } + + $: component?.addEventListener("scroll", onScroll) + onDestroy(() => { + component?.removeEventListener("scroll", null) + })