Merge branch 'master' into BUDI-8084/single-attachment-column-setting

This commit is contained in:
Adria Navarro 2024-03-19 11:09:44 +01:00 committed by GitHub
commit a276c71ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 70 additions and 149 deletions

View File

@ -85,7 +85,7 @@
} }
const automationErrorMessage = appId => { const automationErrorMessage = appId => {
const app = enrichedApps.find(app => app.devId === appId) const app = $enrichedApps.find(app => app.devId === appId)
const errors = automationErrors[appId] const errors = automationErrors[appId]
return `${app.name} - Automation error (${errorCount(errors)})` return `${app.name} - Automation error (${errorCount(errors)})`
} }

View File

@ -852,6 +852,42 @@ describe.each([
expect(Object.keys(row).length).toEqual(1) expect(Object.keys(row).length).toEqual(1)
expect(row._id).toEqual(existing._id) 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", () => { describe("view 2.0", () => {

View File

@ -1,6 +1,7 @@
import { import {
context, context,
db, db,
HTTPError,
SearchParams as InternalSearchParams, SearchParams as InternalSearchParams,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import env from "../../../../environment" import env from "../../../../environment"
@ -31,6 +32,7 @@ import sdk from "../../../../sdk"
import { ExportRowsParams, ExportRowsResult } from "../search" import { ExportRowsParams, ExportRowsResult } from "../search"
import { searchInputMapping } from "./utils" import { searchInputMapping } from "./utils"
import pick from "lodash/pick" import pick from "lodash/pick"
import { breakRowIdField } from "../../../../integrations/utils"
export async function search(options: SearchParams) { export async function search(options: SearchParams) {
const { tableId } = options const { tableId } = options
@ -103,7 +105,16 @@ export async function exportRows(
let response = ( let response = (
await db.allDocs({ await db.allDocs({
include_docs: true, 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) ).rows.map(row => row.doc)

View File

@ -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"`,
})
})
})
})

View File

@ -143,16 +143,12 @@ export abstract class TestAPI {
return await request return await request
} }
protected _request = async <T>( protected _checkResponse = (
method: Method, response: Response,
url: string, expectations?: Expectations
opts?: RequestOpts ) => {
): Promise<T> => {
const { expectations } = opts || {}
const { status = 200 } = expectations || {} const { status = 200 } = expectations || {}
const response = await this._requestRaw(method, url, opts)
if (response.status !== status) { if (response.status !== status) {
let message = `Expected status ${status} but got ${response.status}` let message = `Expected status ${status} but got ${response.status}`
@ -191,6 +187,17 @@ export abstract class TestAPI {
expect(response.body).toMatchObject(expectations.body) expect(response.body).toMatchObject(expectations.body)
} }
return response.body return response
}
protected _request = async <T>(
method: Method,
url: string,
opts?: RequestOpts
): Promise<T> => {
return this._checkResponse(
await this._requestRaw(method, url, opts),
opts?.expectations
).body
} }
} }

View File

@ -31,6 +31,6 @@ export class LegacyViewAPI extends TestAPI {
query: { view: viewName, format }, query: { view: viewName, format },
expectations, expectations,
}) })
return response.text return this._checkResponse(response, expectations).text
} }
} }

View File

@ -8,10 +8,8 @@ import {
BulkImportResponse, BulkImportResponse,
SearchRowResponse, SearchRowResponse,
SearchParams, SearchParams,
DeleteRowRequest,
DeleteRows, DeleteRows,
DeleteRow, DeleteRow,
ExportRowsResponse,
} from "@budibase/types" } from "@budibase/types"
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
@ -117,6 +115,7 @@ export class RowAPI extends TestAPI {
expectations, expectations,
} }
) )
this._checkResponse(response, expectations)
return response.text return response.text
} }

View File

@ -2062,9 +2062,9 @@ find-up@^4.0.0, find-up@^4.1.0:
path-exists "^4.0.0" path-exists "^4.0.0"
follow-redirects@^1.14.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0: follow-redirects@^1.14.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0:
version "1.15.2" version "1.15.6"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
forever-agent@~0.6.1: forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"