Merge branch 'master' into default-app-design

This commit is contained in:
Andrew Kingston 2025-03-13 14:49:43 +00:00 committed by GitHub
commit 52ee07d152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 342 additions and 301 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.5.2", "version": "3.5.3",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -147,9 +147,7 @@ export class FlagSet<T extends { [name: string]: boolean }> {
for (const [name, value] of Object.entries(posthogFlags)) { for (const [name, value] of Object.entries(posthogFlags)) {
if (!this.isFlagName(name)) { if (!this.isFlagName(name)) {
// We don't want an unexpected PostHog flag to break the app, so we // We don't want an unexpected PostHog flag to break the app
// just log it and continue.
console.warn(`Unexpected posthog flag "${name}": ${value}`)
continue continue
} }

View File

@ -8,6 +8,7 @@ import {
Row, Row,
SearchFilters, SearchFilters,
SortOrder, SortOrder,
SortType,
TableSchema, TableSchema,
} from "@budibase/types" } from "@budibase/types"
import { APIClient } from "../api/types" import { APIClient } from "../api/types"
@ -71,6 +72,8 @@ export default abstract class BaseDataFetch<
options: DataFetchOptions<TQuery> & { options: DataFetchOptions<TQuery> & {
datasource: TDatasource datasource: TDatasource
sortType: SortType | null
// Client side feature customisation // Client side feature customisation
clientSideSearching: boolean clientSideSearching: boolean
clientSideSorting: boolean clientSideSorting: boolean
@ -103,6 +106,7 @@ export default abstract class BaseDataFetch<
// Sorting config // Sorting config
sortColumn: null, sortColumn: null,
sortOrder: SortOrder.ASCENDING, sortOrder: SortOrder.ASCENDING,
sortType: null,
// Pagination config // Pagination config
paginate: true, paginate: true,
@ -223,12 +227,31 @@ export default abstract class BaseDataFetch<
this.options.sortColumn = this.getDefaultSortColumn(definition, schema) this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
} }
// If no sort order, default to ascending // If we don't have a sort column specified then just ensure we don't set
if (!this.options.sortOrder) { // any sorting params
if (!this.options.sortColumn) {
this.options.sortOrder = SortOrder.ASCENDING this.options.sortOrder = SortOrder.ASCENDING
this.options.sortType = null
} else { } else {
// Ensure sortOrder matches the enum // Otherwise determine what sort type to use base on sort column
this.options.sortOrder = this.options.sortOrder.toLowerCase() as SortOrder this.options.sortType = SortType.STRING
const fieldSchema = schema?.[this.options.sortColumn]
if (
fieldSchema?.type === FieldType.NUMBER ||
fieldSchema?.type === FieldType.BIGINT ||
("calculationType" in fieldSchema && fieldSchema?.calculationType)
) {
this.options.sortType = SortType.NUMBER
}
// If no sort order, default to ascending
if (!this.options.sortOrder) {
this.options.sortOrder = SortOrder.ASCENDING
} else {
// Ensure sortOrder matches the enum
this.options.sortOrder =
this.options.sortOrder.toLowerCase() as SortOrder
}
} }
// Build the query // Build the query
@ -271,6 +294,7 @@ export default abstract class BaseDataFetch<
const { const {
sortColumn, sortColumn,
sortOrder, sortOrder,
sortType,
limit, limit,
clientSideSearching, clientSideSearching,
clientSideSorting, clientSideSorting,
@ -287,8 +311,8 @@ export default abstract class BaseDataFetch<
} }
// If we don't support sorting, do a client-side sort // If we don't support sorting, do a client-side sort
if (!this.features.supportsSort && clientSideSorting && sortColumn) { if (!this.features.supportsSort && clientSideSorting && sortType) {
rows = sort(rows, sortColumn, sortOrder) rows = sort(rows, sortColumn as any, sortOrder, sortType)
} }
// If we don't support pagination, do a client-side limit // If we don't support pagination, do a client-side limit

View File

@ -29,7 +29,8 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
} }
async getData() { async getData() {
const { datasource, limit, sortColumn, sortOrder, paginate } = this.options const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
this.options
const { tableId } = datasource const { tableId } = datasource
const { cursor, query } = get(this.store) const { cursor, query } = get(this.store)
@ -40,6 +41,7 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
limit, limit,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder ?? SortOrder.ASCENDING, sortOrder: sortOrder ?? SortOrder.ASCENDING,
sortType,
paginate, paginate,
bookmark: cursor, bookmark: cursor,
}) })

