diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index ee9af34965..854410dcf6 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -852,6 +852,42 @@ describe.each([ expect(Object.keys(row).length).toEqual(1) expect(row._id).toEqual(existing._id) }) + + it("should handle single quotes in row filtering", async () => { + const existing = await config.api.row.save(table._id!, {}) + const res = await config.api.row.exportRows(table._id!, { + rows: [`['${existing._id!}']`], + }) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + expect(row._id).toEqual(existing._id) + }) + + it("should return an error on composite keys", async () => { + const existing = await config.api.row.save(table._id!, {}) + await config.api.row.exportRows( + table._id!, + { + rows: [`['${existing._id!}']`, "['d001', '10111']"], + }, + { + status: 400, + body: { + message: "Export data does not support composite keys.", + }, + } + ) + }) + + it("should return an error if no table is found", async () => { + const existing = await config.api.row.save(table._id!, {}) + await config.api.row.exportRows( + "1234567", + { rows: [existing._id!] }, + { status: 404 } + ) + }) }) describe("view 2.0", () => { diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts index 2d3c32e02e..8147ca46ad 100644 --- a/packages/server/src/sdk/app/rows/search/internal.ts +++ b/packages/server/src/sdk/app/rows/search/internal.ts @@ -1,6 +1,7 @@ import { context, db, + HTTPError, SearchParams as InternalSearchParams, } from "@budibase/backend-core" import env from "../../../../environment" @@ -31,6 +32,7 @@ import sdk from "../../../../sdk" import { ExportRowsParams, ExportRowsResult } from "../search" import { searchInputMapping } from "./utils" import pick from "lodash/pick" +import { breakRowIdField } from "../../../../integrations/utils" export async function search(options: SearchParams) { const { tableId } = options @@ -103,7 +105,16 @@ export async function exportRows( let response = ( await db.allDocs({ include_docs: true, - keys: rowIds, + keys: rowIds.map((row: string) => { + const ids = breakRowIdField(row) + if (ids.length > 1) { + throw new HTTPError( + "Export data does not support composite keys.", + 400 + ) + } + return ids[0] + }), }) ).rows.map(row => row.doc) diff --git a/packages/server/src/sdk/tests/rows/row.spec.ts b/packages/server/src/sdk/tests/rows/row.spec.ts deleted file mode 100644 index 8b01356e35..0000000000 --- a/packages/server/src/sdk/tests/rows/row.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { exportRows } from "../../app/rows/search/external" -import sdk from "../.." -import { ExternalRequest } from "../../../api/controllers/row/ExternalRequest" -import { ExportRowsParams } from "../../app/rows/search" -import { Format } from "../../../api/controllers/view/exporters" -import { HTTPError } from "@budibase/backend-core" -import { Operation } from "@budibase/types" - -const mockDatasourcesGet = jest.fn() -const mockTableGet = jest.fn() -sdk.datasources.get = mockDatasourcesGet -sdk.tables.getTable = mockTableGet - -jest.mock("../../../api/controllers/row/ExternalRequest") -jest.mock("../../../utilities/rowProcessor", () => ({ - outputProcessing: jest.fn((_, rows) => rows), -})) - -jest.mock("../../../api/controllers/view/exporters", () => ({ - ...jest.requireActual("../../../api/controllers/view/exporters"), - Format: { - CSV: "csv", - }, -})) -jest.mock("../../../utilities/fileSystem") - -describe("external row sdk", () => { - describe("exportRows", () => { - function getExportOptions(): ExportRowsParams { - return { - tableId: "datasource__tablename", - format: Format.CSV, - query: {}, - } - } - - const externalRequestCall = jest.fn() - beforeAll(() => { - jest - .spyOn(ExternalRequest.prototype, "run") - .mockImplementation(externalRequestCall.mockResolvedValue([])) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it("should throw a 400 if no datasource entities are present", async () => { - const exportOptions = getExportOptions() - await expect(exportRows(exportOptions)).rejects.toThrowError( - new HTTPError("Datasource has not been configured for plus API.", 400) - ) - }) - - it("should handle single quotes from a row ID", async () => { - mockDatasourcesGet.mockImplementation(async () => ({ - entities: { - tablename: { - schema: {}, - }, - }, - })) - const exportOptions = getExportOptions() - exportOptions.rowIds = ["['d001']"] - - await exportRows(exportOptions) - - expect(ExternalRequest).toBeCalledTimes(1) - expect(ExternalRequest).toBeCalledWith( - Operation.READ, - exportOptions.tableId, - undefined - ) - - expect(externalRequestCall).toBeCalledTimes(1) - expect(externalRequestCall).toBeCalledWith( - expect.objectContaining({ - filters: { - oneOf: { - _id: ["d001"], - }, - }, - }) - ) - }) - - it("should throw a 400 if any composite keys are present", async () => { - const exportOptions = getExportOptions() - exportOptions.rowIds = ["[123]", "['d001'%2C'10111']"] - await expect(exportRows(exportOptions)).rejects.toThrowError( - new HTTPError("Export data does not support composite keys.", 400) - ) - }) - - it("should throw a 400 if no table name was found", async () => { - const exportOptions = getExportOptions() - exportOptions.tableId = "datasource__" - exportOptions.rowIds = ["[123]"] - - await expect(exportRows(exportOptions)).rejects.toThrowError( - new HTTPError("Could not find table name.", 400) - ) - }) - - it("should only export specified columns", async () => { - mockDatasourcesGet.mockImplementation(async () => ({ - entities: { - tablename: { - schema: { - name: {}, - age: {}, - dob: {}, - }, - }, - }, - })) - const headers = ["name", "dob"] - - const result = await exportRows({ - tableId: "datasource__tablename", - format: Format.CSV, - query: {}, - columns: headers, - }) - - expect(result).toEqual({ - fileName: "export.csv", - content: `"name","dob"`, - }) - }) - }) -}) diff --git a/packages/server/src/tests/utilities/api/base.ts b/packages/server/src/tests/utilities/api/base.ts index 3f534fba86..4df58ff425 100644 --- a/packages/server/src/tests/utilities/api/base.ts +++ b/packages/server/src/tests/utilities/api/base.ts @@ -143,16 +143,12 @@ export abstract class TestAPI { return await request } - protected _request = async ( - method: Method, - url: string, - opts?: RequestOpts - ): Promise => { - const { expectations } = opts || {} + protected _checkResponse = ( + response: Response, + expectations?: Expectations + ) => { const { status = 200 } = expectations || {} - const response = await this._requestRaw(method, url, opts) - if (response.status !== status) { let message = `Expected status ${status} but got ${response.status}` @@ -191,6 +187,17 @@ export abstract class TestAPI { expect(response.body).toMatchObject(expectations.body) } - return response.body + return response + } + + protected _request = async ( + method: Method, + url: string, + opts?: RequestOpts + ): Promise => { + return this._checkResponse( + await this._requestRaw(method, url, opts), + opts?.expectations + ).body } } diff --git a/packages/server/src/tests/utilities/api/legacyView.ts b/packages/server/src/tests/utilities/api/legacyView.ts index b018988670..ae250a81e2 100644 --- a/packages/server/src/tests/utilities/api/legacyView.ts +++ b/packages/server/src/tests/utilities/api/legacyView.ts @@ -31,6 +31,6 @@ export class LegacyViewAPI extends TestAPI { query: { view: viewName, format }, expectations, }) - return response.text + return this._checkResponse(response, expectations).text } } diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts index 86664574cb..052e66b86e 100644 --- a/packages/server/src/tests/utilities/api/row.ts +++ b/packages/server/src/tests/utilities/api/row.ts @@ -8,10 +8,8 @@ import { BulkImportResponse, SearchRowResponse, SearchParams, - DeleteRowRequest, DeleteRows, DeleteRow, - ExportRowsResponse, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -117,6 +115,7 @@ export class RowAPI extends TestAPI { expectations, } ) + this._checkResponse(response, expectations) return response.text }