From 6b9b0801a89a7864efee7ddb86e2e86f904c9bfb Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 5 Nov 2024 16:24:00 +0000 Subject: [PATCH] Converting tests over to using datasourceDescribe. --- .../routes/tests/queries/generic-sql.spec.ts | 1713 ++++++++--------- .../server/src/integration-test/mysql.spec.ts | 62 +- .../src/integration-test/postgres.spec.ts | 378 ++-- .../sdk/app/rows/search/tests/search.spec.ts | 376 ++-- .../src/tests/filters/datasource-tests.ts | 9 + packages/server/src/tests/filters/index.ts | 13 + .../src/tests/filters/non-datasource-tests.ts | 9 + 7 files changed, 1293 insertions(+), 1267 deletions(-) create mode 100644 packages/server/src/tests/filters/datasource-tests.ts create mode 100644 packages/server/src/tests/filters/index.ts create mode 100644 packages/server/src/tests/filters/non-datasource-tests.ts diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index 4e9a1e5548..e08b662d15 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -3,613 +3,490 @@ import { Operation, Query, QueryPreview, - SourceName, TableSourceType, } from "@budibase/types" -import * as setup from "../utilities" import { DatabaseName, - getDatasource, - knexClient, + datasourceDescribe, } from "../../../../integrations/tests/utils" import { Expectations } from "src/tests/utilities/api/base" import { events } from "@budibase/backend-core" import { Knex } from "knex" -describe.each( - [ - DatabaseName.POSTGRES, - DatabaseName.MYSQL, - DatabaseName.SQL_SERVER, - DatabaseName.MARIADB, - DatabaseName.ORACLE, - ].map(name => [name, getDatasource(name)]) -)("queries (%s)", (dbName, dsProvider) => { - const config = setup.getConfig() - const isOracle = dbName === DatabaseName.ORACLE - const isMsSQL = dbName === DatabaseName.SQL_SERVER - const isPostgres = dbName === DatabaseName.POSTGRES - const mainTableName = "test_table" +datasourceDescribe( + { name: "queries (%s)", exclude: [DatabaseName.MONGODB, DatabaseName.SQS] }, + ({ config, dsProvider, isOracle, isMSSQL, isPostgres }) => { + const mainTableName = "test_table" - let rawDatasource: Datasource - let datasource: Datasource - let client: Knex + let rawDatasource: Datasource + let datasource: Datasource + let client: Knex - async function createQuery( - query: Partial, - expectations?: Expectations - ): Promise { - const defaultQuery: Query = { - datasourceId: datasource._id!, - name: "New Query", - parameters: [], - fields: {}, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - } - if (query.fields?.sql && typeof query.fields.sql !== "string") { - throw new Error("Unable to create with knex structure in 'sql' field") - } - return await config.api.query.save( - { ...defaultQuery, ...query }, - expectations - ) - } - - beforeAll(async () => { - await config.init() - }) - - beforeEach(async () => { - rawDatasource = await dsProvider - datasource = await config.api.datasource.create(rawDatasource) - - // The Datasource API doesn ot return the password, but we need it later to - // connect to the underlying database, so we fill it back in here. - datasource.config!.password = rawDatasource.config!.password - - client = await knexClient(rawDatasource) - - await client.schema.dropTableIfExists(mainTableName) - await client.schema.createTable(mainTableName, table => { - table.increments("id").primary() - table.string("name") - table.timestamp("birthday") - table.integer("number") - }) - - await client(mainTableName).insert([ - { name: "one" }, - { name: "two" }, - { name: "three" }, - { name: "four" }, - { name: "five" }, - ]) - - jest.clearAllMocks() - }) - - afterEach(async () => { - const ds = await config.api.datasource.get(datasource._id!) - await config.api.datasource.delete(ds) - }) - - afterAll(async () => { - setup.afterAll() - }) - - describe("query admin", () => { - describe("create", () => { - it("should be able to create a query", async () => { - const query = await createQuery({ - name: "New Query", - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - expect(query).toMatchObject({ - datasourceId: datasource._id!, - name: "New Query", - parameters: [], - fields: { - sql: client(mainTableName).select("*").toString(), - }, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - createdAt: expect.any(String), - updatedAt: expect.any(String), - }) - - expect(events.query.created).toHaveBeenCalledTimes(1) - expect(events.query.updated).not.toHaveBeenCalled() - }) - }) - - describe("update", () => { - it("should be able to update a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - jest.clearAllMocks() - - const updatedQuery = await config.api.query.save({ - ...query, - name: "Updated Query", - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, - }) - - expect(updatedQuery).toMatchObject({ - datasourceId: datasource._id!, - name: "Updated Query", - parameters: [], - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - }) - - expect(events.query.created).not.toHaveBeenCalled() - expect(events.query.updated).toHaveBeenCalledTimes(1) - }) - }) - - describe("delete", () => { - it("should be able to delete a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - await config.api.query.delete(query) - await config.api.query.get(query._id!, { status: 404 }) - - const queries = await config.api.query.fetch() - expect(queries).not.toContainEqual(query) - - expect(events.query.deleted).toHaveBeenCalledTimes(1) - expect(events.query.deleted).toHaveBeenCalledWith(datasource, query) - }) - }) - - describe("read", () => { - it("should be able to list queries", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - const queries = await config.api.query.fetch() - expect(queries).toContainEqual(query) - }) - - it("should strip sensitive fields for prod apps", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - await config.api.application.publish(config.getAppId()) - const prodQuery = await config.api.query.getProd(query._id!) - - expect(prodQuery._id).toEqual(query._id) - expect(prodQuery.fields).toBeUndefined() - expect(prodQuery.parameters).toBeUndefined() - expect(prodQuery.schema).toBeDefined() - }) - - isPostgres && - it("should be able to handle a JSON aggregate with newlines", async () => { - const jsonStatement = `COALESCE(json_build_object('name', name),'{"name":{}}'::json)` - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .select([ - "*", - client.raw( - `${jsonStatement} as json,\n${jsonStatement} as json2` - ), - ]) - .toString(), - }, - }) - const res = await config.api.query.execute( - query._id!, - {}, - { - status: 200, - } - ) - expect(res).toBeDefined() - }) - }) - }) - - describe("preview", () => { - it("should be able to preview a query", async () => { - const request: QueryPreview = { + async function createQuery( + query: Partial, + expectations?: Expectations + ): Promise { + const defaultQuery: Query = { datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, + name: "New Query", parameters: [], - transformer: "return data", - name: datasource.name!, + fields: {}, schema: {}, + queryVerb: "read", + transformer: "return data", readable: true, } - const response = await config.api.query.preview(request) - expect(response.schema).toEqual({ - birthday: { - name: "birthday", - type: "string", - }, - id: { - name: "id", - type: "number", - }, - name: { - name: "name", - type: "string", - }, - number: { - name: "number", - type: "string", - }, - }) - expect(response.rows).toEqual([ - { - birthday: null, - id: 1, - name: "one", - number: null, - }, - ]) - expect(events.query.previewed).toHaveBeenCalledTimes(1) + if (query.fields?.sql && typeof query.fields.sql !== "string") { + throw new Error("Unable to create with knex structure in 'sql' field") + } + return await config.api.query.save( + { ...defaultQuery, ...query }, + expectations + ) + } + + beforeAll(async () => { + const ds = await dsProvider + rawDatasource = ds.rawDatasource! + datasource = ds.datasource! + client = ds.client! }) - it("should update schema when column type changes from number to string", async () => { - const tableName = "schema_change_test" - await client.schema.dropTableIfExists(tableName) + beforeEach(async () => { + // The Datasource API doesn ot return the password, but we need it later to + // connect to the underlying database, so we fill it back in here. + datasource.config!.password = rawDatasource.config!.password - await client.schema.createTable(tableName, table => { + await client.schema.dropTableIfExists(mainTableName) + await client.schema.createTable(mainTableName, table => { table.increments("id").primary() table.string("name") - table.integer("data") + table.timestamp("birthday") + table.integer("number") }) - await client(tableName).insert({ - name: "test", - data: 123, - }) - - const firstPreview = await config.api.query.preview({ - datasourceId: datasource._id!, - name: "Test Query", - queryVerb: "read", - fields: { - sql: client(tableName).select("*").toString(), - }, - parameters: [], - transformer: "return data", - schema: {}, - readable: true, - }) - - expect(firstPreview.schema).toEqual( - expect.objectContaining({ - data: { type: "number", name: "data" }, - }) - ) - - await client(tableName).delete() - await client.schema.alterTable(tableName, table => { - table.string("data").alter() - }) - - await client(tableName).insert({ - name: "test", - data: "string value", - }) - - const secondPreview = await config.api.query.preview({ - datasourceId: datasource._id!, - name: "Test Query", - queryVerb: "read", - fields: { - sql: client(tableName).select("*").toString(), - }, - parameters: [], - transformer: "return data", - schema: firstPreview.schema, - readable: true, - }) - - expect(secondPreview.schema).toEqual( - expect.objectContaining({ - data: { type: "string", name: "data" }, - }) - ) - }) - - it("should work with static variables", async () => { - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - staticVariables: { - foo: "bar", - }, - }, - }) - - const request: QueryPreview = { - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - } - - const response = await config.api.query.preview(request) - - let key = isOracle ? "FOO" : "foo" - expect(response.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, - }) - - expect(response.rows).toEqual([ - { - [key]: "bar", - }, + await client(mainTableName).insert([ + { name: "one" }, + { name: "two" }, + { name: "three" }, + { name: "four" }, + { name: "five" }, ]) + + jest.clearAllMocks() }) - it("should work with dynamic variables", async () => { - const basedOnQuery = await createQuery({ - fields: { - sql: client(mainTableName).select("name").where({ id: 1 }).toString(), - }, - }) - - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - dynamicVariables: [ - { - queryId: basedOnQuery._id!, - name: "foo", - value: "{{ data[0].name }}", + describe("query admin", () => { + describe("create", () => { + it("should be able to create a query", async () => { + const query = await createQuery({ + name: "New Query", + fields: { + sql: client(mainTableName).select("*").toString(), }, - ], - }, - }) + }) - const preview = await config.api.query.preview({ - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - }) - - let key = isOracle ? "FOO" : "foo" - expect(preview.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, - }) - - expect(preview.rows).toEqual([ - { - [key]: "one", - }, - ]) - }) - - it("should handle the dynamic base query being deleted", async () => { - const basedOnQuery = await createQuery({ - fields: { - sql: client(mainTableName).select("name").where({ id: 1 }).toString(), - }, - }) - - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - dynamicVariables: [ - { - queryId: basedOnQuery._id!, - name: "foo", - value: "{{ data[0].name }}", + expect(query).toMatchObject({ + datasourceId: datasource._id!, + name: "New Query", + parameters: [], + fields: { + sql: client(mainTableName).select("*").toString(), }, - ], - }, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + createdAt: expect.any(String), + updatedAt: expect.any(String), + }) + + expect(events.query.created).toHaveBeenCalledTimes(1) + expect(events.query.updated).not.toHaveBeenCalled() + }) }) - await config.api.query.delete(basedOnQuery) - - const preview = await config.api.query.preview({ - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - }) - - let key = isOracle ? "FOO" : "foo" - expect(preview.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, - }) - - expect(preview.rows).toEqual([ - { - [key]: datasource.source === SourceName.SQL_SERVER ? "" : null, - }, - ]) - }) - }) - - describe("query verbs", () => { - describe("create", () => { - it("should be able to insert with bindings", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(), - }, - parameters: [ - { - name: "foo", - default: "bar", + describe("update", () => { + it("should be able to update a query", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).select("*").toString(), }, - ], - queryVerb: "create", + }) + + jest.clearAllMocks() + + const updatedQuery = await config.api.query.save({ + ...query, + name: "Updated Query", + fields: { + sql: client(mainTableName).where({ id: 1 }).toString(), + }, + }) + + expect(updatedQuery).toMatchObject({ + datasourceId: datasource._id!, + name: "Updated Query", + parameters: [], + fields: { + sql: client(mainTableName).where({ id: 1 }).toString(), + }, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + }) + + expect(events.query.created).not.toHaveBeenCalled() + expect(events.query.updated).toHaveBeenCalledTimes(1) + }) + }) + + describe("delete", () => { + it("should be able to delete a query", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).select("*").toString(), + }, + }) + + await config.api.query.delete(query) + await config.api.query.get(query._id!, { status: 404 }) + + const queries = await config.api.query.fetch() + expect(queries).not.toContainEqual(query) + + expect(events.query.deleted).toHaveBeenCalledTimes(1) + expect(events.query.deleted).toHaveBeenCalledWith(datasource, query) + }) + }) + + describe("read", () => { + it("should be able to list queries", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).select("*").toString(), + }, + }) + + const queries = await config.api.query.fetch() + expect(queries).toContainEqual(query) }) - const result = await config.api.query.execute(query._id!, { - parameters: { - foo: "baz", - }, + it("should strip sensitive fields for prod apps", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).select("*").toString(), + }, + }) + + await config.api.application.publish(config.getAppId()) + const prodQuery = await config.api.query.getProd(query._id!) + + expect(prodQuery._id).toEqual(query._id) + expect(prodQuery.fields).toBeUndefined() + expect(prodQuery.parameters).toBeUndefined() + expect(prodQuery.schema).toBeDefined() }) - expect(result.data).toEqual([ - { - created: true, - }, - ]) - - const rows = await client(mainTableName).where({ name: "baz" }).select() - expect(rows).toHaveLength(1) - for (const row of rows) { - expect(row).toMatchObject({ name: "baz" }) - } - }) - - it("should not allow handlebars as parameters", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(), - }, - parameters: [ - { - name: "foo", - default: "bar", - }, - ], - queryVerb: "create", - }) - - await config.api.query.execute( - query._id!, - { - parameters: { - foo: "{{ 'test' }}", - }, - }, - { - status: 400, - body: { - message: - "Parameter 'foo' input contains a handlebars binding - this is not allowed.", - }, - } - ) - }) - - // Oracle doesn't automatically coerce strings into dates. - !isOracle && - it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])( - "should coerce %s into a date", - async datetimeStr => { - const date = new Date(datetimeStr) + isPostgres && + it("should be able to handle a JSON aggregate with newlines", async () => { + const jsonStatement = `COALESCE(json_build_object('name', name),'{"name":{}}'::json)` const query = await createQuery({ fields: { sql: client(mainTableName) - .insert({ - name: "foo", - birthday: client.raw("{{ birthday }}"), - }) + .select([ + "*", + client.raw( + `${jsonStatement} as json,\n${jsonStatement} as json2` + ), + ]) .toString(), }, - parameters: [ - { - name: "birthday", - default: "", - }, - ], - queryVerb: "create", }) + const res = await config.api.query.execute( + query._id!, + {}, + { + status: 200, + } + ) + expect(res).toBeDefined() + }) + }) + }) - const result = await config.api.query.execute(query._id!, { - parameters: { birthday: datetimeStr }, - }) + describe("preview", () => { + it("should be able to preview a query", async () => { + const request: QueryPreview = { + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: client(mainTableName).where({ id: 1 }).toString(), + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + } + const response = await config.api.query.preview(request) + expect(response.schema).toEqual({ + birthday: { + name: "birthday", + type: "string", + }, + id: { + name: "id", + type: "number", + }, + name: { + name: "name", + type: "string", + }, + number: { + name: "number", + type: "string", + }, + }) + expect(response.rows).toEqual([ + { + birthday: null, + id: 1, + name: "one", + number: null, + }, + ]) + expect(events.query.previewed).toHaveBeenCalledTimes(1) + }) - expect(result.data).toEqual([{ created: true }]) + it("should update schema when column type changes from number to string", async () => { + const tableName = "schema_change_test" + await client.schema.dropTableIfExists(tableName) - const rows = await client(mainTableName) - .where({ birthday: datetimeStr }) - .select() - expect(rows).toHaveLength(1) + await client.schema.createTable(tableName, table => { + table.increments("id").primary() + table.string("name") + table.integer("data") + }) - for (const row of rows) { - expect(new Date(row.birthday)).toEqual(date) - } - } + await client(tableName).insert({ + name: "test", + data: 123, + }) + + const firstPreview = await config.api.query.preview({ + datasourceId: datasource._id!, + name: "Test Query", + queryVerb: "read", + fields: { + sql: client(tableName).select("*").toString(), + }, + parameters: [], + transformer: "return data", + schema: {}, + readable: true, + }) + + expect(firstPreview.schema).toEqual( + expect.objectContaining({ + data: { type: "number", name: "data" }, + }) ) - it.each(["2021,02,05", "202205-1500"])( - "should not coerce %s as a date", - async notDateStr => { + await client(tableName).delete() + await client.schema.alterTable(tableName, table => { + table.string("data").alter() + }) + + await client(tableName).insert({ + name: "test", + data: "string value", + }) + + const secondPreview = await config.api.query.preview({ + datasourceId: datasource._id!, + name: "Test Query", + queryVerb: "read", + fields: { + sql: client(tableName).select("*").toString(), + }, + parameters: [], + transformer: "return data", + schema: firstPreview.schema, + readable: true, + }) + + expect(secondPreview.schema).toEqual( + expect.objectContaining({ + data: { type: "string", name: "data" }, + }) + ) + }) + + it("should work with static variables", async () => { + const datasource = await config.api.datasource.create({ + ...rawDatasource, + config: { + ...rawDatasource.config, + staticVariables: { + foo: "bar", + }, + }, + }) + + const request: QueryPreview = { + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + } + + const response = await config.api.query.preview(request) + + let key = isOracle ? "FOO" : "foo" + expect(response.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(response.rows).toEqual([ + { + [key]: "bar", + }, + ]) + }) + + it("should work with dynamic variables", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const basedOnQuery = await createQuery({ + datasourceId: datasource._id!, + fields: { + sql: client(mainTableName) + .select("name") + .where({ id: 1 }) + .toString(), + }, + }) + + await config.api.datasource.update({ + ...datasource, + config: { + ...datasource.config, + dynamicVariables: [ + { + queryId: basedOnQuery._id!, + name: "foo", + value: "{{ data[0].name }}", + }, + ], + }, + }) + + const preview = await config.api.query.preview({ + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + }) + + let key = isOracle ? "FOO" : "foo" + expect(preview.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(preview.rows).toEqual([ + { + [key]: "one", + }, + ]) + }) + + it("should handle the dynamic base query being deleted", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const basedOnQuery = await createQuery({ + datasourceId: datasource._id!, + fields: { + sql: client(mainTableName) + .select("name") + .where({ id: 1 }) + .toString(), + }, + }) + + await config.api.datasource.update({ + ...datasource, + config: { + ...datasource.config, + dynamicVariables: [ + { + queryId: basedOnQuery._id!, + name: "foo", + value: "{{ data[0].name }}", + }, + ], + }, + }) + + await config.api.query.delete(basedOnQuery) + + const preview = await config.api.query.preview({ + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + }) + + let key = isOracle ? "FOO" : "foo" + expect(preview.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(preview.rows).toEqual([{ [key]: isMSSQL ? "" : null }]) + }) + }) + + describe("query verbs", () => { + describe("create", () => { + it("should be able to insert with bindings", async () => { const query = await createQuery({ fields: { sql: client(mainTableName) - .insert({ name: client.raw("{{ name }}") }) + .insert({ name: "{{ foo }}" }) .toString(), }, parameters: [ { - name: "name", - default: "", + name: "foo", + default: "bar", }, ], queryVerb: "create", @@ -617,271 +494,449 @@ describe.each( const result = await config.api.query.execute(query._id!, { parameters: { - name: notDateStr, + foo: "baz", }, }) - expect(result.data).toEqual([{ created: true }]) + expect(result.data).toEqual([ + { + created: true, + }, + ]) const rows = await client(mainTableName) - .where({ name: notDateStr }) + .where({ name: "baz" }) .select() expect(rows).toHaveLength(1) - } - ) - }) - - describe("read", () => { - it("should execute a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").orderBy("id").toString(), - }, + for (const row of rows) { + expect(row).toMatchObject({ name: "baz" }) + } }) - const result = await config.api.query.execute(query._id!) + it("should not allow handlebars as parameters", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .insert({ name: "{{ foo }}" }) + .toString(), + }, + parameters: [ + { + name: "foo", + default: "bar", + }, + ], + queryVerb: "create", + }) - expect(result.data).toEqual([ - { - id: 1, - name: "one", - birthday: null, - number: null, - }, - { - id: 2, - name: "two", - birthday: null, - number: null, - }, - { - id: 3, - name: "three", - birthday: null, - number: null, - }, - { - id: 4, - name: "four", - birthday: null, - number: null, - }, - { - id: 5, - name: "five", - birthday: null, - number: null, - }, - ]) + await config.api.query.execute( + query._id!, + { + parameters: { + foo: "{{ 'test' }}", + }, + }, + { + status: 400, + body: { + message: + "Parameter 'foo' input contains a handlebars binding - this is not allowed.", + }, + } + ) + }) + + // Oracle doesn't automatically coerce strings into dates. + !isOracle && + it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])( + "should coerce %s into a date", + async datetimeStr => { + const date = new Date(datetimeStr) + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .insert({ + name: "foo", + birthday: client.raw("{{ birthday }}"), + }) + .toString(), + }, + parameters: [ + { + name: "birthday", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { birthday: datetimeStr }, + }) + + expect(result.data).toEqual([{ created: true }]) + + const rows = await client(mainTableName) + .where({ birthday: datetimeStr }) + .select() + expect(rows).toHaveLength(1) + + for (const row of rows) { + expect(new Date(row.birthday)).toEqual(date) + } + } + ) + + it.each(["2021,02,05", "202205-1500"])( + "should not coerce %s as a date", + async notDateStr => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .insert({ name: client.raw("{{ name }}") }) + .toString(), + }, + parameters: [ + { + name: "name", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + name: notDateStr, + }, + }) + + expect(result.data).toEqual([{ created: true }]) + + const rows = await client(mainTableName) + .where({ name: notDateStr }) + .select() + expect(rows).toHaveLength(1) + } + ) }) - it("should be able to transform a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).where({ id: 1 }).select("*").toString(), - }, - transformer: ` + describe("read", () => { + it("should execute a query", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).select("*").orderBy("id").toString(), + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + birthday: null, + number: null, + }, + { + id: 2, + name: "two", + birthday: null, + number: null, + }, + { + id: 3, + name: "three", + birthday: null, + number: null, + }, + { + id: 4, + name: "four", + birthday: null, + number: null, + }, + { + id: 5, + name: "five", + birthday: null, + number: null, + }, + ]) + }) + + it("should be able to transform a query", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .where({ id: 1 }) + .select("*") + .toString(), + }, + transformer: ` data[0].id = data[0].id + 1; return data; `, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 2, + name: "one", + birthday: null, + number: null, + }, + ]) }) - const result = await config.api.query.execute(query._id!) + it("should coerce numeric bindings", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .where({ id: client.raw("{{ id }}") }) + .select("*") + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + }) - expect(result.data).toEqual([ - { - id: 2, - name: "one", - birthday: null, - number: null, - }, - ]) + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + birthday: null, + number: null, + }, + ]) + }) }) - it("should coerce numeric bindings", async () => { + describe("update", () => { + it("should be able to update rows", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .update({ name: client.raw("{{ name }}") }) + .where({ id: client.raw("{{ id }}") }) + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + { + name: "name", + default: "updated", + }, + ], + queryVerb: "update", + }) + + await config.api.query.execute(query._id!, { + parameters: { + id: "1", + name: "foo", + }, + }) + + const rows = await client(mainTableName).where({ id: 1 }).select() + expect(rows).toEqual([ + { id: 1, name: "foo", birthday: null, number: null }, + ]) + }) + + it("should be able to execute an update that updates no rows", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .update({ name: "updated" }) + .where({ id: 100 }) + .toString(), + }, + queryVerb: "update", + }) + + await config.api.query.execute(query._id!) + + const rows = await client(mainTableName).select() + for (const row of rows) { + expect(row.name).not.toEqual("updated") + } + }) + + it("should be able to execute a delete that deletes no rows", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName).where({ id: 100 }).delete().toString(), + }, + queryVerb: "delete", + }) + + await config.api.query.execute(query._id!) + + const rows = await client(mainTableName).select() + expect(rows).toHaveLength(5) + }) + }) + + describe("delete", () => { + it("should be able to delete rows", async () => { + const query = await createQuery({ + fields: { + sql: client(mainTableName) + .where({ id: client.raw("{{ id }}") }) + .delete() + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + queryVerb: "delete", + }) + + await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + const rows = await client(mainTableName).where({ id: 1 }).select() + expect(rows).toHaveLength(0) + }) + }) + }) + + describe("query through datasource", () => { + it("should be able to query the datasource", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const entityId = mainTableName + await config.api.datasource.update({ + ...datasource, + entities: { + [entityId]: { + name: entityId, + schema: {}, + type: "table", + primary: ["id"], + sourceId: datasource._id!, + sourceType: TableSourceType.EXTERNAL, + }, + }, + }) + + const res = await config.api.datasource.query({ + endpoint: { + datasourceId: datasource._id!, + operation: Operation.READ, + entityId, + }, + resource: { + fields: ["id", "name"], + }, + filters: { + string: { + name: "two", + }, + }, + }) + expect(res).toHaveLength(1) + expect(res[0]).toEqual({ + id: 2, + name: "two", + // the use of table.* introduces the possibility of nulls being returned + birthday: null, + number: null, + }) + }) + + // this parameter really only impacts SQL queries + describe("confirm nullDefaultSupport", () => { + let queryParams: Partial + beforeAll(async () => { + queryParams = { + fields: { + sql: client(mainTableName) + .insert({ + name: client.raw("{{ bindingName }}"), + number: client.raw("{{ bindingNumber }}"), + }) + .toString(), + }, + parameters: [ + { + name: "bindingName", + default: "", + }, + { + name: "bindingNumber", + default: "", + }, + ], + queryVerb: "create", + } + }) + + it("should error for old queries", async () => { + const query = await createQuery(queryParams) + await config.api.query.save({ ...query, nullDefaultSupport: false }) + let error: string | undefined + try { + await config.api.query.execute(query._id!, { + parameters: { + bindingName: "testing", + }, + }) + } catch (err: any) { + error = err.message + } + if (isMSSQL || isOracle) { + expect(error).toBeUndefined() + } else { + expect(error).toBeDefined() + expect(error).toContain("integer") + } + }) + + it("should not error for new queries", async () => { + const query = await createQuery(queryParams) + const results = await config.api.query.execute(query._id!, { + parameters: { + bindingName: "testing", + }, + }) + expect(results).toEqual({ data: [{ created: true }] }) + }) + }) + }) + + describe("edge cases", () => { + it("should find rows with a binding containing a slash", async () => { + const slashValue = "1/10" + await client(mainTableName).insert([{ name: slashValue }]) + const query = await createQuery({ fields: { sql: client(mainTableName) - .where({ id: client.raw("{{ id }}") }) .select("*") - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - ], - }) - - const result = await config.api.query.execute(query._id!, { - parameters: { - id: "1", - }, - }) - - expect(result.data).toEqual([ - { - id: 1, - name: "one", - birthday: null, - number: null, - }, - ]) - }) - }) - - describe("update", () => { - it("should be able to update rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .update({ name: client.raw("{{ name }}") }) - .where({ id: client.raw("{{ id }}") }) - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - { - name: "name", - default: "updated", - }, - ], - queryVerb: "update", - }) - - await config.api.query.execute(query._id!, { - parameters: { - id: "1", - name: "foo", - }, - }) - - const rows = await client(mainTableName).where({ id: 1 }).select() - expect(rows).toEqual([ - { id: 1, name: "foo", birthday: null, number: null }, - ]) - }) - - it("should be able to execute an update that updates no rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .update({ name: "updated" }) - .where({ id: 100 }) - .toString(), - }, - queryVerb: "update", - }) - - await config.api.query.execute(query._id!) - - const rows = await client(mainTableName).select() - for (const row of rows) { - expect(row.name).not.toEqual("updated") - } - }) - - it("should be able to execute a delete that deletes no rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).where({ id: 100 }).delete().toString(), - }, - queryVerb: "delete", - }) - - await config.api.query.execute(query._id!) - - const rows = await client(mainTableName).select() - expect(rows).toHaveLength(5) - }) - }) - - describe("delete", () => { - it("should be able to delete rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .where({ id: client.raw("{{ id }}") }) - .delete() - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - ], - queryVerb: "delete", - }) - - await config.api.query.execute(query._id!, { - parameters: { - id: "1", - }, - }) - - const rows = await client(mainTableName).where({ id: 1 }).select() - expect(rows).toHaveLength(0) - }) - }) - }) - - describe("query through datasource", () => { - it("should be able to query the datasource", async () => { - const entityId = mainTableName - await config.api.datasource.update({ - ...datasource, - entities: { - [entityId]: { - name: entityId, - schema: {}, - type: "table", - primary: ["id"], - sourceId: datasource._id!, - sourceType: TableSourceType.EXTERNAL, - }, - }, - }) - const res = await config.api.datasource.query({ - endpoint: { - datasourceId: datasource._id!, - operation: Operation.READ, - entityId, - }, - resource: { - fields: ["id", "name"], - }, - filters: { - string: { - name: "two", - }, - }, - }) - expect(res).toHaveLength(1) - expect(res[0]).toEqual({ - id: 2, - name: "two", - // the use of table.* introduces the possibility of nulls being returned - birthday: null, - number: null, - }) - }) - - // this parameter really only impacts SQL queries - describe("confirm nullDefaultSupport", () => { - let queryParams: Partial - beforeAll(async () => { - queryParams = { - fields: { - sql: client(mainTableName) - .insert({ - name: client.raw("{{ bindingName }}"), - number: client.raw("{{ bindingNumber }}"), - }) + .where("name", "=", client.raw("{{ bindingName }}")) .toString(), }, parameters: [ @@ -889,75 +944,17 @@ describe.each( name: "bindingName", default: "", }, - { - name: "bindingNumber", - default: "", - }, ], - queryVerb: "create", - } - }) - - it("should error for old queries", async () => { - const query = await createQuery(queryParams) - await config.api.query.save({ ...query, nullDefaultSupport: false }) - let error: string | undefined - try { - await config.api.query.execute(query._id!, { - parameters: { - bindingName: "testing", - }, - }) - } catch (err: any) { - error = err.message - } - if (isMsSQL || isOracle) { - expect(error).toBeUndefined() - } else { - expect(error).toBeDefined() - expect(error).toContain("integer") - } - }) - - it("should not error for new queries", async () => { - const query = await createQuery(queryParams) + queryVerb: "read", + }) const results = await config.api.query.execute(query._id!, { parameters: { - bindingName: "testing", + bindingName: slashValue, }, }) - expect(results).toEqual({ data: [{ created: true }] }) + expect(results).toBeDefined() + expect(results.data.length).toEqual(1) }) }) - }) - - describe("edge cases", () => { - it("should find rows with a binding containing a slash", async () => { - const slashValue = "1/10" - await client(mainTableName).insert([{ name: slashValue }]) - - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .select("*") - .where("name", "=", client.raw("{{ bindingName }}")) - .toString(), - }, - parameters: [ - { - name: "bindingName", - default: "", - }, - ], - queryVerb: "read", - }) - const results = await config.api.query.execute(query._id!, { - parameters: { - bindingName: slashValue, - }, - }) - expect(results).toBeDefined() - expect(results.data.length).toEqual(1) - }) - }) -}) + } +) diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index eb6c840abc..8022b75c83 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -1,10 +1,5 @@ -import * as setup from "../api/routes/tests/utilities" import { Datasource, FieldType } from "@budibase/types" -import { - DatabaseName, - getDatasource, - knexClient, -} from "../integrations/tests/utils" +import { DatabaseName, datasourceDescribe } from "../integrations/tests/utils" import { generator } from "@budibase/backend-core/tests" import { Knex } from "knex" @@ -15,31 +10,24 @@ function uniqueTableName(length?: number): string { .substring(0, length || 10) } -const config = setup.getConfig()! - -describe("mysql integrations", () => { - let datasource: Datasource - let client: Knex - - beforeAll(async () => { - await config.init() - const rawDatasource = await getDatasource(DatabaseName.MYSQL) - datasource = await config.api.datasource.create(rawDatasource) - client = await knexClient(rawDatasource) - }) - - afterAll(config.end) - - describe("Integration compatibility with mysql search_path", () => { - let datasource: Datasource +datasourceDescribe( + { + name: "Integration compatibility with mysql search_path", + only: [DatabaseName.MYSQL], + }, + ({ config, dsProvider }) => { let rawDatasource: Datasource + let datasource: Datasource let client: Knex + const database = generator.guid() const database2 = generator.guid() beforeAll(async () => { - rawDatasource = await getDatasource(DatabaseName.MYSQL) - client = await knexClient(rawDatasource) + const ds = await dsProvider + rawDatasource = ds.rawDatasource! + datasource = ds.datasource! + client = ds.client! await client.raw(`CREATE DATABASE \`${database}\`;`) await client.raw(`CREATE DATABASE \`${database2}\`;`) @@ -87,11 +75,25 @@ describe("mysql integrations", () => { const schema = res.datasource.entities![repeated_table_name].schema expect(Object.keys(schema).sort()).toEqual(["id", "val1"]) }) - }) + } +) + +datasourceDescribe( + { + name: "POST /api/datasources/:datasourceId/schema", + only: [DatabaseName.MYSQL], + }, + ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex + + beforeAll(async () => { + const ds = await dsProvider + datasource = ds.datasource! + client = ds.client! + }) - describe("POST /api/datasources/:datasourceId/schema", () => { let tableName: string - beforeEach(async () => { tableName = uniqueTableName() }) @@ -122,5 +124,5 @@ describe("mysql integrations", () => { expect(table).toBeDefined() expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS) }) - }) -}) + } +) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 7654d84551..f384eb01ba 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -1,105 +1,230 @@ -import * as setup from "../api/routes/tests/utilities" import { Datasource, FieldType, Table } from "@budibase/types" import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { DatabaseName, - getDatasource, + datasourceDescribe, knexClient, } from "../integrations/tests/utils" import { Knex } from "knex" -const config = setup.getConfig()! +datasourceDescribe( + { name: "postgres integrations", only: [DatabaseName.POSTGRES] }, + ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex -describe("postgres integrations", () => { - let datasource: Datasource - let client: Knex - - beforeAll(async () => { - await config.init() - const rawDatasource = await getDatasource(DatabaseName.POSTGRES) - datasource = await config.api.datasource.create(rawDatasource) - client = await knexClient(rawDatasource) - }) - - afterAll(config.end) - - describe("POST /api/datasources/:datasourceId/schema", () => { - let tableName: string - - beforeEach(async () => { - tableName = generator.guid().replaceAll("-", "").substring(0, 10) + beforeAll(async () => { + const ds = await dsProvider + datasource = ds.datasource! + client = ds.client! }) - afterEach(async () => { - await client.schema.dropTableIfExists(tableName) - }) + afterAll(config.end) - it("recognises when a table has no primary key", async () => { - await client.schema.createTable(tableName, table => { - table.increments("id", { primaryKey: false }) + describe("POST /api/datasources/:datasourceId/schema", () => { + let tableName: string + + beforeEach(async () => { + tableName = generator.guid().replaceAll("-", "").substring(0, 10) }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, + afterEach(async () => { + await client.schema.dropTableIfExists(tableName) }) - expect(response.errors).toEqual({ - [tableName]: "Table must have a primary key.", - }) - }) + it("recognises when a table has no primary key", async () => { + await client.schema.createTable(tableName, table => { + table.increments("id", { primaryKey: false }) + }) - it("recognises when a table is using a reserved column name", async () => { - await client.schema.createTable(tableName, table => { - table.increments("_id").primary() - }) + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - - expect(response.errors).toEqual({ - [tableName]: "Table contains invalid columns.", - }) - }) - - it("recognises enum columns as options", async () => { - const tableName = `orders_${generator - .guid() - .replaceAll("-", "") - .substring(0, 6)}` - - await client.schema.createTable(tableName, table => { - table.increments("order_id").primary() - table.string("customer_name").notNullable() - table.enum("status", ["pending", "processing", "shipped"], { - useNative: true, - enumName: `${tableName}_status`, + expect(response.errors).toEqual({ + [tableName]: "Table must have a primary key.", }) }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, + it("recognises when a table is using a reserved column name", async () => { + await client.schema.createTable(tableName, table => { + table.increments("_id").primary() + }) + + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + + expect(response.errors).toEqual({ + [tableName]: "Table contains invalid columns.", + }) }) - const table = response.datasource.entities?.[tableName] + it("recognises enum columns as options", async () => { + const tableName = `orders_${generator + .guid() + .replaceAll("-", "") + .substring(0, 6)}` - expect(table).toBeDefined() - expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS) + await client.schema.createTable(tableName, table => { + table.increments("order_id").primary() + table.string("customer_name").notNullable() + table.enum("status", ["pending", "processing", "shipped"], { + useNative: true, + enumName: `${tableName}_status`, + }) + }) + + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + + const table = response.datasource.entities?.[tableName] + + expect(table).toBeDefined() + expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS) + }) }) - }) - describe("Integration compatibility with postgres search_path", () => { + describe("check custom column types", () => { + beforeAll(async () => { + await client.schema.createTable("binaryTable", table => { + table.binary("id").primary() + table.string("column1") + table.integer("column2") + }) + }) + + it("should handle binary columns", async () => { + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + expect(response.datasource.entities).toBeDefined() + const table = response.datasource.entities?.["binaryTable"] + expect(table).toBeDefined() + expect(table?.schema.id.externalType).toBe("bytea") + const row = await config.api.row.save(table?._id!, { + id: "1111", + column1: "hello", + column2: 222, + }) + expect(row._id).toBeDefined() + const decoded = decodeURIComponent(row._id!).replace(/'/g, '"') + expect(JSON.parse(decoded)[0]).toBe("1111") + }) + }) + + describe("check fetching null/not null table", () => { + beforeAll(async () => { + await client.schema.createTable("nullableTable", table => { + table.increments("order_id").primary() + table.integer("order_number").notNullable() + }) + }) + + it("should be able to change the table to allow nullable and refetch this", async () => { + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + const entities = response.datasource.entities + expect(entities).toBeDefined() + const nullableTable = entities?.["nullableTable"] + expect(nullableTable).toBeDefined() + expect( + nullableTable?.schema["order_number"].constraints?.presence + ).toEqual(true) + + // need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase + // is aware of - therefore we can try to fetch and make sure BB updates correctly + await client.schema.alterTable("nullableTable", table => { + table.setNullable("order_number") + }) + + const responseAfter = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + const entitiesAfter = responseAfter.datasource.entities + expect(entitiesAfter).toBeDefined() + const nullableTableAfter = entitiesAfter?.["nullableTable"] + expect(nullableTableAfter).toBeDefined() + expect( + nullableTableAfter?.schema["order_number"].constraints?.presence + ).toBeUndefined() + }) + }) + + describe("money field 💰", () => { + const tableName = "moneytable" + let table: Table + + beforeAll(async () => { + await client.raw(` + CREATE TABLE ${tableName} ( + id serial PRIMARY KEY, + price money + ) + `) + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + table = response.datasource.entities![tableName] + }) + + it("should be able to import a money field", async () => { + expect(table).toBeDefined() + expect(table?.schema.price.type).toBe(FieldType.NUMBER) + }) + + it("should be able to search a money field", async () => { + await config.api.row.bulkImport(table._id!, { + rows: [{ price: 200 }, { price: 300 }], + }) + + const { rows } = await config.api.row.search(table._id!, { + query: { + equal: { + price: 200, + }, + }, + }) + expect(rows).toHaveLength(1) + expect(rows[0].price).toBe("200.00") + }) + + it("should be able to update a money field", async () => { + let row = await config.api.row.save(table._id!, { price: 200 }) + expect(row.price).toBe("200.00") + + row = await config.api.row.save(table._id!, { ...row, price: 300 }) + expect(row.price).toBe("300.00") + + row = await config.api.row.save(table._id!, { ...row, price: "400.00" }) + expect(row.price).toBe("400.00") + }) + }) + } +) + +datasourceDescribe( + { + name: "Integration compatibility with postgres search_path", + only: [DatabaseName.POSTGRES], + }, + ({ config, dsProvider }) => { let datasource: Datasource let client: Knex let schema1: string let schema2: string beforeEach(async () => { + const ds = await dsProvider + datasource = ds.datasource! + const rawDatasource = ds.rawDatasource! + schema1 = generator.guid().replaceAll("-", "") schema2 = generator.guid().replaceAll("-", "") - const rawDatasource = await getDatasource(DatabaseName.POSTGRES) client = await knexClient(rawDatasource) await client.schema.createSchema(schema1) @@ -161,122 +286,5 @@ describe("postgres integrations", () => { const schema = response.datasource.entities?.[repeated_table_name].schema expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"]) }) - }) - - describe("check custom column types", () => { - beforeAll(async () => { - await client.schema.createTable("binaryTable", table => { - table.binary("id").primary() - table.string("column1") - table.integer("column2") - }) - }) - - it("should handle binary columns", async () => { - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - expect(response.datasource.entities).toBeDefined() - const table = response.datasource.entities?.["binaryTable"] - expect(table).toBeDefined() - expect(table?.schema.id.externalType).toBe("bytea") - const row = await config.api.row.save(table?._id!, { - id: "1111", - column1: "hello", - column2: 222, - }) - expect(row._id).toBeDefined() - const decoded = decodeURIComponent(row._id!).replace(/'/g, '"') - expect(JSON.parse(decoded)[0]).toBe("1111") - }) - }) - - describe("check fetching null/not null table", () => { - beforeAll(async () => { - await client.schema.createTable("nullableTable", table => { - table.increments("order_id").primary() - table.integer("order_number").notNullable() - }) - }) - - it("should be able to change the table to allow nullable and refetch this", async () => { - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - const entities = response.datasource.entities - expect(entities).toBeDefined() - const nullableTable = entities?.["nullableTable"] - expect(nullableTable).toBeDefined() - expect( - nullableTable?.schema["order_number"].constraints?.presence - ).toEqual(true) - - // need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase - // is aware of - therefore we can try to fetch and make sure BB updates correctly - await client.schema.alterTable("nullableTable", table => { - table.setNullable("order_number") - }) - - const responseAfter = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - const entitiesAfter = responseAfter.datasource.entities - expect(entitiesAfter).toBeDefined() - const nullableTableAfter = entitiesAfter?.["nullableTable"] - expect(nullableTableAfter).toBeDefined() - expect( - nullableTableAfter?.schema["order_number"].constraints?.presence - ).toBeUndefined() - }) - }) - - describe("money field 💰", () => { - const tableName = "moneytable" - let table: Table - - beforeAll(async () => { - await client.raw(` - CREATE TABLE ${tableName} ( - id serial PRIMARY KEY, - price money - ) - `) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - table = response.datasource.entities![tableName] - }) - - it("should be able to import a money field", async () => { - expect(table).toBeDefined() - expect(table?.schema.price.type).toBe(FieldType.NUMBER) - }) - - it("should be able to search a money field", async () => { - await config.api.row.bulkImport(table._id!, { - rows: [{ price: 200 }, { price: 300 }], - }) - - const { rows } = await config.api.row.search(table._id!, { - query: { - equal: { - price: 200, - }, - }, - }) - expect(rows).toHaveLength(1) - expect(rows[0].price).toBe("200.00") - }) - - it("should be able to update a money field", async () => { - let row = await config.api.row.save(table._id!, { price: 200 }) - expect(row.price).toBe("200.00") - - row = await config.api.row.save(table._id!, { ...row, price: 300 }) - expect(row.price).toBe("300.00") - - row = await config.api.row.save(table._id!, { ...row, price: "400.00" }) - expect(row.price).toBe("400.00") - }) - }) -}) + } +) diff --git a/packages/server/src/sdk/app/rows/search/tests/search.spec.ts b/packages/server/src/sdk/app/rows/search/tests/search.spec.ts index cf91033c40..3fe23228f8 100644 --- a/packages/server/src/sdk/app/rows/search/tests/search.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/search.spec.ts @@ -7,226 +7,214 @@ import { Table, } from "@budibase/types" -import TestConfiguration from "../../../../../tests/utilities/TestConfiguration" import { search } from "../../../../../sdk/app/rows/search" import { generator } from "@budibase/backend-core/tests" import { DatabaseName, - getDatasource, + datasourceDescribe, } from "../../../../../integrations/tests/utils" import { tableForDatasource } from "../../../../../tests/utilities/structures" // These test cases are only for things that cannot be tested through the API // (e.g. limiting searches to returning specific fields). If it's possible to // test through the API, it should be done there instead. -describe.each([ - ["internal", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], -])("search sdk (%s)", (name, dsProvider) => { - const isInternal = name === "internal" - const config = new TestConfiguration() +datasourceDescribe( + { name: "search sdk (%s)", exclude: [DatabaseName.MONGODB] }, + ({ config, dsProvider, isInternal }) => { + let datasource: Datasource | undefined + let table: Table - let datasource: Datasource | undefined - let table: Table - - beforeAll(async () => { - await config.init() - - if (dsProvider) { - datasource = await config.createDatasource({ - datasource: await dsProvider, - }) - } - }) - - beforeEach(async () => { - const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata = - isInternal - ? { - name: "id", - type: FieldType.AUTO, - subtype: AutoFieldSubType.AUTO_ID, - autocolumn: true, - } - : { - name: "id", - type: FieldType.NUMBER, - autocolumn: true, - } - - table = await config.api.table.save( - tableForDatasource(datasource, { - primary: ["id"], - schema: { - id: idFieldSchema, - name: { - name: "name", - type: FieldType.STRING, - }, - surname: { - name: "surname", - type: FieldType.STRING, - }, - age: { - name: "age", - type: FieldType.NUMBER, - }, - address: { - name: "address", - type: FieldType.STRING, - }, - }, - }) - ) - - for (let i = 0; i < 10; i++) { - await config.api.row.save(table._id!, { - name: generator.first(), - surname: generator.last(), - age: generator.age(), - address: generator.address(), - }) - } - }) - - afterAll(async () => { - config.end() - }) - - it("querying by fields will always return data attribute columns", async () => { - await config.doInContext(config.appId, async () => { - const { rows } = await search({ - tableId: table._id!, - query: {}, - fields: ["name", "age"], - }) - - expect(rows).toHaveLength(10) - for (const row of rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).toContain("age") - expect(keys).not.toContain("surname") - expect(keys).not.toContain("address") - } + beforeAll(async () => { + const ds = await dsProvider + datasource = ds.datasource }) - }) - !isInternal && - it("will decode _id in oneOf query", async () => { - await config.doInContext(config.appId, async () => { - const result = await search({ - tableId: table._id!, - query: { - oneOf: { - _id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"], + beforeEach(async () => { + const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata = + isInternal + ? { + name: "id", + type: FieldType.AUTO, + subtype: AutoFieldSubType.AUTO_ID, + autocolumn: true, + } + : { + name: "id", + type: FieldType.NUMBER, + autocolumn: true, + } + + table = await config.api.table.save( + tableForDatasource(datasource, { + primary: ["id"], + schema: { + id: idFieldSchema, + name: { + name: "name", + type: FieldType.STRING, + }, + surname: { + name: "surname", + type: FieldType.STRING, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + address: { + name: "address", + type: FieldType.STRING, }, }, }) + ) - expect(result.rows).toHaveLength(3) - expect(result.rows.map(row => row.id)).toEqual( - expect.arrayContaining([1, 4, 8]) - ) - }) - }) - - it("does not allow accessing hidden fields", async () => { - await config.doInContext(config.appId, async () => { - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - name: { - ...table.schema.name, - visible: true, - }, - age: { - ...table.schema.age, - visible: false, - }, - }, - }) - const result = await search({ - tableId: table._id!, - query: {}, - }) - expect(result.rows).toHaveLength(10) - for (const row of result.rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).toContain("surname") - expect(keys).toContain("address") - expect(keys).not.toContain("age") + for (let i = 0; i < 10; i++) { + await config.api.row.save(table._id!, { + name: generator.first(), + surname: generator.last(), + age: generator.age(), + address: generator.address(), + }) } }) - }) - it("does not allow accessing hidden fields even if requested", async () => { - await config.doInContext(config.appId, async () => { - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - name: { - ...table.schema.name, - visible: true, - }, - age: { - ...table.schema.age, - visible: false, - }, - }, - }) - const result = await search({ - tableId: table._id!, - query: {}, - fields: ["name", "age"], - }) - expect(result.rows).toHaveLength(10) - for (const row of result.rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).not.toContain("age") - expect(keys).not.toContain("surname") - expect(keys).not.toContain("address") - } + afterAll(async () => { + config.end() }) - }) - it.each([ - [["id", "name", "age"], 3], - [["name", "age"], 10], - ])( - "cannot query by non search fields (fields: %s)", - async (queryFields, expectedRows) => { + it("querying by fields will always return data attribute columns", async () => { await config.doInContext(config.appId, async () => { const { rows } = await search({ tableId: table._id!, - query: { - $or: { - conditions: [ - { - $and: { - conditions: [ - { range: { id: { low: 2, high: 4 } } }, - { range: { id: { low: 3, high: 5 } } }, - ], - }, - }, - { equal: { id: 7 } }, - ], - }, - }, - fields: queryFields, + query: {}, + fields: ["name", "age"], }) - expect(rows).toHaveLength(expectedRows) + expect(rows).toHaveLength(10) + for (const row of rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).toContain("age") + expect(keys).not.toContain("surname") + expect(keys).not.toContain("address") + } }) - } - ) -}) + }) + + !isInternal && + it("will decode _id in oneOf query", async () => { + await config.doInContext(config.appId, async () => { + const result = await search({ + tableId: table._id!, + query: { + oneOf: { + _id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"], + }, + }, + }) + + expect(result.rows).toHaveLength(3) + expect(result.rows.map(row => row.id)).toEqual( + expect.arrayContaining([1, 4, 8]) + ) + }) + }) + + it("does not allow accessing hidden fields", async () => { + await config.doInContext(config.appId, async () => { + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + name: { + ...table.schema.name, + visible: true, + }, + age: { + ...table.schema.age, + visible: false, + }, + }, + }) + const result = await search({ + tableId: table._id!, + query: {}, + }) + expect(result.rows).toHaveLength(10) + for (const row of result.rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).toContain("surname") + expect(keys).toContain("address") + expect(keys).not.toContain("age") + } + }) + }) + + it("does not allow accessing hidden fields even if requested", async () => { + await config.doInContext(config.appId, async () => { + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + name: { + ...table.schema.name, + visible: true, + }, + age: { + ...table.schema.age, + visible: false, + }, + }, + }) + const result = await search({ + tableId: table._id!, + query: {}, + fields: ["name", "age"], + }) + expect(result.rows).toHaveLength(10) + for (const row of result.rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).not.toContain("age") + expect(keys).not.toContain("surname") + expect(keys).not.toContain("address") + } + }) + }) + + it.each([ + [["id", "name", "age"], 3], + [["name", "age"], 10], + ])( + "cannot query by non search fields (fields: %s)", + async (queryFields, expectedRows) => { + await config.doInContext(config.appId, async () => { + const { rows } = await search({ + tableId: table._id!, + query: { + $or: { + conditions: [ + { + $and: { + conditions: [ + { range: { id: { low: 2, high: 4 } } }, + { range: { id: { low: 3, high: 5 } } }, + ], + }, + }, + { equal: { id: 7 } }, + ], + }, + }, + fields: queryFields, + }) + + expect(rows).toHaveLength(expectedRows) + }) + } + ) + } +) diff --git a/packages/server/src/tests/filters/datasource-tests.ts b/packages/server/src/tests/filters/datasource-tests.ts new file mode 100644 index 0000000000..ca8b431cf6 --- /dev/null +++ b/packages/server/src/tests/filters/datasource-tests.ts @@ -0,0 +1,9 @@ +import { DATASOURCE_TEST_FILES } from "." + +export default (paths: string[]) => { + return { + filtered: paths + .filter(path => DATASOURCE_TEST_FILES.includes(path)) + .map(path => ({ test: path })), + } +} diff --git a/packages/server/src/tests/filters/index.ts b/packages/server/src/tests/filters/index.ts new file mode 100644 index 0000000000..993ca89f28 --- /dev/null +++ b/packages/server/src/tests/filters/index.ts @@ -0,0 +1,13 @@ +export const DATASOURCE_TEST_FILES = [ + "src/integration-test/mysql.spec.ts", + "src/integration-test/postgres.spec.ts", + "src/api/routes/tests/queries/generic-sql.spec.ts", + "src/sdk/app/rows/search/tests/search.spec.ts", + "src/api/routes/tests/queries/mongodb.spec.ts", + "src/api/routes/tests/search.spec.ts", + "src/api/routes/tests/datasource.spec.ts", + "src/api/routes/tests/viewV2.spec.ts", + "src/api/routes/tests/row.spec.ts", + "src/api/routes/tests/rowAction.spec.ts", + "src/api/routes/tests/table.spec.ts", +] diff --git a/packages/server/src/tests/filters/non-datasource-tests.ts b/packages/server/src/tests/filters/non-datasource-tests.ts new file mode 100644 index 0000000000..0fc8650da3 --- /dev/null +++ b/packages/server/src/tests/filters/non-datasource-tests.ts @@ -0,0 +1,9 @@ +import { DATASOURCE_TEST_FILES } from "." + +export default (paths: string[]) => { + return { + filtered: paths + .filter(path => !DATASOURCE_TEST_FILES.includes(path)) + .map(path => ({ test: path })), + } +}