diff --git a/lerna.json b/lerna.json index 092e9a133e..a4bcb56d38 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.32.10", + "version": "2.32.11", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 36521b10c6..607fee7580 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -124,9 +124,11 @@ export async function buildSqlFieldList( ([columnName, column]) => column.type !== FieldType.LINK && column.type !== FieldType.FORMULA && - !existing.find((field: string) => field === columnName) + !existing.find( + (field: string) => field === `${table.name}.${columnName}` + ) ) - .map(column => `${table.name}.${column[0]}`) + .map(([columnName]) => `${table.name}.${columnName}`) } let fields: string[] = [] diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 1ec5ca792a..110899e292 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -28,11 +28,13 @@ import { RowSearchParams, SearchFilters, SearchResponse, + SearchRowRequest, SortOrder, SortType, Table, TableSchema, User, + ViewV2Schema, } from "@budibase/types" import _ from "lodash" import tk from "timekeeper" @@ -64,36 +66,30 @@ describe.each([ let envCleanup: (() => void) | undefined let datasource: Datasource | undefined let client: Knex | undefined - let table: Table + let tableOrViewId: string let rows: Row[] async function basicRelationshipTables(type: RelationshipType) { - const relatedTable = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - }, - generator.guid().substring(0, 10) - ) - table = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - //@ts-ignore - API accepts this structure, will build out rest of definition - productCat: { - type: FieldType.LINK, - relationshipType: type, - name: "productCat", - fieldName: "product", - tableId: relatedTable._id!, - constraints: { - type: "array", - }, + const relatedTable = await createTable({ + name: { name: "name", type: FieldType.STRING }, + }) + const tableId = await createTable({ + name: { name: "name", type: FieldType.STRING }, + //@ts-ignore - API accepts this structure, will build out rest of definition + productCat: { + type: FieldType.LINK, + relationshipType: type, + name: "productCat", + fieldName: "product", + tableId: relatedTable, + constraints: { + type: "array", }, }, - generator.guid().substring(0, 10) - ) + }) return { - relatedTable: await config.api.table.get(relatedTable._id!), - table, + relatedTable: await config.api.table.get(relatedTable), + tableId, } } @@ -136,403 +132,423 @@ describe.each([ } }) - async function createTable(schema: TableSchema, name?: string) { - return await config.api.table.save( - tableForDatasource(datasource, { schema, name }) + 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[]) { // Shuffling to avoid false positives given a fixed order - await config.api.row.bulkImport(table._id!, { - rows: _.shuffle(arr), - }) - rows = await config.api.row.fetch(table._id!) + for (const row of _.shuffle(arr)) { + await config.api.row.save(tableOrViewId, row) + } + rows = await config.api.row.fetch(tableOrViewId) } - class SearchAssertion { - constructor(private readonly query: RowSearchParams) {} + describe.each([ + ["table", createTable], + [ + "view", + async (schema: TableSchema) => { + const tableId = await createTable(schema) + const viewId = await createView( + tableId, + Object.keys(schema).reduce((viewSchema, fieldName) => { + const field = schema[fieldName] + viewSchema[fieldName] = { + visible: field.visible ?? true, + readonly: false, + } + return viewSchema + }, {}) + ) + return viewId + }, + ], + ])("from %s", (sourceType, createTableOrView) => { + const isView = sourceType === "view" - private async performSearch(): Promise> { - if (isInMemory) { - return dataFilters.search(_.cloneDeep(rows), this.query) - } else { - const sourceId = this.query.viewId || this.query.tableId - if (!sourceId) { - throw new Error("No source ID provided") - } - return config.api.row.search(sourceId, this.query) - } + if (isView && isLucene) { + // Some tests don't have the expected result in views via lucene, and given that it is getting deprecated, we exclude them from the tests + return } - // 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>(expected: T, found: T) { - if (!expected) { - throw new Error("Expected is undefined") - } - if (!found) { - return false - } + class SearchAssertion { + constructor(private readonly query: SearchRowRequest) {} - 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 - } + private async performSearch(): Promise> { + if (isInMemory) { + return dataFilters.search(_.cloneDeep(rows), { + ...this.query, + tableId: tableOrViewId, + }) } else { - if (expected[key] !== found[key]) { - return false + 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>(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( - expectedRow: T, - foundRows: T[] - ): NonNullable { - 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)}` - ) + return true } - 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 - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(expectedRows.length) - // eslint-disable-next-line jest/no-standalone-expect - 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 - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(expectedRows.length) - // eslint-disable-next-line jest/no-standalone-expect - expect([...foundRows]).toEqual( - expect.arrayContaining( - expectedRows.map((expectedRow: any) => - expect.objectContaining(this.popRow(expectedRow, foundRows)) + // 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( + expectedRow: T, + foundRows: T[] + ): NonNullable { + 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)}` ) - ) - ) - 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) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - const keys = Object.keys(properties) as Array> - for (let key of keys) { - // eslint-disable-next-line jest/no-standalone-expect - expect(response[key]).toBeDefined() - if (properties[key]) { - // eslint-disable-next-line jest/no-standalone-expect - 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)[]) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - for (let property of properties) { + 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 + // eslint-disable-next-line jest/no-standalone-expect - 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 - - // eslint-disable-next-line jest/no-standalone-expect - expect([...foundRows]).toEqual( - expect.arrayContaining( + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect([...foundRows]).toEqual( 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() - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(length) - } - } - - function expectSearch(query: Omit) { - return new SearchAssertion({ ...query, tableId: table._id! }) - } - - function expectQuery(query: SearchFilters) { - return expectSearch({ query }) - } - - describe("boolean", () => { - beforeAll(async () => { - table = await createTable({ - 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: JSON.stringify(currentUser), - }, - { - name: "single user", - single_user: JSON.stringify(globalUsers[0]), - }, - { - name: "deprecated single user, session user", - deprecated_single_user: JSON.stringify([currentUser]), - }, - { - name: "deprecated single user", - deprecated_single_user: JSON.stringify([globalUsers[0]]), - }, - { - name: "multi user", - multi_user: JSON.stringify(globalUsers), - }, - { - name: "multi user with session user", - multi_user: JSON.stringify([...globalUsers, currentUser]), - }, - { - name: "deprecated multi user", - deprecated_multi_user: JSON.stringify(globalUsers), - }, - { - name: "deprecated multi user with session user", - deprecated_multi_user: JSON.stringify([ - ...globalUsers, - currentUser, - ]), - }, - ] + return cloned } - 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, - } - }) + // 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 + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect([...foundRows]).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining(this.popRow(expectedRow, foundRows)) + ) + ) ) + return cloned + } - table = await createTable({ - 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", - }, - }, + // 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) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + const keys = Object.keys(properties) as Array> + for (let key of keys) { + // eslint-disable-next-line jest/no-standalone-expect + expect(response[key]).toBeDefined() + if (properties[key]) { + // eslint-disable-next-line jest/no-standalone-expect + 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)[]) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + for (let property of properties) { + // eslint-disable-next-line jest/no-standalone-expect + 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 + + // eslint-disable-next-line jest/no-standalone-expect + 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() + + // eslint-disable-next-line jest/no-standalone-expect + 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(rows(config.getUser())) + await createRows([{ isTrue: true }, { isTrue: false }]) }) - // !! 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(), - }, - ]) + 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 }, + ]) + }) }) - !isLucene && - it("should return all rows matching the session user firstname when logical operator used", async () => { - await expectQuery({ - $and: { - conditions: [{ equal: { name: "{{ [user].firstName }}" } }], + 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, @@ -541,488 +557,492 @@ describe.each([ ]) }) - it("should parse the date binding and return all rows after the resolved value", async () => { - await tk.withFreeze(serverTime, async () => { + !isLucene && + 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: "{{ [now] }}", - high: "9999-00-00T00:00:00.000Z", + low: "0000-00-00T00:00:00.000Z", + high: "{{ [now] }}", }, }, }).toContainExactly([ - { - name: config.getUser().firstName, - appointment: future.toISOString(), - }, + { 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 }], + }, + ]) + }) }) - 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() }, - ]) + describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + name: { name: "name", type: FieldType.STRING }, + }) + await createRows([{ name: "foo" }, { name: "bar" }]) }) - 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 } + describe("misc", () => { + it("should return all if no query is passed", async () => { + await expectSearch({} as RowSearchParams).toContainExactly([ + { name: "foo" }, + { name: "bar" }, + ]) }) - 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 } + it("should return all if empty query is passed", async () => { + await expectQuery({}).toContainExactly([ + { name: "foo" }, + { name: "bar" }, + ]) }) - 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 }], - }, - ]) - }) - }) - - describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => { - beforeAll(async () => { - table = await createTable({ - name: { name: "name", type: FieldType.STRING }, - }) - 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" }]) - }) - - 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" }]) - }) - - 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" }, - ]) - }) - }) - - 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() - }) - - !isLucene && - it("ignores low if it's an empty object", async () => { + it("should return all if onEmptyFilter is RETURN_ALL", async () => { await expectQuery({ - // @ts-ignore - range: { name: { low: {}, high: "z" } }, + onEmptyFilter: EmptyFilterOption.RETURN_ALL, }).toContainExactly([{ name: "foo" }, { name: "bar" }]) }) - !isLucene && - it("ignores high if it's an empty object", async () => { + // 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({ - // @ts-ignore - range: { name: { low: "a", high: {} } }, + allOr: true, + equal: { name: "foo" }, + oneOf: { name: ["bar"] }, }).toContainExactly([{ name: "foo" }, { name: "bar" }]) }) - }) - describe("empty", () => { - it("finds no empty rows", async () => { - await expectQuery({ empty: { name: null } }).toFindNothing() + 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" }]) + }) }) - it("should not be affected by when filter empty behaviour", async () => { - await expectQuery({ - empty: { name: null }, - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toFindNothing() - }) - }) + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ notEqual: { name: "foo" } }).toContainExactly([ + { name: "bar" }, + ]) + }) - describe("notEmpty", () => { - it("finds all non-empty rows", async () => { - await expectQuery({ notEmpty: { name: null } }).toContainExactly([ - { name: "foo" }, - { name: "bar" }, - ]) + it("fails to find nonexistent row", async () => { + await expectQuery({ notEqual: { name: "bar" } }).toContainExactly([ + { name: "foo" }, + ]) + }) }) - 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("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([ + { name: "foo" }, + ]) + }) - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ name: "bar" }, { 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([]) + }) }) - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) + 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("sortType STRING", () => { + 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" }, + ]) + }) + }) + + 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() + }) + + !isLucene && + it("ignores low if it's an empty object", async () => { + await expectQuery({ + // @ts-ignore + range: { name: { low: {}, high: "z" } }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + !isLucene && + 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", - sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING, }).toMatchExactly([{ name: "bar" }, { name: "foo" }]) }) @@ -1031,345 +1051,355 @@ describe.each([ 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!, - }) - - table = resp.datasource.entities![tableName] - - await createRows([{ 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("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({ + it("sorts descending", async () => { + await expectSearch({ query: {}, sort: "name", + sortType: SortType.STRING, sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) + }) }) - }) - }) - describe("numbers", () => { - beforeAll(async () => { - table = await createTable({ - age: { name: "age", type: FieldType.NUMBER }, - }) - await createRows([{ age: 1 }, { age: 10 }]) - }) + !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!, + }) - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }]) - }) + tableOrViewId = resp.datasource.entities![tableName]._id! - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { age: 2 } }).toFindNothing() + 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("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { age: 1 } }).toContainExactly([ - { age: 10 }, - ]) + describe("numbers", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + age: { name: "age", type: FieldType.NUMBER }, + }) + await createRows([{ age: 1 }, { age: 10 }]) }) - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { age: 10 } }).toContainExactly([ - { age: 1 }, - ]) - }) - }) + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ equal: { age: 1 } }).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({ equal: { age: 2 } }).toFindNothing() + }) }) - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { 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 }, + ]) + }) }) - // I couldn't find a way to make this work in Lucene and given that - // we're getting rid of Lucene soon I wasn't inclined to spend time on - // it. - !isLucene && - it("can convert from a string", async () => { + 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() + }) + + // I couldn't find a way to make this work in Lucene and given that + // we're getting rid of Lucene soon I wasn't inclined to spend time on + // it. + !isLucene && + it("can convert from a string", async () => { + await expectQuery({ + oneOf: { + // @ts-ignore + age: "1", + }, + }).toContainExactly([{ age: 1 }]) + }) + + // I couldn't find a way to make this work in Lucene and given that + // we're getting rid of Lucene soon I wasn't inclined to spend time on + // it. + !isLucene && + 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({ - oneOf: { - // @ts-ignore - age: "1", + 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 }]) }) - // I couldn't find a way to make this work in Lucene and given that - // we're getting rid of Lucene soon I wasn't inclined to spend time on - // it. - !isLucene && - it("can find multiple values for same column", async () => { + it("less than", async () => { await expectQuery({ - oneOf: { - // @ts-ignore - age: "1,10", + range: { + age: { high: 5, low: Number.MIN_SAFE_INTEGER }, }, - }).toContainExactly([{ age: 1 }, { age: 10 }]) + }).toContainExactly([{ age: 1 }]) }) - }) - - 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 }]) + 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 }]) + }) }) - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { age: { low: 5, high: 10 } }, - }).toContainExactly([{ age: 10 }]) - }) + describe("sortType NUMBER", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { 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 }]) + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }]) + }) }) }) - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ age: 1 }, { age: 10 }]) + 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 }]) }) - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ age: 10 }, { age: 1 }]) - }) - }) + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_1ST }, + ]) + }) - describe("sortType NUMBER", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortType: SortType.NUMBER, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ age: 1 }, { age: 10 }]) + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing() + }) }) - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortType: SortType.NUMBER, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ age: 10 }, { age: 1 }]) - }) - }) - }) + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_10TH }, + ]) + }) - 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 () => { - table = await createTable({ - dob: { name: "dob", type: FieldType.DATETIME }, + it("fails to find nonexistent row", async () => { + await expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([ + { dob: JAN_1ST }, + ]) + }) }) - await createRows([{ dob: JAN_1ST }, { dob: JAN_10TH }]) - }) + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([ + { dob: JAN_1ST }, + ]) + }) - 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({ oneOf: { dob: [JAN_2ND] } }).toFindNothing() + }) }) - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { 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 }]) + }) - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ - { dob: JAN_10TH }, - ]) + 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 }]) + }) }) - 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", () => { + describe("sort", () => { it("sorts ascending", async () => { await expectSearch({ query: {}, sort: "dob", - sortType: SortType.STRING, sortOrder: SortOrder.ASCENDING, }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) }) @@ -1378,157 +1408,143 @@ describe.each([ 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 () => { - table = await createTable({ - 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", + 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" }, @@ -1543,7 +1559,6 @@ describe.each([ await expectSearch({ query: {}, sort: "time", - sortType: SortType.STRING, sortOrder: SortOrder.DESCENDING, }).toMatchExactly([ { time: "15:30:00" }, @@ -1554,447 +1569,479 @@ describe.each([ { 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.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { - beforeAll(async () => { - table = await createTable({ - numbers: { - name: "numbers", - type: FieldType.ARRAY, - constraints: { - type: JsonFieldSubType.ARRAY, - inclusion: ["one", "two", "three"], + describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { + 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"] }, - ]) + }) + await createRows([{ numbers: ["one", "two"] }, { numbers: ["three"] }]) }) - 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 () => { - table = await createTable({ - 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() - }) - }) - - // Range searches against bigints don't seem to work at all in Lucene, and I - // couldn't figure out why. Given that we're replacing Lucene with SQS, - // we've decided not to spend time on it. - !isLucene && - describe("range", () => { + describe("contains", () => { it("successfully finds a row", async () => { await expectQuery({ - range: { num: { low: SMALL, high: "5" } }, - }).toContainExactly([{ num: SMALL }]) + contains: { numbers: ["one"] }, + }).toContainExactly([{ numbers: ["one", "two"] }]) }) - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { num: { low: SMALL, high: MEDIUM } }, - }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]) + it("fails to find nonexistent row", async () => { + await expectQuery({ contains: { numbers: ["none"] } }).toFindNothing() }) - it("successfully finds a row with a high bound", async () => { + it("fails to find row containing all", 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" } }, + contains: { numbers: ["one", "two", "three"] }, }).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 }]) + it("finds all with empty list", async () => { + await expectQuery({ contains: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) }) }) - }) - isInternal && - describe("auto", () => { - beforeAll(async () => { - table = await createTable({ - auto: { - name: "auto", - type: FieldType.AUTO, - autocolumn: true, - subtype: AutoFieldSubType.AUTO_ID, - }, + describe("notContains", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notContains: { numbers: ["one"] }, + }).toContainExactly([{ numbers: ["three"] }]) }) - await createRows(new Array(10).fill({})) + + 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: { auto: 1 } }).toContainExactly([ - { auto: 1 }, + 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: { auto: 0 } }).toFindNothing() + await expectQuery({ equal: { num: "2" } }).toFindNothing() }) }) - describe("not equal", () => { + describe("notEqual", () => { 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 }, + await expectQuery({ notEqual: { num: SMALL } }).toContainExactly([ + { num: MEDIUM }, + { num: BIG }, ]) }) 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 }, + await expectQuery({ notEqual: { num: 10 } }).toContainExactly([ + { num: SMALL }, + { num: MEDIUM }, + { num: BIG }, ]) }) }) describe("oneOf", () => { it("successfully finds a row", async () => { - await expectQuery({ oneOf: { auto: [1] } }).toContainExactly([ - { auto: 1 }, + 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: { auto: [0] } }).toFindNothing() + await expectQuery({ oneOf: { num: [2] } }).toFindNothing() }) }) - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { auto: { low: 1, high: 1 } }, - }).toContainExactly([{ auto: 1 }]) - }) + // Range searches against bigints don't seem to work at all in Lucene, and I + // couldn't figure out why. Given that we're replacing Lucene with SQS, + // we've decided not to spend time on it. + !isLucene && + 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: { auto: { low: 1, high: 2 } }, - }).toContainExactly([{ auto: 1 }, { auto: 2 }]) - }) + 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: { auto: { low: 2, high: 2 } }, - }).toContainExactly([{ auto: 2 }]) - }) + 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: { auto: { low: 0, high: 0 } }, - }).toFindNothing() - }) + it("successfully finds no rows", async () => { + await expectQuery({ + range: { num: { low: "5", high: "5" } }, + }).toFindNothing() + }) - isSqs && it("can search using just a low value", async () => { await expectQuery({ - range: { auto: { low: 9 } }, - }).toContainExactly([{ auto: 9 }, { auto: 10 }]) + range: { num: { low: MEDIUM } }, + }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) }) - isSqs && it("can search using just a high value", async () => { await expectQuery({ - range: { auto: { high: 2 } }, + 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 }]) }) - }) - isSqs && - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "auto", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { auto: 1 }, - { auto: 2 }, - { auto: 3 }, - { auto: 4 }, - { auto: 5 }, - { auto: 6 }, - { auto: 7 }, - { auto: 8 }, - { auto: 9 }, - { auto: 10 }, - ]) + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { auto: { low: 2, high: 2 } }, + }).toContainExactly([{ auto: 2 }]) }) - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "auto", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { auto: 10 }, - { auto: 9 }, - { auto: 8 }, - { auto: 7 }, - { auto: 6 }, - { auto: 5 }, - { auto: 4 }, - { auto: 3 }, - { auto: 2 }, - { auto: 1 }, - ]) + it("successfully finds no rows", async () => { + await expectQuery({ + range: { auto: { low: 0, high: 0 } }, + }).toFindNothing() }) - // 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( - table._id!, - { - tableId: table._id!, - 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: number = 0 - do { - const response = await config.api.row.search(table._id!, { - tableId: table._id!, - 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[] = [] - - // eslint-disable-next-line no-constant-condition - while (true) { - const response = await config.api.row.search(table._id!, { - tableId: table._id!, - limit: 3, - query: {}, - bookmark, - paginate: true, + isSqs && + it("can search using just a low value", async () => { + await expectQuery({ + range: { auto: { low: 9 } }, + }).toContainExactly([{ auto: 9 }, { auto: 10 }]) }) - rows.push(...response.rows) + isSqs && + it("can search using just a high value", async () => { + await expectQuery({ + range: { auto: { high: 2 } }, + }).toContainExactly([{ auto: 1 }, { auto: 2 }]) + }) + }) - if (!response.bookmark || !response.hasNextPage) { - break + isSqs && + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "auto", + sortOrder: SortOrder.ASCENDING, + }).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, + }).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: number = 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[] = [] + + // eslint-disable-next-line no-constant-condition + 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 } - 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]) + 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 () => { - table = await createTable({ - "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", () => { + describe("field name 1:name", () => { beforeAll(async () => { - const arrayTable = await createTable( - { + 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", @@ -2004,17 +2051,14 @@ describe.each([ inclusion: ["option 1", "option 2"], }, }, - }, - "array" - ) - table = await createTable( - { + }) + tableOrViewId = await createTableOrView({ relationship: { type: FieldType.LINK, relationshipType: RelationshipType.MANY_TO_ONE, name: "relationship", fieldName: "relate", - tableId: arrayTable._id!, + tableId: arrayTable, constraints: { type: "array", }, @@ -2026,1165 +2070,1162 @@ describe.each([ `let array = [];$("relationship").forEach(rel => array = array.concat(rel.array));return array.sort().join(",")` ), }, - }, - "main" - ) - const arrayRows = await Promise.all([ - config.api.row.save(arrayTable._id!, { - name: "foo", - array: ["option 1"], - }), - config.api.row.save(arrayTable._id!, { - name: "bar", - array: ["option 2"], - }), - ]) - await Promise.all([ - config.api.row.save(table._id!, { - 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()}` }) - - table = await createTable({ - user: { - name: "user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - }, - }) - - await createRows([ - { user: JSON.stringify(user1) }, - { user: JSON.stringify(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()}` }) - - table = await createTable({ - users: { - name: "users", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { type: "array" }, - }, - number: { - name: "number", - type: FieldType.NUMBER, - }, - }) - - await createRows([ - { number: 1, users: JSON.stringify([user1]) }, - { number: 2, users: JSON.stringify([user2]) }, - { number: 3, users: JSON.stringify([user1, user2]) }, - { number: 4, users: JSON.stringify([]) }, - ]) - }) - - 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() - }) - }) - }) - - // This will never work for Lucene. - !isLucene && - // It also can't work for in-memory searching because the related table name - // isn't available. - !isInMemory && - describe("relations", () => { - let productCategoryTable: Table, productCatRows: Row[] - - beforeAll(async () => { - const { relatedTable } = await basicRelationshipTables( - RelationshipType.ONE_TO_MANY - ) - 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(table._id!, { - name: "foo", - productCat: [productCatRows[0]._id], - }), - config.api.row.save(table._id!, { - name: "bar", - productCat: [productCatRows[1]._id], - }), - config.api.row.save(table._id!, { - 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 }]) - }) - }) - - isSql && - describe("big relations", () => { - beforeAll(async () => { - const { relatedTable } = await basicRelationshipTables( - RelationshipType.MANY_TO_ONE - ) - const mainRow = await config.api.row.save(table._id!, { - 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) - }) - }) - ;(isSqs || isLucene) && - describe("relations to same table", () => { - let relatedTable: Table, relatedRows: Row[] - - beforeAll(async () => { - relatedTable = await createTable( - { - name: { name: "name", type: FieldType.STRING }, - }, - "productCategory" - ) - table = await createTable({ - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable._id!, { name: "foo" }), - config.api.row.save(relatedTable._id!, { name: "bar" }), - config.api.row.save(relatedTable._id!, { name: "baz" }), - config.api.row.save(relatedTable._id!, { name: "boo" }), - ]) - await Promise.all([ - config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }), - config.api.row.save(table._id!, { - name: "test2", - related1: [relatedRows[2]._id!], - related2: [relatedRows[3]._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 }], - }, - ]) - }) - - isSqs && - it("should be able to filter down to second row with equal", async () => { - await expectSearch({ - query: { - equal: { - ["related1.name"]: "baz", - }, - }, - }).toContainExactly([ - { - name: "test2", - related1: [{ _id: relatedRows[2]._id }], - }, + 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], + }), ]) }) - isSqs && - it("should be able to filter down to first row with not equal", async () => { - await expectSearch({ - query: { - notEqual: { - ["1:related2.name"]: "bar", - ["2:related2.name"]: "baz", - ["3:related2.name"]: "boo", - }, + 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() + }) + }) + }) + + // This will never work for Lucene. + !isLucene && + // It also can't work for in-memory searching because the related table name + // isn't available. + !isInMemory && + describe("relations", () => { + let productCategoryTable: Table, productCatRows: Row[] + + beforeAll(async () => { + const { relatedTable, tableId } = await basicRelationshipTables( + RelationshipType.ONE_TO_MANY + ) + 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 }]) + }) + }) + + isSql && + describe("big relations", () => { + beforeAll(async () => { + const { relatedTable, tableId } = await basicRelationshipTables( + RelationshipType.MANY_TO_ONE + ) + 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) + }) + }) + ;(isSqs || isLucene) && + 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!], + }), + ]) + }) + + 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 }], }, ]) }) - }) - isInternal && - describe("no column error backwards compat", () => { + isSqs && + it("should be able to filter down to second row with equal", async () => { + await expectSearch({ + query: { + equal: { + ["related1.name"]: "baz", + }, + }, + }).toContainExactly([ + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + }, + ]) + }) + + isSqs && + it("should be able to filter down to first row with not equal", async () => { + await expectSearch({ + query: { + notEqual: { + ["1:related2.name"]: "bar", + ["2:related2.name"]: "baz", + ["3:related2.name"]: "boo", + }, + }, + }).toContainExactly([ + { + name: "test", + related1: [{ _id: relatedRows[0]._id }], + }, + ]) + }) + }) + + 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: [] }) + }) + }) + + // lucene can't count the total rows + !isLucene && + 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 () => { - table = await createTable({ + // 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("shouldn't error when column doesn't exist", async () => { + it("can get rows with all table data", async () => { await expectSearch({ - query: { - string: { - "1:something": "a", - }, - }, - }).toMatch({ rows: [] }) + query: {}, + }).toContain([ + { name: "foo", invalid: "id1" }, + { name: "bar", invalid: "id2" }, + ]) }) }) - // lucene can't count the total rows - !isLucene && - describe("row counting", () => { + 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 () => { - table = await createTable({ - name: { - name: "name", + 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" }]) + 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 - table = await createTable({ - // @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 () => { - table = await createTable({ - [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 () => { + it("should be able to query a column with non-ascii characters", async () => { await expectSearch({ query: { equal: { - [`1:${column}`]: "a", + [`1:${name}`]: "a", }, }, - }).toContainExactly([{ [column]: "a" }]) - }) - } - ) - - isInternal && - describe("sample data", () => { - beforeAll(async () => { - await config.api.application.addSampleData(config.appId!) - table = DEFAULT_EMPLOYEE_TABLE_SCHEMA - rows = await config.api.row.fetch(table._id!) - }) - - it("should be able to search sample data", async () => { - await expectSearch({ - query: {}, - }).toContain([ - { - "First Name": "Mandy", - }, - ]) + }).toContainExactly([{ [name]: "a" }]) }) }) - 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 () => { - table = await createTable({ - 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 () => { - table = await createTable({ - [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 () => { - table = await createTable({ - "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" }]) - }) - }) - - // This was never actually supported in Lucene but SQS does support it, so may - // as well have a test for it. - ;(isSqs || isInMemory) && - describe("space at start of column name", () => { - beforeAll(async () => { - table = await createTable({ - " 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" }]) - }) - }) - - isSqs && - describe("duplicate columns", () => { - beforeAll(async () => { - table = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - await context.doInAppContext(config.getAppId(), async () => { - const db = context.getAppDB() - const tableDoc = await db.get(table._id!) - 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, - }, - }) - table = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - rel: { - name: "rel", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_MANY, - tableId: toRelateTable._id!, - fieldName: "rel", - }, - }) - const [row1, row2] = await Promise.all([ - config.api.row.save(toRelateTable._id!, { name: "tag 1" }), - config.api.row.save(toRelateTable._id!, { name: "tag 2" }), - ]) - row = await config.api.row.save(table._id!, { - 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 composite key", () => { - beforeAll(async () => { - table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - idColumn1: { - name: "idColumn1", - type: FieldType.NUMBER, - }, - idColumn2: { - name: "idColumn2", - type: FieldType.NUMBER, - }, - }, - primary: ["idColumn1", "idColumn2"], - }) - ) - 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 toRelateTable = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - link: { - name: "link", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_ONE, - tableId: toRelateTable._id!, - fieldName: "link", - }, + // 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, }, }) - ) - toRelateTable = await config.api.table.get(toRelateTable._id!) - await config.api.table.save({ - ...toRelateTable, - primaryDisplay: "link", + await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }]) }) - const relatedRows = await Promise.all([ - config.api.row.save(toRelateTable._id!, { name: "test" }), - ]) - await Promise.all([ - config.api.row.save(table._id!, { + + 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" }]) + }) + }) + + // This was never actually supported in Lucene but SQS does support it, so may + // as well have a test for it. + ;(isSqs || isInMemory) && + 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" }]) + }) + }) + + isSqs && + !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
(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 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: "link", + }, + }) + + const toRelateTable = await config.api.table.get(toRelateTableId) + await config.api.table.save({ + ...toRelateTable, + primaryDisplay: "link", + }) + 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: "test" }] }, - ]) - }) - }) - - !isLucene && - describe("$and", () => { - beforeAll(async () => { - table = await createTable({ - 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, - }, + 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" }] }, + ]) + }) + }) + + !isLucene && + 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" } }, + ], }, - ], - }, - }).toFindNothing() - ).rejects.toThrow( - 'Invalid body - "query.$and.conditions[1].$and.conditions" is required' - ) - }) - - 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) - }) - }) - - !isLucene && - describe("$or", () => { - beforeAll(async () => { - table = await createTable({ - 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" }, }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 7, name: "Hanna" }, - { age: 8, name: "Jan" }, - ]) - }) + ], + }, + }).toContainExactly([{ age: 1, name: "Jane" }]) + }) - it("returns nothing when filtering out all data", async () => { - await expectQuery({ - $or: { - conditions: [{ equal: { age: 6 } }, { equal: { name: "John" } }], - }, - }).toFindNothing() - }) + it("returns nothing when filtering out all data", async () => { + await expectQuery({ + $and: { + conditions: [{ equal: { age: 7 } }, { equal: { name: "Jack" } }], + }, + }).toFindNothing() + }) - it("can nest $and under $or filters", async () => { - await expectQuery({ - $or: { - conditions: [ - { + !isInMemory && + it("validates conditions that are not objects", async () => { + await expect( + expectQuery({ $and: { conditions: [ - { - range: { age: { low: 1, high: 8 } }, - }, - { equal: { name: "Jan" } }, + { equal: { age: 10 } }, + "invalidCondition" as any, ], }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 8, name: "Jan" }, - ]) - }) + }).toFindNothing() + ).rejects.toThrow( + 'Invalid body - "query.$and.conditions[1]" must be of type object' + ) + }) - it("can nest $or under $and filters", async () => { - await expectQuery({ - $and: { - conditions: [ - { - $or: { + !isInMemory && + it("validates $and without conditions", async () => { + await expect( + expectQuery({ + $and: { conditions: [ + { equal: { age: 10 } }, { - range: { age: { low: 1, high: 8 } }, + $and: { + conditions: undefined as any, + }, }, - { equal: { name: "Jan" } }, ], }, - equal: { name: "Jane" }, + }).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: "" } }], + }, }, - ], - }, - }).toContainExactly([{ age: 1, name: "Jane" }]) - }) + }).toFindNothing() + }) - it("returns no rows when onEmptyFilter set to none", async () => { - await expectSearch({ - query: { - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - $or: { - conditions: [{ equal: { name: "" } }], + it("returns all rows when onEmptyFilter set to all", async () => { + await expectSearch({ + query: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $and: { + 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) - table = await createTable({ - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable._id!, row), - ]) - await config.api.row.save(table._id!, { - name: "foo", - related1: [relatedRows[0]._id], + }).toHaveLength(4) }) }) - it("retrieve the row with relationships", async () => { - await expectQuery({}).toContainExactly([ - { + !isLucene && + 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: [{ _id: relatedRows[0]._id }], - }, - ]) + related1: [relatedRows[0]._id], + }) + }) + + it("retrieve the row with relationships", async () => { + await expectQuery({}).toContainExactly([ + { + name: "foo", + related1: [{ _id: relatedRows[0]._id }], + }, + ]) + }) }) - }) + }) }) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 1d6c1d50cd..1780b8ff27 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -22,9 +22,10 @@ import { RelationshipType, TableSchema, RenameColumn, - ViewFieldMetadata, FeatureFlag, BBReferenceFieldSubType, + ViewV2Schema, + ViewCalculationFieldMetadata, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" @@ -540,6 +541,33 @@ describe.each([ status: 201, }) }) + + it("can create a view with calculation fields", async () => { + let view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", + }, + }, + }) + + expect(Object.keys(view.schema!)).toHaveLength(1) + + let sum = view.schema!.sum as ViewCalculationFieldMetadata + expect(sum).toBeDefined() + expect(sum.calculationType).toEqual(CalculationType.SUM) + expect(sum.field).toEqual("Price") + + view = await config.api.viewV2.get(view.id) + sum = view.schema!.sum as ViewCalculationFieldMetadata + expect(sum).toBeDefined() + expect(sum.calculationType).toEqual(CalculationType.SUM) + expect(sum.field).toEqual("Price") + }) }) describe("update", () => { @@ -1152,10 +1180,7 @@ describe.each([ return table } - const createView = async ( - tableId: string, - schema: Record - ) => + const createView = async (tableId: string, schema: ViewV2Schema) => await config.api.viewV2.create({ name: generator.guid(), tableId, @@ -2546,6 +2571,51 @@ describe.each([ } }) }) + + !isLucene && + it("should not need required fields to be present", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + presence: true, + }, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + }, + }) + ) + + await Promise.all([ + config.api.row.save(table._id!, { name: "Steve", age: 30 }), + config.api.row.save(table._id!, { name: "Jane", age: 31 }), + ]) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "age", + }, + }, + }) + + const response = await config.api.viewV2.search(view.id, { + query: {}, + }) + + expect(response.rows).toHaveLength(1) + expect(response.rows[0].sum).toEqual(61) + }) }) describe("permissions", () => { diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 45f5ee6e5a..f359bcc2cf 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -23,8 +23,8 @@ import { Row, Table, TableSchema, - ViewFieldMetadata, ViewV2, + ViewV2Schema, } from "@budibase/types" import sdk from "../../sdk" import { helpers } from "@budibase/shared-core" @@ -262,7 +262,7 @@ export async function squashLinks( FeatureFlag.ENRICHED_RELATIONSHIPS ) - let viewSchema: Record = {} + let viewSchema: ViewV2Schema = {} if (sdk.views.isView(source)) { if (helpers.views.isCalculationView(source)) { return enriched diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 8de5818805..e83ea381b5 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -93,6 +93,8 @@ export async function search( // Lucene does not accept conditional filters, so we need to keep the old logic const query: SearchFilters = viewQuery + delete options.query.onEmptyFilter + // Extract existing fields const existingFields = view.query @@ -115,6 +117,9 @@ export async function search( conditions: [viewQuery, options.query], }, } + if (viewQuery.onEmptyFilter) { + options.query.onEmptyFilter = viewQuery.onEmptyFilter + } } } diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 24e4da3172..83ee78e165 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -258,19 +258,12 @@ export async function enrichSchema( view: ViewV2, tableSchema: TableSchema ): Promise { - const tableCache: Record = {} - async function populateRelTableSchema( tableId: string, viewFields: Record ) { - if (!tableCache[tableId]) { - tableCache[tableId] = await sdk.tables.getTable(tableId) - } - const relTable = tableCache[tableId] - + const relTable = await sdk.tables.getTable(tableId) const result: Record = {} - for (const relTableFieldName of Object.keys(relTable.schema)) { const relTableField = relTable.schema[relTableFieldName] if ([FieldType.LINK, FieldType.FORMULA].includes(relTableField.type)) { @@ -299,15 +292,24 @@ export async function enrichSchema( const viewSchema = view.schema || {} const anyViewOrder = Object.values(viewSchema).some(ui => ui.order != null) - for (const key of Object.keys(tableSchema).filter( - k => tableSchema[k].visible !== false - )) { + + const visibleSchemaFields = Object.keys(viewSchema).filter(key => { + if (helpers.views.isCalculationField(viewSchema[key])) { + return viewSchema[key].visible !== false + } + return key in tableSchema && tableSchema[key].visible !== false + }) + const visibleTableFields = Object.keys(tableSchema).filter( + key => tableSchema[key].visible !== false + ) + const visibleFields = new Set([...visibleSchemaFields, ...visibleTableFields]) + for (const key of visibleFields) { // if nothing specified in view, then it is not visible const ui = viewSchema[key] || { visible: false } schema[key] = { ...tableSchema[key], ...ui, - order: anyViewOrder ? ui?.order ?? undefined : tableSchema[key].order, + order: anyViewOrder ? ui?.order ?? undefined : tableSchema[key]?.order, columns: undefined, } @@ -319,10 +321,7 @@ export async function enrichSchema( } } - return { - ...view, - schema: schema, - } + return { ...view, schema } } export function syncSchema( diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts index c5614d69e7..6bec59fdf7 100644 --- a/packages/server/src/tests/utilities/api/row.ts +++ b/packages/server/src/tests/utilities/api/row.ts @@ -7,11 +7,11 @@ import { BulkImportRequest, BulkImportResponse, SearchRowResponse, - RowSearchParams, DeleteRows, DeleteRow, PaginatedSearchRowResponse, RowExportFormat, + SearchRowRequest, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -136,7 +136,7 @@ export class RowAPI extends TestAPI { ) } - search = async ( + search = async ( sourceId: string, params?: T, expectations?: Expectations diff --git a/packages/types/src/documents/app/view.ts b/packages/types/src/documents/app/view.ts index a957564039..271df45c65 100644 --- a/packages/types/src/documents/app/view.ts +++ b/packages/types/src/documents/app/view.ts @@ -71,9 +71,11 @@ export interface ViewV2 { order?: SortOrder type?: SortType } - schema?: Record + schema?: ViewV2Schema } +export type ViewV2Schema = Record + export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema export interface ViewCountOrSumSchema { diff --git a/yarn.lock b/yarn.lock index b95f6f4f0f..1198e98ad6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2051,7 +2051,7 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.32.10": +"@budibase/backend-core@2.32.11": version "0.0.0" dependencies: "@budibase/nano" "10.1.5" @@ -2132,15 +2132,15 @@ through2 "^2.0.0" "@budibase/pro@npm:@budibase/pro@latest": - version "2.32.10" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.32.10.tgz#19fcbb3ced74791a7e96dfdc5a1270165792eea5" - integrity sha512-TbVp2bjmA0rHK+TKi9NVW06+O23fhDm7IJ/FlpWPHIBIZW7xDkCYu6LUOhSwSWMbOTcWzaJFuMbpN1HoTc/YjQ== + version "2.32.11" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.32.11.tgz#c94d534f829ca0ef252677757e157a7e58b87b4d" + integrity sha512-mOkqJpqHKWsfTWZwWcvBCYFUIluSUHltQNinc1ZRsg9rC3OKoHSDop6gzm744++H/GzGRN8V86kLhCgtNIlkpA== dependencies: "@anthropic-ai/sdk" "^0.27.3" - "@budibase/backend-core" "2.32.10" - "@budibase/shared-core" "2.32.10" - "@budibase/string-templates" "2.32.10" - "@budibase/types" "2.32.10" + "@budibase/backend-core" "2.32.11" + "@budibase/shared-core" "2.32.11" + "@budibase/string-templates" "2.32.11" + "@budibase/types" "2.32.11" "@koa/router" "8.0.8" bull "4.10.1" dd-trace "5.2.0" @@ -2153,13 +2153,13 @@ scim-patch "^0.8.1" scim2-parse-filter "^0.2.8" -"@budibase/shared-core@2.32.10": +"@budibase/shared-core@2.32.11": version "0.0.0" dependencies: "@budibase/types" "0.0.0" cron-validate "1.4.5" -"@budibase/string-templates@2.32.10": +"@budibase/string-templates@2.32.11": version "0.0.0" dependencies: "@budibase/handlebars-helpers" "^0.13.2" @@ -2167,7 +2167,7 @@ handlebars "^4.7.8" lodash.clonedeep "^4.5.0" -"@budibase/types@2.32.10": +"@budibase/types@2.32.11": version "0.0.0" dependencies: scim-patch "^0.8.1"