View File

@ -1,5 +1,4 @@
import { import {
SearchViewRowRequest,
SortOrder, SortOrder,
ViewDatasource, ViewDatasource,
ViewV2Enriched, ViewV2Enriched,
@ -41,7 +40,8 @@ export default class ViewV2Fetch extends BaseDataFetch<
} }
async getData() { async getData() {
const { datasource, limit, sortColumn, sortOrder, paginate } = this.options const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
this.options
const { cursor, query, definition } = get(this.store) const { cursor, query, definition } = get(this.store)
// If this is a calculation view and we have no calculations, return nothing // If this is a calculation view and we have no calculations, return nothing
@ -68,13 +68,14 @@ export default class ViewV2Fetch extends BaseDataFetch<
} }
try { try {
const request: SearchViewRowRequest = { const request = {
query, query,
paginate, paginate,
limit, limit,
bookmark: cursor, bookmark: cursor,
sort: sortColumn, sort: sortColumn,
sortOrder: sortOrder, sortOrder: sortOrder,
sortType,
} }
if (paginate) { if (paginate) {
const res = await this.API.viewV2.fetch(datasource.id, { const res = await this.API.viewV2.fetch(datasource.id, {

View File

@ -263,6 +263,7 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
limit: searchRequest.limit, limit: searchRequest.limit,
sort: searchRequest.sort ?? undefined, sort: searchRequest.sort ?? undefined,
sortOrder: searchRequest.sortOrder, sortOrder: searchRequest.sortOrder,
sortType: searchRequest.sortType ?? undefined,
countRows: searchRequest.countRows, countRows: searchRequest.countRows,
version: searchRequest.version, version: searchRequest.version,
disableEscaping: searchRequest.disableEscaping, disableEscaping: searchRequest.disableEscaping,

View File

@ -63,12 +63,14 @@ function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
return { return {
sort: request.sort, sort: request.sort,
sortOrder: request.sortOrder, sortOrder: request.sortOrder,
sortType: request.sortType ?? undefined,
} }
} }
if (view.sort) { if (view.sort) {
return { return {
sort: view.sort.field, sort: view.sort.field,
sortOrder: view.sort.order, sortOrder: view.sort.order,
sortType: view.sort.type,
} }
} }

View File

@ -38,7 +38,7 @@ import {
import _ from "lodash" import _ from "lodash"
import tk from "timekeeper" import tk from "timekeeper"
import { encodeJSBinding } from "@budibase/string-templates" import { encodeJSBinding } from "@budibase/string-templates"
import { dataFilters, InMemorySearchQuery } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import { Knex } from "knex" import { Knex } from "knex"
import { generator, structures, mocks } from "@budibase/backend-core/tests" import { generator, structures, mocks } from "@budibase/backend-core/tests"
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
@ -200,26 +200,31 @@ if (descriptions.length) {
const isView = sourceType === "view" const isView = sourceType === "view"
class SearchAssertion { class SearchAssertion {
constructor( constructor(private readonly query: SearchRowRequest) {}
private readonly query: SearchRowRequest & {
sortType?: SortType
}
) {}
private async performSearch(): Promise<SearchResponse<Row>> { private async performSearch(): Promise<SearchResponse<Row>> {
if (isInMemory) { if (isInMemory) {
const inMemoryQuery: RequiredKeys<InMemorySearchQuery> = { const inMemoryQuery: RequiredKeys<
Omit<RowSearchParams, "tableId">
> = {
sort: this.query.sort ?? undefined, sort: this.query.sort ?? undefined,
query: { ...this.query.query }, query: { ...this.query.query },
paginate: this.query.paginate,
bookmark: this.query.bookmark ?? undefined,
limit: this.query.limit, limit: this.query.limit,
sortOrder: this.query.sortOrder, sortOrder: this.query.sortOrder,
sortType: this.query.sortType ?? undefined, sortType: this.query.sortType ?? undefined,
version: this.query.version,
disableEscaping: this.query.disableEscaping,
countRows: this.query.countRows, countRows: this.query.countRows,
viewId: undefined,
fields: undefined,
indexer: undefined,
rows: undefined,
} }
return dataFilters.search(_.cloneDeep(rows), inMemoryQuery) return dataFilters.search(_.cloneDeep(rows), inMemoryQuery)
} else { } else {
const { sortType, ...query } = this.query return config.api.row.search(tableOrViewId, this.query)
return config.api.row.search(tableOrViewId, query)
} }
} }
@ -395,9 +400,7 @@ if (descriptions.length) {
} }
} }
function expectSearch( function expectSearch(query: SearchRowRequest) {
query: SearchRowRequest & { sortType?: SortType }
) {
return new SearchAssertion(query) return new SearchAssertion(query)
} }
@ -1116,27 +1119,26 @@ if (descriptions.length) {
}).toMatchExactly([{ name: "foo" }, { name: "bar" }]) }).toMatchExactly([{ name: "foo" }, { name: "bar" }])
}) })
isInMemory && describe("sortType STRING", () => {
describe("sortType STRING", () => { it("sorts ascending", async () => {
it("sorts ascending", async () => { await expectSearch({
await expectSearch({ query: {},
query: {}, sort: "name",
sort: "name", sortType: SortType.STRING,
sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING,
sortOrder: SortOrder.ASCENDING, }).toMatchExactly([{ name: "bar" }, { name: "foo" }])
}).toMatchExactly([{ name: "bar" }, { name: "foo" }])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "name",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ name: "foo" }, { name: "bar" }])
})
}) })
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "name",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ name: "foo" }, { name: "bar" }])
})
})
!isInternal && !isInternal &&
!isInMemory && !isInMemory &&
// This test was added because we automatically add in a sort by the // This test was added because we automatically add in a sort by the
@ -1317,26 +1319,25 @@ if (descriptions.length) {
}) })
}) })
isInMemory && describe("sortType NUMBER", () => {
describe("sortType NUMBER", () => { it("sorts ascending", async () => {
it("sorts ascending", async () => { await expectSearch({
await expectSearch({ query: {},
query: {}, sort: "age",
sort: "age", sortType: SortType.NUMBER,
sortType: SortType.NUMBER, sortOrder: SortOrder.ASCENDING,
sortOrder: SortOrder.ASCENDING, }).toMatchExactly([{ age: 1 }, { age: 10 }])
}).toMatchExactly([{ age: 1 }, { age: 10 }])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "age",
sortType: SortType.NUMBER,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ age: 10 }, { age: 1 }])
})
}) })
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "age",
sortType: SortType.NUMBER,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ age: 10 }, { age: 1 }])
})
})
}) })
describe("dates", () => { describe("dates", () => {
@ -1472,26 +1473,25 @@ if (descriptions.length) {
}).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
}) })
isInMemory && describe("sortType STRING", () => {
describe("sortType STRING", () => { it("sorts ascending", async () => {
it("sorts ascending", async () => { await expectSearch({
await expectSearch({ query: {},
query: {}, sort: "dob",
sort: "dob", sortType: SortType.STRING,
sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING,
sortOrder: SortOrder.ASCENDING, }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
}).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "dob",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
})
}) })
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "dob",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
})
})
}) })
}) })
@ -1639,196 +1639,220 @@ if (descriptions.length) {
]) ])
}) })
isInMemory && describe("sortType STRING", () => {
describe("sortType STRING", () => { it("sorts ascending", async () => {
it("sorts ascending", async () => { await expectSearch({
await expectSearch({ query: {},
query: {}, sort: "time",
sort: "time", sortType: SortType.STRING,
sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING,
sortOrder: SortOrder.ASCENDING, }).toMatchExactly([
}).toMatchExactly([ { timeid: NULL_TIME__ID },
{ timeid: NULL_TIME__ID }, { time: "00:00:00" },
{ time: "00:00:00" }, { time: "10:00:00" },
{ time: "10:00:00" }, { time: "10:45:00" },
{ time: "10:45:00" }, { time: "12:00:00" },
{ time: "12:00:00" }, { time: "15:30:00" },
{ time: "15:30:00" }, ])
])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "time",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ time: "15:30:00" },
{ time: "12:00:00" },
{ time: "10:45:00" },
{ time: "10:00:00" },
{ time: "00:00:00" },
{ timeid: NULL_TIME__ID },
])
})
}) })
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "time",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ time: "15:30:00" },
{ time: "12:00:00" },
{ time: "10:45:00" },
{ time: "10:00:00" },
{ time: "00:00:00" },
{ timeid: NULL_TIME__ID },
])
})
})
}) })
}) })
!isInMemory && describe("datetime - date only", () => {
describe("datetime - date only", () => { describe.each([true, false])(
describe.each([true, false])( "saved with timestamp: %s",
"saved with timestamp: %s", saveWithTimestamp => {
saveWithTimestamp => { describe.each([true, false])(
describe.each([true, false])( "search with timestamp: %s",
"search with timestamp: %s", searchWithTimestamp => {
searchWithTimestamp => { const SAVE_SUFFIX = saveWithTimestamp
const SAVE_SUFFIX = saveWithTimestamp ? "T00:00:00.000Z"
? "T00:00:00.000Z" : ""
: "" const SEARCH_SUFFIX = searchWithTimestamp
const SEARCH_SUFFIX = searchWithTimestamp ? "T00:00:00.000Z"
? "T00:00:00.000Z" : ""
: ""
const JAN_1ST = `2020-01-01` const JAN_1ST = `2020-01-01`
const JAN_10TH = `2020-01-10` const JAN_10TH = `2020-01-10`
const JAN_30TH = `2020-01-30` const JAN_30TH = `2020-01-30`
const UNEXISTING_DATE = `2020-01-03` const UNEXISTING_DATE = `2020-01-03`
const NULL_DATE__ID = `null_date__id` const NULL_DATE__ID = `null_date__id`
beforeAll(async () => { beforeAll(async () => {
tableOrViewId = await createTableOrView({ tableOrViewId = await createTableOrView({
dateid: { dateid: {
name: "dateid", name: "dateid",
type: FieldType.STRING, type: FieldType.STRING,
},
date: {
name: "date",
type: FieldType.DATETIME,
dateOnly: true,
},
})
await createRows([
{ dateid: NULL_DATE__ID, date: null },
{ date: `${JAN_1ST}${SAVE_SUFFIX}` },
{ date: `${JAN_10TH}${SAVE_SUFFIX}` },
])
})
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
}).toContainExactly([{ date: JAN_1ST }])
})
it("successfully finds an ISO8601 row", async () => {
await expectQuery({
equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` },
}).toContainExactly([{ date: JAN_10TH }])
})
it("finds a row with ISO8601 timestamp", async () => {
await expectQuery({
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
}).toContainExactly([{ date: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
equal: {
date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`,
}, },
date: { }).toFindNothing()
name: "date", })
type: FieldType.DATETIME, })
dateOnly: true,
},
})
await createRows([ describe("notEqual", () => {
{ dateid: NULL_DATE__ID, date: null }, it("successfully finds a row", async () => {
{ date: `${JAN_1ST}${SAVE_SUFFIX}` }, await expectQuery({
{ date: `${JAN_10TH}${SAVE_SUFFIX}` }, notEqual: {
date: `${JAN_1ST}${SEARCH_SUFFIX}`,
},
}).toContainExactly([
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
]) ])
}) })
describe("equal", () => { it("fails to find nonexistent row", async () => {
it("successfully finds a row", async () => { await expectQuery({
await expectQuery({ notEqual: {
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, date: `${JAN_30TH}${SEARCH_SUFFIX}`,
}).toContainExactly([{ date: JAN_1ST }]) },
}) }).toContainExactly([
{ date: JAN_1ST },
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
])
})
})
it("successfully finds an ISO8601 row", async () => { describe("oneOf", () => {
await expectQuery({ it("successfully finds a row", async () => {
equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, await expectQuery({
}).toContainExactly([{ date: JAN_10TH }]) oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] },
}) }).toContainExactly([{ date: JAN_1ST }])
it("finds a row with ISO8601 timestamp", async () => {
await expectQuery({
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
}).toContainExactly([{ date: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
equal: {
date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`,
},
}).toFindNothing()
})
}) })
describe("notEqual", () => { it("fails to find nonexistent row", async () => {
it("successfully finds a row", async () => { await expectQuery({
await expectQuery({ oneOf: {
notEqual: { date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`],
date: `${JAN_1ST}${SEARCH_SUFFIX}`, },
}, }).toFindNothing()
}).toContainExactly([ })
{ date: JAN_10TH }, })
{ dateid: NULL_DATE__ID },
])
})
it("fails to find nonexistent row", async () => { describe("range", () => {
await expectQuery({ it("successfully finds a row", async () => {
notEqual: { await expectQuery({
date: `${JAN_30TH}${SEARCH_SUFFIX}`, range: {
date: {
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
high: `${JAN_1ST}${SEARCH_SUFFIX}`,
}, },
}).toContainExactly([ },
{ date: JAN_1ST }, }).toContainExactly([{ date: JAN_1ST }])
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
])
})
}) })
describe("oneOf", () => { it("successfully finds multiple rows", async () => {
it("successfully finds a row", async () => { await expectQuery({
await expectQuery({ range: {
oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, date: {
}).toContainExactly([{ date: JAN_1ST }]) low: `${JAN_1ST}${SEARCH_SUFFIX}`,
}) high: `${JAN_10TH}${SEARCH_SUFFIX}`,
it("fails to find nonexistent row", async () => {
await expectQuery({
oneOf: {
date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`],
}, },
}).toFindNothing() },
}) }).toContainExactly([
{ date: JAN_1ST },
{ date: JAN_10TH },
])
}) })
describe("range", () => { it("successfully finds no rows", async () => {
it("successfully finds a row", async () => { await expectQuery({
await expectQuery({ range: {
range: { date: {
date: { low: `${JAN_30TH}${SEARCH_SUFFIX}`,
low: `${JAN_1ST}${SEARCH_SUFFIX}`, high: `${JAN_30TH}${SEARCH_SUFFIX}`,
high: `${JAN_1ST}${SEARCH_SUFFIX}`,
},
}, },
}).toContainExactly([{ date: JAN_1ST }]) },
}) }).toFindNothing()
})
})
it("successfully finds multiple rows", async () => { describe("sort", () => {
await expectQuery({ it("sorts ascending", async () => {
range: { await expectSearch({
date: { query: {},
low: `${JAN_1ST}${SEARCH_SUFFIX}`, sort: "date",
high: `${JAN_10TH}${SEARCH_SUFFIX}`, sortOrder: SortOrder.ASCENDING,
}, }).toMatchExactly([
}, { dateid: NULL_DATE__ID },
}).toContainExactly([ { date: JAN_1ST },
{ date: JAN_1ST }, { date: JAN_10TH },
{ date: JAN_10TH }, ])
])
})
it("successfully finds no rows", async () => {
await expectQuery({
range: {
date: {
low: `${JAN_30TH}${SEARCH_SUFFIX}`,
high: `${JAN_30TH}${SEARCH_SUFFIX}`,
},
},
}).toFindNothing()
})
}) })
describe("sort", () => { it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "date",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ date: JAN_10TH },
{ date: JAN_1ST },
{ dateid: NULL_DATE__ID },
])
})
describe("sortType STRING", () => {
it("sorts ascending", async () => { it("sorts ascending", async () => {
await expectSearch({ await expectSearch({
query: {}, query: {},
sort: "date", sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING, sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([ }).toMatchExactly([
{ dateid: NULL_DATE__ID }, { dateid: NULL_DATE__ID },
@ -1841,6 +1865,7 @@ if (descriptions.length) {
await expectSearch({ await expectSearch({
query: {}, query: {},
sort: "date", sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING, sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([ }).toMatchExactly([
{ date: JAN_10TH }, { date: JAN_10TH },
@ -1848,41 +1873,13 @@ if (descriptions.length) {
{ dateid: NULL_DATE__ID }, { dateid: NULL_DATE__ID },
]) ])
}) })
isInMemory &&
describe("sortType STRING", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([
{ dateid: NULL_DATE__ID },
{ date: JAN_1ST },
{ date: JAN_10TH },
])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ date: JAN_10TH },
{ date: JAN_1ST },
{ dateid: NULL_DATE__ID },
])
})
})
}) })
} })
) }
} )
) }
}) )
})
isInternal && isInternal &&
!isInMemory && !isInMemory &&

View File

@ -24,6 +24,7 @@ import {
SearchResponse, SearchResponse,
SearchViewRowRequest, SearchViewRowRequest,
SortOrder, SortOrder,
SortType,
StaticQuotaName, StaticQuotaName,
Table, Table,
TableSchema, TableSchema,
@ -153,6 +154,7 @@ if (descriptions.length) {
sort: { sort: {
field: "fieldToSort", field: "fieldToSort",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -215,6 +217,7 @@ if (descriptions.length) {
sort: { sort: {
field: "fieldToSort", field: "fieldToSort",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -1144,6 +1147,7 @@ if (descriptions.length) {
sort: { sort: {
field: generator.word(), field: generator.word(),
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.STRING,
}, },
schema: { schema: {
id: { visible: true }, id: { visible: true },
@ -3149,6 +3153,7 @@ if (descriptions.length) {
{ {
field: string field: string
order?: SortOrder order?: SortOrder
type?: SortType
}, },
string[] string[]
][] = [ ][] = [
@ -3156,6 +3161,7 @@ if (descriptions.length) {
{ {
field: "name", field: "name",
order: SortOrder.ASCENDING, order: SortOrder.ASCENDING,
type: SortType.STRING,
}, },
["Alice", "Bob", "Charly", "Danny"], ["Alice", "Bob", "Charly", "Danny"],
], ],
@ -3172,6 +3178,22 @@ if (descriptions.length) {
}, },
["Danny", "Charly", "Bob", "Alice"], ["Danny", "Charly", "Bob", "Alice"],
], ],
[
{
field: "name",
order: SortOrder.DESCENDING,
type: SortType.STRING,
},
["Danny", "Charly", "Bob", "Alice"],
],
[
{
field: "age",
order: SortOrder.ASCENDING,
type: SortType.NUMBER,
},
["Danny", "Alice", "Charly", "Bob"],
],
[ [
{ {
field: "age", field: "age",
@ -3182,13 +3204,15 @@ if (descriptions.length) {
[ [
{ {
field: "age", field: "age",
order: SortOrder.DESCENDING,
}, },
["Danny", "Alice", "Charly", "Bob"], ["Bob", "Charly", "Alice", "Danny"],
], ],
[ [
{ {
field: "age", field: "age",
order: SortOrder.DESCENDING, order: SortOrder.DESCENDING,
type: SortType.NUMBER,
}, },
["Bob", "Charly", "Alice", "Danny"], ["Bob", "Charly", "Alice", "Danny"],
], ],
@ -3275,6 +3299,7 @@ if (descriptions.length) {
sort: { sort: {
field: "name", field: "name",
order: SortOrder.ASCENDING, order: SortOrder.ASCENDING,
type: SortType.STRING,
}, },
schema: viewSchema, schema: viewSchema,
}) })
@ -3282,6 +3307,7 @@ if (descriptions.length) {
const response = await config.api.viewV2.search(view.id, { const response = await config.api.viewV2.search(view.id, {
sort: sortParams.field, sort: sortParams.field,
sortOrder: sortParams.order, sortOrder: sortParams.order,
sortType: sortParams.type,
query: {}, query: {},
}) })

View File

@ -46,6 +46,7 @@ export async function search(
query: options.query, query: options.query,
sort: options.sort, sort: options.sort,
sortOrder: options.sortOrder, sortOrder: options.sortOrder,
sortType: options.sortType,
limit: options.limit, limit: options.limit,
bookmark: options.bookmark, bookmark: options.bookmark,
paginate: options.paginate, paginate: options.paginate,

View File

@ -1,6 +1,5 @@
import { import {
Aggregation, Aggregation,
AutoFieldSubType,
CalculationType, CalculationType,
DocumentType, DocumentType,
EnrichedQueryJson, EnrichedQueryJson,
@ -424,11 +423,7 @@ export async function search(
} }
} else if (sortField) { } else if (sortField) {
const sortType = const sortType =
sortField.type === FieldType.NUMBER || sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING
(sortField.type === FieldType.AUTO &&
sortField.subtype === AutoFieldSubType.AUTO_ID)
? SortType.NUMBER
: SortType.STRING
request.sort = { request.sort = {
[mapToUserColumn(sortField.name)]: { [mapToUserColumn(sortField.name)]: {
direction: params.sortOrder || SortOrder.ASCENDING, direction: params.sortOrder || SortOrder.ASCENDING,

View File

@ -11,6 +11,7 @@ import {
SortType, SortType,
FieldConstraints, FieldConstraints,
SortOrder, SortOrder,
RowSearchParams,
EmptyFilterOption, EmptyFilterOption,
SearchResponse, SearchResponse,
Table, Table,
@ -24,8 +25,6 @@ import {
isArraySearchOperator, isArraySearchOperator,
isRangeSearchOperator, isRangeSearchOperator,
SearchFilter, SearchFilter,
WithRequired,
SearchParams,
} from "@budibase/types" } from "@budibase/types"
import dayjs from "dayjs" import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@ -522,19 +521,9 @@ export function fixupFilterArrays(filters: SearchFilters) {
return filters return filters
} }
type SearchQuery = WithRequired<
Pick<
SearchParams,
"query" | "sort" | "sortOrder" | "sortType" | "limit" | "countRows"
>,
"query"
>
export type InMemorySearchQuery = SearchQuery
export function search<T extends Record<string, any>>( export function search<T extends Record<string, any>>(
docs: T[], docs: T[],
query: SearchQuery query: Omit<RowSearchParams, "tableId">
): SearchResponse<T> { ): SearchResponse<T> {
let result = runQuery(docs, query.query) let result = runQuery(docs, query.query)
if (query.sort) { if (query.sort) {

View File

@ -1,6 +1,5 @@
export * from "./constants" export * from "./constants"
export * as dataFilters from "./filters" export * as dataFilters from "./filters"
export type * from "./filters"
export * as helpers from "./helpers" export * as helpers from "./helpers"
export * as utils from "./utils" export * as utils from "./utils"
export * as sdk from "./sdk" export * as sdk from "./sdk"

View File

@ -8,7 +8,11 @@ import {
SearchFilterKey, SearchFilterKey,
} from "../../../../sdk" } from "../../../../sdk"
import { Row } from "../../../../documents" import { Row } from "../../../../documents"
import { PaginationResponse, SortOrder } from "../../../../api/web/pagination" import {
PaginationResponse,
SortOrder,
SortType,
} from "../../../../api/web/pagination"
import { z } from "zod" import { z } from "zod"
const fieldKey = z const fieldKey = z
@ -66,6 +70,7 @@ const searchRowRequest = z.object({
limit: z.number().optional(), limit: z.number().optional(),
sort: z.string().nullish(), sort: z.string().nullish(),
sortOrder: z.nativeEnum(SortOrder).optional(), sortOrder: z.nativeEnum(SortOrder).optional(),
sortType: z.nativeEnum(SortType).nullish(),
version: z.string().optional(), version: z.string().optional(),
disableEscaping: z.boolean().optional(), disableEscaping: z.boolean().optional(),
countRows: z.boolean().optional(), countRows: z.boolean().optional(),
@ -78,6 +83,7 @@ export type SearchViewRowRequest = Pick<
SearchRowRequest, SearchRowRequest,
| "sort" | "sort"
| "sortOrder" | "sortOrder"
| "sortType"
| "limit" | "limit"
| "bookmark" | "bookmark"
| "paginate" | "paginate"

View File

@ -50,7 +50,7 @@ export interface SearchParams {
// when searching for rows we want a more extensive search type that requires certain properties // when searching for rows we want a more extensive search type that requires certain properties
export interface RowSearchParams export interface RowSearchParams
extends WithRequired<Omit<SearchParams, "sortType">, "tableId" | "query"> {} extends WithRequired<SearchParams, "tableId" | "query"> {}
export interface SearchResponse<T> { export interface SearchResponse<T> {
rows: T[] rows: T[]