From 6bbce23910284a96b9fa8ea4addc6cd4e05f35c1 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 12 Oct 2023 16:31:32 +0100 Subject: [PATCH] Updating user fetch functionality to send up lucene syntax for searching to global user endpoint. --- packages/frontend-core/src/api/user.js | 25 ++++++----- packages/frontend-core/src/fetch/UserFetch.js | 26 +++++++---- .../server/src/api/routes/tests/row.spec.ts | 4 +- .../src/api/routes/tests/viewV2.spec.ts | 13 +++--- packages/shared-core/src/filters.ts | 45 ++++++++++++------- packages/types/src/api/web/searchFilter.ts | 36 ++++++++++----- yarn.lock | 2 +- 7 files changed, 97 insertions(+), 54 deletions(-) diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index 6c616d7baf..92b3671c78 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -10,25 +10,30 @@ 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 + } + console.log(opts) return await API.post({ url: `/api/global/users/search`, body: opts, diff --git a/packages/frontend-core/src/fetch/UserFetch.js b/packages/frontend-core/src/fetch/UserFetch.js index 5372d0ec33..2158b3ca57 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.isLuceneFilter(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..3ac8ceaf16 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -15,7 +15,7 @@ import { QuotaUsageType, RelationshipType, Row, - SaveTableRequest, + SaveTableRequest, SearchQueryOperators, SortOrder, SortType, StaticQuotaName, @@ -1141,7 +1141,7 @@ 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..7f4cf53ff1 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, @@ -10,8 +11,8 @@ import { UpdateViewRequest, ViewV2, } from "@budibase/types" -import { generator } from "@budibase/backend-core/tests" -import { generateDatasourceID } from "../../../db/utils" +import {generator} from "@budibase/backend-core/tests" +import {generateDatasourceID} from "../../../db/utils" function priceTable(): Table { return { @@ -89,7 +90,7 @@ 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 +185,7 @@ 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 +208,7 @@ describe.each([ primaryDisplay: generator.word(), query: [ { - operator: "equal", + operator: SearchQueryOperators.EQUAL, field: generator.word(), value: generator.word(), }, @@ -279,7 +280,7 @@ 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/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index ab43e86279..246b089af0 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -1,15 +1,16 @@ 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" +import {OperatorOptions, SqlNumberTypeRangeMap} from "./constants" +import {deepGet} from "./helpers" const HBS_REGEX = /{{([^{].*?)}}/g @@ -238,6 +239,18 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => { 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 * @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) - const stringMatch = match("string", (docValue: string, testValue: string) => { + 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) => { + const fuzzyMatch = match(SearchQueryOperators.FUZZY, (docValue: string, testValue: string) => { return ( !docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase()) ) @@ -288,7 +301,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { // Process a range match const rangeMatch = match( - "range", + SearchQueryOperators.RANGE, ( docValue: string | number | null, 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) const equalMatch = match( - "equal", + SearchQueryOperators.EQUAL, (docValue: any, testValue: string | null) => { 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) 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) => { + 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) => { + 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) => { + const oneOf = match(SearchQueryOperators.ONE_OF, (docValue: any, testValue: any) => { if (typeof testValue === "string") { testValue = testValue.split(",") if (typeof docValue === "number") { @@ -339,19 +352,19 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => { 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) }) 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)) } 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/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==