Merge branch 'develop' into global-bindings

This commit is contained in:
Andrew Kingston 2023-09-20 11:54:00 +01:00 committed by GitHub
commit 2ef9912f3f
25 changed files with 449 additions and 79 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.10.9-alpha.3", "version": "2.10.12-alpha.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -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),
})
})
})
})

View File

@ -6,6 +6,7 @@ import env from "../environment"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import { UserDB } from "../users" import { UserDB } from "../users"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { User } from "@budibase/types"
const EXPIRY_SECONDS = 3600 const EXPIRY_SECONDS = 3600
@ -27,6 +28,35 @@ async function populateFromDB(userId: string, tenantId: string) {
return user 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. * Get the requested user by id.
* Use redis cache to first read the user. * Use redis cache to first read the user.
@ -77,6 +107,36 @@ export async function getUser(
return user 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) { export async function invalidateUser(userId: string) {
const client = await redis.getUserClient() const client = await redis.getUserClient()
await client.delete(userId) await client.delete(userId)

View File

@ -102,6 +102,7 @@ describe("sso", () => {
// modified external id to match user format // modified external id to match user format
ssoUser._id = "us_" + details.userId ssoUser._id = "us_" + details.userId
delete ssoUser.userId
// new sso user won't have a password // new sso user won't have a password
delete ssoUser.password delete ssoUser.password

View File

@ -250,7 +250,7 @@ class RedisWrapper {
const prefixedKeys = keys.map(key => addDbPrefix(db, key)) const prefixedKeys = keys.map(key => addDbPrefix(db, key))
let response = await this.getClient().mget(prefixedKeys) let response = await this.getClient().mget(prefixedKeys)
if (Array.isArray(response)) { if (Array.isArray(response)) {
let final: any = {} let final: Record<string, any> = {}
let count = 0 let count = 0
for (let result of response) { for (let result of response) {
if (result) { if (result) {

View File

@ -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,
}
}

View File

@ -13,8 +13,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { generator } from "./generator" import { generator } from "./generator"
import { email, uuid } from "./common" import { email, uuid } from "./common"
import * as shared from "./shared" import * as users from "./users"
import { user } from "./shared"
import sample from "lodash/sample" import sample from "lodash/sample"
export function OAuth(): OAuth2 { export function OAuth(): OAuth2 {
@ -26,7 +25,7 @@ export function OAuth(): OAuth2 {
export function authDetails(userDoc?: User): SSOAuthDetails { export function authDetails(userDoc?: User): SSOAuthDetails {
if (!userDoc) { if (!userDoc) {
userDoc = user() userDoc = users.user()
} }
const userId = userDoc._id || uuid() const userId = userDoc._id || uuid()
@ -52,7 +51,7 @@ export function providerType(): SSOProviderType {
export function ssoProfile(user?: User): SSOProfile { export function ssoProfile(user?: User): SSOProfile {
if (!user) { if (!user) {
user = shared.user() user = users.user()
} }
return { return {
id: user._id!, id: user._id!,

View File

@ -4,11 +4,33 @@ import {
BuilderUser, BuilderUser,
SSOAuthDetails, SSOAuthDetails,
SSOUser, SSOUser,
User,
} from "@budibase/types" } from "@budibase/types"
import { user } from "./shared"
import { authDetails } from "./sso" import { authDetails } from "./sso"
import { uuid } from "./common"
import { generator } from "./generator"
import { tenant } from "."
import { generateGlobalUserID } from "../../../../src/docIds"
export { user, newEmail } from "./shared" export const newEmail = () => {
return `${uuid()}@test.com`
}
export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
const userId = userProps?._id || generateGlobalUserID()
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 => { export const adminUser = (userProps?: any): AdminUser => {
return { return {

View File

@ -126,8 +126,9 @@
transition: top 130ms ease-out, left 130ms ease-out; transition: top 130ms ease-out, left 130ms ease-out;
} }
.spectrum-Tooltip-label { .spectrum-Tooltip-label {
text-overflow: ellipsis; display: -webkit-box;
white-space: nowrap; -webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;

View File

@ -502,7 +502,7 @@
</div> </div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div> <div>
<div> <div class="row">
<Label>Time zones</Label> <Label>Time zones</Label>
<AbsTooltip <AbsTooltip
position="top" position="top"
@ -690,18 +690,19 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.tooltip-alignment { .tooltip-alignment {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} }
.label-length { .label-length {
flex-basis: 40%; flex-basis: 40%;
} }
.input-length { .input-length {
flex-grow: 1; flex-grow: 1;
} }
.row {
gap: 8px;
display: flex;
}
</style> </style>

View File

@ -235,7 +235,7 @@
const baseExtensions = buildBaseExtensions() const baseExtensions = buildBaseExtensions()
editor = new EditorView({ editor = new EditorView({
doc: value, doc: value?.toString(),
extensions: buildExtensions(baseExtensions), extensions: buildExtensions(baseExtensions),
parent: textarea, parent: textarea,
}) })

View File

@ -0,0 +1,102 @@
<script>
import { notifications, Button, Icon, Body } from "@budibase/bbui"
import { admin, auth } from "stores/portal"
$: user = $auth.user
let loading = false
let complete = false
const resetPassword = async () => {
if (loading || complete) return
loading = true
try {
await fetch(`${$admin.accountPortalUrl}/api/auth/reset`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: user.email }),
})
complete = true
} catch (e) {
notifications.error("There was an issue sending your validation email.")
} finally {
loading = false
}
}
</script>
{#if user?.account?.verified === false}
<section class="banner">
<div class="icon">
<Icon name="Info" />
</div>
<div class="copy">
<Body size="S">
Please verify your account. We've sent the verification link to <span
class="email">{user.email}</span
></Body
>
</div>
<div class="button" class:disabled={loading || complete}>
<Button on:click={resetPassword}
>{complete ? "Email sent" : "Resend email"}</Button
>
</div>
</section>
{/if}
<style>
.banner {
flex-shrink: 0;
display: flex;
background-color: var(--grey-2);
height: 48px;
align-items: center;
padding: 0 15px;
}
.icon {
margin-right: 15px;
color: var(--ink);
display: flex;
}
.copy {
flex-grow: 1;
overflow: hidden;
}
.copy :global(p) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.email {
font-weight: 600;
}
.button {
margin-left: 15px;
}
.button :global(button) {
color: var(--ink);
background-color: var(--grey-3);
border: none;
}
.button :global(button):hover {
background-color: var(--grey-4);
}
.disabled :global(button) {
pointer-events: none;
color: var(--ink);
background-color: var(--grey-4);
border: none;
}
</style>

View File

@ -3,7 +3,6 @@
import { admin, auth, licensing } from "stores/portal" import { admin, auth, licensing } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { banner, BANNER_TYPES } from "@budibase/bbui"
import { API } from "api" import { API } from "api"
import Branding from "./Branding.svelte" import Branding from "./Branding.svelte"
@ -17,32 +16,6 @@
$: user = $auth.user $: user = $auth.user
$: useAccountPortal = cloud && !$admin.disableAccountPortal $: useAccountPortal = cloud && !$admin.disableAccountPortal
let showVerificationPrompt = false
const checkVerification = user => {
if (!showVerificationPrompt && user?.account?.verified === false) {
showVerificationPrompt = true
banner.queue([
{
message: `Please verify your account. We've sent the verification link to ${user.email}`,
type: BANNER_TYPES.NEUTRAL,
showCloseButton: false,
extraButtonAction: () => {
fetch(`${$admin.accountPortalUrl}/api/auth/reset`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: user.email }),
})
},
extraButtonText: "Resend email",
},
])
}
}
$: checkVerification(user)
const validateTenantId = async () => { const validateTenantId = async () => {
const host = window.location.host const host = window.location.host

View File

@ -22,6 +22,7 @@
import { isActive, goto, layout, redirect } from "@roxi/routify" import { isActive, goto, layout, redirect } from "@roxi/routify"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
import CommandPalette from "components/commandPalette/CommandPalette.svelte" import CommandPalette from "components/commandPalette/CommandPalette.svelte"
import TourWrap from "components/portal/onboarding/TourWrap.svelte" import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import TourPopover from "components/portal/onboarding/TourPopover.svelte" import TourPopover from "components/portal/onboarding/TourPopover.svelte"
@ -136,6 +137,7 @@
{/if} {/if}
<div class="root" class:blur={$store.showPreview}> <div class="root" class:blur={$store.showPreview}>
<VerificationPromptBanner />
<div class="top-nav"> <div class="top-nav">
{#if $store.initialised} {#if $store.initialised}
<div class="topleftnav"> <div class="topleftnav">

View File

@ -8,6 +8,7 @@
import Logo from "./_components/Logo.svelte" import Logo from "./_components/Logo.svelte"
import UserDropdown from "./_components/UserDropdown.svelte" import UserDropdown from "./_components/UserDropdown.svelte"
import HelpMenu from "components/common/HelpMenu.svelte" import HelpMenu from "components/common/HelpMenu.svelte"
import VerificationPromptBanner from "components/common/VerificationPromptBanner.svelte"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
let loaded = false let loaded = false
@ -55,6 +56,7 @@
{:else} {:else}
<HelpMenu /> <HelpMenu />
<div class="container"> <div class="container">
<VerificationPromptBanner />
<div class="nav"> <div class="nav">
<div class="branding"> <div class="branding">
<Logo /> <Logo />

View File

@ -17,6 +17,13 @@
let fieldApi let fieldApi
let localFiles = [] let localFiles = []
$: {
// If the field state is reset, clear the local files
if (!fieldState?.value?.length) {
localFiles = []
}
}
const { API, notificationStore, uploadStore } = getContext("sdk") const { API, notificationStore, uploadStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")

View File

@ -28,7 +28,7 @@ export async function handleRequest(
) { ) {
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string // make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
if (opts && opts.filters) { if (opts && opts.filters) {
opts.filters = utils.removeEmptyFilters(opts.filters) opts.filters = sdk.rows.removeEmptyFilters(opts.filters)
} }
if ( if (
!dataFilters.hasFilters(opts?.filters) && !dataFilters.hasFilters(opts?.filters) &&

View File

@ -72,6 +72,11 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
// user metadata doesn't exist yet - don't allow creation
if (utils.isUserMetadataTable(tableId) && !body._rev) {
ctx.throw(400, "Cannot create new user entry.")
}
// if it has an ID already then its a patch // if it has an ID already then its a patch
if (body && body._id) { if (body && body._id) {
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>) return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)

View File

@ -175,3 +175,7 @@ export function removeEmptyFilters(filters: SearchFilters) {
} }
return filters return filters
} }
export function isUserMetadataTable(tableId: string) {
return tableId === InternalTables.USER_METADATA
}

View File

@ -3,7 +3,7 @@ import { databaseTestProviders } from "../../../integrations/tests/utils"
import tk from "timekeeper" import tk from "timekeeper"
import { outputProcessing } from "../../../utilities/rowProcessor" import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities" import * as setup from "./utilities"
import { context, roles, tenancy } from "@budibase/backend-core" import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
FieldType, FieldType,
@ -1415,6 +1415,23 @@ describe.each([
}) })
}) })
isInternal &&
it("doesn't allow creating in user table", async () => {
const userTableId = InternalTable.USER_METADATA
const response = await config.api.row.save(
userTableId,
{
tableId: userTableId,
firstName: "Joe",
lastName: "Joe",
email: "joe@joe.com",
roles: {},
},
{ expectStatus: 400 }
)
expect(response.message).toBe("Cannot create new user entry.")
})
describe("permissions", () => { describe("permissions", () => {
let viewId: string let viewId: string
let tableId: string let tableId: string

View File

@ -1,4 +1,8 @@
import { Knex, knex } from "knex" import { Knex, knex } from "knex"
import { db as dbCore } from "@budibase/backend-core"
import { QueryOptions } from "../../definitions/datasource"
import { isIsoDateString, SqlClient } from "../utils"
import SqlTableQueryBuilder from "./sqlTable"
import { import {
Operation, Operation,
QueryJson, QueryJson,
@ -6,11 +10,8 @@ import {
SearchFilters, SearchFilters,
SortDirection, SortDirection,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core"
import { QueryOptions } from "../../definitions/datasource"
import { isIsoDateString, SqlClient } from "../utils"
import SqlTableQueryBuilder from "./sqlTable"
import environment from "../../environment" import environment from "../../environment"
import { isValidFilter } from "../utils"
const envLimit = environment.SQL_MAX_ROWS const envLimit = environment.SQL_MAX_ROWS
? parseInt(environment.SQL_MAX_ROWS) ? parseInt(environment.SQL_MAX_ROWS)
@ -261,15 +262,17 @@ class InternalBuilder {
if (isEmptyObject(value.high)) { if (isEmptyObject(value.high)) {
value.high = "" value.high = ""
} }
if (value.low && value.high) { const lowValid = isValidFilter(value.low),
highValid = isValidFilter(value.high)
if (lowValid && highValid) {
// Use a between operator if we have 2 valid range values // Use a between operator if we have 2 valid range values
const fnc = allOr ? "orWhereBetween" : "whereBetween" const fnc = allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [value.low, value.high]) query = query[fnc](key, [value.low, value.high])
} else if (value.low) { } else if (lowValid) {
// Use just a single greater than operator if we only have a low // Use just a single greater than operator if we only have a low
const fnc = allOr ? "orWhere" : "where" const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, ">", value.low) query = query[fnc](key, ">", value.low)
} else if (value.high) { } else if (highValid) {
// Use just a single less than operator if we only have a high // Use just a single less than operator if we only have a high
const fnc = allOr ? "orWhere" : "where" const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, "<", value.high) query = query[fnc](key, "<", value.high)

View File

@ -1,6 +1,11 @@
import { SourceName, SqlQuery, Datasource, Table } from "@budibase/types" import { SqlQuery, Table, SearchFilters } from "@budibase/types"
import { DocumentType, SEPARATOR } from "../db/utils" import { DocumentType, SEPARATOR } from "../db/utils"
import { FieldTypes, BuildSchemaErrors, InvalidColumns } from "../constants" import {
FieldTypes,
BuildSchemaErrors,
InvalidColumns,
NoEmptyFilterStrings,
} from "../constants"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
@ -343,3 +348,36 @@ export function getPrimaryDisplay(testValue: unknown): string | undefined {
} }
return testValue as string return testValue as string
} }
export function isValidFilter(value: any) {
return value != null && value !== ""
}
// don't do a pure falsy check, as 0 is included
// https://github.com/Budibase/budibase/issues/10118
export function removeEmptyFilters(filters: SearchFilters) {
for (let filterField of NoEmptyFilterStrings) {
if (!filters[filterField]) {
continue
}
for (let filterType of Object.keys(filters)) {
if (filterType !== filterField) {
continue
}
// don't know which one we're checking, type could be anything
const value = filters[filterType] as unknown
if (typeof value === "object") {
for (let [key, value] of Object.entries(
filters[filterType] as object
)) {
if (value == null || value === "") {
// @ts-ignore
delete filters[filterField][key]
}
}
}
}
}
return filters
}

View File

@ -1,13 +1,14 @@
import { AppStatus } from "../../../db/utils" import { AppStatus } from "../../../db/utils"
import { App, ContextUser } from "@budibase/types" import { App, ContextUser, User } from "@budibase/types"
import { getLocksById } from "../../../utilities/redis" import { getLocksById } from "../../../utilities/redis"
import { enrichApps } from "../../users/sessions" import { enrichApps } from "../../users/sessions"
import { checkAppMetadata } from "../../../automations/logging" import { checkAppMetadata } from "../../../automations/logging"
import { db as dbCore, users } from "@budibase/backend-core" import { db as dbCore, users } from "@budibase/backend-core"
import { groups } from "@budibase/pro"
export function filterAppList(user: ContextUser, apps: App[]) { export function filterAppList(user: User, apps: App[]) {
let appList: string[] = [] let appList: string[] = []
const roleApps = Object.keys(user.roles || {}) const roleApps = Object.keys(user.roles)
if (users.hasAppBuilderPermissions(user)) { if (users.hasAppBuilderPermissions(user)) {
appList = user.builder?.apps || [] appList = user.builder?.apps || []
appList = appList.concat(roleApps) appList = appList.concat(roleApps)
@ -23,7 +24,12 @@ export async function fetch(status: AppStatus, user: ContextUser) {
const dev = status === AppStatus.DEV const dev = status === AppStatus.DEV
const all = status === AppStatus.ALL const all = status === AppStatus.ALL
let apps = (await dbCore.getAllApps({ dev, all })) as App[] let apps = (await dbCore.getAllApps({ dev, all })) as App[]
apps = filterAppList(user, apps)
const enrichedUser = await groups.enrichUserRolesFromGroups({
...user,
roles: user.roles || {},
})
apps = filterAppList(enrichedUser, apps)
const appIds = apps const appIds = apps
.filter((app: any) => app.status === "development") .filter((app: any) => app.status === "development")

View File

@ -3,6 +3,7 @@ import { isExternalTable } from "../../../integrations/utils"
import * as internal from "./search/internal" import * as internal from "./search/internal"
import * as external from "./search/external" import * as external from "./search/external"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
export { isValidFilter, removeEmptyFilters } from "../../../integrations/utils"
export interface ViewParams { export interface ViewParams {
calculation: string calculation: string

View File

@ -1,8 +1,8 @@
import * as utils from "../utils" import * as search from "../../app/rows/search"
describe("removeEmptyFilters", () => { describe("removeEmptyFilters", () => {
it("0 should not be removed", () => { it("0 should not be removed", () => {
const filters = utils.removeEmptyFilters({ const filters = search.removeEmptyFilters({
equal: { equal: {
column: 0, column: 0,
}, },
@ -11,7 +11,7 @@ describe("removeEmptyFilters", () => {
}) })
it("empty string should be removed", () => { it("empty string should be removed", () => {
const filters = utils.removeEmptyFilters({ const filters = search.removeEmptyFilters({
equal: { equal: {
column: "", column: "",
}, },