diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 934a838e6a..4801ac4c55 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -485,6 +485,25 @@ describe.each([ ) expect(response.message).toBe("Cannot create new user entry.") }) + + it("should not mis-parse date string out of JSON", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + }, + }) + ) + + const row = await config.api.row.save(table._id!, { + name: `{ "foo": "2023-01-26T11:48:57.000Z" }`, + }) + + expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`) + }) }) describe("get", () => { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index e462e91e42..5de788dc9c 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -17,6 +17,7 @@ import { TableSchema, User, Row, + RelationshipType, } from "@budibase/types" import _ from "lodash" import tk from "timekeeper" @@ -73,7 +74,7 @@ describe.each([ }) async function createTable(schema: TableSchema) { - table = await config.api.table.save( + return await config.api.table.save( tableForDatasource(datasource, { schema }) ) } @@ -190,7 +191,7 @@ describe.each([ describe("boolean", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ isTrue: { name: "isTrue", type: FieldType.BOOLEAN }, }) await createRows([{ isTrue: true }, { isTrue: false }]) @@ -320,7 +321,7 @@ describe.each([ }) ) - await createTable({ + table = await createTable({ name: { name: "name", type: FieldType.STRING }, appointment: { name: "appointment", type: FieldType.DATETIME }, single_user: { @@ -596,7 +597,7 @@ describe.each([ describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ name: { name: "name", type: FieldType.STRING }, }) await createRows([{ name: "foo" }, { name: "bar" }]) @@ -716,6 +717,20 @@ describe.each([ expectQuery({ range: { name: { low: "g", high: "h" } }, }).toFindNothing()) + + !isLucene && + it("ignores low if it's an empty object", () => + expectQuery({ + // @ts-ignore + range: { name: { low: {}, high: "z" } }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }])) + + !isLucene && + it("ignores high if it's an empty object", () => + expectQuery({ + // @ts-ignore + range: { name: { low: "a", high: {} } }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }])) }) describe("empty", () => { @@ -780,7 +795,7 @@ describe.each([ describe("numbers", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ age: { name: "age", type: FieldType.NUMBER }, }) await createRows([{ age: 1 }, { age: 10 }]) @@ -889,7 +904,7 @@ describe.each([ const JAN_10TH = "2020-01-10T00:00:00.000Z" beforeAll(async () => { - await createTable({ + table = await createTable({ dob: { name: "dob", type: FieldType.DATETIME }, }) @@ -1012,7 +1027,7 @@ describe.each([ const NULL_TIME__ID = `null_time__id` beforeAll(async () => { - await createTable({ + table = await createTable({ timeid: { name: "timeid", type: FieldType.STRING }, time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, }) @@ -1154,7 +1169,7 @@ describe.each([ describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ numbers: { name: "numbers", type: FieldType.ARRAY, @@ -1234,7 +1249,7 @@ describe.each([ const BIG = "9223372036854775807" beforeAll(async () => { - await createTable({ + table = await createTable({ num: { name: "num", type: FieldType.BIGINT }, }) await createRows([{ num: SMALL }, { num: MEDIUM }, { num: BIG }]) @@ -1325,7 +1340,7 @@ describe.each([ isInternal && describe("auto", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ auto: { name: "auto", type: FieldType.AUTO, @@ -1452,6 +1467,25 @@ describe.each([ { 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 } = await config.api.row.search(table._id!, { + tableId: table._id!, + query: {}, + }) + + for (let i = 0; i < 10; i++) { + const response = await config.api.row.search(table._id!, { + tableId: table._id!, + limit: 1, + query: {}, + }) + expect(response.rows).toEqual(rows) + } + }) }) // TODO(samwho): fix for SQS @@ -1490,7 +1524,7 @@ describe.each([ describe("field name 1:name", () => { beforeAll(async () => { - await createTable({ + table = await createTable({ "1:name": { name: "1:name", type: FieldType.STRING }, }) await createRows([{ "1:name": "bar" }, { "1:name": "foo" }]) @@ -1506,4 +1540,51 @@ describe.each([ expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing()) }) }) + + // This will never work for Lucene. + !isLucene && + describe("relations", () => { + let otherTable: Table + let rows: Row[] + + beforeAll(async () => { + otherTable = await createTable({ + one: { name: "one", type: FieldType.STRING }, + }) + table = await createTable({ + two: { name: "two", type: FieldType.STRING }, + other: { + type: FieldType.LINK, + relationshipType: RelationshipType.ONE_TO_MANY, + name: "other", + fieldName: "other", + tableId: otherTable._id!, + constraints: { + type: "array", + }, + }, + }) + + rows = await Promise.all([ + config.api.row.save(otherTable._id!, { one: "foo" }), + config.api.row.save(otherTable._id!, { one: "bar" }), + ]) + + await Promise.all([ + config.api.row.save(table._id!, { + two: "foo", + other: [rows[0]._id], + }), + config.api.row.save(table._id!, { + two: "bar", + other: [rows[1]._id], + }), + ]) + }) + + it("can search through relations", () => + expectQuery({ + equal: { [`${otherTable.name}.one`]: "foo" }, + }).toContainExactly([{ two: "foo", other: [{ _id: rows[0]._id }] }])) + }) }) diff --git a/packages/server/src/integrations/tests/sql.spec.ts b/packages/server/src/integrations/tests/sql.spec.ts index 0a76e52e92..f9c4fd7a4e 100644 --- a/packages/server/src/integrations/tests/sql.spec.ts +++ b/packages/server/src/integrations/tests/sql.spec.ts @@ -58,16 +58,6 @@ function generateReadJson({ } } -function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson { - return { - endpoint: endpoint(table, "CREATE"), - meta: { - table: TABLE, - }, - body, - } -} - function generateRelationshipJson(config: { schema?: string } = {}): QueryJson { return { endpoint: { @@ -148,24 +138,6 @@ describe("SQL query builder", () => { sql = new Sql(client, limit) }) - it("should allow filtering on a related field", () => { - const query = sql._query( - generateReadJson({ - filters: { - equal: { - age: 10, - "task.name": "task 1", - }, - }, - }) - ) - // order of bindings changes because relationship filters occur outside inner query - expect(query).toEqual({ - bindings: [10, limit, "task 1"], - sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."age" = $1 limit $2) as "${TABLE_NAME}" where "task"."name" = $3`, - }) - }) - it("should add the schema to the LEFT JOIN", () => { const query = sql._query(generateRelationshipJson({ schema: "production" })) expect(query).toEqual({ @@ -192,44 +164,6 @@ describe("SQL query builder", () => { }) }) - it("should ignore high range value if it is an empty object", () => { - const query = sql._query( - generateReadJson({ - filters: { - range: { - dob: { - low: "2000-01-01 00:00:00", - high: {}, - }, - }, - }, - }) - ) - expect(query).toEqual({ - bindings: ["2000-01-01 00:00:00", 500], - sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" >= $1 limit $2) as "${TABLE_NAME}"`, - }) - }) - - it("should ignore low range value if it is an empty object", () => { - const query = sql._query( - generateReadJson({ - filters: { - range: { - dob: { - low: {}, - high: "2010-01-01 00:00:00", - }, - }, - }, - }) - ) - expect(query).toEqual({ - bindings: ["2010-01-01 00:00:00", 500], - sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" <= $1 limit $2) as "${TABLE_NAME}"`, - }) - }) - it("should lowercase the values for Oracle LIKE statements", () => { let query = new Sql(SqlClient.ORACLE, limit)._query( generateReadJson({ @@ -274,44 +208,4 @@ describe("SQL query builder", () => { sql: `select * from (select * from (select * from "test" where LOWER("test"."name") LIKE :1) where rownum <= :2) "test"`, }) }) - - it("should sort SQL Server tables by the primary key if no sort data is provided", () => { - let query = new Sql(SqlClient.MS_SQL, limit)._query( - generateReadJson({ - sort: {}, - paginate: { - limit: 10, - }, - }) - ) - expect(query).toEqual({ - bindings: [10], - sql: `select * from (select top (@p0) * from [test] order by [test].[id] asc) as [test]`, - }) - }) - - it("should not parse JSON string as Date", () => { - let query = new Sql(SqlClient.POSTGRES, limit)._query( - generateCreateJson(TABLE_NAME, { - name: '{ "created_at":"2023-09-09T03:21:06.024Z" }', - }) - ) - expect(query).toEqual({ - bindings: ['{ "created_at":"2023-09-09T03:21:06.024Z" }'], - sql: `insert into "test" ("name") values ($1) returning *`, - }) - }) - - it("should parse and trim valid string as Date", () => { - const dateObj = new Date("2023-09-09T03:21:06.024Z") - let query = new Sql(SqlClient.POSTGRES, limit)._query( - generateCreateJson(TABLE_NAME, { - name: " 2023-09-09T03:21:06.024Z ", - }) - ) - expect(query).toEqual({ - bindings: [dateObj], - sql: `insert into "test" ("name") values ($1) returning *`, - }) - }) }) diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 8da2551792..44d971cc1a 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -56,8 +56,8 @@ function buildInternalFieldList( return fieldList } -function tableInFilter(name: string) { - return `:${name}.` +function tableNameInFieldRegex(tableName: string) { + return new RegExp(`^${tableName}.|:${tableName}.`, "g") } function cleanupFilters(filters: SearchFilters, tables: Table[]) { @@ -73,15 +73,13 @@ function cleanupFilters(filters: SearchFilters, tables: Table[]) { // relationship, switch to table ID const tableRelated = tables.find( table => - table.originalName && key.includes(tableInFilter(table.originalName)) + table.originalName && + key.match(tableNameInFieldRegex(table.originalName)) ) if (tableRelated && tableRelated.originalName) { - filter[ - key.replace( - tableInFilter(tableRelated.originalName), - tableInFilter(tableRelated._id!) - ) - ] = filter[key] + // only replace the first, not replaceAll + filter[key.replace(tableRelated.originalName, tableRelated._id!)] = + filter[key] delete filter[key] } } diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 77a6431335..7213cc66f1 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -37,7 +37,7 @@ export function tableForDatasource( ): Table { return merge( { - name: generator.guid(), + name: generator.guid().substring(0, 10), type: "table", sourceType: datasource ? TableSourceType.EXTERNAL