@budibase/server
Version:
Budibase Web Server
1,296 lines (1,167 loc) • 159 kB
text/typescript
import {
context,
db as dbCore,
MAX_VALID_DATE,
MIN_VALID_DATE,
setEnv,
SQLITE_DESIGN_DOC_ID,
utils,
withEnv as withCoreEnv,
} from "@budibase/backend-core"
import { datasourceDescribe } from "../../../integrations/tests/utils"
import { tableForDatasource } from "../../../tests/utilities/structures"
import { generator, mocks, structures } from "@budibase/backend-core/tests"
import { dataFilters, isViewId } from "@budibase/shared-core"
import { encodeJSBinding } from "@budibase/string-templates"
import {
AIOperationEnum,
AutoFieldSubType,
BBReferenceFieldSubType,
Datasource,
EmptyFilterOption,
FieldType,
JsonFieldSubType,
LogicalOperator,
RelationshipType,
RequiredKeys,
Row,
RowSearchParams,
SearchFilters,
SearchResponse,
SearchRowRequest,
SortOrder,
SortType,
Table,
TableSchema,
User,
ViewV2Schema,
} from "@budibase/types"
import { Knex } from "knex"
import _ from "lodash"
import { cloneDeep } from "lodash/fp"
import tk from "timekeeper"
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
import { generateRowIdField } from "../../../integrations/utils"
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/ai/openai"
const descriptions = datasourceDescribe({ plus: true })
if (descriptions.length) {
describe.each(descriptions)(
"search ($dbName)",
({ config, dsProvider, isInternal, isOracle, isSql }) => {
let datasource: Datasource | undefined
let client: Knex | undefined
let tableOrViewId: string
let rows: Row[]
async function basicRelationshipTables(
type: RelationshipType,
opts?: {
tableName?: string
primaryColumn?: string
otherColumn?: string
}
) {
const relatedTable = await config.api.table.save(
tableForDatasource(datasource, {
name: opts?.tableName,
schema: { name: { name: "name", type: FieldType.STRING } },
})
)
const columnName = opts?.primaryColumn || "productCat"
const table = await config.api.table.save(
tableForDatasource(datasource, {
// @ts-expect-error - API accepts this structure, will build out rest of definition
schema: {
name: { name: "name", type: FieldType.STRING },
[columnName]: {
type: FieldType.LINK,
relationshipType: type,
name: columnName,
fieldName: opts?.otherColumn || "product",
tableId: relatedTable._id!,
constraints: {
type: "array",
},
},
},
})
)
return {
relatedTable: await config.api.table.get(relatedTable._id!),
tableId: table._id!,
}
}
beforeAll(async () => {
const ds = await dsProvider()
datasource = ds.datasource
client = ds.client
config.devWorkspace = await config.api.workspace.update(
config.getDevWorkspaceId(),
{
snippets: [
{
name: "WeeksAgo",
code: `
return function (weeks) {
const currentTime = new Date(${Date.now()});
currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1)));
return currentTime.toISOString();
}
`,
},
],
}
)
})
async function createTableWithSchema(schema?: TableSchema) {
const table = await config.api.table.save(
tableForDatasource(datasource, { schema })
)
return table._id!
}
async function createViewWithSchema(
tableId: string,
schema?: ViewV2Schema
) {
const view = await config.api.viewV2.create({
tableId: tableId,
name: generator.guid(),
schema,
})
return view.id
}
async function createRows(arr: Record<string, any>[]) {
// Shuffling to avoid false positives given a fixed order
for (const row of _.shuffle(arr)) {
await config.api.row.save(tableOrViewId, row)
}
rows = await config.api.row.fetch(tableOrViewId)
}
async function getTable(tableOrViewId: string): Promise<Table> {
if (isViewId(tableOrViewId)) {
const view = await config.api.viewV2.get(tableOrViewId)
return await config.api.table.get(view.tableId)
} else {
return await config.api.table.get(tableOrViewId)
}
}
async function assertTableExists(nameOrTable: string | Table) {
const name =
typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name
expect(await client!.schema.hasTable(name)).toBeTrue()
}
async function assertTableNumRows(
nameOrTable: string | Table,
numRows: number
) {
const name =
typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name
const row = await client!.from(name).count()
const count = parseInt(Object.values(row[0])[0] as string)
expect(count).toEqual(numRows)
}
describe.each([true, false])("in-memory: %s", isInMemory => {
// We only run the in-memory tests during the SQS (isInternal) run
if (isInMemory && !isInternal) {
return
}
type CreateFn = (schema?: TableSchema) => Promise<string>
let tableOrView: [string, CreateFn][] = [
["table", createTableWithSchema],
]
if (!isInMemory) {
tableOrView.push([
"view",
async (schema?: TableSchema) => {
const tableId = await createTableWithSchema(schema)
const viewId = await createViewWithSchema(
tableId,
Object.keys(schema || {}).reduce<ViewV2Schema>(
(viewSchema, fieldName) => {
const field = schema![fieldName]
viewSchema[fieldName] = {
visible: field.visible ?? true,
readonly: false,
}
return viewSchema
},
{}
)
)
return viewId
},
])
}
describe.each(tableOrView)(
"from %s",
(sourceType, createTableOrView) => {
const isView = sourceType === "view"
class SearchAssertion {
constructor(private readonly query: SearchRowRequest) {}
private async performSearch(): Promise<SearchResponse<Row>> {
if (isInMemory) {
const inMemoryQuery: RequiredKeys<
Omit<RowSearchParams, "tableId">
> = {
sort: this.query.sort ?? undefined,
query: { ...this.query.query },
paginate: this.query.paginate,
bookmark: this.query.bookmark ?? undefined,
limit: this.query.limit,
sortOrder: this.query.sortOrder,
sortType: this.query.sortType ?? undefined,
version: this.query.version,
disableEscaping: this.query.disableEscaping,
countRows: this.query.countRows,
viewId: undefined,
fields: undefined,
indexer: undefined,
rows: undefined,
}
return dataFilters.search(_.cloneDeep(rows), inMemoryQuery)
} else {
return config.api.row.search(tableOrViewId, this.query)
}
}
// We originally used _.isMatch to compare rows, but found that when
// comparing arrays it would return true if the source array was a subset of
// the target array. This would sometimes create false matches. This
// function is a more strict version of _.isMatch that only returns true if
// the source array is an exact match of the target.
//
// _.isMatch("100", "1") also returns true which is not what we want.
private isMatch<T extends Record<string, any>>(
expected: T,
found: T
) {
if (!expected) {
throw new Error("Expected is undefined")
}
if (!found) {
return false
}
for (const key of Object.keys(expected)) {
if (Array.isArray(expected[key])) {
if (!Array.isArray(found[key])) {
return false
}
if (expected[key].length !== found[key].length) {
return false
}
if (!_.isMatch(found[key], expected[key])) {
return false
}
} else if (typeof expected[key] === "object") {
if (!this.isMatch(expected[key], found[key])) {
return false
}
} else {
if (expected[key] !== found[key]) {
return false
}
}
}
return true
}
// This function exists to ensure that the same row is not matched twice.
// When a row gets matched, we make sure to remove it from the list of rows
// we're matching against.
private popRow<T extends { [key: string]: any }>(
expectedRow: T,
foundRows: T[]
): NonNullable<T> {
const row = foundRows.find(row =>
this.isMatch(expectedRow, row)
)
if (!row) {
const fields = Object.keys(expectedRow)
// To make the error message more readable, we only include the fields
// that are present in the expected row.
const searchedObjects = foundRows.map(row =>
_.pick(row, fields)
)
throw new Error(
`Failed to find row:\n\n${JSON.stringify(
expectedRow,
null,
2
)}\n\nin\n\n${JSON.stringify(searchedObjects, null, 2)}`
)
}
foundRows.splice(foundRows.indexOf(row), 1)
return row
}
// Asserts that the query returns rows matching exactly the set of rows
// passed in. The order of the rows matters. Rows returned in an order
// different to the one passed in will cause the assertion to fail. Extra
// rows returned by the query will also cause the assertion to fail.
async toMatchExactly(expectedRows: any[]) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
const foundRows = response.rows
expect(foundRows).toHaveLength(expectedRows.length)
expect([...foundRows]).toEqual(
expectedRows.map((expectedRow: any) =>
expect.objectContaining(this.popRow(expectedRow, foundRows))
)
)
return cloned
}
// Asserts that the query returns rows matching exactly the set of rows
// passed in. The order of the rows is not important, but extra rows will
// cause the assertion to fail.
async toContainExactly(expectedRows: any[]) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
const foundRows = response.rows
expect(foundRows).toHaveLength(expectedRows.length)
expect([...foundRows]).toEqual(
expect.arrayContaining(
expectedRows.map((expectedRow: any) =>
expect.objectContaining(
this.popRow(expectedRow, foundRows)
)
)
)
)
return cloned
}
// Asserts that the query returns some property values - this cannot be used
// to check row values, however this shouldn't be important for checking properties
// typing for this has to be any, Jest doesn't expose types for matchers like expect.any(...)
async toMatch(properties: Record<string, any>) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
const keys = Object.keys(properties) as Array<
keyof SearchResponse<Row>
>
for (let key of keys) {
expect(response[key]).toBeDefined()
if (properties[key]) {
expect(response[key]).toEqual(properties[key])
}
}
return cloned
}
// Asserts that the query doesn't return a property, e.g. pagination parameters.
async toNotHaveProperty(
properties: (keyof SearchResponse<Row>)[]
) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
for (let property of properties) {
expect(response[property]).toBeUndefined()
}
return cloned
}
// Asserts that the query returns rows matching the set of rows passed in.
// The order of the rows is not important. Extra rows will not cause the
// assertion to fail.
async toContain(expectedRows: any[]) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
const foundRows = response.rows
expect([...foundRows]).toEqual(
expect.arrayContaining(
expectedRows.map((expectedRow: any) =>
expect.objectContaining(
this.popRow(expectedRow, foundRows)
)
)
)
)
return cloned
}
async toFindNothing() {
await this.toContainExactly([])
}
async toHaveLength(length: number) {
const { rows: foundRows } = await this.performSearch()
expect(foundRows).toHaveLength(length)
}
}
function expectSearch(query: SearchRowRequest) {
return new SearchAssertion(query)
}
function expectQuery(query: SearchFilters) {
return expectSearch({ query })
}
describe("boolean", () => {
beforeAll(async () => {
tableOrViewId = await createTableOrView({
isTrue: { name: "isTrue", type: FieldType.BOOLEAN },
})
await createRows([{ isTrue: true }, { isTrue: false }])
})
describe("equal", () => {
it("successfully finds true row", async () => {
await expectQuery({ equal: { isTrue: true } }).toMatchExactly(
[{ isTrue: true }]
)
})
it("successfully finds false row", async () => {
await expectQuery({
equal: { isTrue: false },
}).toMatchExactly([{ isTrue: false }])
})
})
describe("notEqual", () => {
it("successfully finds false row", async () => {
await expectQuery({
notEqual: { isTrue: true },
}).toContainExactly([{ isTrue: false }])
})
it("successfully finds true row", async () => {
await expectQuery({
notEqual: { isTrue: false },
}).toContainExactly([{ isTrue: true }])
})
})
describe("oneOf", () => {
it("successfully finds true row", async () => {
await expectQuery({
oneOf: { isTrue: [true] },
}).toContainExactly([{ isTrue: true }])
})
it("successfully finds false row", async () => {
await expectQuery({
oneOf: { isTrue: [false] },
}).toContainExactly([{ isTrue: false }])
})
})
describe("sort", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([{ isTrue: false }, { isTrue: true }])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ isTrue: true }, { isTrue: false }])
})
})
})
!isInMemory &&
describe("bindings", () => {
let globalUsers: any = []
const serverTime = new Date()
// In MariaDB and MySQL we only store dates to second precision, so we need
// to remove milliseconds from the server time to ensure searches work as
// expected.
serverTime.setMilliseconds(0)
const future = new Date(
serverTime.getTime() + 1000 * 60 * 60 * 24 * 30
)
const rows = (currentUser: User) => {
return [
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
{
name: currentUser.firstName,
appointment: future.toISOString(),
},
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
{
name: "single user, session user",
single_user: currentUser,
},
{
name: "single user",
single_user: globalUsers[0],
},
{
name: "deprecated single user, session user",
deprecated_single_user: [currentUser],
},
{
name: "deprecated single user",
deprecated_single_user: [globalUsers[0]],
},
{
name: "multi user",
multi_user: globalUsers,
},
{
name: "multi user with session user",
multi_user: [...globalUsers, currentUser],
},
{
name: "deprecated multi user",
deprecated_multi_user: globalUsers,
},
{
name: "deprecated multi user with session user",
deprecated_multi_user: [...globalUsers, currentUser],
},
]
}
beforeAll(async () => {
// Set up some global users
globalUsers = await Promise.all(
Array(2)
.fill(0)
.map(async () => {
const globalUser = await config.globalUser()
const userMedataId = globalUser._id
? dbCore.generateUserMetadataID(globalUser._id)
: null
return {
_id: globalUser._id,
_meta: userMedataId,
}
})
)
tableOrViewId = await createTableOrView({
name: { name: "name", type: FieldType.STRING },
appointment: {
name: "appointment",
type: FieldType.DATETIME,
},
single_user: {
name: "single_user",
type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER,
},
deprecated_single_user: {
name: "deprecated_single_user",
type: FieldType.BB_REFERENCE,
subtype: BBReferenceFieldSubType.USER,
},
multi_user: {
name: "multi_user",
type: FieldType.BB_REFERENCE,
subtype: BBReferenceFieldSubType.USER,
constraints: {
type: "array",
},
},
deprecated_multi_user: {
name: "deprecated_multi_user",
type: FieldType.BB_REFERENCE,
subtype: BBReferenceFieldSubType.USERS,
constraints: {
type: "array",
},
},
})
await createRows(rows(config.getUser()))
})
// !! Current User is auto generated per run
it("should return all rows matching the session user firstname", async () => {
await expectQuery({
equal: { name: "{{ [user].firstName }}" },
}).toContainExactly([
{
name: config.getUser().firstName,
appointment: future.toISOString(),
},
])
})
it("should return all rows matching the session user firstname when logical operator used", async () => {
await expectQuery({
$and: {
conditions: [
{ equal: { name: "{{ [user].firstName }}" } },
],
},
}).toContainExactly([
{
name: config.getUser().firstName,
appointment: future.toISOString(),
},
])
})
it("should parse the date binding and return all rows after the resolved value", async () => {
await tk.withFreeze(serverTime, async () => {
await expectQuery({
range: {
appointment: {
low: "{{ [now] }}",
high: "9999-00-00T00:00:00.000Z",
},
},
}).toContainExactly([
{
name: config.getUser().firstName,
appointment: future.toISOString(),
},
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
])
})
})
it("should parse the date binding and return all rows before the resolved value", async () => {
await expectQuery({
range: {
appointment: {
low: "0000-00-00T00:00:00.000Z",
high: "{{ [now] }}",
},
},
}).toContainExactly([
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
])
})
it("should parse the encoded js snippet. Return rows with appointments up to 1 week in the past", async () => {
const jsBinding = "return snippets.WeeksAgo();"
const encodedBinding = encodeJSBinding(jsBinding)
await expectQuery({
range: {
appointment: {
low: "0000-00-00T00:00:00.000Z",
high: encodedBinding,
},
},
}).toContainExactly([
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
])
})
it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => {
const jsBinding = `const currentTime = new Date(${Date.now()})\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();`
const encodedBinding = encodeJSBinding(jsBinding)
await expectQuery({
range: {
appointment: {
low: "0000-00-00T00:00:00.000Z",
high: encodedBinding,
},
},
}).toContainExactly([
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
])
})
it("should match a single user row by the session user id", async () => {
await expectQuery({
equal: { single_user: "{{ [user]._id }}" },
}).toContainExactly([
{
name: "single user, session user",
single_user: { _id: config.getUser()._id },
},
])
})
it("should match a deprecated single user row by the session user id", async () => {
await expectQuery({
equal: { deprecated_single_user: "{{ [user]._id }}" },
}).toContainExactly([
{
name: "deprecated single user, session user",
deprecated_single_user: [{ _id: config.getUser()._id }],
},
])
})
it("should match the session user id in a multi user field", async () => {
const allUsers = [...globalUsers, config.getUser()].map(
(user: any) => {
return { _id: user._id }
}
)
await expectQuery({
contains: { multi_user: ["{{ [user]._id }}"] },
}).toContainExactly([
{
name: "multi user with session user",
multi_user: allUsers,
},
])
})
it("should match the session user id in a deprecated multi user field", async () => {
const allUsers = [...globalUsers, config.getUser()].map(
(user: any) => {
return { _id: user._id }
}
)
await expectQuery({
contains: { deprecated_multi_user: ["{{ [user]._id }}"] },
}).toContainExactly([
{
name: "deprecated multi user with session user",
deprecated_multi_user: allUsers,
},
])
})
it("should not match the session user id in a multi user field", async () => {
await expectQuery({
notContains: { multi_user: ["{{ [user]._id }}"] },
notEmpty: { multi_user: true },
}).toContainExactly([
{
name: "multi user",
multi_user: globalUsers.map((user: any) => {
return { _id: user._id }
}),
},
])
})
it("should not match the session user id in a deprecated multi user field", async () => {
await expectQuery({
notContains: {
deprecated_multi_user: ["{{ [user]._id }}"],
},
notEmpty: { deprecated_multi_user: true },
}).toContainExactly([
{
name: "deprecated multi user",
deprecated_multi_user: globalUsers.map((user: any) => {
return { _id: user._id }
}),
},
])
})
it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => {
await expectQuery({
oneOf: {
single_user: [
"{{ default [user]._id '_empty_' }}",
globalUsers[0]._id,
],
},
}).toContainExactly([
{
name: "single user, session user",
single_user: { _id: config.getUser()._id },
},
{
name: "single user",
single_user: { _id: globalUsers[0]._id },
},
])
})
it("should match the session user id and a user table row id using helpers, user binding and a static user id. (deprecated single user)", async () => {
await expectQuery({
oneOf: {
deprecated_single_user: [
"{{ default [user]._id '_empty_' }}",
globalUsers[0]._id,
],
},
}).toContainExactly([
{
name: "deprecated single user, session user",
deprecated_single_user: [{ _id: config.getUser()._id }],
},
{
name: "deprecated single user",
deprecated_single_user: [{ _id: globalUsers[0]._id }],
},
])
})
it("should resolve 'default' helper to '_empty_' when binding resolves to nothing", async () => {
await expectQuery({
oneOf: {
single_user: [
"{{ default [user]._idx '_empty_' }}",
globalUsers[0]._id,
],
},
}).toContainExactly([
{
name: "single user",
single_user: { _id: globalUsers[0]._id },
},
])
})
it("should resolve 'default' helper to '_empty_' when binding resolves to nothing (deprecated single user)", async () => {
await expectQuery({
oneOf: {
deprecated_single_user: [
"{{ default [user]._idx '_empty_' }}",
globalUsers[0]._id,
],
},
}).toContainExactly([
{
name: "deprecated single user",
deprecated_single_user: [{ _id: globalUsers[0]._id }],
},
])
})
})
const stringTypes = [
FieldType.STRING,
FieldType.LONGFORM,
FieldType.BARCODEQR,
] as const
describe.each(stringTypes)("%s", type => {
beforeAll(async () => {
tableOrViewId = await createTableOrView({
name: { name: "name", type },
})
await createRows([{ name: "foo" }, { name: "bar" }])
})
describe("misc", () => {
it("should return all if no query is passed", async () => {
await expectSearch({} as RowSearchParams).toContainExactly([
{ name: "foo" },
{ name: "bar" },
])
})
it("should return all if empty query is passed", async () => {
await expectQuery({}).toContainExactly([
{ name: "foo" },
{ name: "bar" },
])
})
it("should return all if onEmptyFilter is RETURN_ALL", async () => {
await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
// onEmptyFilter cannot be sent to view searches
!isView &&
it("should return nothing if onEmptyFilter is RETURN_NONE", async () => {
await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
}).toFindNothing()
})
it("should respect limit", async () => {
await expectSearch({
limit: 1,
paginate: true,
query: {},
}).toHaveLength(1)
})
})
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({
equal: { name: "foo" },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ equal: { name: "none" } }).toFindNothing()
})
it("works as an or condition", async () => {
await expectQuery({
allOr: true,
equal: { name: "foo" },
oneOf: { name: ["bar"] },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("can have multiple values for same column", async () => {
await expectQuery({
allOr: true,
equal: { "1:name": "foo", "2:name": "bar" },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
})
describe("non-existent fields", () => {
it("should return 400 when searching for non-existent fields", async () => {
await config.api.row.search(
tableOrViewId,
{
query: {
equal: { nonExistentField: "value" },
},
},
{
status: 400,
body: {
message: expect.stringContaining("nonExistentField"),
},
}
)
})
})
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({
notEqual: { name: "foo" },
}).toContainExactly([{ name: "bar" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
notEqual: { name: "bar" },
}).toContainExactly([{ name: "foo" }])
})
})
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({
oneOf: { name: ["foo"] },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
oneOf: { name: ["none"] },
}).toFindNothing()
})
it("can have multiple values for same column", async () => {
await expectQuery({
oneOf: {
name: ["foo", "bar"],
},
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("splits comma separated strings", async () => {
await expectQuery({
oneOf: {
// @ts-ignore
name: "foo,bar",
},
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("trims whitespace", async () => {
await expectQuery({
oneOf: {
// @ts-ignore
name: "foo, bar",
},
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("empty arrays returns all when onEmptyFilter is set to return 'all'", async () => {
await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
oneOf: { name: [] },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
// onEmptyFilter cannot be sent to view searches
!isView &&
it("empty arrays returns all when onEmptyFilter is set to return 'none'", async () => {
await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
oneOf: { name: [] },
}).toContainExactly([])
})
})
describe("fuzzy", () => {
it("successfully finds a row", async () => {
await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly(
[{ name: "foo" }]
)
})
it("fails to find nonexistent row", async () => {
await expectQuery({ fuzzy: { name: "none" } }).toFindNothing()
})
})
describe("string", () => {
it("successfully finds a row", async () => {
await expectQuery({
string: { name: "fo" },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
string: { name: "none" },
}).toFindNothing()
})
it("is case-insensitive", async () => {
await expectQuery({
string: { name: "FO" },
}).toContainExactly([{ name: "foo" }])
})
it("should not coerce string to date for string columns", async () => {
await expectQuery({
string: { name: "2020-01-01" },
}).toFindNothing()
})
})
describe("range", () => {
it("successfully finds multiple rows", async () => {
await expectQuery({
range: { name: { low: "a", high: "z" } },
}).toContainExactly([{ name: "bar" }, { name: "foo" }])
})
it("successfully finds a row with a high bound", async () => {
await expectQuery({
range: { name: { low: "a", high: "c" } },
}).toContainExactly([{ name: "bar" }])
})
it("successfully finds a row with a low bound", async () => {
await expectQuery({
range: { name: { low: "f", high: "z" } },
}).toContainExactly([{ name: "foo" }])
})
it("successfully finds no rows", async () => {
await expectQuery({
range: { name: { low: "g", high: "h" } },
}).toFindNothing()
})
it("ignores low if it's an empty object", async () => {
await expectQuery({
// @ts-ignore
range: { name: { low: {}, high: "z" } },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("ignores high if it's an empty object", async () => {
await expectQuery({
// @ts-ignore
range: { name: { low: "a", high: {} } },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
})
describe("empty", () => {
it("finds no empty rows", async () => {
await expectQuery({ empty: { name: null } }).toFindNothing()
})
it("should not be affected by when filter empty behaviour", async () => {
await expectQuery({
empty: { name: null },
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
}).toFindNothing()
})
})
describe("notEmpty", () => {
it("finds all non-empty rows", async () => {
await expectQuery({
notEmpty: { name: null },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("should not be affected by when filter empty behaviour", async () => {
await expectQuery({
notEmpty: { name: null },
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
})
describe("sort", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([{ name: "bar" }, { name: "foo" }])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ name: "foo" }, { name: "bar" }])
})
describe("sortType STRING", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "name",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING,
}).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" }])
})
})
!isInternal &&
!isInMemory &&
// This test was added because we automatically add in a sort by the
// primary key, and we used to do this unconditionally which caused
// problems because it was possible for the primary key to appear twice
// in the resulting SQL ORDER BY clause, resulting in an SQL error.
// We now check first to make sure that the primary key isn't already
// in the sort before adding it.
describe("sort on primary key", () => {
beforeAll(async () => {
const tableName = structures.uuid().substring(0, 10)
await client!.schema.createTable(tableName, t => {
t.string("name").primary()
})
const resp = await config.api.datasource.fetchSchema({
datasourceId: datasource!._id!,
})
tableOrViewId = resp.datasource.entities![tableName]._id!
await createRows([{ name: "foo" }, { name: "bar" }])
})
it("should be able to sort by a primary key column ascending", async () =>
expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([{ name: "bar" }, { name: "foo" }]))
it("should be able to sort by a primary key column descending", async () =>
expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([{ name: "foo" }, { name: "bar" }]))
})
})
})
describe("numbers", () => {
beforeAll(async () => {
tableOrViewId = await createTableOrView({
age: { name: "age", type: FieldType.NUMBER },
})
await createRows([{ age: 1 }, { age: 10 }])
})
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ equal: { age: 1 } }).toContainExactly([
{ age: 1 },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ equal: { age: 2 } }).toFindNothing()
})
})
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({ notEqual: { age: 1 } }).toContainExactly([
{ age: 10 },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ notEqual: { age: 10 } }).toContainExactly(
[{ age: 1 }]
)
})
})
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({ oneOf: { age: [1] } }).toContainExactly([
{ age: 1 },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ oneOf: { age: [2] } }).toFindNothing()
})
it("can convert from a string", async () => {
await expectQuery({
oneOf: {
// @ts-ignore
age: "1",
},
}).toContainExactly([{ age: 1 }])
})
it("can find multiple values for same column", async () => {
await expectQuery({
oneOf: {
// @ts-ignore
age: "1,10",
},
}).toContainExactly([{ age: 1 }, { age: 10 }])
})
})
describe("range", () => {
it("successfully finds a row", async () => {
await expectQuery({
range: { age: { low: 1, high: 5 } },
}).toContainExactly([{ age: 1 }])
})
it("successfully finds multiple rows", async () => {
await expectQuery({
range: { age: { low: 1, high: 10 } },
}).toContainExactly([{ age: 1 }, { age: 10 }])
})
it("successfully finds