Merge branch 'master' into BUDI-7573/use_existing_image_cache
This commit is contained in:
commit
dc4fc27fc8
|
@ -2,7 +2,7 @@ name: Close stale issues and PRs # https://github.com/actions/stale
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '*/30 * * * *' # Every 30 mins
|
- cron: "*/30 * * * *" # Every 30 mins
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
@ -10,20 +10,37 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
# stale rules
|
operations-per-run: 1
|
||||||
days-before-stale: 60
|
# stale rules for PRs
|
||||||
days-before-pr-stale: 7
|
days-before-pr-stale: 7
|
||||||
stale-issue-label: stale
|
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
|
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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.11.31",
|
"version": "2.11.34",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -14,13 +14,14 @@ import {
|
||||||
} from "../db"
|
} from "../db"
|
||||||
import {
|
import {
|
||||||
BulkDocsResponse,
|
BulkDocsResponse,
|
||||||
|
ContextUser,
|
||||||
|
SearchQuery,
|
||||||
|
SearchQueryOperators,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
ContextUser,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getGlobalDB } from "../context"
|
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { user as userCache } from "../cache"
|
import { getGlobalDB } from "../context"
|
||||||
|
|
||||||
type GetOpts = { cleanup?: boolean }
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
@ -39,6 +40,31 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
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 (
|
export const bulkGetGlobalUsersById = async (
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
opts?: GetOpts
|
opts?: GetOpts
|
||||||
|
@ -211,8 +237,8 @@ export const searchGlobalUsersByEmail = async (
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
export const paginatedUsers = async ({
|
export const paginatedUsers = async ({
|
||||||
page,
|
bookmark,
|
||||||
email,
|
query,
|
||||||
appId,
|
appId,
|
||||||
}: SearchUsersRequest = {}) => {
|
}: SearchUsersRequest = {}) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
@ -222,18 +248,20 @@ export const paginatedUsers = async ({
|
||||||
limit: PAGE_LIMIT + 1,
|
limit: PAGE_LIMIT + 1,
|
||||||
}
|
}
|
||||||
// add a startkey if the page was specified (anchor)
|
// add a startkey if the page was specified (anchor)
|
||||||
if (page) {
|
if (bookmark) {
|
||||||
opts.startkey = page
|
opts.startkey = bookmark
|
||||||
}
|
}
|
||||||
// property specifies what to use for the page/anchor
|
// property specifies what to use for the page/anchor
|
||||||
let userList: User[],
|
let userList: User[],
|
||||||
property = "_id",
|
property = "_id",
|
||||||
getKey
|
getKey
|
||||||
if (appId) {
|
if (query?.equal?._id) {
|
||||||
|
userList = [await getById(query.equal._id)]
|
||||||
|
} else if (appId) {
|
||||||
userList = await searchGlobalUsersByApp(appId, opts)
|
userList = await searchGlobalUsersByApp(appId, opts)
|
||||||
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
|
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
|
||||||
} else if (email) {
|
} else if (query?.string?.email) {
|
||||||
userList = await searchGlobalUsersByEmail(email, opts)
|
userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
|
||||||
property = "email"
|
property = "email"
|
||||||
} else {
|
} else {
|
||||||
// no search, query allDocs
|
// no search, query allDocs
|
||||||
|
|
|
@ -123,7 +123,10 @@
|
||||||
prevUserSearch = search
|
prevUserSearch = search
|
||||||
try {
|
try {
|
||||||
userPageInfo.loading()
|
userPageInfo.loading()
|
||||||
await users.search({ userPage, email: search })
|
await users.search({
|
||||||
|
bookmark: userPage,
|
||||||
|
query: { string: { email: search } },
|
||||||
|
})
|
||||||
userPageInfo.fetched($users.hasNextPage, $users.nextPage)
|
userPageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error getting user list")
|
||||||
|
|
|
@ -31,7 +31,10 @@
|
||||||
prevSearch = search
|
prevSearch = search
|
||||||
try {
|
try {
|
||||||
pageInfo.loading()
|
pageInfo.loading()
|
||||||
await users.search({ page, email: search })
|
await users.search({
|
||||||
|
bookmark: page,
|
||||||
|
query: { string: { email: search } },
|
||||||
|
})
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting user list")
|
notifications.error("Error getting user list")
|
||||||
|
|
|
@ -105,19 +105,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchRows(searchTerm, primaryDisplay)
|
$: fetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
|
|
||||||
const fetchRows = (searchTerm, primaryDisplay) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
const allRowsFetched =
|
const allRowsFetched =
|
||||||
$fetch.loaded &&
|
$fetch.loaded &&
|
||||||
!Object.keys($fetch.query?.string || {}).length &&
|
!Object.keys($fetch.query?.string || {}).length &&
|
||||||
!$fetch.hasNextPage
|
!$fetch.hasNextPage
|
||||||
// Don't request until we have the primary display
|
// Don't request until we have the primary display or default value has been fetched
|
||||||
if (!allRowsFetched && primaryDisplay) {
|
if (allRowsFetched || !primaryDisplay) {
|
||||||
fetch.update({
|
return
|
||||||
query: { string: { [primaryDisplay]: searchTerm } },
|
}
|
||||||
|
if (defaultVal && !optionsObj[defaultVal]) {
|
||||||
|
await fetch.update({
|
||||||
|
query: { equal: { _id: defaultVal } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
await fetch.update({
|
||||||
|
query: { string: { [primaryDisplay]: searchTerm } },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const flatten = values => {
|
const flatten = values => {
|
||||||
|
|
|
@ -10,24 +10,28 @@ export const buildUserEndpoints = API => ({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of users in the current tenant.
|
* Gets a list of users in the current tenant.
|
||||||
* @param {string} page The page to retrieve
|
* @param {string} bookmark The page to retrieve
|
||||||
* @param {string} search The starts with string to search username/email by.
|
* @param {object} query search filters for lookup by user (all operators not supported).
|
||||||
* @param {string} appId Facilitate app/role based user searching
|
* @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 = {}
|
const opts = {}
|
||||||
if (page) {
|
if (bookmark) {
|
||||||
opts.page = page
|
opts.bookmark = bookmark
|
||||||
}
|
}
|
||||||
if (email) {
|
if (query) {
|
||||||
opts.email = email
|
opts.query = query
|
||||||
}
|
}
|
||||||
if (appId) {
|
if (appId) {
|
||||||
opts.appId = appId
|
opts.appId = appId
|
||||||
}
|
}
|
||||||
if (typeof paginated === "boolean") {
|
if (typeof paginate === "boolean") {
|
||||||
opts.paginated = paginated
|
opts.paginate = paginate
|
||||||
|
}
|
||||||
|
if (limit) {
|
||||||
|
opts.limit = limit
|
||||||
}
|
}
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/global/users/search`,
|
url: `/api/global/users/search`,
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
const email = Object.values(searchParams.query.string)[0]
|
const email = Object.values(searchParams.query.string)[0]
|
||||||
|
|
||||||
const results = await API.searchUsers({
|
const results = await API.searchUsers({
|
||||||
email,
|
query: { string: { email } },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mapping to the expected data within RelationshipCell
|
// Mapping to the expected data within RelationshipCell
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch from "./DataFetch.js"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
import { LuceneUtils } from "../utils"
|
||||||
|
|
||||||
export default class UserFetch extends DataFetch {
|
export default class UserFetch extends DataFetch {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
|
@ -27,16 +28,25 @@ export default class UserFetch extends DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
|
const { limit, paginate } = this.options
|
||||||
const { cursor, query } = get(this.store)
|
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 {
|
try {
|
||||||
// "query" normally contains a lucene query, but users uses a non-standard
|
const opts = {
|
||||||
// search endpoint so we use query uniquely here
|
bookmark: cursor,
|
||||||
const res = await this.API.searchUsers({
|
query: finalQuery,
|
||||||
page: cursor,
|
appId: appId,
|
||||||
email: query.email,
|
paginate: paginated || paginate,
|
||||||
appId: query.appId,
|
limit,
|
||||||
paginated: query.paginated,
|
}
|
||||||
})
|
const res = await this.API.searchUsers(opts)
|
||||||
return {
|
return {
|
||||||
rows: res?.data || [],
|
rows: res?.data || [],
|
||||||
hasNextPage: res?.hasNextPage || false,
|
hasNextPage: res?.hasNextPage || false,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
SaveTableRequest,
|
SaveTableRequest,
|
||||||
|
SearchQueryOperators,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
|
@ -1141,7 +1142,9 @@ describe.each([
|
||||||
)
|
)
|
||||||
|
|
||||||
const createViewResponse = await config.createView({
|
const createViewResponse = await config.createView({
|
||||||
query: [{ operator: "equal", field: "age", value: 40 }],
|
query: [
|
||||||
|
{ operator: SearchQueryOperators.EQUAL, field: "age", value: 40 },
|
||||||
|
],
|
||||||
schema: viewSchema,
|
schema: viewSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
SearchQueryOperators,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
Table,
|
Table,
|
||||||
|
@ -89,7 +90,13 @@ describe.each([
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [{ operator: "equal", field: "field", value: "value" }],
|
query: [
|
||||||
|
{
|
||||||
|
operator: SearchQueryOperators.EQUAL,
|
||||||
|
field: "field",
|
||||||
|
value: "value",
|
||||||
|
},
|
||||||
|
],
|
||||||
sort: {
|
sort: {
|
||||||
field: "fieldToSort",
|
field: "fieldToSort",
|
||||||
order: SortOrder.DESCENDING,
|
order: SortOrder.DESCENDING,
|
||||||
|
@ -184,7 +191,13 @@ describe.each([
|
||||||
const tableId = table._id!
|
const tableId = table._id!
|
||||||
await config.api.viewV2.update({
|
await config.api.viewV2.update({
|
||||||
...view,
|
...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({
|
expect((await config.api.table.get(tableId)).views).toEqual({
|
||||||
|
@ -207,7 +220,7 @@ describe.each([
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: "equal",
|
operator: SearchQueryOperators.EQUAL,
|
||||||
field: generator.word(),
|
field: generator.word(),
|
||||||
value: generator.word(),
|
value: generator.word(),
|
||||||
},
|
},
|
||||||
|
@ -279,7 +292,13 @@ describe.each([
|
||||||
{
|
{
|
||||||
...view,
|
...view,
|
||||||
tableId: generator.guid(),
|
tableId: generator.guid(),
|
||||||
query: [{ operator: "equal", field: "newField", value: "thatValue" }],
|
query: [
|
||||||
|
{
|
||||||
|
operator: SearchQueryOperators.EQUAL,
|
||||||
|
field: "newField",
|
||||||
|
value: "thatValue",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ expectStatus: 404 }
|
{ expectStatus: 404 }
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,6 +43,10 @@ describe("postgres integrations", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await databaseTestProviders.postgres.stopContainer()
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
async function createAuxTable(prefix: string) {
|
async function createAuxTable(prefix: string) {
|
||||||
return await config.createTable({
|
return await config.createTable({
|
||||||
|
|
|
@ -36,3 +36,10 @@ export async function getDsConfig(): Promise<Datasource> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function stopContainer() {
|
||||||
|
if (container) {
|
||||||
|
await container.stop()
|
||||||
|
container = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {
|
import {
|
||||||
Datasource,
|
Datasource,
|
||||||
|
FieldSubtype,
|
||||||
FieldType,
|
FieldType,
|
||||||
SortDirection,
|
|
||||||
SortType,
|
|
||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQueryFields,
|
SearchQueryFields,
|
||||||
FieldSubtype,
|
SearchQueryOperators,
|
||||||
|
SortDirection,
|
||||||
|
SortType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
import { deepGet } from "./helpers"
|
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)
|
// Process a string match (fails if the value does not start with the string)
|
||||||
const stringMatch = match("string", (docValue: string, testValue: string) => {
|
const stringMatch = match(
|
||||||
return (
|
SearchQueryOperators.STRING,
|
||||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
(docValue: string, testValue: string) => {
|
||||||
)
|
return (
|
||||||
})
|
!docValue ||
|
||||||
|
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
const fuzzyMatch = match("fuzzy", (docValue: string, testValue: string) => {
|
const fuzzyMatch = match(
|
||||||
return (
|
SearchQueryOperators.FUZZY,
|
||||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
(docValue: string, testValue: string) => {
|
||||||
)
|
return (
|
||||||
})
|
!docValue ||
|
||||||
|
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process a range match
|
// Process a range match
|
||||||
const rangeMatch = match(
|
const rangeMatch = match(
|
||||||
"range",
|
SearchQueryOperators.RANGE,
|
||||||
(
|
(
|
||||||
docValue: string | number | null,
|
docValue: string | number | null,
|
||||||
testValue: { low: number; high: number }
|
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)
|
// Process an equal match (fails if the value is different)
|
||||||
const equalMatch = match(
|
const equalMatch = match(
|
||||||
"equal",
|
SearchQueryOperators.EQUAL,
|
||||||
(docValue: any, testValue: string | null) => {
|
(docValue: any, testValue: string | null) => {
|
||||||
return testValue != null && testValue !== "" && docValue !== testValue
|
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)
|
// Process a not-equal match (fails if the value is the same)
|
||||||
const notEqualMatch = match(
|
const notEqualMatch = match(
|
||||||
"notEqual",
|
SearchQueryOperators.NOT_EQUAL,
|
||||||
(docValue: any, testValue: string | null) => {
|
(docValue: any, testValue: string | null) => {
|
||||||
return testValue != null && testValue !== "" && docValue === testValue
|
return testValue != null && testValue !== "" && docValue === testValue
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
// Process an empty match (fails if the value is not empty)
|
||||||
const emptyMatch = match("empty", (docValue: string | null) => {
|
const emptyMatch = match(
|
||||||
return docValue != null && docValue !== ""
|
SearchQueryOperators.EMPTY,
|
||||||
})
|
(docValue: string | null) => {
|
||||||
|
return docValue != null && docValue !== ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
// Process a not-empty match (fails is the value is empty)
|
||||||
const notEmptyMatch = match("notEmpty", (docValue: string | null) => {
|
const notEmptyMatch = match(
|
||||||
return docValue == null || docValue === ""
|
SearchQueryOperators.NOT_EMPTY,
|
||||||
})
|
(docValue: string | null) => {
|
||||||
|
return docValue == null || docValue === ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process an includes match (fails if the value is not included)
|
// Process an includes match (fails if the value is not included)
|
||||||
const oneOf = match("oneOf", (docValue: any, testValue: any) => {
|
const oneOf = match(
|
||||||
if (typeof testValue === "string") {
|
SearchQueryOperators.ONE_OF,
|
||||||
testValue = testValue.split(",")
|
(docValue: any, testValue: any) => {
|
||||||
if (typeof docValue === "number") {
|
if (typeof testValue === "string") {
|
||||||
testValue = testValue.map((item: string) => parseFloat(item))
|
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) => {
|
const containsAny = match(
|
||||||
return !docValue?.includes(...testValue)
|
SearchQueryOperators.CONTAINS_ANY,
|
||||||
})
|
(docValue: any, testValue: any) => {
|
||||||
|
return !docValue?.includes(...testValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const contains = match(
|
const contains = match(
|
||||||
"contains",
|
SearchQueryOperators.CONTAINS,
|
||||||
(docValue: string | any[], testValue: any[]) => {
|
(docValue: string | any[], testValue: any[]) => {
|
||||||
return !testValue?.every((item: any) => docValue?.includes(item))
|
return !testValue?.every((item: any) => docValue?.includes(item))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const notContains = match(
|
const notContains = match(
|
||||||
"notContains",
|
SearchQueryOperators.NOT_CONTAINS,
|
||||||
(docValue: string | any[], testValue: any[]) => {
|
(docValue: string | any[], testValue: any[]) => {
|
||||||
return testValue?.every((item: any) => docValue?.includes(item))
|
return testValue?.every((item: any) => docValue?.includes(item))
|
||||||
}
|
}
|
||||||
|
@ -433,7 +454,7 @@ export const hasFilters = (query?: SearchQuery) => {
|
||||||
if (skipped.includes(key) || typeof value !== "object") {
|
if (skipped.includes(key) || typeof value !== "object") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (Object.keys(value).length !== 0) {
|
if (Object.keys(value || {}).length !== 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,43 +10,57 @@ export type SearchFilter = {
|
||||||
externalType?: string
|
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 = {
|
export type SearchQuery = {
|
||||||
allOr?: boolean
|
allOr?: boolean
|
||||||
onEmptyFilter?: EmptyFilterOption
|
onEmptyFilter?: EmptyFilterOption
|
||||||
string?: {
|
[SearchQueryOperators.STRING]?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
fuzzy?: {
|
[SearchQueryOperators.FUZZY]?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
range?: {
|
[SearchQueryOperators.RANGE]?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
high: number | string
|
high: number | string
|
||||||
low: number | string
|
low: number | string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
equal?: {
|
[SearchQueryOperators.EQUAL]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
notEqual?: {
|
[SearchQueryOperators.NOT_EQUAL]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
empty?: {
|
[SearchQueryOperators.EMPTY]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
notEmpty?: {
|
[SearchQueryOperators.NOT_EMPTY]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
oneOf?: {
|
[SearchQueryOperators.ONE_OF]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
contains?: {
|
[SearchQueryOperators.CONTAINS]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
notContains?: {
|
[SearchQueryOperators.NOT_CONTAINS]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
containsAny?: {
|
[SearchQueryOperators.CONTAINS_ANY]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { User } from "../../documents"
|
import { User } from "../../documents"
|
||||||
|
import { SearchQuery } from "./searchFilter"
|
||||||
|
|
||||||
export interface SaveUserResponse {
|
export interface SaveUserResponse {
|
||||||
_id: string
|
_id: string
|
||||||
|
@ -51,10 +52,10 @@ export interface InviteUsersResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchUsersRequest {
|
export interface SearchUsersRequest {
|
||||||
page?: string
|
bookmark?: string
|
||||||
email?: string
|
query?: SearchQuery
|
||||||
appId?: string
|
appId?: string
|
||||||
paginated?: boolean
|
paginate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateAdminUserRequest {
|
export interface CreateAdminUserRequest {
|
||||||
|
|
|
@ -197,7 +197,12 @@ export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
const body = ctx.request.body
|
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)
|
await getAppUsers(ctx)
|
||||||
} else {
|
} else {
|
||||||
const paginated = await userSdk.core.paginatedUsers(body)
|
const paginated = await userSdk.core.paginatedUsers(body)
|
||||||
|
|
|
@ -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", () => {
|
describe("DELETE /api/global/users/:userId", () => {
|
||||||
it("should be able to destroy a basic user", async () => {
|
it("should be able to destroy a basic user", async () => {
|
||||||
const user = await config.createUser()
|
const user = await config.createUser()
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
InviteUsersRequest,
|
InviteUsersRequest,
|
||||||
User,
|
User,
|
||||||
CreateAdminUserRequest,
|
CreateAdminUserRequest,
|
||||||
|
SearchQuery,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import structures from "../structures"
|
import structures from "../structures"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
@ -133,6 +134,15 @@ export class UserAPI extends TestAPI {
|
||||||
.expect(status ? status : 200)
|
.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) => {
|
getUser = (userId: string, opts?: TestAPIOpts) => {
|
||||||
return this.request
|
return this.request
|
||||||
.get(`/api/global/users/${userId}`)
|
.get(`/api/global/users/${userId}`)
|
||||||
|
|
|
@ -21862,7 +21862,7 @@ vlq@^0.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
||||||
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
|
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
|
||||||
|
|
||||||
vm2@^3.9.19:
|
vm2@^3.9.19, vm2@^3.9.8:
|
||||||
version "3.9.19"
|
version "3.9.19"
|
||||||
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a"
|
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a"
|
||||||
integrity sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==
|
integrity sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==
|
||||||
|
|
Loading…
Reference in New Issue