Updating user fetch functionality to send up lucene syntax for searching to global user endpoint.

This commit is contained in:
mike12345567 2023-10-12 16:31:32 +01:00
parent 702ee4d504
commit 6bbce23910
7 changed files with 97 additions and 54 deletions

View File

@ -10,25 +10,30 @@ 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
}
console.log(opts)
return await API.post({ return await API.post({
url: `/api/global/users/search`, url: `/api/global/users/search`,
body: opts, body: opts,

View File

@ -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.isLuceneFilter(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,

View File

@ -15,7 +15,7 @@ import {
QuotaUsageType, QuotaUsageType,
RelationshipType, RelationshipType,
Row, Row,
SaveTableRequest, SaveTableRequest, SearchQueryOperators,
SortOrder, SortOrder,
SortType, SortType,
StaticQuotaName, StaticQuotaName,
@ -1141,7 +1141,7 @@ 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,
}) })

View File

@ -3,6 +3,7 @@ import {
CreateViewRequest, CreateViewRequest,
FieldSchema, FieldSchema,
FieldType, FieldType,
SearchQueryOperators,
SortOrder, SortOrder,
SortType, SortType,
Table, Table,
@ -10,8 +11,8 @@ import {
UpdateViewRequest, UpdateViewRequest,
ViewV2, ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { generator } from "@budibase/backend-core/tests" import {generator} from "@budibase/backend-core/tests"
import { generateDatasourceID } from "../../../db/utils" import {generateDatasourceID} from "../../../db/utils"
function priceTable(): Table { function priceTable(): Table {
return { return {
@ -89,7 +90,7 @@ 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 +185,7 @@ 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 +208,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 +280,7 @@ 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 }
) )

View File

@ -1,15 +1,16 @@
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"
const HBS_REGEX = /{{([^{].*?)}}/g const HBS_REGEX = /{{([^{].*?)}}/g
@ -238,6 +239,18 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
return query return query
} }
// type unknown
export const isLuceneFilter = (search: any) => {
if (typeof search !== "object") {
return false
}
const operators = Object.values(SearchQueryOperators) as string[]
const anySearchKey = Object.keys(search).find(key => {
return operators.includes(key) && typeof search[key] === "object"
})
return !!anySearchKey
}
/** /**
* Performs a client-side lucene search on an array of data * Performs a client-side lucene search on an array of data
* @param docs the data * @param docs the data
@ -273,14 +286,14 @@ 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(SearchQueryOperators.STRING, (docValue: string, testValue: string) => {
return ( return (
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase()) !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(SearchQueryOperators.FUZZY, (docValue: string, testValue: string) => {
return ( return (
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase()) !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
) )
@ -288,7 +301,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// 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 +317,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,24 +325,24 @@ 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(SearchQueryOperators.EMPTY, (docValue: string | null) => {
return docValue != null && docValue !== "" 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(SearchQueryOperators.NOT_EMPTY, (docValue: string | null) => {
return docValue == null || docValue === "" 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(SearchQueryOperators.ONE_OF, (docValue: any, testValue: any) => {
if (typeof testValue === "string") { if (typeof testValue === "string") {
testValue = testValue.split(",") testValue = testValue.split(",")
if (typeof docValue === "number") { if (typeof docValue === "number") {
@ -339,19 +352,19 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
return !testValue?.includes(docValue) return !testValue?.includes(docValue)
}) })
const containsAny = match("containsAny", (docValue: any, testValue: any) => { const containsAny = match(SearchQueryOperators.CONTAINS_ANY, (docValue: any, testValue: any) => {
return !docValue?.includes(...testValue) 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))
} }

View File

@ -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[]
} }
} }

View File

@ -21750,7 +21750,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==