4123 lines
154 KiB
TypeScript
4123 lines
154 KiB
TypeScript
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||
import { datasourceDescribe } from "../../../integrations/tests/utils"
|
||
import {
|
||
context,
|
||
db as dbCore,
|
||
docIds,
|
||
MAX_VALID_DATE,
|
||
MIN_VALID_DATE,
|
||
setEnv,
|
||
SQLITE_DESIGN_DOC_ID,
|
||
utils,
|
||
withEnv as withCoreEnv,
|
||
} from "@budibase/backend-core"
|
||
|
||
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 _ from "lodash"
|
||
import tk from "timekeeper"
|
||
import { encodeJSBinding } from "@budibase/string-templates"
|
||
import { dataFilters } from "@budibase/shared-core"
|
||
import { Knex } from "knex"
|
||
import { generator, structures, mocks } from "@budibase/backend-core/tests"
|
||
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
|
||
import { generateRowIdField } from "../../../integrations/utils"
|
||
import { cloneDeep } from "lodash/fp"
|
||
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/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 createTable({
|
||
name: { name: opts?.tableName || "name", type: FieldType.STRING },
|
||
})
|
||
|
||
const columnName = opts?.primaryColumn || "productCat"
|
||
//@ts-ignore - API accepts this structure, will build out rest of definition
|
||
const tableId = await createTable({
|
||
name: { name: opts?.tableName || "name", type: FieldType.STRING },
|
||
[columnName]: {
|
||
type: FieldType.LINK,
|
||
relationshipType: type,
|
||
name: columnName,
|
||
fieldName: opts?.otherColumn || "product",
|
||
tableId: relatedTable,
|
||
constraints: {
|
||
type: "array",
|
||
},
|
||
},
|
||
})
|
||
return {
|
||
relatedTable: await config.api.table.get(relatedTable),
|
||
tableId,
|
||
}
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
const ds = await dsProvider()
|
||
datasource = ds.datasource
|
||
client = ds.client
|
||
|
||
config.app = await config.api.application.update(config.getAppId(), {
|
||
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 createTable(schema?: TableSchema) {
|
||
const table = await config.api.table.save(
|
||
tableForDatasource(datasource, { schema })
|
||
)
|
||
return table._id!
|
||
}
|
||
|
||
async function createView(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 (docIds.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", createTable]]
|
||
|
||
if (!isInMemory) {
|
||
tableOrView.push([
|
||
"view",
|
||
async (schema?: TableSchema) => {
|
||
const tableId = await createTable(schema)
|
||
const viewId = await createView(
|
||
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] 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("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 a row with a high bound", async () => {
|
||
await expectQuery({
|
||
range: { age: { low: 5, high: 10 } },
|
||
}).toContainExactly([{ age: 10 }])
|
||
})
|
||
|
||
it("successfully finds no rows", async () => {
|
||
await expectQuery({
|
||
range: { age: { low: 5, high: 9 } },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("greater than equal to", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
age: { low: 10, high: Number.MAX_SAFE_INTEGER },
|
||
},
|
||
}).toContainExactly([{ age: 10 }])
|
||
})
|
||
|
||
it("greater than", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
age: { low: 5, high: Number.MAX_SAFE_INTEGER },
|
||
},
|
||
}).toContainExactly([{ age: 10 }])
|
||
})
|
||
|
||
it("less than equal to", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
age: { high: 1, low: Number.MIN_SAFE_INTEGER },
|
||
},
|
||
}).toContainExactly([{ age: 1 }])
|
||
})
|
||
|
||
it("less than", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
age: { high: 5, low: Number.MIN_SAFE_INTEGER },
|
||
},
|
||
}).toContainExactly([{ age: 1 }])
|
||
})
|
||
})
|
||
|
||
describe("sort", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "age",
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).toMatchExactly([{ age: 1 }, { age: 10 }])
|
||
})
|
||
|
||
it("sorts descending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "age",
|
||
sortOrder: SortOrder.DESCENDING,
|
||
}).toMatchExactly([{ age: 10 }, { age: 1 }])
|
||
})
|
||
})
|
||
|
||
describe("sortType NUMBER", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "age",
|
||
sortType: SortType.NUMBER,
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).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 }])
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("dates", () => {
|
||
const JAN_1ST = "2020-01-01T00:00:00.000Z"
|
||
const JAN_2ND = "2020-01-02T00:00:00.000Z"
|
||
const JAN_5TH = "2020-01-05T00:00:00.000Z"
|
||
const JAN_9TH = "2020-01-09T00:00:00.000Z"
|
||
const JAN_10TH = "2020-01-10T00:00:00.000Z"
|
||
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
dob: { name: "dob", type: FieldType.DATETIME },
|
||
})
|
||
|
||
await createRows([{ dob: JAN_1ST }, { dob: JAN_10TH }])
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
equal: { dob: JAN_1ST },
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: { dob: JAN_1ST },
|
||
}).toContainExactly([{ dob: JAN_10TH }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notEqual: { dob: JAN_10TH },
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { dob: [JAN_1ST] },
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
oneOf: { dob: [JAN_2ND] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("range", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
range: { dob: { low: JAN_1ST, high: JAN_5TH } },
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
|
||
it("successfully finds multiple rows", async () => {
|
||
await expectQuery({
|
||
range: { dob: { low: JAN_1ST, high: JAN_10TH } },
|
||
}).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
|
||
})
|
||
|
||
it("successfully finds a row with a high bound", async () => {
|
||
await expectQuery({
|
||
range: { dob: { low: JAN_5TH, high: JAN_10TH } },
|
||
}).toContainExactly([{ dob: JAN_10TH }])
|
||
})
|
||
|
||
it("successfully finds no rows", async () => {
|
||
await expectQuery({
|
||
range: { dob: { low: JAN_5TH, high: JAN_9TH } },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("greater than equal to", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
dob: {
|
||
low: JAN_10TH,
|
||
high: MAX_VALID_DATE.toISOString(),
|
||
},
|
||
},
|
||
}).toContainExactly([{ dob: JAN_10TH }])
|
||
})
|
||
|
||
it("greater than", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
dob: { low: JAN_5TH, high: MAX_VALID_DATE.toISOString() },
|
||
},
|
||
}).toContainExactly([{ dob: JAN_10TH }])
|
||
})
|
||
|
||
it("less than equal to", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
dob: { high: JAN_1ST, low: MIN_VALID_DATE.toISOString() },
|
||
},
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
|
||
it("less than", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
dob: { high: JAN_5TH, low: MIN_VALID_DATE.toISOString() },
|
||
},
|
||
}).toContainExactly([{ dob: JAN_1ST }])
|
||
})
|
||
})
|
||
|
||
describe("sort", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "dob",
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
|
||
})
|
||
|
||
it("sorts descending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "dob",
|
||
sortOrder: SortOrder.DESCENDING,
|
||
}).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
|
||
})
|
||
|
||
describe("sortType STRING", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "dob",
|
||
sortType: SortType.STRING,
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).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 }])
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
!isInternal &&
|
||
describe("datetime - time only", () => {
|
||
const T_1000 = "10:00:00"
|
||
const T_1045 = "10:45:00"
|
||
const T_1200 = "12:00:00"
|
||
const T_1530 = "15:30:00"
|
||
const T_0000 = "00:00:00"
|
||
|
||
const UNEXISTING_TIME = "10:01:00"
|
||
|
||
const NULL_TIME__ID = `null_time__id`
|
||
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
timeid: { name: "timeid", type: FieldType.STRING },
|
||
time: {
|
||
name: "time",
|
||
type: FieldType.DATETIME,
|
||
timeOnly: true,
|
||
},
|
||
})
|
||
|
||
await createRows([
|
||
{ timeid: NULL_TIME__ID, time: null },
|
||
{ time: T_1000 },
|
||
{ time: T_1045 },
|
||
{ time: T_1200 },
|
||
{ time: T_1530 },
|
||
{ time: T_0000 },
|
||
])
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
equal: { time: T_1000 },
|
||
}).toContainExactly([{ time: "10:00:00" }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
equal: { time: UNEXISTING_TIME },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: { time: T_1000 },
|
||
}).toContainExactly([
|
||
{ timeid: NULL_TIME__ID },
|
||
{ time: "10:45:00" },
|
||
{ time: "12:00:00" },
|
||
{ time: "15:30:00" },
|
||
{ time: "00:00:00" },
|
||
])
|
||
})
|
||
|
||
it("return all when requesting non-existing", async () => {
|
||
await expectQuery({
|
||
notEqual: { time: UNEXISTING_TIME },
|
||
}).toContainExactly([
|
||
{ timeid: NULL_TIME__ID },
|
||
{ time: "10:00:00" },
|
||
{ time: "10:45:00" },
|
||
{ time: "12:00:00" },
|
||
{ time: "15:30:00" },
|
||
{ time: "00:00:00" },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { time: [T_1000] },
|
||
}).toContainExactly([{ time: "10:00:00" }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
oneOf: { time: [UNEXISTING_TIME] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("range", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
range: { time: { low: T_1045, high: T_1045 } },
|
||
}).toContainExactly([{ time: "10:45:00" }])
|
||
})
|
||
|
||
it("successfully finds multiple rows", async () => {
|
||
await expectQuery({
|
||
range: { time: { low: T_1045, high: T_1530 } },
|
||
}).toContainExactly([
|
||
{ time: "10:45:00" },
|
||
{ time: "12:00:00" },
|
||
{ time: "15:30:00" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds no rows", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
time: { low: UNEXISTING_TIME, high: UNEXISTING_TIME },
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("sort", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "time",
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).toMatchExactly([
|
||
{ timeid: NULL_TIME__ID },
|
||
{ time: "00:00:00" },
|
||
{ time: "10:00:00" },
|
||
{ time: "10:45:00" },
|
||
{ time: "12:00:00" },
|
||
{ time: "15:30:00" },
|
||
])
|
||
})
|
||
|
||
it("sorts descending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "time",
|
||
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 },
|
||
])
|
||
})
|
||
|
||
describe("sortType STRING", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "time",
|
||
sortType: SortType.STRING,
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).toMatchExactly([
|
||
{ timeid: NULL_TIME__ID },
|
||
{ time: "00:00:00" },
|
||
{ time: "10:00:00" },
|
||
{ time: "10:45:00" },
|
||
{ time: "12:00: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 },
|
||
])
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("datetime - date only", () => {
|
||
describe.each([true, false])(
|
||
"saved with timestamp: %s",
|
||
saveWithTimestamp => {
|
||
describe.each([true, false])(
|
||
"search with timestamp: %s",
|
||
searchWithTimestamp => {
|
||
const SAVE_SUFFIX = saveWithTimestamp
|
||
? "T00:00:00.000Z"
|
||
: ""
|
||
const SEARCH_SUFFIX = searchWithTimestamp
|
||
? "T00:00:00.000Z"
|
||
: ""
|
||
|
||
const JAN_1ST = `2020-01-01`
|
||
const JAN_10TH = `2020-01-10`
|
||
const JAN_30TH = `2020-01-30`
|
||
const UNEXISTING_DATE = `2020-01-03`
|
||
const NULL_DATE__ID = `null_date__id`
|
||
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
dateid: {
|
||
name: "dateid",
|
||
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}`,
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: {
|
||
date: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||
},
|
||
}).toContainExactly([
|
||
{ date: JAN_10TH },
|
||
{ dateid: NULL_DATE__ID },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notEqual: {
|
||
date: `${JAN_30TH}${SEARCH_SUFFIX}`,
|
||
},
|
||
}).toContainExactly([
|
||
{ date: JAN_1ST },
|
||
{ date: JAN_10TH },
|
||
{ dateid: NULL_DATE__ID },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] },
|
||
}).toContainExactly([{ date: JAN_1ST }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
oneOf: {
|
||
date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`],
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("range", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
date: {
|
||
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||
high: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||
},
|
||
},
|
||
}).toContainExactly([{ date: JAN_1ST }])
|
||
})
|
||
|
||
it("successfully finds multiple rows", async () => {
|
||
await expectQuery({
|
||
range: {
|
||
date: {
|
||
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||
high: `${JAN_10TH}${SEARCH_SUFFIX}`,
|
||
},
|
||
},
|
||
}).toContainExactly([
|
||
{ date: JAN_1ST },
|
||
{ 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 ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "date",
|
||
sortOrder: SortOrder.ASCENDING,
|
||
}).toMatchExactly([
|
||
{ dateid: NULL_DATE__ID },
|
||
{ date: JAN_1ST },
|
||
{ date: JAN_10TH },
|
||
])
|
||
})
|
||
|
||
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 () => {
|
||
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 &&
|
||
!isInMemory &&
|
||
describe("AI Column", () => {
|
||
const UNEXISTING_AI_COLUMN = "Real LLM Response"
|
||
let envCleanup: () => void
|
||
|
||
beforeAll(async () => {
|
||
mocks.licenses.useBudibaseAI()
|
||
mocks.licenses.useAICustomConfigs()
|
||
|
||
envCleanup = setEnv({ OPENAI_API_KEY: "mock" })
|
||
mockChatGPTResponse("Mock LLM Response")
|
||
|
||
tableOrViewId = await createTableOrView({
|
||
product: { name: "product", type: FieldType.STRING },
|
||
ai: {
|
||
name: "AI",
|
||
type: FieldType.AI,
|
||
operation: AIOperationEnum.PROMPT,
|
||
prompt: "Translate '{{ product }}' into German",
|
||
},
|
||
})
|
||
|
||
await createRows([
|
||
{ product: "Big Mac" },
|
||
{ product: "McCrispy" },
|
||
])
|
||
})
|
||
|
||
afterAll(() => {
|
||
envCleanup()
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds rows based on AI column", async () => {
|
||
await expectQuery({
|
||
equal: { ai: "Mock LLM Response" },
|
||
}).toContainExactly([
|
||
{ product: "Big Mac" },
|
||
{ product: "McCrispy" },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
equal: { ai: UNEXISTING_AI_COLUMN },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("Returns nothing when searching notEqual on the mock AI response", async () => {
|
||
await expectQuery({
|
||
notEqual: { ai: "Mock LLM Response" },
|
||
}).toContainExactly([])
|
||
})
|
||
|
||
it("return all when requesting non-existing response", async () => {
|
||
await expectQuery({
|
||
notEqual: { ai: "Real LLM Response" },
|
||
}).toContainExactly([
|
||
{ product: "Big Mac" },
|
||
{ product: "McCrispy" },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: {
|
||
ai: ["Mock LLM Response", "Other LLM Response"],
|
||
},
|
||
}).toContainExactly([
|
||
{ product: "Big Mac" },
|
||
{ product: "McCrispy" },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
oneOf: { ai: ["Whopper"] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("arrays", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
numbers: {
|
||
name: "numbers",
|
||
type: FieldType.ARRAY,
|
||
constraints: {
|
||
type: JsonFieldSubType.ARRAY,
|
||
inclusion: ["one", "two", "three"],
|
||
},
|
||
},
|
||
})
|
||
await createRows([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
|
||
describe("contains", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
contains: { numbers: ["one"] },
|
||
}).toContainExactly([{ numbers: ["one", "two"] }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
contains: { numbers: ["none"] },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("fails to find row containing all", async () => {
|
||
await expectQuery({
|
||
contains: { numbers: ["one", "two", "three"] },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("finds all with empty list", async () => {
|
||
await expectQuery({
|
||
contains: { numbers: [] },
|
||
}).toContainExactly([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("notContains", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notContains: { numbers: ["one"] },
|
||
}).toContainExactly([{ numbers: ["three"] }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notContains: { numbers: ["one", "two", "three"] },
|
||
}).toContainExactly([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
|
||
// Not sure if this is correct behaviour but changing it would be a
|
||
// breaking change.
|
||
it("finds all with empty list", async () => {
|
||
await expectQuery({
|
||
notContains: { numbers: [] },
|
||
}).toContainExactly([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("containsAny", () => {
|
||
it("successfully finds rows", async () => {
|
||
await expectQuery({
|
||
containsAny: { numbers: ["one", "two", "three"] },
|
||
}).toContainExactly([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
containsAny: { numbers: ["none"] },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("finds all with empty list", async () => {
|
||
await expectQuery({
|
||
containsAny: { numbers: [] },
|
||
}).toContainExactly([
|
||
{ numbers: ["one", "two"] },
|
||
{ numbers: ["three"] },
|
||
])
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("bigints", () => {
|
||
const SMALL = "1"
|
||
const MEDIUM = "10000000"
|
||
|
||
// Our bigints are int64s in most datasources.
|
||
let BIG = "9223372036854775807"
|
||
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
num: { name: "num", type: FieldType.BIGINT },
|
||
})
|
||
await createRows([
|
||
{ num: SMALL },
|
||
{ num: MEDIUM },
|
||
{ num: BIG },
|
||
])
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({ equal: { num: SMALL } }).toContainExactly(
|
||
[{ num: SMALL }]
|
||
)
|
||
})
|
||
|
||
it("successfully finds a big value", async () => {
|
||
await expectQuery({ equal: { num: BIG } }).toContainExactly([
|
||
{ num: BIG },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ equal: { num: "2" } }).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: { num: SMALL },
|
||
}).toContainExactly([{ num: MEDIUM }, { num: BIG }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ notEqual: { num: 10 } }).toContainExactly(
|
||
[{ num: SMALL }, { num: MEDIUM }, { num: BIG }]
|
||
)
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { num: [SMALL] },
|
||
}).toContainExactly([{ num: SMALL }])
|
||
})
|
||
|
||
it("successfully finds all rows", async () => {
|
||
await expectQuery({
|
||
oneOf: { num: [SMALL, MEDIUM, BIG] },
|
||
}).toContainExactly([
|
||
{ num: SMALL },
|
||
{ num: MEDIUM },
|
||
{ num: BIG },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ oneOf: { num: [2] } }).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("range", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
range: { num: { low: SMALL, high: "5" } },
|
||
}).toContainExactly([{ num: SMALL }])
|
||
})
|
||
|
||
it("successfully finds multiple rows", async () => {
|
||
await expectQuery({
|
||
range: { num: { low: SMALL, high: MEDIUM } },
|
||
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }])
|
||
})
|
||
|
||
it("successfully finds a row with a high bound", async () => {
|
||
await expectQuery({
|
||
range: { num: { low: MEDIUM, high: BIG } },
|
||
}).toContainExactly([{ num: MEDIUM }, { num: BIG }])
|
||
})
|
||
|
||
it("successfully finds no rows", async () => {
|
||
await expectQuery({
|
||
range: { num: { low: "5", high: "5" } },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("can search using just a low value", async () => {
|
||
await expectQuery({
|
||
range: { num: { low: MEDIUM } },
|
||
}).toContainExactly([{ num: MEDIUM }, { num: BIG }])
|
||
})
|
||
|
||
it("can search using just a high value", async () => {
|
||
await expectQuery({
|
||
range: { num: { high: MEDIUM } },
|
||
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }])
|
||
})
|
||
})
|
||
})
|
||
|
||
isInternal &&
|
||
describe("auto", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
auto: {
|
||
name: "auto",
|
||
type: FieldType.AUTO,
|
||
autocolumn: true,
|
||
subtype: AutoFieldSubType.AUTO_ID,
|
||
},
|
||
})
|
||
await createRows(new Array(10).fill({}))
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({ equal: { auto: 1 } }).toContainExactly([
|
||
{ auto: 1 },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ equal: { auto: 0 } }).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("not equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: { auto: 1 },
|
||
}).toContainExactly([
|
||
{ auto: 2 },
|
||
{ auto: 3 },
|
||
{ auto: 4 },
|
||
{ auto: 5 },
|
||
{ auto: 6 },
|
||
{ auto: 7 },
|
||
{ auto: 8 },
|
||
{ auto: 9 },
|
||
{ auto: 10 },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notEqual: { auto: 0 },
|
||
}).toContainExactly([
|
||
{ auto: 1 },
|
||
{ auto: 2 },
|
||
{ auto: 3 },
|
||
{ auto: 4 },
|
||
{ auto: 5 },
|
||
{ auto: 6 },
|
||
{ auto: 7 },
|
||
{ auto: 8 },
|
||
{ auto: 9 },
|
||
{ auto: 10 },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { auto: [1] },
|
||
}).toContainExactly([{ auto: 1 }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({ oneOf: { auto: [0] } }).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("range", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
range: { auto: { low: 1, high: 1 } },
|
||
}).toContainExactly([{ auto: 1 }])
|
||
})
|
||
|
||
it("successfully finds multiple rows", async () => {
|
||
await expectQuery({
|
||
range: { auto: { low: 1, high: 2 } },
|
||
}).toContainExactly([{ auto: 1 }, { auto: 2 }])
|
||
})
|
||
|
||
it("successfully finds a row with a high bound", async () => {
|
||
await expectQuery({
|
||
range: { auto: { low: 2, high: 2 } },
|
||
}).toContainExactly([{ auto: 2 }])
|
||
})
|
||
|
||
it("successfully finds no rows", async () => {
|
||
await expectQuery({
|
||
range: { auto: { low: 0, high: 0 } },
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("can search using just a low value", async () => {
|
||
await expectQuery({
|
||
range: { auto: { low: 9 } },
|
||
}).toContainExactly([{ auto: 9 }, { auto: 10 }])
|
||
})
|
||
|
||
it("can search using just a high value", async () => {
|
||
await expectQuery({
|
||
range: { auto: { high: 2 } },
|
||
}).toContainExactly([{ auto: 1 }, { auto: 2 }])
|
||
})
|
||
})
|
||
|
||
describe("sort", () => {
|
||
it("sorts ascending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "auto",
|
||
sortOrder: SortOrder.ASCENDING,
|
||
sortType: SortType.NUMBER,
|
||
}).toMatchExactly([
|
||
{ auto: 1 },
|
||
{ auto: 2 },
|
||
{ auto: 3 },
|
||
{ auto: 4 },
|
||
{ auto: 5 },
|
||
{ auto: 6 },
|
||
{ auto: 7 },
|
||
{ auto: 8 },
|
||
{ auto: 9 },
|
||
{ auto: 10 },
|
||
])
|
||
})
|
||
|
||
it("sorts descending", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
sort: "auto",
|
||
sortOrder: SortOrder.DESCENDING,
|
||
sortType: SortType.NUMBER,
|
||
}).toMatchExactly([
|
||
{ auto: 10 },
|
||
{ auto: 9 },
|
||
{ auto: 8 },
|
||
{ auto: 7 },
|
||
{ auto: 6 },
|
||
{ auto: 5 },
|
||
{ auto: 4 },
|
||
{ auto: 3 },
|
||
{ auto: 2 },
|
||
{ auto: 1 },
|
||
])
|
||
})
|
||
|
||
// This is important for pagination. The order of results must always
|
||
// be stable or pagination will break. We don't want the user to need
|
||
// to specify an order for pagination to work.
|
||
it("is stable without a sort specified", async () => {
|
||
let { rows: fullRowList } = await config.api.row.search(
|
||
tableOrViewId,
|
||
{
|
||
tableId: tableOrViewId,
|
||
query: {},
|
||
}
|
||
)
|
||
|
||
// repeat the search many times to check the first row is always the same
|
||
let bookmark: string | number | undefined,
|
||
hasNextPage: boolean | undefined = true,
|
||
rowCount = 0
|
||
do {
|
||
const response = await config.api.row.search(
|
||
tableOrViewId,
|
||
{
|
||
tableId: tableOrViewId,
|
||
limit: 1,
|
||
paginate: true,
|
||
query: {},
|
||
bookmark,
|
||
}
|
||
)
|
||
bookmark = response.bookmark
|
||
hasNextPage = response.hasNextPage
|
||
expect(response.rows.length).toEqual(1)
|
||
const foundRow = response.rows[0]
|
||
expect(foundRow).toEqual(fullRowList[rowCount++])
|
||
} while (hasNextPage)
|
||
})
|
||
})
|
||
|
||
describe("pagination", () => {
|
||
it("should paginate through all rows", async () => {
|
||
// @ts-ignore
|
||
let bookmark: string | number = undefined
|
||
let rows: Row[] = []
|
||
|
||
while (true) {
|
||
const response = await config.api.row.search(
|
||
tableOrViewId,
|
||
{
|
||
tableId: tableOrViewId,
|
||
limit: 3,
|
||
query: {},
|
||
bookmark,
|
||
paginate: true,
|
||
}
|
||
)
|
||
|
||
rows.push(...response.rows)
|
||
|
||
if (!response.bookmark || !response.hasNextPage) {
|
||
break
|
||
}
|
||
bookmark = response.bookmark
|
||
}
|
||
|
||
const autoValues = rows
|
||
.map(row => row.auto)
|
||
.sort((a, b) => a - b)
|
||
expect(autoValues).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("field name 1:name", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
"1:name": { name: "1:name", type: FieldType.STRING },
|
||
})
|
||
await createRows([{ "1:name": "bar" }, { "1:name": "foo" }])
|
||
})
|
||
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
equal: { "1:1:name": "bar" },
|
||
}).toContainExactly([{ "1:name": "bar" }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
equal: { "1:1:name": "none" },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe("related formulas", () => {
|
||
beforeAll(async () => {
|
||
const arrayTable = await createTable({
|
||
name: { name: "name", type: FieldType.STRING },
|
||
array: {
|
||
name: "array",
|
||
type: FieldType.ARRAY,
|
||
constraints: {
|
||
type: JsonFieldSubType.ARRAY,
|
||
inclusion: ["option 1", "option 2"],
|
||
},
|
||
},
|
||
})
|
||
tableOrViewId = await createTableOrView({
|
||
relationship: {
|
||
type: FieldType.LINK,
|
||
relationshipType: RelationshipType.MANY_TO_ONE,
|
||
name: "relationship",
|
||
fieldName: "relate",
|
||
tableId: arrayTable,
|
||
constraints: {
|
||
type: "array",
|
||
},
|
||
},
|
||
formula: {
|
||
type: FieldType.FORMULA,
|
||
name: "formula",
|
||
formula: encodeJSBinding(
|
||
`let array = [];$("relationship").forEach(rel => array = array.concat(rel.array));return array.sort().join(",")`
|
||
),
|
||
},
|
||
})
|
||
const arrayRows = await Promise.all([
|
||
config.api.row.save(arrayTable, {
|
||
name: "foo",
|
||
array: ["option 1"],
|
||
}),
|
||
config.api.row.save(arrayTable, {
|
||
name: "bar",
|
||
array: ["option 2"],
|
||
}),
|
||
])
|
||
await Promise.all([
|
||
config.api.row.save(tableOrViewId, {
|
||
relationship: [arrayRows[0]._id, arrayRows[1]._id],
|
||
}),
|
||
])
|
||
})
|
||
|
||
it("formula is correct with relationship arrays", async () => {
|
||
await expectQuery({}).toContain([
|
||
{ formula: "option 1,option 2" },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("user", () => {
|
||
let user1: User
|
||
let user2: User
|
||
|
||
beforeAll(async () => {
|
||
user1 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||
user2 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||
|
||
tableOrViewId = await createTableOrView({
|
||
user: {
|
||
name: "user",
|
||
type: FieldType.BB_REFERENCE_SINGLE,
|
||
subtype: BBReferenceFieldSubType.USER,
|
||
},
|
||
})
|
||
|
||
await createRows([
|
||
{ user: user1 },
|
||
{ user: user2 },
|
||
{ user: null },
|
||
])
|
||
})
|
||
|
||
describe("equal", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
equal: { user: user1._id },
|
||
}).toContainExactly([{ user: { _id: user1._id } }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
equal: { user: "us_none" },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notEqual", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notEqual: { user: user1._id },
|
||
}).toContainExactly([{ user: { _id: user2._id } }, {}])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notEqual: { user: "us_none" },
|
||
}).toContainExactly([
|
||
{ user: { _id: user1._id } },
|
||
{ user: { _id: user2._id } },
|
||
{},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("oneOf", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
oneOf: { user: [user1._id] },
|
||
}).toContainExactly([{ user: { _id: user1._id } }])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
oneOf: { user: ["us_none"] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("empty", () => {
|
||
it("finds empty rows", async () => {
|
||
await expectQuery({ empty: { user: null } }).toContainExactly(
|
||
[{}]
|
||
)
|
||
})
|
||
})
|
||
|
||
describe("notEmpty", () => {
|
||
it("finds non-empty rows", async () => {
|
||
await expectQuery({
|
||
notEmpty: { user: null },
|
||
}).toContainExactly([
|
||
{ user: { _id: user1._id } },
|
||
{ user: { _id: user2._id } },
|
||
])
|
||
})
|
||
})
|
||
})
|
||
|
||
describe("multi user", () => {
|
||
let user1: User
|
||
let user2: User
|
||
|
||
beforeAll(async () => {
|
||
user1 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||
user2 = await config.createUser({ _id: `us_${utils.newid()}` })
|
||
|
||
tableOrViewId = await createTableOrView({
|
||
users: {
|
||
name: "users",
|
||
type: FieldType.BB_REFERENCE,
|
||
subtype: BBReferenceFieldSubType.USER,
|
||
constraints: { type: "array" },
|
||
},
|
||
number: {
|
||
name: "number",
|
||
type: FieldType.NUMBER,
|
||
},
|
||
})
|
||
|
||
await createRows([
|
||
{ number: 1, users: [user1] },
|
||
{ number: 2, users: [user2] },
|
||
{ number: 3, users: [user1, user2] },
|
||
{ number: 4, users: [] },
|
||
])
|
||
})
|
||
|
||
describe("contains", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
contains: { users: [user1._id] },
|
||
}).toContainExactly([
|
||
{ users: [{ _id: user1._id }] },
|
||
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||
])
|
||
})
|
||
|
||
it("successfully finds a row searching with a string", async () => {
|
||
await expectQuery({
|
||
// @ts-expect-error this test specifically goes against the type to
|
||
// test that we coerce the string to an array.
|
||
contains: { "1:users": user1._id },
|
||
}).toContainExactly([
|
||
{ users: [{ _id: user1._id }] },
|
||
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
contains: { users: ["us_none"] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("notContains", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
notContains: { users: [user1._id] },
|
||
}).toContainExactly([{ users: [{ _id: user2._id }] }, {}])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
notContains: { users: ["us_none"] },
|
||
}).toContainExactly([
|
||
{ users: [{ _id: user1._id }] },
|
||
{ users: [{ _id: user2._id }] },
|
||
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||
{},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("containsAny", () => {
|
||
it("successfully finds rows", async () => {
|
||
await expectQuery({
|
||
containsAny: { users: [user1._id, user2._id] },
|
||
}).toContainExactly([
|
||
{ users: [{ _id: user1._id }] },
|
||
{ users: [{ _id: user2._id }] },
|
||
{ users: [{ _id: user1._id }, { _id: user2._id }] },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
containsAny: { users: ["us_none"] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
|
||
describe("multi-column equals", () => {
|
||
it("successfully finds a row", async () => {
|
||
await expectQuery({
|
||
equal: { number: 1 },
|
||
contains: { users: [user1._id] },
|
||
}).toContainExactly([
|
||
{ users: [{ _id: user1._id }], number: 1 },
|
||
])
|
||
})
|
||
|
||
it("fails to find nonexistent row", async () => {
|
||
await expectQuery({
|
||
equal: { number: 2 },
|
||
contains: { users: [user1._id] },
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
})
|
||
|
||
// It also can't work for in-memory searching because the related table name
|
||
// isn't available.
|
||
!isInMemory &&
|
||
describe.each([
|
||
RelationshipType.ONE_TO_MANY,
|
||
RelationshipType.MANY_TO_ONE,
|
||
RelationshipType.MANY_TO_MANY,
|
||
])("relations (%s)", relationshipType => {
|
||
let productCategoryTable: Table, productCatRows: Row[]
|
||
|
||
beforeAll(async () => {
|
||
const { relatedTable, tableId } =
|
||
await basicRelationshipTables(relationshipType)
|
||
tableOrViewId = tableId
|
||
productCategoryTable = relatedTable
|
||
|
||
productCatRows = await Promise.all([
|
||
config.api.row.save(productCategoryTable._id!, {
|
||
name: "foo",
|
||
}),
|
||
config.api.row.save(productCategoryTable._id!, {
|
||
name: "bar",
|
||
}),
|
||
])
|
||
|
||
await Promise.all([
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "foo",
|
||
productCat: [productCatRows[0]._id],
|
||
}),
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "bar",
|
||
productCat: [productCatRows[1]._id],
|
||
}),
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "baz",
|
||
productCat: [],
|
||
}),
|
||
])
|
||
})
|
||
|
||
it("should be able to filter by relationship using column name", async () => {
|
||
await expectQuery({
|
||
equal: { ["productCat.name"]: "foo" },
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should be able to filter by relationship using table name", async () => {
|
||
await expectQuery({
|
||
equal: { [`${productCategoryTable.name}.name`]: "foo" },
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("shouldn't return any relationship for last row", async () => {
|
||
await expectQuery({
|
||
equal: { ["name"]: "baz" },
|
||
}).toContainExactly([{ name: "baz", productCat: undefined }])
|
||
})
|
||
|
||
describe("logical filters", () => {
|
||
const logicalOperators = [
|
||
LogicalOperator.AND,
|
||
LogicalOperator.OR,
|
||
]
|
||
|
||
describe("$and", () => {
|
||
it("should allow single conditions", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should allow exclusive conditions", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([])
|
||
})
|
||
|
||
it.each([logicalOperators])(
|
||
"should allow nested ands with single conditions (with %s as root)",
|
||
async rootOperator => {
|
||
await expectQuery({
|
||
[rootOperator]: {
|
||
conditions: [
|
||
{
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
}
|
||
)
|
||
|
||
it.each([logicalOperators])(
|
||
"should allow nested ands with exclusive conditions (with %s as root)",
|
||
async rootOperator => {
|
||
await expectQuery({
|
||
[rootOperator]: {
|
||
conditions: [
|
||
{
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([])
|
||
}
|
||
)
|
||
|
||
it.each([logicalOperators])(
|
||
"should allow nested ands with multiple conditions (with %s as root)",
|
||
async rootOperator => {
|
||
await expectQuery({
|
||
[rootOperator]: {
|
||
conditions: [
|
||
{
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([])
|
||
}
|
||
)
|
||
})
|
||
|
||
describe("$ors", () => {
|
||
it("should allow single conditions", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should allow exclusive conditions", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
{
|
||
name: "bar",
|
||
productCat: [{ _id: productCatRows[1]._id }],
|
||
},
|
||
{ name: "baz", productCat: undefined },
|
||
])
|
||
})
|
||
|
||
it.each([logicalOperators])(
|
||
"should allow nested ors with single conditions (with %s as root)",
|
||
async rootOperator => {
|
||
await expectQuery({
|
||
[rootOperator]: {
|
||
conditions: [
|
||
{
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
])
|
||
}
|
||
)
|
||
|
||
it.each([logicalOperators])(
|
||
"should allow nested ors with exclusive conditions (with %s as root)",
|
||
async rootOperator => {
|
||
await expectQuery({
|
||
[rootOperator]: {
|
||
conditions: [
|
||
{
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
{
|
||
name: "bar",
|
||
productCat: [{ _id: productCatRows[1]._id }],
|
||
},
|
||
{ name: "baz", productCat: undefined },
|
||
])
|
||
}
|
||
)
|
||
|
||
it("should allow nested ors with multiple conditions", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
equal: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
notEqual: { ["productCat.name"]: "foo" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
productCat: [{ _id: productCatRows[0]._id }],
|
||
},
|
||
{
|
||
name: "bar",
|
||
productCat: [{ _id: productCatRows[1]._id }],
|
||
},
|
||
{ name: "baz", productCat: undefined },
|
||
])
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe("relationship - table with spaces", () => {
|
||
let primaryTable: Table, row: Row
|
||
|
||
beforeAll(async () => {
|
||
const { relatedTable, tableId } =
|
||
await basicRelationshipTables(
|
||
RelationshipType.ONE_TO_MANY,
|
||
{
|
||
tableName: "table with spaces",
|
||
primaryColumn: "related",
|
||
otherColumn: "related",
|
||
}
|
||
)
|
||
tableOrViewId = tableId
|
||
primaryTable = relatedTable
|
||
|
||
row = await config.api.row.save(primaryTable._id!, {
|
||
name: "foo",
|
||
})
|
||
|
||
await config.api.row.save(tableOrViewId, {
|
||
name: "foo",
|
||
related: [row._id],
|
||
})
|
||
})
|
||
|
||
it("should be able to search by table name with spaces", async () => {
|
||
await expectQuery({
|
||
equal: {
|
||
["table with spaces.name"]: "foo",
|
||
},
|
||
}).toContain([{ name: "foo" }])
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe.each([
|
||
RelationshipType.MANY_TO_ONE,
|
||
RelationshipType.MANY_TO_MANY,
|
||
])("big relations (%s)", relationshipType => {
|
||
beforeAll(async () => {
|
||
const { relatedTable, tableId } =
|
||
await basicRelationshipTables(relationshipType)
|
||
tableOrViewId = tableId
|
||
const mainRow = await config.api.row.save(tableOrViewId, {
|
||
name: "foo",
|
||
})
|
||
for (let i = 0; i < 11; i++) {
|
||
await config.api.row.save(relatedTable._id!, {
|
||
name: i,
|
||
product: [mainRow._id!],
|
||
})
|
||
}
|
||
})
|
||
|
||
it("can only pull 10 related rows", async () => {
|
||
await withCoreEnv(
|
||
{ SQL_MAX_RELATED_ROWS: "10" },
|
||
async () => {
|
||
const response = await expectQuery({}).toContain([
|
||
{ name: "foo" },
|
||
])
|
||
expect(response.rows[0].productCat).toBeArrayOfSize(10)
|
||
}
|
||
)
|
||
})
|
||
|
||
it("can pull max rows when env not set (defaults to 500)", async () => {
|
||
const response = await expectQuery({}).toContain([
|
||
{ name: "foo" },
|
||
])
|
||
expect(response.rows[0].productCat).toBeArrayOfSize(11)
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe("relations to same table", () => {
|
||
let relatedTable: string, relatedRows: Row[]
|
||
|
||
beforeAll(async () => {
|
||
relatedTable = await createTable({
|
||
name: { name: "name", type: FieldType.STRING },
|
||
})
|
||
tableOrViewId = await createTableOrView({
|
||
name: { name: "name", type: FieldType.STRING },
|
||
related1: {
|
||
type: FieldType.LINK,
|
||
name: "related1",
|
||
fieldName: "main1",
|
||
tableId: relatedTable,
|
||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||
},
|
||
related2: {
|
||
type: FieldType.LINK,
|
||
name: "related2",
|
||
fieldName: "main2",
|
||
tableId: relatedTable,
|
||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||
},
|
||
})
|
||
relatedRows = await Promise.all([
|
||
config.api.row.save(relatedTable, { name: "foo" }),
|
||
config.api.row.save(relatedTable, { name: "bar" }),
|
||
config.api.row.save(relatedTable, { name: "baz" }),
|
||
config.api.row.save(relatedTable, { name: "boo" }),
|
||
])
|
||
await Promise.all([
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "test",
|
||
related1: [relatedRows[0]._id!],
|
||
related2: [relatedRows[1]._id!],
|
||
}),
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "test2",
|
||
related1: [relatedRows[2]._id!],
|
||
related2: [relatedRows[3]._id!],
|
||
}),
|
||
config.api.row.save(tableOrViewId, {
|
||
name: "test3",
|
||
related1: [relatedRows[1]._id],
|
||
related2: [relatedRows[2]._id!],
|
||
}),
|
||
])
|
||
})
|
||
|
||
it("should be able to relate to same table", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
}).toContainExactly([
|
||
{
|
||
name: "test",
|
||
related1: [{ _id: relatedRows[0]._id }],
|
||
related2: [{ _id: relatedRows[1]._id }],
|
||
},
|
||
{
|
||
name: "test2",
|
||
related1: [{ _id: relatedRows[2]._id }],
|
||
related2: [{ _id: relatedRows[3]._id }],
|
||
},
|
||
{
|
||
name: "test3",
|
||
related1: [{ _id: relatedRows[1]._id }],
|
||
related2: [{ _id: relatedRows[2]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should be able to filter via the first relation field with equal", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: {
|
||
["related1.name"]: "baz",
|
||
},
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "test2",
|
||
related1: [{ _id: relatedRows[2]._id }],
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should be able to filter via the second relation field with not equal", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
notEqual: {
|
||
["1:related2.name"]: "foo",
|
||
["2:related2.name"]: "baz",
|
||
["3:related2.name"]: "boo",
|
||
},
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "test",
|
||
},
|
||
])
|
||
})
|
||
|
||
it("should be able to filter on both fields", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
notEqual: {
|
||
["related1.name"]: "foo",
|
||
["related2.name"]: "baz",
|
||
},
|
||
},
|
||
}).toContainExactly([
|
||
{
|
||
name: "test2",
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
isInternal &&
|
||
describe("no column error backwards compat", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
})
|
||
|
||
it("shouldn't error when column doesn't exist", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
string: {
|
||
"1:something": "a",
|
||
},
|
||
},
|
||
}).toMatch({ rows: [] })
|
||
})
|
||
})
|
||
|
||
describe("row counting", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([{ name: "a" }, { name: "b" }])
|
||
})
|
||
|
||
it("should be able to count rows when option set", async () => {
|
||
await expectSearch({
|
||
countRows: true,
|
||
query: {
|
||
notEmpty: {
|
||
name: true,
|
||
},
|
||
},
|
||
}).toMatch({ totalRows: 2, rows: expect.any(Array) })
|
||
})
|
||
|
||
it("shouldn't count rows when option is not set", async () => {
|
||
await expectSearch({
|
||
countRows: false,
|
||
query: {
|
||
notEmpty: {
|
||
name: true,
|
||
},
|
||
},
|
||
}).toNotHaveProperty(["totalRows"])
|
||
})
|
||
})
|
||
|
||
describe("Invalid column definitions", () => {
|
||
beforeAll(async () => {
|
||
// need to create an invalid table - means ignoring typescript
|
||
tableOrViewId = await createTableOrView({
|
||
// @ts-ignore
|
||
invalid: {
|
||
type: FieldType.STRING,
|
||
},
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([
|
||
{ name: "foo", invalid: "id1" },
|
||
{ name: "bar", invalid: "id2" },
|
||
])
|
||
})
|
||
|
||
it("can get rows with all table data", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
}).toContain([
|
||
{ name: "foo", invalid: "id1" },
|
||
{ name: "bar", invalid: "id2" },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe.each([
|
||
"data_name_test",
|
||
"name_data_test",
|
||
"name_test_data_",
|
||
])("special (%s) case", column => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
[column]: {
|
||
name: column,
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([{ [column]: "a" }, { [column]: "b" }])
|
||
})
|
||
|
||
it("should be able to query a column with data_ in it", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: {
|
||
[`1:${column}`]: "a",
|
||
},
|
||
},
|
||
}).toContainExactly([{ [column]: "a" }])
|
||
})
|
||
})
|
||
|
||
isInternal &&
|
||
describe("sample data", () => {
|
||
beforeAll(async () => {
|
||
await config.api.application.addSampleData(config.appId!)
|
||
tableOrViewId = DEFAULT_EMPLOYEE_TABLE_SCHEMA._id!
|
||
rows = await config.api.row.fetch(tableOrViewId)
|
||
})
|
||
|
||
it("should be able to search sample data", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
}).toContain([
|
||
{
|
||
"First Name": "Mandy",
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe.each([
|
||
{
|
||
low: "2024-07-03T00:00:00.000Z",
|
||
high: "9999-00-00T00:00:00.000Z",
|
||
},
|
||
{
|
||
low: "2024-07-03T00:00:00.000Z",
|
||
high: "9998-00-00T00:00:00.000Z",
|
||
},
|
||
{
|
||
low: "0000-00-00T00:00:00.000Z",
|
||
high: "2024-07-04T00:00:00.000Z",
|
||
},
|
||
{
|
||
low: "0001-00-00T00:00:00.000Z",
|
||
high: "2024-07-04T00:00:00.000Z",
|
||
},
|
||
])("date special cases", ({ low, high }) => {
|
||
const earlyDate = "2024-07-03T10:00:00.000Z",
|
||
laterDate = "2024-07-03T11:00:00.000Z"
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
date: {
|
||
name: "date",
|
||
type: FieldType.DATETIME,
|
||
},
|
||
})
|
||
await createRows([{ date: earlyDate }, { date: laterDate }])
|
||
})
|
||
|
||
it("should be able to handle a date search", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
range: {
|
||
"1:date": { low, high },
|
||
},
|
||
},
|
||
}).toContainExactly([{ date: earlyDate }, { date: laterDate }])
|
||
})
|
||
})
|
||
|
||
describe.each([
|
||
"名前", // Japanese for "name"
|
||
"Benutzer-ID", // German for "user ID", includes a hyphen
|
||
"numéro", // French for "number", includes an accent
|
||
"år", // Swedish for "year", includes a ring above
|
||
"naïve", // English word borrowed from French, includes an umlaut
|
||
"الاسم", // Arabic for "name"
|
||
"оплата", // Russian for "payment"
|
||
"पता", // Hindi for "address"
|
||
"用戶名", // Chinese for "username"
|
||
"çalışma_zamanı", // Turkish for "runtime", includes an underscore and a cedilla
|
||
"preço", // Portuguese for "price", includes a cedilla
|
||
"사용자명", // Korean for "username"
|
||
"usuario_ñoño", // Spanish, uses an underscore and includes "ñ"
|
||
"файл", // Bulgarian for "file"
|
||
"δεδομένα", // Greek for "data"
|
||
"geändert_am", // German for "modified on", includes an umlaut
|
||
"ব্যবহারকারীর_নাম", // Bengali for "user name", includes an underscore
|
||
"São_Paulo", // Portuguese, includes an underscore and a tilde
|
||
"età", // Italian for "age", includes an accent
|
||
"ชื่อผู้ใช้", // Thai for "username"
|
||
])("non-ascii column name: %s", name => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
[name]: {
|
||
name,
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([{ [name]: "a" }, { [name]: "b" }])
|
||
})
|
||
|
||
it("should be able to query a column with non-ascii characters", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: {
|
||
[`1:${name}`]: "a",
|
||
},
|
||
},
|
||
}).toContainExactly([{ [name]: "a" }])
|
||
})
|
||
})
|
||
|
||
// This is currently not supported in external datasources, it produces SQL
|
||
// errors at time of writing. We supported it (potentially by accident) in
|
||
// Lucene, though, so we need to make sure it's supported in SQS as well. We
|
||
// found real cases in production of column names ending in a space.
|
||
isInternal &&
|
||
describe("space at end of column name", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
"name ": {
|
||
name: "name ",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }])
|
||
})
|
||
|
||
it("should be able to query a column that ends with a space", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
string: {
|
||
"name ": "foo",
|
||
},
|
||
},
|
||
}).toContainExactly([{ ["name "]: "foo" }])
|
||
})
|
||
|
||
it("should be able to query a column that ends with a space using numeric notation", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
string: {
|
||
"1:name ": "foo",
|
||
},
|
||
},
|
||
}).toContainExactly([{ ["name "]: "foo" }])
|
||
})
|
||
})
|
||
|
||
isInternal &&
|
||
describe("space at start of column name", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
" name": {
|
||
name: " name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await createRows([{ [" name"]: "foo" }, { [" name"]: "bar" }])
|
||
})
|
||
|
||
it("should be able to query a column that starts with a space", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
string: {
|
||
" name": "foo",
|
||
},
|
||
},
|
||
}).toContainExactly([{ [" name"]: "foo" }])
|
||
})
|
||
|
||
it("should be able to query a column that starts with a space using numeric notation", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
string: {
|
||
"1: name": "foo",
|
||
},
|
||
},
|
||
}).toContainExactly([{ [" name"]: "foo" }])
|
||
})
|
||
})
|
||
|
||
isInternal &&
|
||
!isView &&
|
||
describe("duplicate columns", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
await context.doInAppContext(config.getAppId(), async () => {
|
||
const db = context.getAppDB()
|
||
const tableDoc = await db.get<Table>(tableOrViewId)
|
||
tableDoc.schema.Name = {
|
||
name: "Name",
|
||
type: FieldType.STRING,
|
||
}
|
||
try {
|
||
// remove the SQLite definitions so that they can be rebuilt as part of the search
|
||
const sqliteDoc = await db.get(SQLITE_DESIGN_DOC_ID)
|
||
await db.remove(sqliteDoc)
|
||
} catch (err) {
|
||
// no-op
|
||
}
|
||
})
|
||
await createRows([{ name: "foo", Name: "bar" }])
|
||
})
|
||
|
||
it("should handle invalid duplicate column names", async () => {
|
||
await expectSearch({
|
||
query: {},
|
||
}).toContainExactly([{ name: "foo" }])
|
||
})
|
||
})
|
||
|
||
!isInMemory &&
|
||
describe("search by _id", () => {
|
||
let row: Row
|
||
|
||
beforeAll(async () => {
|
||
const toRelateTable = await createTable({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
tableOrViewId = await createTableOrView({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
rel: {
|
||
name: "rel",
|
||
type: FieldType.LINK,
|
||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||
tableId: toRelateTable,
|
||
fieldName: "rel",
|
||
},
|
||
})
|
||
const [row1, row2] = await Promise.all([
|
||
config.api.row.save(toRelateTable, { name: "tag 1" }),
|
||
config.api.row.save(toRelateTable, { name: "tag 2" }),
|
||
])
|
||
row = await config.api.row.save(tableOrViewId, {
|
||
name: "product 1",
|
||
rel: [row1._id, row2._id],
|
||
})
|
||
})
|
||
|
||
it("can filter by the row ID with limit 1", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: { _id: row._id },
|
||
},
|
||
limit: 1,
|
||
}).toContainExactly([row])
|
||
})
|
||
|
||
isInternal &&
|
||
describe("search by _id for relations", () => {
|
||
it("can filter by the related _id", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: { "rel._id": row.rel[0]._id },
|
||
},
|
||
}).toContainExactly([row])
|
||
|
||
await expectSearch({
|
||
query: {
|
||
equal: { "rel._id": row.rel[1]._id },
|
||
},
|
||
}).toContainExactly([row])
|
||
})
|
||
|
||
it("can filter by the related _id and find nothing", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: { "rel._id": "rel_none" },
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
})
|
||
})
|
||
|
||
!isInternal &&
|
||
describe("search by composite key", () => {
|
||
beforeAll(async () => {
|
||
const table = await config.api.table.save(
|
||
tableForDatasource(datasource, {
|
||
schema: {
|
||
idColumn1: {
|
||
name: "idColumn1",
|
||
type: FieldType.NUMBER,
|
||
},
|
||
idColumn2: {
|
||
name: "idColumn2",
|
||
type: FieldType.NUMBER,
|
||
},
|
||
},
|
||
primary: ["idColumn1", "idColumn2"],
|
||
})
|
||
)
|
||
tableOrViewId = table._id!
|
||
await createRows([{ idColumn1: 1, idColumn2: 2 }])
|
||
})
|
||
|
||
it("can filter by the row ID with limit 1", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
equal: { _id: generateRowIdField([1, 2]) },
|
||
},
|
||
limit: 1,
|
||
}).toContain([
|
||
{
|
||
idColumn1: 1,
|
||
idColumn2: 2,
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe("primaryDisplay", () => {
|
||
beforeAll(async () => {
|
||
let toRelateTableId = await createTable({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
tableOrViewId = await createTableOrView({
|
||
name: {
|
||
name: "name",
|
||
type: FieldType.STRING,
|
||
},
|
||
link: {
|
||
name: "link",
|
||
type: FieldType.LINK,
|
||
relationshipType: RelationshipType.MANY_TO_ONE,
|
||
tableId: toRelateTableId,
|
||
fieldName: "main",
|
||
},
|
||
})
|
||
|
||
const toRelateTable = await config.api.table.get(
|
||
toRelateTableId
|
||
)
|
||
await config.api.table.save({
|
||
...toRelateTable,
|
||
primaryDisplay: "name",
|
||
})
|
||
const relatedRows = await Promise.all([
|
||
config.api.row.save(toRelateTable._id!, {
|
||
name: "related",
|
||
}),
|
||
])
|
||
await config.api.row.save(tableOrViewId, {
|
||
name: "test",
|
||
link: relatedRows.map(row => row._id),
|
||
})
|
||
})
|
||
|
||
it("should be able to query, primary display on related table shouldn't be used", async () => {
|
||
// this test makes sure that if a relationship has been specified as the primary display on a table
|
||
// it is ignored and another column is used instead
|
||
await expectQuery({}).toContain([
|
||
{ name: "test", link: [{ primaryDisplay: "related" }] },
|
||
])
|
||
})
|
||
})
|
||
|
||
describe("$and", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
age: { name: "age", type: FieldType.NUMBER },
|
||
name: { name: "name", type: FieldType.STRING },
|
||
})
|
||
await createRows([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 10, name: "Jack" },
|
||
{ age: 7, name: "Hanna" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds a row for one level condition", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ equal: { age: 10 } },
|
||
{ equal: { name: "Jack" } },
|
||
],
|
||
},
|
||
}).toContainExactly([{ age: 10, name: "Jack" }])
|
||
})
|
||
|
||
it("successfully finds a row for one level with multiple conditions", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ equal: { age: 10 } },
|
||
{ equal: { name: "Jack" } },
|
||
],
|
||
},
|
||
}).toContainExactly([{ age: 10, name: "Jack" }])
|
||
})
|
||
|
||
it("successfully finds multiple rows for one level with multiple conditions", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ range: { age: { low: 1, high: 9 } } },
|
||
{ string: { name: "Ja" } },
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds rows for nested filters", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
range: { age: { low: 1, high: 10 } },
|
||
},
|
||
{ string: { name: "Ja" } },
|
||
],
|
||
},
|
||
equal: { name: "Jane" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([{ age: 1, name: "Jane" }])
|
||
})
|
||
|
||
it("returns nothing when filtering out all data", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ equal: { age: 7 } },
|
||
{ equal: { name: "Jack" } },
|
||
],
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
|
||
!isInMemory &&
|
||
it("validates conditions that are not objects", async () => {
|
||
await expect(
|
||
expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ equal: { age: 10 } },
|
||
"invalidCondition" as any,
|
||
],
|
||
},
|
||
}).toFindNothing()
|
||
).rejects.toThrow(
|
||
'Invalid body - "query.$and.conditions[1]" must be of type object'
|
||
)
|
||
})
|
||
|
||
!isInMemory &&
|
||
it("validates $and without conditions", async () => {
|
||
await expect(
|
||
expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{ equal: { age: 10 } },
|
||
{
|
||
$and: {
|
||
conditions: undefined as any,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
}).toFindNothing()
|
||
).rejects.toThrow(
|
||
'Invalid body - "query.$and.conditions[1].$and.conditions" is required'
|
||
)
|
||
})
|
||
|
||
// onEmptyFilter cannot be sent to view searches
|
||
!isView &&
|
||
it("returns no rows when onEmptyFilter set to none", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||
$and: {
|
||
conditions: [{ equal: { name: "" } }],
|
||
},
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("returns all rows when onEmptyFilter set to all", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||
$and: {
|
||
conditions: [{ equal: { name: "" } }],
|
||
},
|
||
},
|
||
}).toHaveLength(4)
|
||
})
|
||
})
|
||
|
||
describe("$or", () => {
|
||
beforeAll(async () => {
|
||
tableOrViewId = await createTableOrView({
|
||
age: { name: "age", type: FieldType.NUMBER },
|
||
name: { name: "name", type: FieldType.STRING },
|
||
})
|
||
await createRows([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 10, name: "Jack" },
|
||
{ age: 7, name: "Hanna" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds a row for one level condition", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{ equal: { age: 7 } },
|
||
{ equal: { name: "Jack" } },
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 10, name: "Jack" },
|
||
{ age: 7, name: "Hanna" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds a row for one level with multiple conditions", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{ equal: { age: 7 } },
|
||
{ equal: { name: "Jack" } },
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 10, name: "Jack" },
|
||
{ age: 7, name: "Hanna" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds multiple rows for one level with multiple conditions", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{ range: { age: { low: 1, high: 9 } } },
|
||
{ string: { name: "Jan" } },
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 7, name: "Hanna" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("successfully finds rows for nested filters", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
range: { age: { low: 1, high: 7 } },
|
||
},
|
||
{ string: { name: "Jan" } },
|
||
],
|
||
},
|
||
equal: { name: "Jane" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 7, name: "Hanna" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("returns nothing when filtering out all data", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{ equal: { age: 6 } },
|
||
{ equal: { name: "John" } },
|
||
],
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("can nest $and under $or filters", async () => {
|
||
await expectQuery({
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
range: { age: { low: 1, high: 8 } },
|
||
},
|
||
{ equal: { name: "Jan" } },
|
||
],
|
||
},
|
||
equal: { name: "Jane" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([
|
||
{ age: 1, name: "Jane" },
|
||
{ age: 8, name: "Jan" },
|
||
])
|
||
})
|
||
|
||
it("can nest $or under $and filters", async () => {
|
||
await expectQuery({
|
||
$and: {
|
||
conditions: [
|
||
{
|
||
$or: {
|
||
conditions: [
|
||
{
|
||
range: { age: { low: 1, high: 8 } },
|
||
},
|
||
{ equal: { name: "Jan" } },
|
||
],
|
||
},
|
||
equal: { name: "Jane" },
|
||
},
|
||
],
|
||
},
|
||
}).toContainExactly([{ age: 1, name: "Jane" }])
|
||
})
|
||
|
||
// onEmptyFilter cannot be sent to view searches
|
||
!isView &&
|
||
it("returns no rows when onEmptyFilter set to none", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||
$or: {
|
||
conditions: [{ equal: { name: "" } }],
|
||
},
|
||
},
|
||
}).toFindNothing()
|
||
})
|
||
|
||
it("returns all rows when onEmptyFilter set to all", async () => {
|
||
await expectSearch({
|
||
query: {
|
||
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||
$or: {
|
||
conditions: [{ equal: { name: "" } }],
|
||
},
|
||
},
|
||
}).toHaveLength(4)
|
||
})
|
||
})
|
||
|
||
isSql &&
|
||
describe("max related columns", () => {
|
||
let relatedRows: Row[]
|
||
|
||
beforeAll(async () => {
|
||
const relatedSchema: TableSchema = {}
|
||
const row: Row = {}
|
||
for (let i = 0; i < 100; i++) {
|
||
const name = `column${i}`
|
||
relatedSchema[name] = { name, type: FieldType.NUMBER }
|
||
row[name] = i
|
||
}
|
||
const relatedTable = await createTable(relatedSchema)
|
||
tableOrViewId = await createTableOrView({
|
||
name: { name: "name", type: FieldType.STRING },
|
||
related1: {
|
||
type: FieldType.LINK,
|
||
name: "related1",
|
||
fieldName: "main1",
|
||
tableId: relatedTable,
|
||
relationshipType: RelationshipType.MANY_TO_MANY,
|
||
},
|
||
})
|
||
relatedRows = await Promise.all([
|
||
config.api.row.save(relatedTable, row),
|
||
])
|
||
await config.api.row.save(tableOrViewId, {
|
||
name: "foo",
|
||
related1: [relatedRows[0]._id],
|
||
})
|
||
})
|
||
|
||
it("retrieve the row with relationships", async () => {
|
||
await expectQuery({}).toContainExactly([
|
||
{
|
||
name: "foo",
|
||
related1: [{ _id: relatedRows[0]._id }],
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
!isInternal &&
|
||
describe("SQL injection", () => {
|
||
const badStrings = [
|
||
"1; DROP TABLE %table_name%;",
|
||
"1; DELETE FROM %table_name%;",
|
||
"1; UPDATE %table_name% SET name = 'foo';",
|
||
"1; INSERT INTO %table_name% (name) VALUES ('foo');",
|
||
"' OR '1'='1' --",
|
||
"'; DROP TABLE %table_name%; --",
|
||
"' OR 1=1 --",
|
||
"' UNION SELECT null, null, null; --",
|
||
"' AND (SELECT COUNT(*) FROM %table_name%) > 0 --",
|
||
"\"; EXEC xp_cmdshell('dir'); --",
|
||
"\"' OR 'a'='a",
|
||
"OR 1=1;",
|
||
"'; SHUTDOWN --",
|
||
]
|
||
|
||
describe.each(badStrings)(
|
||
"bad string: %s",
|
||
badStringTemplate => {
|
||
// The SQL that knex generates when you try to use a double quote in a
|
||
// field name is always invalid and never works, so we skip it for these
|
||
// tests.
|
||
const skipFieldNameCheck =
|
||
isOracle && badStringTemplate.includes('"')
|
||
|
||
!skipFieldNameCheck &&
|
||
it("should not allow SQL injection as a field name", async () => {
|
||
const tableOrViewId = await createTableOrView()
|
||
const table = await getTable(tableOrViewId)
|
||
const badString = badStringTemplate.replace(
|
||
/%table_name%/g,
|
||
table.name
|
||
)
|
||
|
||
await config.api.table.save({
|
||
...table,
|
||
schema: {
|
||
...table.schema,
|
||
[badString]: {
|
||
name: badString,
|
||
type: FieldType.STRING,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (docIds.isViewId(tableOrViewId)) {
|
||
const view = await config.api.viewV2.get(
|
||
tableOrViewId
|
||
)
|
||
await config.api.viewV2.update({
|
||
...view,
|
||
schema: {
|
||
[badString]: { visible: true },
|
||
},
|
||
})
|
||
}
|
||
|
||
await config.api.row.save(tableOrViewId, {
|
||
[badString]: "foo",
|
||
})
|
||
|
||
await assertTableExists(table)
|
||
await assertTableNumRows(table, 1)
|
||
|
||
const { rows } = await config.api.row.search(
|
||
tableOrViewId,
|
||
{ query: {} },
|
||
{ status: 200 }
|
||
)
|
||
|
||
expect(rows).toHaveLength(1)
|
||
|
||
await assertTableExists(table)
|
||
await assertTableNumRows(table, 1)
|
||
})
|
||
|
||
it("should not allow SQL injection as a field value", async () => {
|
||
const tableOrViewId = await createTableOrView({
|
||
foo: {
|
||
name: "foo",
|
||
type: FieldType.STRING,
|
||
},
|
||
})
|
||
const table = await getTable(tableOrViewId)
|
||
const badString = badStringTemplate.replace(
|
||
/%table_name%/g,
|
||
table.name
|
||
)
|
||
|
||
await config.api.row.save(tableOrViewId, { foo: "foo" })
|
||
|
||
await assertTableExists(table)
|
||
await assertTableNumRows(table, 1)
|
||
|
||
const { rows } = await config.api.row.search(
|
||
tableOrViewId,
|
||
{ query: { equal: { foo: badString } } },
|
||
{ status: 200 }
|
||
)
|
||
|
||
expect(rows).toBeEmpty()
|
||
await assertTableExists(table)
|
||
await assertTableNumRows(table, 1)
|
||
})
|
||
}
|
||
)
|
||
})
|
||
}
|
||
)
|
||
})
|
||
}
|
||
)
|
||
}
|