diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml
index c8bc31f41d..840d580892 100644
--- a/.github/workflows/budibase_ci.yml
+++ b/.github/workflows/budibase_ci.yml
@@ -10,7 +10,6 @@ on:
push:
branches:
- master
- - develop
pull_request:
workflow_dispatch:
@@ -262,11 +261,7 @@ jobs:
branch="${{ github.base_ref || github.ref_name }}"
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
- if [[ $branch == "master" ]]; then
- base_commit=$(git rev-parse origin/master)
- elif [[ $branch == "develop" ]]; then
- base_commit=$(git rev-parse origin/develop)
- fi
+ base_commit=$(git rev-parse origin/master)
if [[ ! -z $base_commit ]]; then
echo "target_branch=$branch"
diff --git a/.github/workflows/close-featurebranch.yml b/.github/workflows/close-featurebranch.yml
index 5f232b2f26..46cb781730 100644
--- a/.github/workflows/close-featurebranch.yml
+++ b/.github/workflows/close-featurebranch.yml
@@ -4,7 +4,13 @@ on:
pull_request:
types: [closed]
branches:
- - develop
+ - master
+ workflow_dispatch:
+ inputs:
+ BRANCH:
+ type: string
+ description: Which featurebranch branch to destroy?
+ required: true
jobs:
release:
@@ -13,7 +19,7 @@ jobs:
- uses: actions/checkout@v3
- uses: passeidireto/trigger-external-workflow-action@main
env:
- PAYLOAD_BRANCH: ${{ github.head_ref }}
+ PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with:
repository: budibase/budibase-deploys
diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml
index 2c6302b56a..f1fb12c087 100644
--- a/.github/workflows/deploy-featurebranch.yml
+++ b/.github/workflows/deploy-featurebranch.yml
@@ -3,7 +3,6 @@ name: deploy-featurebranch
on:
pull_request:
branches:
- - develop
- master
jobs:
diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml
deleted file mode 100644
index 9b7bca4770..0000000000
--- a/.github/workflows/deploy-preprod.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: "deploy-preprod"
-on:
- workflow_dispatch:
- workflow_call:
-
-jobs:
- deploy-to-legacy-preprod-env:
- runs-on: ubuntu-latest
- steps:
- - name: Fail if not a tag
- run: |
- if [[ $GITHUB_REF != refs/tags/* ]]; then
- echo "Workflow Dispatch can only be run on tags"
- exit 1
- fi
-
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0
-
- - name: Fail if tag is not in master
- run: |
- if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
- echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
- exit 1
- fi
-
- - name: Get the latest budibase release version
- id: version
- run: |
- release_version=$(cat lerna.json | jq -r '.version')
- echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
-
- - uses: passeidireto/trigger-external-workflow-action@main
- env:
- PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
- with:
- repository: budibase/budibase-deploys
- event: budicloud-preprod-deploy
- github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
-
diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml
deleted file mode 100644
index bd727b7865..0000000000
--- a/.github/workflows/release-develop.yml
+++ /dev/null
@@ -1,124 +0,0 @@
-name: Budibase Prerelease
-concurrency:
- group: release-prerelease
- cancel-in-progress: false
-
-on:
- push:
- tags:
- - "*-alpha.*"
- workflow_dispatch:
-
-env:
- # Posthog token used by ui at build time
- # disable unless needed for testing
- # POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
- INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
- PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- FEATURE_PREVIEW_URL: https://budirelease.live
-
-jobs:
- release-images:
- runs-on: ubuntu-latest
-
- steps:
- - name: Fail if not a tag
- run: |
- if [[ $GITHUB_REF != refs/tags/* ]]; then
- echo "Workflow Dispatch can only be run on tags"
- exit 1
- fi
-
- - uses: actions/checkout@v2
- with:
- submodules: true
- token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- fetch-depth: 0
-
- - name: Fail if tag is not develop
- run: |
- if ! git merge-base --is-ancestor ${{ github.sha }} origin/develop; then
- echo "Tag is not in develop"
- exit 1
- fi
-
- - uses: actions/setup-node@v1
- with:
- node-version: 18.x
-
- - run: yarn install --frozen-lockfile
- - name: Update versions
- run: ./scripts/updateVersions.sh
- - run: yarn build
- - run: yarn build:sdk
-
- - name: Publish budibase packages to NPM
- env:
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: |
- # setup the username and email.
- git config --global user.name "Budibase Staging Release Bot"
- git config --global user.email "<>"
- git submodule foreach git commit -a -m 'Release process'
- git commit -a -m 'Release process'
- echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
- yarn release:develop
-
- - name: Build/release Docker images
- run: |
- docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
- yarn build:docker:develop
- env:
- DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
- DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
-
- release-helm-chart:
- needs: [release-images]
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Setup Helm
- uses: azure/setup-helm@v1
- id: helm-install
-
- # due to helm repo index issue: https://github.com/helm/helm/issues/7363
- # we need to create new package in a different dir, merge the index and move the package back
- - name: Build and release helm chart
- run: |
- git config user.name "Budibase Helm Bot"
- git config user.email "<>"
- git reset --hard
- git fetch
- mkdir sync
- echo "Packaging chart to sync dir"
- helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
- echo "Packaging successful"
- git checkout gh-pages
- echo "Indexing helm repo"
- helm repo index --merge docs/index.yaml sync
- mv -f sync/* docs
- rm -rf sync
- echo "Pushing new helm release"
- git add -A
- git commit -m "Helm Release: develop"
- git push
-
- trigger-deploy-to-qa-env:
- needs: [release-helm-chart]
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Get the current budibase release version
- id: version
- run: |
- release_version=$(cat lerna.json | jq -r '.version')
- echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
-
- - uses: passeidireto/trigger-external-workflow-action@main
- env:
- PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
- with:
- repository: budibase/budibase-deploys
- event: budicloud-qa-deploy
- github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml
index b4991cbfbe..3af3a751ad 100644
--- a/.github/workflows/release-master.yml
+++ b/.github/workflows/release-master.yml
@@ -110,19 +110,13 @@ jobs:
git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}"
git push
- deploy-to-legacy-preprod-env:
- needs: [release-images]
- uses: ./.github/workflows/deploy-preprod.yml
- secrets: inherit
- # Trigger deploy to new EKS preprod environment
- trigger-deploy-to-preprod-env:
+ trigger-deploy-to-qa-env:
needs: [release-helm-chart]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
-
- - name: Get the latest budibase release version
+ - name: Get the current budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
@@ -133,5 +127,5 @@ jobs:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
repository: budibase/budibase-deploys
- event: budicloud-preprod-deploy
+ event: budicloud-qa-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
diff --git a/.github/workflows/release-singleimage.yml b/.github/workflows/release-singleimage.yml
index bd01ed786a..61ab9a4eb2 100644
--- a/.github/workflows/release-singleimage.yml
+++ b/.github/workflows/release-singleimage.yml
@@ -18,7 +18,7 @@ jobs:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
- root-reserve-mb: 35000
+ root-reserve-mb: 30000
swap-size-mb: 1024
remove-android: 'true'
remove-dotnet: 'true'
@@ -33,14 +33,6 @@ jobs:
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- fetch-depth: 0
-
- - name: Fail if tag is not in master
- run: |
- if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
- echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
- exit 1
- fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
@@ -55,10 +47,6 @@ jobs:
run: yarn
- name: Update versions
run: ./scripts/updateVersions.sh
- - name: Runt Yarn Lint
- run: yarn lint
- - name: Update versions
- run: ./scripts/updateVersions.sh
- name: Run Yarn Build
run: yarn build:docker:pre
- name: Login to Docker Hub
diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml
index f87d561db9..49e3473e63 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 * * * *' # Every 30 mins
+ - cron: "*/30 * * * *" # Every 30 mins
jobs:
stale:
@@ -10,20 +10,37 @@ jobs:
steps:
- uses: actions/stale@v8
with:
- # stale rules
- days-before-stale: 60
+ operations-per-run: 1
+ # stale rules for PRs
days-before-pr-stale: 7
stale-issue-label: stale
- stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days."
-
- # close rules
- # days after being marked as stale to close
- days-before-close: 30
- close-issue-label: closed-stale
- close-issue-message: This issue has been automatically closed it has not had any activity in 90 days."
- days-before-pr-close: 7
-
- # exemptions
exempt-pr-labels: pinned,security,roadmap
+ days-before-pr-close: 7
+ - uses: actions/stale@v8
+ with:
+ operations-per-run: 3
+ # stale rules for high priority bugs
+ days-before-stale: 30
+ only-issue-labels: bug,High priority
+ stale-issue-label: warn
+
+ - uses: actions/stale@v8
+ with:
+ operations-per-run: 3
+ # stale rules for medium priority bugs
+ days-before-stale: 90
+ only-issue-labels: bug,Medium priority
+ stale-issue-label: warn
+
+ - uses: actions/stale@v8
+ with:
+ operations-per-run: 3
+ # stale rules for all bugs
+ days-before-stale: 180
+ stale-issue-label: stale
+ only-issue-labels: bug
+ stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months."
+
+ days-before-close: 30
diff --git a/.github/workflows/tag-prerelease.yml b/.github/workflows/tag-prerelease.yml
deleted file mode 100644
index f6446c55f5..0000000000
--- a/.github/workflows/tag-prerelease.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Tag prerelease
-concurrency:
- group: tag-prerelease
- cancel-in-progress: false
-
-on:
- push:
- branches:
- - develop
- paths:
- - ".aws/**"
- - ".github/**"
- - "charts/**"
- - "packages/**"
- - "scripts/**"
- - "package.json"
- - "yarn.lock"
- workflow_dispatch:
-
-jobs:
- tag-prerelease:
- runs-on: ubuntu-latest
-
- steps:
- - name: Fail if branch is not develop
- if: github.ref != 'refs/heads/develop'
- run: |
- echo "Ref is not develop, you must run this job from develop."
- exit 1
- - uses: actions/checkout@v2
- with:
- submodules: true
- token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
-
- - run: cd scripts && yarn
- - name: Tag prerelease
- run: |
- cd scripts
- # setup the username and email.
- git config --global user.name "Budibase Staging Release Bot"
- git config --global user.email "<>"
- ./versionCommit.sh prerelease
diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml
index 191c3ad9ef..eaf71ae61a 100644
--- a/.github/workflows/tag-release.yml
+++ b/.github/workflows/tag-release.yml
@@ -4,17 +4,6 @@ concurrency:
cancel-in-progress: false
on:
- push:
- branches:
- - master
- paths:
- - ".aws/**"
- - ".github/**"
- - "charts/**"
- - "packages/**"
- - "scripts/**"
- - "package.json"
- - "yarn.lock"
workflow_dispatch:
inputs:
versioning:
diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile
index 9fdf2449d1..95e383edb0 100644
--- a/hosting/single/Dockerfile
+++ b/hosting/single/Dockerfile
@@ -12,14 +12,14 @@ RUN chmod +x /cleanup.sh
WORKDIR /app
ADD packages/server .
COPY yarn.lock .
-RUN yarn install --production=true
+RUN yarn install --production=true --network-timeout 100000
RUN /cleanup.sh
# build worker
WORKDIR /worker
ADD packages/worker .
COPY yarn.lock .
-RUN yarn install --production=true
+RUN yarn install --production=true --network-timeout 100000
RUN /cleanup.sh
FROM budibase/couchdb
diff --git a/lerna.json b/lerna.json
index 12cadce9f3..0dd6abdc46 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.11.27-alpha.1",
+ "version": "2.11.34",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts
index a7e1389920..b087a6b538 100644
--- a/packages/backend-core/src/users/users.ts
+++ b/packages/backend-core/src/users/users.ts
@@ -14,13 +14,14 @@ import {
} from "../db"
import {
BulkDocsResponse,
+ ContextUser,
+ SearchQuery,
+ SearchQueryOperators,
SearchUsersRequest,
User,
- ContextUser,
} from "@budibase/types"
-import { getGlobalDB } from "../context"
import * as context from "../context"
-import { user as userCache } from "../cache"
+import { getGlobalDB } from "../context"
type GetOpts = { cleanup?: boolean }
@@ -39,6 +40,31 @@ function removeUserPassword(users: User | User[]) {
return users
}
+export const isSupportedUserSearch = (query: SearchQuery) => {
+ const allowed = [
+ { op: SearchQueryOperators.STRING, key: "email" },
+ { op: SearchQueryOperators.EQUAL, key: "_id" },
+ ]
+ for (let [key, operation] of Object.entries(query)) {
+ if (typeof operation !== "object") {
+ return false
+ }
+ const fields = Object.keys(operation || {})
+ // this filter doesn't contain options - ignore
+ if (fields.length === 0) {
+ continue
+ }
+ const allowedOperation = allowed.find(
+ allow =>
+ allow.op === key && fields.length === 1 && fields[0] === allow.key
+ )
+ if (!allowedOperation) {
+ return false
+ }
+ }
+ return true
+}
+
export const bulkGetGlobalUsersById = async (
userIds: string[],
opts?: GetOpts
@@ -211,8 +237,8 @@ export const searchGlobalUsersByEmail = async (
const PAGE_LIMIT = 8
export const paginatedUsers = async ({
- page,
- email,
+ bookmark,
+ query,
appId,
}: SearchUsersRequest = {}) => {
const db = getGlobalDB()
@@ -222,18 +248,20 @@ export const paginatedUsers = async ({
limit: PAGE_LIMIT + 1,
}
// add a startkey if the page was specified (anchor)
- if (page) {
- opts.startkey = page
+ if (bookmark) {
+ opts.startkey = bookmark
}
// property specifies what to use for the page/anchor
let userList: User[],
property = "_id",
getKey
- if (appId) {
+ if (query?.equal?._id) {
+ userList = [await getById(query.equal._id)]
+ } else if (appId) {
userList = await searchGlobalUsersByApp(appId, opts)
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
- } else if (email) {
- userList = await searchGlobalUsersByEmail(email, opts)
+ } else if (query?.string?.email) {
+ userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
property = "email"
} else {
// no search, query allDocs
diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte
index a65da55c8f..2c8a862535 100644
--- a/packages/builder/src/components/common/NavItem.svelte
+++ b/packages/builder/src/components/common/NavItem.svelte
@@ -102,7 +102,7 @@
{/if}
- {text}
+
{text}
{#if selectedBy}
{/if}
@@ -227,9 +227,6 @@
.text {
font-weight: 600;
font-size: 12px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
flex: 1 1 auto;
color: var(--spectrum-global-color-gray-900);
order: 2;
@@ -238,6 +235,11 @@
align-items: center;
gap: 8px;
}
+ .text span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
.scrollable .text {
flex: 0 0 auto;
max-width: 160px;
diff --git a/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte b/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte
index a6dba59196..a5b0b19c9b 100644
--- a/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte
+++ b/packages/builder/src/pages/builder/portal/account/auditLogs/index.svelte
@@ -123,7 +123,10 @@
prevUserSearch = search
try {
userPageInfo.loading()
- await users.search({ userPage, email: search })
+ await users.search({
+ bookmark: userPage,
+ query: { string: { email: search } },
+ })
userPageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
index da4b12f7f9..cc524f1acf 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
@@ -31,7 +31,10 @@
prevSearch = search
try {
pageInfo.loading()
- await users.search({ page, email: search })
+ await users.search({
+ bookmark: page,
+ query: { string: { email: search } },
+ })
pageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 0dc82cadd2..8d0a4e456f 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -3419,6 +3419,17 @@
"value": "custom"
}
},
+ {
+ "type": "event",
+ "label": "On change",
+ "key": "onChange",
+ "context": [
+ {
+ "label": "Field Value",
+ "key": "value"
+ }
+ ]
+ },
{
"type": "validation/string",
"label": "Validation",
diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte
index 9895413446..04d6919157 100644
--- a/packages/client/src/components/app/forms/CodeScanner.svelte
+++ b/packages/client/src/components/app/forms/CodeScanner.svelte
@@ -128,6 +128,7 @@
{
dispatch("change", value)
}}
diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index 52faf46615..544a1a8434 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -105,19 +105,25 @@
}
}
- $: fetchRows(searchTerm, primaryDisplay)
+ $: fetchRows(searchTerm, primaryDisplay, defaultValue)
- const fetchRows = (searchTerm, primaryDisplay) => {
+ const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
const allRowsFetched =
$fetch.loaded &&
!Object.keys($fetch.query?.string || {}).length &&
!$fetch.hasNextPage
- // Don't request until we have the primary display
- if (!allRowsFetched && primaryDisplay) {
- fetch.update({
- query: { string: { [primaryDisplay]: searchTerm } },
+ // Don't request until we have the primary display or default value has been fetched
+ if (allRowsFetched || !primaryDisplay) {
+ return
+ }
+ if (defaultVal && !optionsObj[defaultVal]) {
+ await fetch.update({
+ query: { equal: { _id: defaultVal } },
})
}
+ await fetch.update({
+ query: { string: { [primaryDisplay]: searchTerm } },
+ })
}
const flatten = values => {
diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js
index 6c616d7baf..95c2167721 100644
--- a/packages/frontend-core/src/api/user.js
+++ b/packages/frontend-core/src/api/user.js
@@ -10,24 +10,28 @@ export const buildUserEndpoints = API => ({
/**
* Gets a list of users in the current tenant.
- * @param {string} page The page to retrieve
- * @param {string} search The starts with string to search username/email by.
+ * @param {string} bookmark The page to retrieve
+ * @param {object} query search filters for lookup by user (all operators not supported).
* @param {string} appId Facilitate app/role based user searching
- * @param {boolean} paginated Allow the disabling of pagination
+ * @param {boolean} paginate Allow the disabling of pagination
+ * @param {number} limit How many users to retrieve in a single search
*/
- searchUsers: async ({ paginated, page, email, appId } = {}) => {
+ searchUsers: async ({ paginate, bookmark, query, appId, limit } = {}) => {
const opts = {}
- if (page) {
- opts.page = page
+ if (bookmark) {
+ opts.bookmark = bookmark
}
- if (email) {
- opts.email = email
+ if (query) {
+ opts.query = query
}
if (appId) {
opts.appId = appId
}
- if (typeof paginated === "boolean") {
- opts.paginated = paginated
+ if (typeof paginate === "boolean") {
+ opts.paginate = paginate
+ }
+ if (limit) {
+ opts.limit = limit
}
return await API.post({
url: `/api/global/users/search`,
diff --git a/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte b/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte
index 4e76c264a1..48b1279346 100644
--- a/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/BBReferenceCell.svelte
@@ -27,7 +27,7 @@
const email = Object.values(searchParams.query.string)[0]
const results = await API.searchUsers({
- email,
+ query: { string: { email } },
})
// Mapping to the expected data within RelationshipCell
diff --git a/packages/frontend-core/src/fetch/UserFetch.js b/packages/frontend-core/src/fetch/UserFetch.js
index 5372d0ec33..b1478c3a6d 100644
--- a/packages/frontend-core/src/fetch/UserFetch.js
+++ b/packages/frontend-core/src/fetch/UserFetch.js
@@ -1,6 +1,7 @@
import { get } from "svelte/store"
import DataFetch from "./DataFetch.js"
import { TableNames } from "../constants"
+import { LuceneUtils } from "../utils"
export default class UserFetch extends DataFetch {
constructor(opts) {
@@ -27,16 +28,25 @@ export default class UserFetch extends DataFetch {
}
async getData() {
+ const { limit, paginate } = this.options
const { cursor, query } = get(this.store)
+ let finalQuery
+ // convert old format to new one - we now allow use of the lucene format
+ const { appId, paginated, ...rest } = query
+ if (!LuceneUtils.hasFilters(query) && rest.email) {
+ finalQuery = { string: { email: rest.email } }
+ } else {
+ finalQuery = rest
+ }
try {
- // "query" normally contains a lucene query, but users uses a non-standard
- // search endpoint so we use query uniquely here
- const res = await this.API.searchUsers({
- page: cursor,
- email: query.email,
- appId: query.appId,
- paginated: query.paginated,
- })
+ const opts = {
+ bookmark: cursor,
+ query: finalQuery,
+ appId: appId,
+ paginate: paginated || paginate,
+ limit,
+ }
+ const res = await this.API.searchUsers(opts)
return {
rows: res?.data || [],
hasNextPage: res?.hasNextPage || false,
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 0a80253210..4c2e7a7494 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -16,6 +16,7 @@ import {
RelationshipType,
Row,
SaveTableRequest,
+ SearchQueryOperators,
SortOrder,
SortType,
StaticQuotaName,
@@ -1141,7 +1142,9 @@ describe.each([
)
const createViewResponse = await config.createView({
- query: [{ operator: "equal", field: "age", value: 40 }],
+ query: [
+ { operator: SearchQueryOperators.EQUAL, field: "age", value: 40 },
+ ],
schema: viewSchema,
})
diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts
index 6d893c1c7f..40060aef48 100644
--- a/packages/server/src/api/routes/tests/viewV2.spec.ts
+++ b/packages/server/src/api/routes/tests/viewV2.spec.ts
@@ -3,6 +3,7 @@ import {
CreateViewRequest,
FieldSchema,
FieldType,
+ SearchQueryOperators,
SortOrder,
SortType,
Table,
@@ -89,7 +90,13 @@ describe.each([
name: generator.name(),
tableId: table._id!,
primaryDisplay: generator.word(),
- query: [{ operator: "equal", field: "field", value: "value" }],
+ query: [
+ {
+ operator: SearchQueryOperators.EQUAL,
+ field: "field",
+ value: "value",
+ },
+ ],
sort: {
field: "fieldToSort",
order: SortOrder.DESCENDING,
@@ -184,7 +191,13 @@ describe.each([
const tableId = table._id!
await config.api.viewV2.update({
...view,
- query: [{ operator: "equal", field: "newField", value: "thatValue" }],
+ query: [
+ {
+ operator: SearchQueryOperators.EQUAL,
+ field: "newField",
+ value: "thatValue",
+ },
+ ],
})
expect((await config.api.table.get(tableId)).views).toEqual({
@@ -207,7 +220,7 @@ describe.each([
primaryDisplay: generator.word(),
query: [
{
- operator: "equal",
+ operator: SearchQueryOperators.EQUAL,
field: generator.word(),
value: generator.word(),
},
@@ -279,7 +292,13 @@ describe.each([
{
...view,
tableId: generator.guid(),
- query: [{ operator: "equal", field: "newField", value: "thatValue" }],
+ query: [
+ {
+ operator: SearchQueryOperators.EQUAL,
+ field: "newField",
+ value: "thatValue",
+ },
+ ],
},
{ expectStatus: 404 }
)
diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts
index bcd1c14389..84c19f8bbc 100644
--- a/packages/server/src/integration-test/postgres.spec.ts
+++ b/packages/server/src/integration-test/postgres.spec.ts
@@ -43,6 +43,10 @@ describe("postgres integrations", () => {
)
})
+ afterAll(async () => {
+ await databaseTestProviders.postgres.stopContainer()
+ })
+
beforeEach(async () => {
async function createAuxTable(prefix: string) {
return await config.createTable({
diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts
index 036e81bbd8..b749551721 100644
--- a/packages/server/src/integrations/tests/utils/postgres.ts
+++ b/packages/server/src/integrations/tests/utils/postgres.ts
@@ -36,3 +36,10 @@ export async function getDsConfig(): Promise {
},
}
}
+
+export async function stopContainer() {
+ if (container) {
+ await container.stop()
+ container = undefined
+ }
+}
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index ab43e86279..1839a53525 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -1,12 +1,13 @@
import {
Datasource,
+ FieldSubtype,
FieldType,
- SortDirection,
- SortType,
SearchFilter,
SearchQuery,
SearchQueryFields,
- FieldSubtype,
+ SearchQueryOperators,
+ SortDirection,
+ SortType,
} from "@budibase/types"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
import { deepGet } from "./helpers"
@@ -273,22 +274,30 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
}
// Process a string match (fails if the value does not start with the string)
- const stringMatch = match("string", (docValue: string, testValue: string) => {
- return (
- !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
- )
- })
+ const stringMatch = match(
+ SearchQueryOperators.STRING,
+ (docValue: string, testValue: string) => {
+ return (
+ !docValue ||
+ !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
+ )
+ }
+ )
// Process a fuzzy match (treat the same as starts with when running locally)
- const fuzzyMatch = match("fuzzy", (docValue: string, testValue: string) => {
- return (
- !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
- )
- })
+ const fuzzyMatch = match(
+ SearchQueryOperators.FUZZY,
+ (docValue: string, testValue: string) => {
+ return (
+ !docValue ||
+ !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
+ )
+ }
+ )
// Process a range match
const rangeMatch = match(
- "range",
+ SearchQueryOperators.RANGE,
(
docValue: string | number | null,
testValue: { low: number; high: number }
@@ -304,7 +313,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process an equal match (fails if the value is different)
const equalMatch = match(
- "equal",
+ SearchQueryOperators.EQUAL,
(docValue: any, testValue: string | null) => {
return testValue != null && testValue !== "" && docValue !== testValue
}
@@ -312,46 +321,58 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a not-equal match (fails if the value is the same)
const notEqualMatch = match(
- "notEqual",
+ SearchQueryOperators.NOT_EQUAL,
(docValue: any, testValue: string | null) => {
return testValue != null && testValue !== "" && docValue === testValue
}
)
// Process an empty match (fails if the value is not empty)
- const emptyMatch = match("empty", (docValue: string | null) => {
- return docValue != null && docValue !== ""
- })
+ const emptyMatch = match(
+ SearchQueryOperators.EMPTY,
+ (docValue: string | null) => {
+ return docValue != null && docValue !== ""
+ }
+ )
// Process a not-empty match (fails is the value is empty)
- const notEmptyMatch = match("notEmpty", (docValue: string | null) => {
- return docValue == null || docValue === ""
- })
+ const notEmptyMatch = match(
+ SearchQueryOperators.NOT_EMPTY,
+ (docValue: string | null) => {
+ return docValue == null || docValue === ""
+ }
+ )
// Process an includes match (fails if the value is not included)
- const oneOf = match("oneOf", (docValue: any, testValue: any) => {
- if (typeof testValue === "string") {
- testValue = testValue.split(",")
- if (typeof docValue === "number") {
- testValue = testValue.map((item: string) => parseFloat(item))
+ const oneOf = match(
+ SearchQueryOperators.ONE_OF,
+ (docValue: any, testValue: any) => {
+ if (typeof testValue === "string") {
+ testValue = testValue.split(",")
+ if (typeof docValue === "number") {
+ testValue = testValue.map((item: string) => parseFloat(item))
+ }
}
+ return !testValue?.includes(docValue)
}
- return !testValue?.includes(docValue)
- })
+ )
- const containsAny = match("containsAny", (docValue: any, testValue: any) => {
- return !docValue?.includes(...testValue)
- })
+ const containsAny = match(
+ SearchQueryOperators.CONTAINS_ANY,
+ (docValue: any, testValue: any) => {
+ return !docValue?.includes(...testValue)
+ }
+ )
const contains = match(
- "contains",
+ SearchQueryOperators.CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return !testValue?.every((item: any) => docValue?.includes(item))
}
)
const notContains = match(
- "notContains",
+ SearchQueryOperators.NOT_CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return testValue?.every((item: any) => docValue?.includes(item))
}
@@ -433,7 +454,7 @@ export const hasFilters = (query?: SearchQuery) => {
if (skipped.includes(key) || typeof value !== "object") {
continue
}
- if (Object.keys(value).length !== 0) {
+ if (Object.keys(value || {}).length !== 0) {
return true
}
}
diff --git a/packages/types/src/api/web/searchFilter.ts b/packages/types/src/api/web/searchFilter.ts
index 6980bc117b..ac3c446e36 100644
--- a/packages/types/src/api/web/searchFilter.ts
+++ b/packages/types/src/api/web/searchFilter.ts
@@ -10,43 +10,57 @@ export type SearchFilter = {
externalType?: string
}
+export enum SearchQueryOperators {
+ STRING = "string",
+ FUZZY = "fuzzy",
+ RANGE = "range",
+ EQUAL = "equal",
+ NOT_EQUAL = "notEqual",
+ EMPTY = "empty",
+ NOT_EMPTY = "notEmpty",
+ ONE_OF = "oneOf",
+ CONTAINS = "contains",
+ NOT_CONTAINS = "notContains",
+ CONTAINS_ANY = "containsAny",
+}
+
export type SearchQuery = {
allOr?: boolean
onEmptyFilter?: EmptyFilterOption
- string?: {
+ [SearchQueryOperators.STRING]?: {
[key: string]: string
}
- fuzzy?: {
+ [SearchQueryOperators.FUZZY]?: {
[key: string]: string
}
- range?: {
+ [SearchQueryOperators.RANGE]?: {
[key: string]: {
high: number | string
low: number | string
}
}
- equal?: {
+ [SearchQueryOperators.EQUAL]?: {
[key: string]: any
}
- notEqual?: {
+ [SearchQueryOperators.NOT_EQUAL]?: {
[key: string]: any
}
- empty?: {
+ [SearchQueryOperators.EMPTY]?: {
[key: string]: any
}
- notEmpty?: {
+ [SearchQueryOperators.NOT_EMPTY]?: {
[key: string]: any
}
- oneOf?: {
+ [SearchQueryOperators.ONE_OF]?: {
[key: string]: any[]
}
- contains?: {
+ [SearchQueryOperators.CONTAINS]?: {
[key: string]: any[]
}
- notContains?: {
+ [SearchQueryOperators.NOT_CONTAINS]?: {
[key: string]: any[]
}
- containsAny?: {
+ [SearchQueryOperators.CONTAINS_ANY]?: {
[key: string]: any[]
}
}
diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts
index 85e2d89ad1..a1e039cfd7 100644
--- a/packages/types/src/api/web/user.ts
+++ b/packages/types/src/api/web/user.ts
@@ -1,4 +1,5 @@
import { User } from "../../documents"
+import { SearchQuery } from "./searchFilter"
export interface SaveUserResponse {
_id: string
@@ -51,10 +52,10 @@ export interface InviteUsersResponse {
}
export interface SearchUsersRequest {
- page?: string
- email?: string
+ bookmark?: string
+ query?: SearchQuery
appId?: string
- paginated?: boolean
+ paginate?: boolean
}
export interface CreateAdminUserRequest {
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index 822a16d33e..8de3a1444e 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -197,7 +197,12 @@ export const getAppUsers = async (ctx: Ctx) => {
export const search = async (ctx: Ctx) => {
const body = ctx.request.body
- if (body.paginated === false) {
+ // TODO: for now only one supported search key, string.email
+ if (body?.query && !userSdk.core.isSupportedUserSearch(body.query)) {
+ ctx.throw(501, "Can only search by string.email or equal._id")
+ }
+
+ if (body.paginate === false) {
await getAppUsers(ctx)
} else {
const paginated = await userSdk.core.paginatedUsers(body)
diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts
index e4504eccfe..a446d10ed0 100644
--- a/packages/worker/src/api/routes/global/tests/users.spec.ts
+++ b/packages/worker/src/api/routes/global/tests/users.spec.ts
@@ -544,6 +544,36 @@ describe("/api/global/users", () => {
})
})
+ describe("POST /api/global/users/search", () => {
+ it("should be able to search by email", async () => {
+ const user = await config.createUser()
+ const response = await config.api.users.searchUsers({
+ query: { string: { email: user.email } },
+ })
+ expect(response.body.data.length).toBe(1)
+ expect(response.body.data[0].email).toBe(user.email)
+ })
+
+ it("should be able to search by _id", async () => {
+ const user = await config.createUser()
+ const response = await config.api.users.searchUsers({
+ query: { equal: { _id: user._id } },
+ })
+ expect(response.body.data.length).toBe(1)
+ expect(response.body.data[0]._id).toBe(user._id)
+ })
+
+ it("should throw an error when unimplemented options used", async () => {
+ const user = await config.createUser()
+ await config.api.users.searchUsers(
+ {
+ query: { equal: { firstName: user.firstName } },
+ },
+ 501
+ )
+ })
+ })
+
describe("DELETE /api/global/users/:userId", () => {
it("should be able to destroy a basic user", async () => {
const user = await config.createUser()
diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts
index e96209eca6..b2a19bcb28 100644
--- a/packages/worker/src/tests/api/users.ts
+++ b/packages/worker/src/tests/api/users.ts
@@ -4,6 +4,7 @@ import {
InviteUsersRequest,
User,
CreateAdminUserRequest,
+ SearchQuery,
} from "@budibase/types"
import structures from "../structures"
import { generator } from "@budibase/backend-core/tests"
@@ -133,6 +134,15 @@ export class UserAPI extends TestAPI {
.expect(status ? status : 200)
}
+ searchUsers = ({ query }: { query?: SearchQuery }, status = 200) => {
+ return this.request
+ .post("/api/global/users/search")
+ .set(this.config.defaultHeaders())
+ .send({ query })
+ .expect("Content-Type", /json/)
+ .expect(status ? status : 200)
+ }
+
getUser = (userId: string, opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/users/${userId}`)
diff --git a/yarn.lock b/yarn.lock
index 81c2815663..11d296b296 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -21750,7 +21750,7 @@ vlq@^0.2.2:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
-vm2@^3.9.19:
+vm2@^3.9.19, vm2@^3.9.8:
version "3.9.19"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a"
integrity sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==