diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml
index d670e222d3..fc35575ec6 100644
--- a/.github/workflows/budibase_ci.yml
+++ b/.github/workflows/budibase_ci.yml
@@ -25,6 +25,13 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
+ - name: Maximize build space
+ uses: easimon/maximize-build-space@master
+ with:
+ root-reserve-mb: 35000
+ swap-size-mb: 1024
+ remove-android: 'true'
+ remove-dotnet: 'true'
- name: Checkout repo and submodules
uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml
index 8cda3a9342..f87d561db9 100644
--- a/.github/workflows/stale_bot.yml
+++ b/.github/workflows/stale_bot.yml
@@ -2,7 +2,7 @@ name: Close stale issues and PRs # https://github.com/actions/stale
on:
workflow_dispatch:
schedule:
- - cron: '30 1 * * *' # 1:30 every morning
+ - cron: '*/30 * * * *' # Every 30 mins
jobs:
stale:
diff --git a/.prettierignore b/.prettierignore
index a73fed4890..64607d74ab 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,4 +9,5 @@ packages/backend-core/coverage
packages/server/client
packages/server/src/definitions/openapi.ts
packages/builder/.routify
-packages/sdk/sdk
\ No newline at end of file
+packages/sdk/sdk
+packages/pro/coverage
\ No newline at end of file
diff --git a/lerna.json b/lerna.json
index e070f6579c..a428ef9af6 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.10.3-alpha.2",
+ "version": "2.10.9-alpha.1",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js
index 8b151564a1..6d673cbd3d 100644
--- a/packages/builder/src/components/design/settings/componentSettings.js
+++ b/packages/builder/src/components/design/settings/componentSettings.js
@@ -1,4 +1,4 @@
-import { Checkbox, Select, RadioGroup, Stepper } from "@budibase/bbui"
+import { Checkbox, Select, RadioGroup, Stepper, Input } from "@budibase/bbui"
import DataSourceSelect from "./controls/DataSourceSelect.svelte"
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte"
import DataProviderSelect from "./controls/DataProviderSelect.svelte"
@@ -60,6 +60,7 @@ const componentMap = {
"field/longform": FormFieldSelect,
"field/datetime": FormFieldSelect,
"field/attachment": FormFieldSelect,
+ "field/s3": Input,
"field/link": FormFieldSelect,
"field/array": FormFieldSelect,
"field/json": FormFieldSelect,
diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
index 7c4d3db7ce..c93a41f541 100644
--- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
@@ -126,7 +126,7 @@
user,
prodAppId
)
- const isAppBuilder = sdk.users.hasAppBuilderPermissions(user, prodAppId)
+ const isAppBuilder = user.builder?.apps?.includes(prodAppId)
let role
if (isAdminOrGlobalBuilder) {
role = Constants.Roles.ADMIN
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/_components/EditDatasourceConfigButton.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/_components/EditDatasourceConfigButton.svelte
index 9e50ab8da3..9654b27b50 100644
--- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/_components/EditDatasourceConfigButton.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[datasourceId]/_components/EditDatasourceConfigButton.svelte
@@ -27,12 +27,14 @@
if (datasource.source === IntegrationTypes.COUCHDB) {
return datasource.config.database
}
- if (
- datasource.source === IntegrationTypes.DYNAMODB ||
- datasource.source === IntegrationTypes.S3
- ) {
+ if (datasource.source === IntegrationTypes.DYNAMODB) {
return `${datasource.config.endpoint}:${datasource.config.region}`
}
+ if (datasource.source === IntegrationTypes.S3) {
+ return datasource.config.endpoint
+ ? `${datasource.config.endpoint}:${datasource.config.region}`
+ : `s3.${datasource.config.region}.amazonaws.com`
+ }
if (datasource.source === IntegrationTypes.ELASTICSEARCH) {
return datasource.config.url
}
diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
index 2a74cd9de5..ec10ec8316 100644
--- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
+++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
@@ -111,7 +111,7 @@
})
}
return availableApps.map(app => {
- const prodAppId = apps.getProdAppID(app.appId)
+ const prodAppId = apps.getProdAppID(app.devId)
return {
name: app.name,
devId: app.devId,
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 83b701360f..81c09ca604 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -3715,7 +3715,7 @@
},
"settings": [
{
- "type": "field/attachment",
+ "type": "field/s3",
"label": "Field",
"key": "field",
"required": true
diff --git a/packages/client/src/components/app/forms/S3Upload.svelte b/packages/client/src/components/app/forms/S3Upload.svelte
index 795e2e4332..dfc5032de9 100644
--- a/packages/client/src/components/app/forms/S3Upload.svelte
+++ b/packages/client/src/components/app/forms/S3Upload.svelte
@@ -2,6 +2,7 @@
import Field from "./Field.svelte"
import { CoreDropzone, ProgressCircle } from "@budibase/bbui"
import { getContext, onMount, onDestroy } from "svelte"
+ import { cloneDeep } from "../../../../../bbui/src/helpers"
export let datasourceId
export let bucket
@@ -14,6 +15,7 @@
let fieldState
let fieldApi
+ let localFiles = []
const { API, notificationStore, uploadStore } = getContext("sdk")
const component = getContext("component")
@@ -90,9 +92,17 @@
}
const handleChange = e => {
- const changed = fieldApi.setValue(e.detail)
+ localFiles = e.detail
+ let files = cloneDeep(e.detail) || []
+ // remove URL as it contains the full base64 image data
+ files.forEach(file => {
+ if (file.type?.startsWith("image")) {
+ delete file.url
+ }
+ })
+ const changed = fieldApi.setValue(files)
if (onChange && changed) {
- onChange({ value: e.detail })
+ onChange({ value: files })
}
}
@@ -118,7 +128,7 @@
{#if fieldState}
{
const id = ctx.params.rowId
const tableId = utils.getTableId(ctx)
- return sdk.rows.external.getRow(tableId, id)
+ const row = await sdk.rows.external.getRow(tableId, id, {
+ relationships: true,
+ })
+
+ if (!row) {
+ ctx.throw(404)
+ }
+
+ return row
}
export async function destroy(ctx: UserCtx) {
@@ -119,7 +131,7 @@ export async function destroy(ctx: UserCtx) {
id: breakRowIdField(_id),
includeSqlRelationships: IncludeRelationship.EXCLUDE,
})) as { row: Row }
- return { response: { ok: true }, row }
+ return { response: { ok: true, id: _id }, row }
}
export async function bulkDestroy(ctx: UserCtx) {
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 88d3f50dbe..f0f2462019 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -14,6 +14,10 @@ import {
SearchRowResponse,
SearchRowRequest,
SearchParams,
+ GetRowResponse,
+ ValidateResponse,
+ ExportRowsRequest,
+ ExportRowsResponse,
} from "@budibase/types"
import * as utils from "./utils"
import { gridSocket } from "../../../websockets"
@@ -111,7 +115,7 @@ export async function fetch(ctx: any) {
})
}
-export async function find(ctx: any) {
+export async function find(ctx: UserCtx) {
const tableId = utils.getTableId(ctx)
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), {
datasourceId: tableId,
@@ -214,11 +218,11 @@ export async function search(ctx: Ctx) {
})
}
-export async function validate(ctx: Ctx) {
+export async function validate(ctx: Ctx) {
const tableId = utils.getTableId(ctx)
// external tables are hard to validate currently
if (isExternalTable(tableId)) {
- ctx.body = { valid: true }
+ ctx.body = { valid: true, errors: {} }
} else {
ctx.body = await sdk.rows.utils.validate({
row: ctx.request.body,
@@ -237,7 +241,9 @@ export async function fetchEnrichedRow(ctx: any) {
)
}
-export const exportRows = async (ctx: any) => {
+export const exportRows = async (
+ ctx: Ctx
+) => {
const tableId = utils.getTableId(ctx)
const format = ctx.query.format
diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts
index 4a412c42eb..b80bd339e6 100644
--- a/packages/server/src/api/controllers/row/internal.ts
+++ b/packages/server/src/api/controllers/row/internal.ts
@@ -131,7 +131,7 @@ export async function save(ctx: UserCtx) {
})
}
-export async function find(ctx: UserCtx) {
+export async function find(ctx: UserCtx): Promise {
const tableId = utils.getTableId(ctx),
rowId = ctx.params.rowId
const table = await sdk.tables.getTable(tableId)
diff --git a/packages/server/src/api/controllers/row/tests/utils.spec.ts b/packages/server/src/api/controllers/row/tests/utils.spec.ts
new file mode 100644
index 0000000000..e0ad637e9d
--- /dev/null
+++ b/packages/server/src/api/controllers/row/tests/utils.spec.ts
@@ -0,0 +1,21 @@
+import * as utils from "../utils"
+
+describe("removeEmptyFilters", () => {
+ it("0 should not be removed", () => {
+ const filters = utils.removeEmptyFilters({
+ equal: {
+ column: 0,
+ },
+ })
+ expect((filters.equal as any).column).toBe(0)
+ })
+
+ it("empty string should be removed", () => {
+ const filters = utils.removeEmptyFilters({
+ equal: {
+ column: "",
+ },
+ })
+ expect(Object.values(filters.equal as any).length).toBe(0)
+ })
+})
diff --git a/packages/server/src/api/controllers/row/utils.ts b/packages/server/src/api/controllers/row/utils.ts
index e85ec4553c..192ba2109c 100644
--- a/packages/server/src/api/controllers/row/utils.ts
+++ b/packages/server/src/api/controllers/row/utils.ts
@@ -1,8 +1,15 @@
import { InternalTables } from "../../../db/utils"
import * as userController from "../user"
import { context } from "@budibase/backend-core"
-import { Ctx, FieldType, Row, Table, UserCtx } from "@budibase/types"
-import { FieldTypes } from "../../../constants"
+import {
+ Ctx,
+ FieldType,
+ Row,
+ SearchFilters,
+ Table,
+ UserCtx,
+} from "@budibase/types"
+import { FieldTypes, NoEmptyFilterStrings } from "../../../constants"
import sdk from "../../../sdk"
import validateJs from "validate.js"
@@ -27,7 +34,7 @@ validateJs.extend(validateJs.validators.datetime, {
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
const db = context.getAppDB()
- let row
+ let row: Row
// TODO remove special user case in future
if (tableId === InternalTables.USER_METADATA) {
ctx.params = {
@@ -139,3 +146,32 @@ export async function validate({
}
return { valid: Object.keys(errors).length === 0, errors }
}
+
+// don't do a pure falsy check, as 0 is included
+// https://github.com/Budibase/budibase/issues/10118
+export function removeEmptyFilters(filters: SearchFilters) {
+ for (let filterField of NoEmptyFilterStrings) {
+ if (!filters[filterField]) {
+ continue
+ }
+
+ for (let filterType of Object.keys(filters)) {
+ if (filterType !== filterField) {
+ continue
+ }
+ // don't know which one we're checking, type could be anything
+ const value = filters[filterType] as unknown
+ if (typeof value === "object") {
+ for (let [key, value] of Object.entries(
+ filters[filterType] as object
+ )) {
+ if (value == null || value === "") {
+ // @ts-ignore
+ delete filters[filterField][key]
+ }
+ }
+ }
+ }
+ }
+ return filters
+}
diff --git a/packages/server/src/api/controllers/table/index.ts b/packages/server/src/api/controllers/table/index.ts
index 759974d6a7..29c41ad985 100644
--- a/packages/server/src/api/controllers/table/index.ts
+++ b/packages/server/src/api/controllers/table/index.ts
@@ -78,9 +78,9 @@ export async function save(ctx: UserCtx) {
ctx.status = 200
ctx.message = `Table ${table.name} saved successfully.`
ctx.eventEmitter &&
- ctx.eventEmitter.emitTable(`table:save`, appId, savedTable)
+ ctx.eventEmitter.emitTable(`table:save`, appId, { ...savedTable })
ctx.body = savedTable
- builderSocket?.emitTableUpdate(ctx, savedTable)
+ builderSocket?.emitTableUpdate(ctx, { ...savedTable })
}
export async function destroy(ctx: UserCtx) {
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index f63be2318f..a74a9f7960 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -1,3 +1,5 @@
+import { databaseTestProviders } from "../../../integrations/tests/utils"
+
import tk from "timekeeper"
import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities"
@@ -8,13 +10,16 @@ import {
MonthlyQuotaName,
PermissionLevel,
QuotaUsageType,
+ RelationshipType,
Row,
+ SaveTableRequest,
SortOrder,
SortType,
StaticQuotaName,
Table,
} from "@budibase/types"
import {
+ expectAnyExternalColsAttributes,
expectAnyInternalColsAttributes,
generator,
mocks,
@@ -26,30 +31,68 @@ tk.freeze(timestamp)
const { basicRow } = setup.structures
-describe("/rows", () => {
- let request = setup.getRequest()
- let config = setup.getConfig()
+describe.each([
+ ["internal", undefined],
+ ["postgres", databaseTestProviders.postgres],
+])("/rows (%s)", (_, dsProvider) => {
+ const isInternal = !dsProvider
+
+ const request = setup.getRequest()
+ const config = setup.getConfig()
let table: Table
- let row: Row
+ let tableId: string
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
+
+ if (dsProvider) {
+ await config.createDatasource({
+ datasource: await dsProvider.getDsConfig(),
+ })
+ }
})
+ const generateTableConfig: () => SaveTableRequest = () => {
+ return {
+ name: generator.word(),
+ type: "table",
+ primary: ["id"],
+ primaryDisplay: "name",
+ schema: {
+ id: {
+ type: FieldType.AUTO,
+ name: "id",
+ autocolumn: true,
+ constraints: {
+ presence: true,
+ },
+ },
+ name: {
+ type: FieldType.STRING,
+ name: "name",
+ constraints: {
+ type: "string",
+ },
+ },
+ description: {
+ type: FieldType.STRING,
+ name: "description",
+ constraints: {
+ type: "string",
+ },
+ },
+ },
+ }
+ }
+
beforeEach(async () => {
mocks.licenses.useCloudFree()
- table = await config.createTable()
- row = basicRow(table._id!)
})
- const loadRow = async (id: string, tbl_Id: string, status = 200) =>
- await request
- .get(`/api/${tbl_Id}/rows/${id}`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(status)
+ const loadRow = (id: string, tbl_Id: string, status = 200) =>
+ config.api.row.get(tbl_Id, id, { expectStatus: status })
const getRowUsage = async () => {
const { total } = await config.doInContext(null, () =>
@@ -78,19 +121,33 @@ describe("/rows", () => {
expect(usage).toBe(expected)
}
+ const defaultRowFields = isInternal
+ ? {
+ type: "row",
+ createdAt: timestamp,
+ updatedAt: timestamp,
+ }
+ : undefined
+
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ const table = await config.createTable(tableConfig)
+ tableId = table._id!
+ })
+
describe("save, load, update", () => {
it("returns a success message when the row is created", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request
- .post(`/api/${row.tableId}/rows`)
- .send(row)
+ .post(`/api/${tableId}/rows`)
+ .send(basicRow(tableId))
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect((res as any).res.statusMessage).toEqual(
- `${table.name} saved successfully`
+ `${config.table!.name} saved successfully`
)
expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined()
@@ -102,47 +159,43 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const newTable = await config.createTable({
- name: "TestTableAuto",
- type: "table",
- schema: {
- ...table.schema,
- "Row ID": {
- name: "Row ID",
- type: FieldType.NUMBER,
- subtype: "autoID",
- icon: "ri-magic-line",
- autocolumn: true,
- constraints: {
- type: "number",
- presence: false,
- numericality: {
- greaterThanOrEqualTo: "",
- lessThanOrEqualTo: "",
+ const tableConfig = generateTableConfig()
+ const newTable = await config.createTable(
+ {
+ ...tableConfig,
+ name: "TestTableAuto",
+ schema: {
+ ...tableConfig.schema,
+ "Row ID": {
+ name: "Row ID",
+ type: FieldType.NUMBER,
+ subtype: "autoID",
+ icon: "ri-magic-line",
+ autocolumn: true,
+ constraints: {
+ type: "number",
+ presence: true,
+ numericality: {
+ greaterThanOrEqualTo: "",
+ lessThanOrEqualTo: "",
+ },
},
},
},
},
- })
+ { skipReassigning: true }
+ )
const ids = [1, 2, 3]
// Performing several create row requests should increment the autoID fields accordingly
const createRow = async (id: number) => {
- const res = await request
- .post(`/api/${newTable._id}/rows`)
- .send({
- name: "row_" + id,
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
- expect((res as any).res.statusMessage).toEqual(
- `${newTable.name} saved successfully`
- )
- expect(res.body.name).toEqual("row_" + id)
- expect(res.body._rev).toBeDefined()
- expect(res.body["Row ID"]).toEqual(id)
+ const res = await config.api.row.save(newTable._id!, {
+ name: "row_" + id,
+ })
+ expect(res.name).toEqual("row_" + id)
+ expect(res._rev).toBeDefined()
+ expect(res["Row ID"]).toEqual(id)
}
for (let i = 0; i < ids.length; i++) {
@@ -158,22 +211,14 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .post(`/api/${table._id}/rows`)
- .send({
- _id: existing._id,
- _rev: existing._rev,
- tableId: table._id,
- name: "Updated Name",
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.save(tableId, {
+ _id: existing._id,
+ _rev: existing._rev,
+ tableId,
+ name: "Updated Name",
+ })
- expect((res as any).res.statusMessage).toEqual(
- `${table.name} updated successfully.`
- )
- expect(res.body.name).toEqual("Updated Name")
+ expect(res.name).toEqual("Updated Name")
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage + 1)
})
@@ -182,42 +227,34 @@ describe("/rows", () => {
const existing = await config.createRow()
const queryUsage = await getQueryUsage()
- const res = await request
- .get(`/api/${table._id}/rows/${existing._id}`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.get(tableId, existing._id!)
expect(res.body).toEqual({
- ...row,
- _id: existing._id,
- _rev: existing._rev,
- type: "row",
- createdAt: timestamp,
- updatedAt: timestamp,
+ ...existing,
+ ...defaultRowFields,
})
await assertQueryUsage(queryUsage + 1)
})
it("should list all rows for given tableId", async () => {
+ const table = await config.createTable(generateTableConfig(), {
+ skipReassigning: true,
+ })
+ const tableId = table._id!
const newRow = {
- tableId: table._id,
+ tableId,
name: "Second Contact",
- status: "new",
+ description: "new",
}
- await config.createRow()
+ const firstRow = await config.createRow({ tableId })
await config.createRow(newRow)
const queryUsage = await getQueryUsage()
- const res = await request
- .get(`/api/${table._id}/rows`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.fetch(tableId)
- expect(res.body.length).toBe(2)
- expect(res.body.find((r: Row) => r.name === newRow.name)).toBeDefined()
- expect(res.body.find((r: Row) => r.name === row.name)).toBeDefined()
+ expect(res.length).toBe(2)
+ expect(res.find((r: Row) => r.name === newRow.name)).toBeDefined()
+ expect(res.find((r: Row) => r.name === firstRow.name)).toBeDefined()
await assertQueryUsage(queryUsage + 1)
})
@@ -225,55 +262,54 @@ describe("/rows", () => {
await config.createRow()
const queryUsage = await getQueryUsage()
- await request
- .get(`/api/${table._id}/rows/not-a-valid-id`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(404)
+ await config.api.row.get(tableId, "1234567", {
+ expectStatus: 404,
+ })
await assertQueryUsage(queryUsage) // no change
})
- it("row values are coerced", async () => {
- const str = {
- type: FieldType.STRING,
- name: "str",
- constraints: { type: "string", presence: false },
- }
- const attachment = {
- type: FieldType.ATTACHMENT,
- name: "attachment",
- constraints: { type: "array", presence: false },
- }
- const bool = {
- type: FieldType.BOOLEAN,
- name: "boolean",
- constraints: { type: "boolean", presence: false },
- }
- const number = {
- type: FieldType.NUMBER,
- name: "str",
- constraints: { type: "number", presence: false },
- }
- const datetime = {
- type: FieldType.DATETIME,
- name: "datetime",
- constraints: {
- type: "string",
- presence: false,
- datetime: { earliest: "", latest: "" },
- },
- }
- const arrayField = {
- type: FieldType.ARRAY,
- constraints: {
- type: "array",
- presence: false,
- inclusion: ["One", "Two", "Three"],
- },
- name: "Sample Tags",
- sortable: false,
- }
- const optsField = {
+ isInternal &&
+ it("row values are coerced", async () => {
+ const str = {
+ type: FieldType.STRING,
+ name: "str",
+ constraints: { type: "string", presence: false },
+ }
+ const attachment = {
+ type: FieldType.ATTACHMENT,
+ name: "attachment",
+ constraints: { type: "array", presence: false },
+ }
+ const bool = {
+ type: FieldType.BOOLEAN,
+ name: "boolean",
+ constraints: { type: "boolean", presence: false },
+ }
+ const number = {
+ type: FieldType.NUMBER,
+ name: "str",
+ constraints: { type: "number", presence: false },
+ }
+ const datetime = {
+ type: FieldType.DATETIME,
+ name: "datetime",
+ constraints: {
+ type: "string",
+ presence: false,
+ datetime: { earliest: "", latest: "" },
+ },
+ }
+ const arrayField = {
+ type: FieldType.ARRAY,
+ constraints: {
+ type: "array",
+ presence: false,
+ inclusion: ["One", "Two", "Three"],
+ },
+ name: "Sample Tags",
+ sortable: false,
+ }
+ const optsField = {
fieldName: "Sample Opts",
name: "Sample Opts",
type: FieldType.OPTIONS,
@@ -282,8 +318,8 @@ describe("/rows", () => {
presence: false,
inclusion: ["Alpha", "Beta", "Gamma"],
},
- },
- table = await config.createTable({
+ }
+ const table = await config.createTable({
name: "TestTable2",
type: "table",
schema: {
@@ -321,92 +357,93 @@ describe("/rows", () => {
},
})
- row = {
- name: "Test Row",
- stringUndefined: undefined,
- stringNull: null,
- stringString: "i am a string",
- numberEmptyString: "",
- numberNull: null,
- numberUndefined: undefined,
- numberString: "123",
- numberNumber: 123,
- datetimeEmptyString: "",
- datetimeNull: null,
- datetimeUndefined: undefined,
- datetimeString: "1984-04-20T00:00:00.000Z",
- datetimeDate: new Date("1984-04-20"),
- boolNull: null,
- boolEmpty: "",
- boolUndefined: undefined,
- boolString: "true",
- boolBool: true,
- tableId: table._id,
- attachmentNull: null,
- attachmentUndefined: undefined,
- attachmentEmpty: "",
- attachmentEmptyArrayStr: "[]",
- arrayFieldEmptyArrayStr: "[]",
- arrayFieldUndefined: undefined,
- arrayFieldNull: null,
- arrayFieldArrayStrKnown: "['One']",
- optsFieldEmptyStr: "",
- optsFieldUndefined: undefined,
- optsFieldNull: null,
- optsFieldStrKnown: "Alpha",
- }
+ const row = {
+ name: "Test Row",
+ stringUndefined: undefined,
+ stringNull: null,
+ stringString: "i am a string",
+ numberEmptyString: "",
+ numberNull: null,
+ numberUndefined: undefined,
+ numberString: "123",
+ numberNumber: 123,
+ datetimeEmptyString: "",
+ datetimeNull: null,
+ datetimeUndefined: undefined,
+ datetimeString: "1984-04-20T00:00:00.000Z",
+ datetimeDate: new Date("1984-04-20"),
+ boolNull: null,
+ boolEmpty: "",
+ boolUndefined: undefined,
+ boolString: "true",
+ boolBool: true,
+ tableId: table._id,
+ attachmentNull: null,
+ attachmentUndefined: undefined,
+ attachmentEmpty: "",
+ attachmentEmptyArrayStr: "[]",
+ arrayFieldEmptyArrayStr: "[]",
+ arrayFieldUndefined: undefined,
+ arrayFieldNull: null,
+ arrayFieldArrayStrKnown: "['One']",
+ optsFieldEmptyStr: "",
+ optsFieldUndefined: undefined,
+ optsFieldNull: null,
+ optsFieldStrKnown: "Alpha",
+ }
- const createdRow = await config.createRow(row)
- const id = createdRow._id!
+ const createdRow = await config.createRow(row)
+ const id = createdRow._id!
- const saved = (await loadRow(id, table._id!)).body
+ const saved = (await loadRow(id, table._id!)).body
- expect(saved.stringUndefined).toBe(undefined)
- expect(saved.stringNull).toBe("")
- expect(saved.stringString).toBe("i am a string")
- expect(saved.numberEmptyString).toBe(null)
- expect(saved.numberNull).toBe(null)
- expect(saved.numberUndefined).toBe(undefined)
- expect(saved.numberString).toBe(123)
- expect(saved.numberNumber).toBe(123)
- expect(saved.datetimeEmptyString).toBe(null)
- expect(saved.datetimeNull).toBe(null)
- expect(saved.datetimeUndefined).toBe(undefined)
- expect(saved.datetimeString).toBe(
- new Date(row.datetimeString).toISOString()
- )
- expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString())
- expect(saved.boolNull).toBe(null)
- expect(saved.boolEmpty).toBe(null)
- expect(saved.boolUndefined).toBe(undefined)
- expect(saved.boolString).toBe(true)
- expect(saved.boolBool).toBe(true)
- expect(saved.attachmentNull).toEqual([])
- expect(saved.attachmentUndefined).toBe(undefined)
- expect(saved.attachmentEmpty).toEqual([])
- expect(saved.attachmentEmptyArrayStr).toEqual([])
- expect(saved.arrayFieldEmptyArrayStr).toEqual([])
- expect(saved.arrayFieldNull).toEqual([])
- expect(saved.arrayFieldUndefined).toEqual(undefined)
- expect(saved.optsFieldEmptyStr).toEqual(null)
- expect(saved.optsFieldUndefined).toEqual(undefined)
- expect(saved.optsFieldNull).toEqual(null)
- expect(saved.arrayFieldArrayStrKnown).toEqual(["One"])
- expect(saved.optsFieldStrKnown).toEqual("Alpha")
- })
+ expect(saved.stringUndefined).toBe(undefined)
+ expect(saved.stringNull).toBe("")
+ expect(saved.stringString).toBe("i am a string")
+ expect(saved.numberEmptyString).toBe(null)
+ expect(saved.numberNull).toBe(null)
+ expect(saved.numberUndefined).toBe(undefined)
+ expect(saved.numberString).toBe(123)
+ expect(saved.numberNumber).toBe(123)
+ expect(saved.datetimeEmptyString).toBe(null)
+ expect(saved.datetimeNull).toBe(null)
+ expect(saved.datetimeUndefined).toBe(undefined)
+ expect(saved.datetimeString).toBe(
+ new Date(row.datetimeString).toISOString()
+ )
+ expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString())
+ expect(saved.boolNull).toBe(null)
+ expect(saved.boolEmpty).toBe(null)
+ expect(saved.boolUndefined).toBe(undefined)
+ expect(saved.boolString).toBe(true)
+ expect(saved.boolBool).toBe(true)
+ expect(saved.attachmentNull).toEqual([])
+ expect(saved.attachmentUndefined).toBe(undefined)
+ expect(saved.attachmentEmpty).toEqual([])
+ expect(saved.attachmentEmptyArrayStr).toEqual([])
+ expect(saved.arrayFieldEmptyArrayStr).toEqual([])
+ expect(saved.arrayFieldNull).toEqual([])
+ expect(saved.arrayFieldUndefined).toEqual(undefined)
+ expect(saved.optsFieldEmptyStr).toEqual(null)
+ expect(saved.optsFieldUndefined).toEqual(undefined)
+ expect(saved.optsFieldNull).toEqual(null)
+ expect(saved.arrayFieldArrayStrKnown).toEqual(["One"])
+ expect(saved.optsFieldStrKnown).toEqual("Alpha")
+ })
})
describe("view save", () => {
- function orderTable(): Table {
- return {
+ it("views have extra data trimmed", async () => {
+ const table = await config.createTable({
name: "orders",
+ primary: ["OrderID"],
schema: {
Country: {
type: FieldType.STRING,
name: "Country",
},
OrderID: {
- type: FieldType.STRING,
+ type: FieldType.NUMBER,
name: "OrderID",
},
Story: {
@@ -414,32 +451,48 @@ describe("/rows", () => {
name: "Story",
},
},
- }
- }
+ })
- it("views have extra data trimmed", async () => {
- const table = await config.createTable(orderTable())
-
- const createViewResponse = await config.api.viewV2.create({
- tableId: table._id,
+ const createViewResponse = await config.createView({
+ name: generator.word(),
schema: {
- Country: {},
- OrderID: {},
+ Country: {
+ visible: true,
+ },
+ OrderID: {
+ visible: true,
+ },
},
})
- const response = await config.api.row.save(createViewResponse.id, {
- Country: "Aussy",
- OrderID: "1111",
- Story: "aaaaa",
- })
+ const createRowResponse = await config.api.row.save(
+ createViewResponse.id,
+ {
+ OrderID: "1111",
+ Country: "Aussy",
+ Story: "aaaaa",
+ }
+ )
- const row = await config.api.row.get(table._id!, response._id!)
+ const row = await config.api.row.get(table._id!, createRowResponse._id!)
expect(row.body.Story).toBeUndefined()
+ expect(row.body).toEqual({
+ ...defaultRowFields,
+ OrderID: 1111,
+ Country: "Aussy",
+ _id: createRowResponse._id,
+ _rev: createRowResponse._rev,
+ tableId: table._id,
+ })
})
})
describe("patch", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should update only the fields that are supplied", async () => {
const existing = await config.createRow()
@@ -489,19 +542,17 @@ describe("/rows", () => {
})
describe("destroy", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should be able to delete a row", async () => {
- const createdRow = await config.createRow(row)
+ const createdRow = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .delete(`/api/${table._id}/rows`)
- .send({
- rows: [createdRow],
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.delete(table._id!, [createdRow])
expect(res.body[0]._id).toEqual(createdRow._id)
await assertRowUsage(rowUsage - 1)
await assertQueryUsage(queryUsage + 1)
@@ -509,19 +560,19 @@ describe("/rows", () => {
})
describe("validate", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should return no errors on valid row", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .post(`/api/${table._id}/rows/validate`)
- .send({ name: "ivan" })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.validate(table._id!, { name: "ivan" })
- expect(res.body.valid).toBe(true)
- expect(Object.keys(res.body.errors)).toEqual([])
+ expect(res.valid).toBe(true)
+ expect(Object.keys(res.errors)).toEqual([])
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
})
@@ -530,35 +581,34 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .post(`/api/${table._id}/rows/validate`)
- .send({ name: 1 })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.validate(table._id!, { name: 1 })
- expect(res.body.valid).toBe(false)
- expect(Object.keys(res.body.errors)).toEqual(["name"])
+ if (isInternal) {
+ expect(res.valid).toBe(false)
+ expect(Object.keys(res.errors)).toEqual(["name"])
+ } else {
+ // Validation for external is not implemented, so it will always return valid
+ expect(res.valid).toBe(true)
+ expect(Object.keys(res.errors)).toEqual([])
+ }
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
})
})
describe("bulkDelete", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should be able to delete a bulk set of rows", async () => {
const row1 = await config.createRow()
const row2 = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .delete(`/api/${table._id}/rows`)
- .send({
- rows: [row1, row2],
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.delete(table._id!, [row1, row2])
expect(res.body.length).toEqual(2)
await loadRow(row1._id!, table._id!, 404)
@@ -567,20 +617,19 @@ describe("/rows", () => {
})
it("should be able to delete a variety of row set types", async () => {
- const row1 = await config.createRow()
- const row2 = await config.createRow()
- const row3 = await config.createRow()
+ const [row1, row2, row3] = await Promise.all([
+ config.createRow(),
+ config.createRow(),
+ config.createRow(),
+ ])
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .delete(`/api/${table._id}/rows`)
- .send({
- rows: [row1, row2._id, { _id: row3._id }],
- })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.delete(table._id!, [
+ row1,
+ row2._id,
+ { _id: row3._id },
+ ])
expect(res.body.length).toEqual(3)
await loadRow(row1._id!, table._id!, 404)
@@ -593,12 +642,7 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .delete(`/api/${table._id}/rows`)
- .send(row1)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.delete(table._id!, row1)
expect(res.body.id).toEqual(row1._id)
await loadRow(row1._id!, table._id!, 404)
@@ -610,31 +654,23 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
- const res = await request
- .delete(`/api/${table._id}/rows`)
- .send({ not: "valid" })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(400)
-
+ const res = await config.api.row.delete(
+ table._id!,
+ { not: "valid" },
+ { expectStatus: 400 }
+ )
expect(res.body.message).toEqual("Invalid delete rows request")
- const res2 = await request
- .delete(`/api/${table._id}/rows`)
- .send({ rows: 123 })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(400)
-
+ const res2 = await config.api.row.delete(
+ table._id!,
+ { rows: 123 },
+ { expectStatus: 400 }
+ )
expect(res2.body.message).toEqual("Invalid delete rows request")
- const res3 = await request
- .delete(`/api/${table._id}/rows`)
- .send("invalid")
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(400)
-
+ const res3 = await config.api.row.delete(table._id!, "invalid", {
+ expectStatus: 400,
+ })
expect(res3.body.message).toEqual("Invalid delete rows request")
await assertRowUsage(rowUsage)
@@ -642,61 +678,86 @@ describe("/rows", () => {
})
})
- describe("fetchView", () => {
- it("should be able to fetch tables contents via 'view'", async () => {
- const row = await config.createRow()
- const rowUsage = await getRowUsage()
- const queryUsage = await getQueryUsage()
+ // Legacy views are not available for external
+ isInternal &&
+ describe("fetchView", () => {
+ beforeEach(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
- const res = await request
- .get(`/api/views/${table._id}`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
- expect(res.body.length).toEqual(1)
- expect(res.body[0]._id).toEqual(row._id)
- await assertRowUsage(rowUsage)
- await assertQueryUsage(queryUsage + 1)
+ it("should be able to fetch tables contents via 'view'", async () => {
+ const row = await config.createRow()
+ const rowUsage = await getRowUsage()
+ const queryUsage = await getQueryUsage()
+
+ const res = await config.api.legacyView.get(table._id!)
+ expect(res.body.length).toEqual(1)
+ expect(res.body[0]._id).toEqual(row._id)
+ await assertRowUsage(rowUsage)
+ await assertQueryUsage(queryUsage + 1)
+ })
+
+ it("should throw an error if view doesn't exist", async () => {
+ const rowUsage = await getRowUsage()
+ const queryUsage = await getQueryUsage()
+
+ await config.api.legacyView.get("derp", { expectStatus: 404 })
+
+ await assertRowUsage(rowUsage)
+ await assertQueryUsage(queryUsage)
+ })
+
+ it("should be able to run on a view", async () => {
+ const view = await config.createLegacyView({
+ tableId: table._id!,
+ name: "ViewTest",
+ filters: [],
+ schema: {},
+ })
+ const row = await config.createRow()
+ const rowUsage = await getRowUsage()
+ const queryUsage = await getQueryUsage()
+
+ const res = await config.api.legacyView.get(view.name)
+ expect(res.body.length).toEqual(1)
+ expect(res.body[0]._id).toEqual(row._id)
+
+ await assertRowUsage(rowUsage)
+ await assertQueryUsage(queryUsage + 1)
+ })
})
- it("should throw an error if view doesn't exist", async () => {
- const rowUsage = await getRowUsage()
- const queryUsage = await getQueryUsage()
-
- await request
- .get(`/api/views/derp`)
- .set(config.defaultHeaders())
- .expect(404)
-
- await assertRowUsage(rowUsage)
- await assertQueryUsage(queryUsage)
- })
-
- it("should be able to run on a view", async () => {
- const view = await config.createLegacyView()
- const row = await config.createRow()
- const rowUsage = await getRowUsage()
- const queryUsage = await getQueryUsage()
-
- const res = await request
- .get(`/api/views/${view.name}`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
- expect(res.body.length).toEqual(1)
- expect(res.body[0]._id).toEqual(row._id)
-
- await assertRowUsage(rowUsage)
- await assertQueryUsage(queryUsage + 1)
- })
- })
-
describe("fetchEnrichedRows", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should allow enriching some linked rows", async () => {
- const { table, firstRow, secondRow } = await tenancy.doInTenant(
+ const { linkedTable, firstRow, secondRow } = await tenancy.doInTenant(
config.getTenantId(),
async () => {
- const table = await config.createLinkedTable()
+ const linkedTable = await config.createLinkedTable(
+ RelationshipType.ONE_TO_MANY,
+ ["link"],
+ {
+ name: generator.word(),
+ type: "table",
+ primary: ["id"],
+ primaryDisplay: "id",
+ schema: {
+ id: {
+ type: FieldType.AUTO,
+ name: "id",
+ autocolumn: true,
+ constraints: {
+ presence: true,
+ },
+ },
+ },
+ }
+ )
const firstRow = await config.createRow({
name: "Test Contact",
description: "original description",
@@ -706,29 +767,30 @@ describe("/rows", () => {
name: "Test 2",
description: "og desc",
link: [{ _id: firstRow._id }],
- tableId: table._id,
+ tableId: linkedTable._id,
})
- return { table, firstRow, secondRow }
+ return { linkedTable, firstRow, secondRow }
}
)
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
// test basic enrichment
- const resBasic = await request
- .get(`/api/${table._id}/rows/${secondRow._id}`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
- expect(resBasic.body.link[0]._id).toBe(firstRow._id)
- expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact")
+ const resBasic = await config.api.row.get(
+ linkedTable._id!,
+ secondRow._id!
+ )
+ expect(resBasic.body.link.length).toBe(1)
+ expect(resBasic.body.link[0]).toEqual({
+ _id: firstRow._id,
+ primaryDisplay: firstRow.name,
+ })
// test full enrichment
- const resEnriched = await request
- .get(`/api/${table._id}/${secondRow._id}/enrich`)
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
+ const resEnriched = await config.api.row.getEnriched(
+ linkedTable._id!,
+ secondRow._id!
+ )
expect(resEnriched.body.link.length).toBe(1)
expect(resEnriched.body.link[0]._id).toBe(firstRow._id)
expect(resEnriched.body.link[0].name).toBe("Test Contact")
@@ -738,43 +800,49 @@ describe("/rows", () => {
})
})
- describe("attachments", () => {
- it("should allow enriching attachment rows", async () => {
- const table = await config.createAttachmentTable()
- const attachmentId = `${structures.uuid()}.csv`
- const row = await config.createRow({
- name: "test",
- description: "test",
- attachment: [
- {
- key: `${config.getAppId()}/attachments/${attachmentId}`,
- },
- ],
- tableId: table._id,
+ isInternal &&
+ describe("attachments", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
})
- // the environment needs configured for this
- await setup.switchToSelfHosted(async () => {
- return context.doInAppContext(config.getAppId(), async () => {
- const enriched = await outputProcessing(table, [row])
- expect((enriched as Row[])[0].attachment[0].url).toBe(
- `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
- )
+
+ it("should allow enriching attachment rows", async () => {
+ const table = await config.createAttachmentTable()
+ const attachmentId = `${structures.uuid()}.csv`
+ const row = await config.createRow({
+ name: "test",
+ description: "test",
+ attachment: [
+ {
+ key: `${config.getAppId()}/attachments/${attachmentId}`,
+ },
+ ],
+ tableId: table._id,
+ })
+ // the environment needs configured for this
+ await setup.switchToSelfHosted(async () => {
+ return context.doInAppContext(config.getAppId(), async () => {
+ const enriched = await outputProcessing(table, [row])
+ expect((enriched as Row[])[0].attachment[0].url).toBe(
+ `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
+ )
+ })
})
})
})
- })
describe("exportData", () => {
+ beforeAll(async () => {
+ const tableConfig = generateTableConfig()
+ table = await config.createTable(tableConfig)
+ })
+
it("should allow exporting all columns", async () => {
const existing = await config.createRow()
- const res = await request
- .post(`/api/${table._id}/rows/exportRows?format=json`)
- .set(config.defaultHeaders())
- .send({
- rows: [existing._id],
- })
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.exportRows(table._id!, {
+ rows: [existing._id!],
+ })
const results = JSON.parse(res.text)
expect(results.length).toEqual(1)
const row = results[0]
@@ -790,15 +858,10 @@ describe("/rows", () => {
it("should allow exporting only certain columns", async () => {
const existing = await config.createRow()
- const res = await request
- .post(`/api/${table._id}/rows/exportRows?format=json`)
- .set(config.defaultHeaders())
- .send({
- rows: [existing._id],
- columns: ["_id"],
- })
- .expect("Content-Type", /json/)
- .expect(200)
+ const res = await config.api.row.exportRows(table._id!, {
+ rows: [existing._id!],
+ columns: ["_id"],
+ })
const results = JSON.parse(res.text)
expect(results.length).toEqual(1)
const row = results[0]
@@ -810,18 +873,27 @@ describe("/rows", () => {
})
describe("view 2.0", () => {
- function userTable(): Table {
+ async function userTable(): Promise {
return {
- name: "user",
- type: "user",
+ name: `users_${generator.word()}`,
+ type: "table",
+ primary: ["id"],
schema: {
+ id: {
+ type: FieldType.AUTO,
+ name: "id",
+ autocolumn: true,
+ constraints: {
+ presence: true,
+ },
+ },
name: {
type: FieldType.STRING,
name: "name",
},
surname: {
type: FieldType.STRING,
- name: "name",
+ name: "surname",
},
age: {
type: FieldType.NUMBER,
@@ -849,9 +921,8 @@ describe("/rows", () => {
describe("create", () => {
it("should persist a new row with only the provided view fields", async () => {
- const table = await config.createTable(userTable())
- const view = await config.api.viewV2.create({
- tableId: table._id!,
+ const table = await config.createTable(await userTable())
+ const view = await config.createView({
schema: {
name: { visible: true },
surname: { visible: true },
@@ -861,7 +932,7 @@ describe("/rows", () => {
const data = randomRowData()
const newRow = await config.api.row.save(view.id, {
- tableId: config.table!._id,
+ tableId: table!._id,
_viewId: view.id,
...data,
})
@@ -871,12 +942,11 @@ describe("/rows", () => {
name: data.name,
surname: data.surname,
address: data.address,
- tableId: config.table!._id,
- type: "row",
- _id: expect.any(String),
- _rev: expect.any(String),
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
+ tableId: table!._id,
+ _id: newRow._id,
+ _rev: newRow._rev,
+ id: newRow.id,
+ ...defaultRowFields,
})
expect(row.body._viewId).toBeUndefined()
expect(row.body.age).toBeUndefined()
@@ -886,10 +956,9 @@ describe("/rows", () => {
describe("patch", () => {
it("should update only the view fields for a row", async () => {
- const table = await config.createTable(userTable())
+ const table = await config.createTable(await userTable())
const tableId = table._id!
- const view = await config.api.viewV2.create({
- tableId,
+ const view = await config.createView({
schema: {
name: { visible: true },
address: { visible: true },
@@ -913,13 +982,12 @@ describe("/rows", () => {
const row = await config.api.row.get(tableId, newRow._id!)
expect(row.body).toEqual({
...newRow,
- type: "row",
name: newData.name,
address: newData.address,
- _id: expect.any(String),
+ _id: newRow._id,
_rev: expect.any(String),
- createdAt: expect.any(String),
- updatedAt: expect.any(String),
+ id: newRow.id,
+ ...defaultRowFields,
})
expect(row.body._viewId).toBeUndefined()
expect(row.body.age).toBeUndefined()
@@ -929,10 +997,9 @@ describe("/rows", () => {
describe("destroy", () => {
it("should be able to delete a row", async () => {
- const table = await config.createTable(userTable())
+ const table = await config.createTable(await userTable())
const tableId = table._id!
- const view = await config.api.viewV2.create({
- tableId,
+ const view = await config.createView({
schema: {
name: { visible: true },
address: { visible: true },
@@ -954,21 +1021,20 @@ describe("/rows", () => {
})
it("should be able to delete multiple rows", async () => {
- const table = await config.createTable(userTable())
+ const table = await config.createTable(await userTable())
const tableId = table._id!
- const view = await config.api.viewV2.create({
- tableId,
+ const view = await config.createView({
schema: {
name: { visible: true },
address: { visible: true },
},
})
- const rows = [
- await config.createRow(),
- await config.createRow(),
- await config.createRow(),
- ]
+ const rows = await Promise.all([
+ config.createRow(),
+ config.createRow(),
+ config.createRow(),
+ ])
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
@@ -989,11 +1055,20 @@ describe("/rows", () => {
describe("view search", () => {
const viewSchema = { age: { visible: true }, name: { visible: true } }
- function userTable(): Table {
+ async function userTable(): Promise {
return {
- name: "user",
- type: "user",
+ name: `users_${generator.word()}`,
+ type: "table",
+ primary: ["id"],
schema: {
+ id: {
+ type: FieldType.AUTO,
+ name: "id",
+ autocolumn: true,
+ constraints: {
+ presence: true,
+ },
+ },
name: {
type: FieldType.STRING,
name: "name",
@@ -1008,42 +1083,61 @@ describe("/rows", () => {
}
}
- it("returns table rows from view", async () => {
- const table = await config.createTable(userTable())
- const rows = []
- for (let i = 0; i < 10; i++) {
- rows.push(await config.createRow({ tableId: table._id }))
- }
+ it("returns empty rows from view when no schema is passed", async () => {
+ const table = await config.createTable(await userTable())
+ const rows = await Promise.all(
+ Array.from({ length: 10 }, () =>
+ config.api.row.save(table._id!, { tableId: table._id })
+ )
+ )
- const createViewResponse = await config.api.viewV2.create()
+ const createViewResponse = await config.createView()
const response = await config.api.viewV2.search(createViewResponse.id)
expect(response.body.rows).toHaveLength(10)
expect(response.body).toEqual({
- rows: expect.arrayContaining(rows.map(expect.objectContaining)),
+ rows: expect.arrayContaining(
+ rows.map(r => ({
+ _viewId: createViewResponse.id,
+ tableId: table._id,
+ _id: r._id,
+ _rev: r._rev,
+ ...defaultRowFields,
+ }))
+ ),
+ ...(isInternal
+ ? {}
+ : {
+ hasNextPage: false,
+ bookmark: null,
+ }),
})
})
it("searching respects the view filters", async () => {
- const table = await config.createTable(userTable())
- const expectedRows = []
- for (let i = 0; i < 10; i++)
- await config.createRow({
- tableId: table._id,
- name: generator.name(),
- age: generator.integer({ min: 10, max: 30 }),
- })
+ const table = await config.createTable(await userTable())
- for (let i = 0; i < 5; i++)
- expectedRows.push(
- await config.createRow({
+ await Promise.all(
+ Array.from({ length: 10 }, () =>
+ config.api.row.save(table._id!, {
+ tableId: table._id,
+ name: generator.name(),
+ age: generator.integer({ min: 10, max: 30 }),
+ })
+ )
+ )
+
+ const expectedRows = await Promise.all(
+ Array.from({ length: 5 }, () =>
+ config.api.row.save(table._id!, {
tableId: table._id,
name: generator.name(),
age: 40,
})
)
+ )
- const createViewResponse = await config.api.viewV2.create({
+ const createViewResponse = await config.createView({
query: [{ operator: "equal", field: "age", value: 40 }],
schema: viewSchema,
})
@@ -1053,8 +1147,22 @@ describe("/rows", () => {
expect(response.body.rows).toHaveLength(5)
expect(response.body).toEqual({
rows: expect.arrayContaining(
- expectedRows.map(expect.objectContaining)
+ expectedRows.map(r => ({
+ _viewId: createViewResponse.id,
+ tableId: table._id,
+ name: r.name,
+ age: r.age,
+ _id: r._id,
+ _rev: r._rev,
+ ...defaultRowFields,
+ }))
),
+ ...(isInternal
+ ? {}
+ : {
+ hasNextPage: false,
+ bookmark: null,
+ }),
})
})
@@ -1127,94 +1235,87 @@ describe("/rows", () => {
],
]
- it.each(sortTestOptions)(
- "allow sorting (%s)",
- async (sortParams, expected) => {
- await config.createTable(userTable())
+ describe("sorting", () => {
+ beforeAll(async () => {
+ const table = await config.createTable(await userTable())
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charly", age: 27 },
{ name: "Danny", age: 15 },
]
- for (const user of users) {
- await config.createRow({
- tableId: config.table!._id,
- ...user,
- })
- }
-
- const createViewResponse = await config.api.viewV2.create({
- sort: sortParams,
- schema: viewSchema,
- })
-
- const response = await config.api.viewV2.search(createViewResponse.id)
-
- expect(response.body.rows).toHaveLength(4)
- expect(response.body).toEqual({
- rows: expected.map(name => expect.objectContaining({ name })),
- })
- }
- )
-
- it.each(sortTestOptions)(
- "allow override the default view sorting (%s)",
- async (sortParams, expected) => {
- await config.createTable(userTable())
- const users = [
- { name: "Alice", age: 25 },
- { name: "Bob", age: 30 },
- { name: "Charly", age: 27 },
- { name: "Danny", age: 15 },
- ]
- for (const user of users) {
- await config.createRow({
- tableId: config.table!._id,
- ...user,
- })
- }
-
- const createViewResponse = await config.api.viewV2.create({
- sort: {
- field: "name",
- order: SortOrder.ASCENDING,
- type: SortType.STRING,
- },
- schema: viewSchema,
- })
-
- const response = await config.api.viewV2.search(
- createViewResponse.id,
- {
- sort: sortParams.field,
- sortOrder: sortParams.order,
- sortType: sortParams.type,
- query: {},
- }
+ await Promise.all(
+ users.map(u =>
+ config.api.row.save(table._id!, {
+ tableId: table._id,
+ ...u,
+ })
+ )
)
+ })
- expect(response.body.rows).toHaveLength(4)
- expect(response.body).toEqual({
- rows: expected.map(name => expect.objectContaining({ name })),
- })
- }
- )
+ it.each(sortTestOptions)(
+ "allow sorting (%s)",
+ async (sortParams, expected) => {
+ const createViewResponse = await config.createView({
+ sort: sortParams,
+ schema: viewSchema,
+ })
+
+ const response = await config.api.viewV2.search(
+ createViewResponse.id
+ )
+
+ expect(response.body.rows).toHaveLength(4)
+ expect(response.body.rows).toEqual(
+ expected.map(name => expect.objectContaining({ name }))
+ )
+ }
+ )
+
+ it.each(sortTestOptions)(
+ "allow override the default view sorting (%s)",
+ async (sortParams, expected) => {
+ const createViewResponse = await config.createView({
+ sort: {
+ field: "name",
+ order: SortOrder.ASCENDING,
+ type: SortType.STRING,
+ },
+ schema: viewSchema,
+ })
+
+ const response = await config.api.viewV2.search(
+ createViewResponse.id,
+ {
+ sort: sortParams.field,
+ sortOrder: sortParams.order,
+ sortType: sortParams.type,
+ query: {},
+ }
+ )
+
+ expect(response.body.rows).toHaveLength(4)
+ expect(response.body.rows).toEqual(
+ expected.map(name => expect.objectContaining({ name }))
+ )
+ }
+ )
+ })
it("when schema is defined, defined columns and row attributes are returned", async () => {
- const table = await config.createTable(userTable())
- const rows = []
- for (let i = 0; i < 10; i++) {
- rows.push(
- await config.createRow({
+ const table = await config.createTable(await userTable())
+ const rows = await Promise.all(
+ Array.from({ length: 10 }, () =>
+ config.api.row.save(table._id!, {
tableId: table._id,
name: generator.name(),
age: generator.age(),
})
)
- }
+ )
- const view = await config.api.viewV2.create({
+ const view = await config.createView({
schema: { name: { visible: true } },
})
const response = await config.api.viewV2.search(view.id)
@@ -1223,7 +1324,9 @@ describe("/rows", () => {
expect(response.body.rows).toEqual(
expect.arrayContaining(
rows.map(r => ({
- ...expectAnyInternalColsAttributes,
+ ...(isInternal
+ ? expectAnyInternalColsAttributes
+ : expectAnyExternalColsAttributes),
_viewId: view.id,
name: r.name,
}))
@@ -1232,23 +1335,21 @@ describe("/rows", () => {
})
it("views without data can be returned", async () => {
- await config.createTable(userTable())
+ const table = await config.createTable(await userTable())
- const createViewResponse = await config.api.viewV2.create()
+ const createViewResponse = await config.createView()
const response = await config.api.viewV2.search(createViewResponse.id)
expect(response.body.rows).toHaveLength(0)
})
it("respects the limit parameter", async () => {
- const table = await config.createTable(userTable())
- const rows = []
- for (let i = 0; i < 10; i++) {
- rows.push(await config.createRow({ tableId: table._id }))
- }
+ await config.createTable(await userTable())
+ await Promise.all(Array.from({ length: 10 }, () => config.createRow()))
+
const limit = generator.integer({ min: 1, max: 8 })
- const createViewResponse = await config.api.viewV2.create()
+ const createViewResponse = await config.createView()
const response = await config.api.viewV2.search(createViewResponse.id, {
limit,
query: {},
@@ -1258,14 +1359,10 @@ describe("/rows", () => {
})
it("can handle pagination", async () => {
- const table = await config.createTable(userTable())
- const rows = []
- for (let i = 0; i < 10; i++) {
- rows.push(await config.createRow({ tableId: table._id }))
- }
- // rows.sort((a, b) => (a._id! > b._id! ? 1 : -1))
+ await config.createTable(await userTable())
+ await Promise.all(Array.from({ length: 10 }, () => config.createRow()))
- const createViewResponse = await config.api.viewV2.create()
+ const createViewResponse = await config.createView()
const allRows = (await config.api.viewV2.search(createViewResponse.id))
.body.rows
@@ -1279,9 +1376,9 @@ describe("/rows", () => {
)
expect(firstPageResponse.body).toEqual({
rows: expect.arrayContaining(allRows.slice(0, 4)),
- totalRows: 10,
+ totalRows: isInternal ? 10 : undefined,
hasNextPage: true,
- bookmark: expect.any(String),
+ bookmark: expect.anything(),
})
const secondPageResponse = await config.api.viewV2.search(
@@ -1296,9 +1393,9 @@ describe("/rows", () => {
)
expect(secondPageResponse.body).toEqual({
rows: expect.arrayContaining(allRows.slice(4, 8)),
- totalRows: 10,
+ totalRows: isInternal ? 10 : undefined,
hasNextPage: true,
- bookmark: expect.any(String),
+ bookmark: expect.anything(),
})
const lastPageResponse = await config.api.viewV2.search(
@@ -1312,9 +1409,9 @@ describe("/rows", () => {
)
expect(lastPageResponse.body).toEqual({
rows: expect.arrayContaining(allRows.slice(8)),
- totalRows: 10,
+ totalRows: isInternal ? 10 : undefined,
hasNextPage: false,
- bookmark: expect.any(String),
+ bookmark: expect.anything(),
})
})
@@ -1323,13 +1420,12 @@ describe("/rows", () => {
let tableId: string
beforeAll(async () => {
- const table = await config.createTable(userTable())
- const rows = []
- for (let i = 0; i < 10; i++) {
- rows.push(await config.createRow({ tableId: table._id }))
- }
+ await config.createTable(await userTable())
+ await Promise.all(
+ Array.from({ length: 10 }, () => config.createRow())
+ )
- const createViewResponse = await config.api.viewV2.create()
+ const createViewResponse = await config.createView()
tableId = table._id!
viewId = createViewResponse.id
diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts
index 9914e6d66f..f56c6e4e44 100644
--- a/packages/server/src/api/routes/tests/table.spec.ts
+++ b/packages/server/src/api/routes/tests/table.spec.ts
@@ -1,6 +1,6 @@
import { generator } from "@budibase/backend-core/tests"
import { events, context } from "@budibase/backend-core"
-import { FieldType, Table } from "@budibase/types"
+import { FieldType, Table, ViewCalculation } from "@budibase/types"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
const { basicTable } = setup.structures
@@ -90,8 +90,10 @@ describe("/tables", () => {
await config.createLegacyView({
name: "TestView",
field: "Price",
- calculation: "stats",
- tableId: testTable._id,
+ calculation: ViewCalculation.STATISTICS,
+ tableId: testTable._id!,
+ schema: {},
+ filters: [],
})
const testRow = await request
@@ -254,7 +256,7 @@ describe("/tables", () => {
}))
await config.api.viewV2.create({ tableId })
- await config.createLegacyView({ tableId, name: generator.guid() })
+ await config.createLegacyView()
const res = await config.api.table.fetch()
@@ -366,7 +368,7 @@ describe("/tables", () => {
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
- const dependentTable = await config.getTable(linkedTable._id)
+ const dependentTable = await config.api.table.get(linkedTable._id!)
expect(dependentTable.schema.TestTable).not.toBeDefined()
})
diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts
index 5e8ae09e55..6d893c1c7f 100644
--- a/packages/server/src/api/routes/tests/viewV2.spec.ts
+++ b/packages/server/src/api/routes/tests/viewV2.spec.ts
@@ -6,6 +6,7 @@ import {
SortOrder,
SortType,
Table,
+ UIFieldMetadata,
UpdateViewRequest,
ViewV2,
} from "@budibase/types"
@@ -418,9 +419,12 @@ describe.each([
const res = await config.api.viewV2.create(newView)
const view = await config.api.viewV2.get(res.id)
expect(view!.schema?.Price).toBeUndefined()
- const updatedTable = await config.getTable(table._id!)
- const viewSchema = updatedTable.views[view!.name!].schema
- expect(viewSchema.Price.visible).toEqual(false)
+ const updatedTable = await config.api.table.get(table._id!)
+ const viewSchema = updatedTable.views![view!.name!].schema as Record<
+ string,
+ UIFieldMetadata
+ >
+ expect(viewSchema.Price?.visible).toEqual(false)
})
})
})
diff --git a/packages/server/src/db/tests/linkController.spec.js b/packages/server/src/db/tests/linkController.spec.js
index 1c50142871..59d0f3f983 100644
--- a/packages/server/src/db/tests/linkController.spec.js
+++ b/packages/server/src/db/tests/linkController.spec.js
@@ -6,7 +6,7 @@ const { RelationshipType } = require("../../constants")
const { cloneDeep } = require("lodash/fp")
describe("test the link controller", () => {
- let config = new TestConfig(false)
+ let config = new TestConfig()
let table1, table2, appId
beforeAll(async () => {
diff --git a/packages/server/src/db/tests/linkTests.spec.js b/packages/server/src/db/tests/linkTests.spec.js
index b7764dacb7..19a0eb88d3 100644
--- a/packages/server/src/db/tests/linkTests.spec.js
+++ b/packages/server/src/db/tests/linkTests.spec.js
@@ -4,7 +4,7 @@ const linkUtils = require("../linkedRows/linkUtils")
const { context } = require("@budibase/backend-core")
describe("test link functionality", () => {
- const config = new TestConfig(false)
+ const config = new TestConfig()
let appId
describe("getLinkedTable", () => {
diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts
index 6b0e7b4266..86a43c8c42 100644
--- a/packages/server/src/integration-test/postgres.spec.ts
+++ b/packages/server/src/integration-test/postgres.spec.ts
@@ -12,18 +12,15 @@ import {
FieldType,
RelationshipType,
Row,
- SourceName,
Table,
} from "@budibase/types"
import _ from "lodash"
import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core"
-import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
+import { databaseTestProviders } from "../integrations/tests/utils"
const config = setup.getConfig()!
-jest.setTimeout(30000)
-
jest.unmock("pg")
jest.mock("../websockets")
@@ -35,62 +32,18 @@ describe("postgres integrations", () => {
manyToOneRelationshipInfo: ForeignTableInfo,
manyToManyRelationshipInfo: ForeignTableInfo
- let host: string
- let port: number
- const containers: StartedTestContainer[] = []
-
beforeAll(async () => {
- const containerPostgres = await new GenericContainer("postgres")
- .withExposedPorts(5432)
- .withEnv("POSTGRES_PASSWORD", "password")
- .withWaitStrategy(
- Wait.forLogMessage(
- "PostgreSQL init process complete; ready for start up."
- )
- )
- .start()
-
- host = containerPostgres.getContainerIpAddress()
- port = containerPostgres.getMappedPort(5432)
-
await config.init()
const apiKey = await config.generateApiKey()
- containers.push(containerPostgres)
-
makeRequest = generateMakeRequest(apiKey, true)
- })
- afterAll(async () => {
- for (let container of containers) {
- await container.stop()
- }
+ postgresDatasource = await config.api.datasource.create(
+ await databaseTestProviders.postgres.getDsConfig()
+ )
})
- function pgDatasourceConfig() {
- return {
- datasource: {
- type: "datasource",
- source: SourceName.POSTGRES,
- plus: true,
- config: {
- host,
- port,
- database: "postgres",
- user: "postgres",
- password: "password",
- schema: "public",
- ssl: false,
- rejectUnauthorized: false,
- ca: false,
- },
- },
- }
- }
-
beforeEach(async () => {
- postgresDatasource = await config.createDatasource(pgDatasourceConfig())
-
async function createAuxTable(prefix: string) {
return await config.createTable({
name: `${prefix}_${generator.word({ length: 6 })}`,
@@ -226,25 +179,6 @@ describe("postgres integrations", () => {
let { rowData } = opts as any
let foreignRows: ForeignRowsInfo[] = []
- async function createForeignRow(tableInfo: ForeignTableInfo) {
- const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}`
-
- const foreignRow = await config.createRow({
- tableId: tableInfo.table._id,
- title: generator.name(),
- })
-
- rowData = {
- ...rowData,
- [foreignKey]: foreignRow.id,
- }
- foreignRows.push({
- row: foreignRow,
-
- relationshipType: tableInfo.relationshipType,
- })
- }
-
if (opts?.createForeignRows?.createOneToMany) {
const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`
@@ -322,6 +256,14 @@ describe("postgres integrations", () => {
})
}
+ const createRandomTableWithRows = async () => {
+ const tableId = (await createDefaultPgTable())._id!
+ return await config.api.row.save(tableId, {
+ tableId,
+ title: generator.name(),
+ })
+ }
+
async function populatePrimaryRows(
count: number,
opts?: {
@@ -357,9 +299,9 @@ describe("postgres integrations", () => {
config: {
ca: false,
database: "postgres",
- host,
+ host: postgresDatasource.config!.host,
password: "--secret-value--",
- port,
+ port: postgresDatasource.config!.port,
rejectUnauthorized: false,
schema: "public",
ssl: false,
@@ -401,12 +343,16 @@ describe("postgres integrations", () => {
it("multiple rows can be persisted", async () => {
const numberOfRows = 10
- const newRows = Array(numberOfRows).fill(generateRandomPrimaryRowData())
+ const newRows: Row[] = Array(numberOfRows).fill(
+ generateRandomPrimaryRowData()
+ )
- for (const newRow of newRows) {
- const res = await createRow(primaryPostgresTable._id, newRow)
- expect(res.status).toBe(200)
- }
+ await Promise.all(
+ newRows.map(async newRow => {
+ const res = await createRow(primaryPostgresTable._id, newRow)
+ expect(res.status).toBe(200)
+ })
+ )
const persistedRows = await config.getRows(primaryPostgresTable._id!)
expect(persistedRows).toHaveLength(numberOfRows)
@@ -567,7 +513,7 @@ describe("postgres integrations", () => {
foreignRows = createdRow.foreignRows
})
- it("only one to many foreign keys are retrieved", async () => {
+ it("only one to primary keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
@@ -575,6 +521,12 @@ describe("postgres integrations", () => {
const one2ManyForeignRows = foreignRows.filter(
x => x.relationshipType === RelationshipType.ONE_TO_MANY
)
+ const many2OneForeignRows = foreignRows.filter(
+ x => x.relationshipType === RelationshipType.MANY_TO_ONE
+ )
+ const many2ManyForeignRows = foreignRows.filter(
+ x => x.relationshipType === RelationshipType.MANY_TO_MANY
+ )
expect(one2ManyForeignRows).toHaveLength(1)
expect(res.body).toEqual({
@@ -585,9 +537,25 @@ describe("postgres integrations", () => {
_rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
one2ManyForeignRows[0].row.id,
+ [oneToManyRelationshipInfo.fieldName]: expect.arrayContaining(
+ one2ManyForeignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
+ [manyToOneRelationshipInfo.fieldName]: expect.arrayContaining(
+ many2OneForeignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
+ [manyToManyRelationshipInfo.fieldName]: expect.arrayContaining(
+ many2ManyForeignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
})
-
- expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
@@ -616,9 +584,13 @@ describe("postgres integrations", () => {
_rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
foreignRows[0].row.id,
+ [oneToManyRelationshipInfo.fieldName]: expect.arrayContaining(
+ foreignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
})
-
- expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
@@ -645,9 +617,13 @@ describe("postgres integrations", () => {
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
+ [manyToOneRelationshipInfo.fieldName]: expect.arrayContaining(
+ foreignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
})
-
- expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
@@ -674,9 +650,13 @@ describe("postgres integrations", () => {
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
+ [manyToManyRelationshipInfo.fieldName]: expect.arrayContaining(
+ foreignRows.map(r => ({
+ _id: r.row._id,
+ primaryDisplay: r.row.title,
+ }))
+ ),
})
-
- expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
})
@@ -730,12 +710,6 @@ describe("postgres integrations", () => {
describe("given than multiple tables have multiple rows", () => {
const rowsCount = 6
beforeEach(async () => {
- const createRandomTableWithRows = async () =>
- await config.createRow({
- tableId: (await createDefaultPgTable())._id,
- title: generator.name(),
- })
-
await createRandomTableWithRows()
await createRandomTableWithRows()
@@ -1023,12 +997,6 @@ describe("postgres integrations", () => {
const rowsCount = 6
beforeEach(async () => {
- const createRandomTableWithRows = async () =>
- await config.createRow({
- tableId: (await createDefaultPgTable())._id,
- title: generator.name(),
- })
-
await createRandomTableWithRows()
await populatePrimaryRows(rowsCount)
await createRandomTableWithRows()
@@ -1046,24 +1014,25 @@ describe("postgres integrations", () => {
describe("POST /api/datasources/verify", () => {
it("should be able to verify the connection", async () => {
- const config = pgDatasourceConfig()
- const response = await makeRequest(
- "post",
- "/api/datasources/verify",
- config
- )
+ const response = await config.api.datasource.verify({
+ datasource: await databaseTestProviders.postgres.getDsConfig(),
+ })
expect(response.status).toBe(200)
expect(response.body.connected).toBe(true)
})
it("should state an invalid datasource cannot connect", async () => {
- const config = pgDatasourceConfig()
- config.datasource.config.password = "wrongpassword"
- const response = await makeRequest(
- "post",
- "/api/datasources/verify",
- config
- )
+ const dbConfig = await databaseTestProviders.postgres.getDsConfig()
+ const response = await config.api.datasource.verify({
+ datasource: {
+ ...dbConfig,
+ config: {
+ ...dbConfig.config,
+ password: "wrongpassword",
+ },
+ },
+ })
+
expect(response.status).toBe(200)
expect(response.body.connected).toBe(false)
expect(response.body.error).toBeDefined()
diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts
index 2cdae682b0..bf19ec9afe 100644
--- a/packages/server/src/integrations/base/sql.ts
+++ b/packages/server/src/integrations/base/sql.ts
@@ -58,7 +58,7 @@ function parse(input: any) {
return null
}
if (isIsoDateString(input)) {
- return new Date(input)
+ return new Date(input.trim())
}
return input
}
diff --git a/packages/server/src/integrations/tests/sql.spec.ts b/packages/server/src/integrations/tests/sql.spec.ts
index 0bf1498f5f..5cc4849d03 100644
--- a/packages/server/src/integrations/tests/sql.spec.ts
+++ b/packages/server/src/integrations/tests/sql.spec.ts
@@ -657,4 +657,29 @@ describe("SQL query builder", () => {
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/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts
new file mode 100644
index 0000000000..a28141db08
--- /dev/null
+++ b/packages/server/src/integrations/tests/utils/index.ts
@@ -0,0 +1,14 @@
+jest.unmock("pg")
+
+import { Datasource } from "@budibase/types"
+import * as pg from "./postgres"
+
+jest.setTimeout(30000)
+
+export interface DatabasePlusTestProvider {
+ getDsConfig(): Promise
+}
+
+export const databaseTestProviders = {
+ postgres: pg,
+}
diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts
new file mode 100644
index 0000000000..036e81bbd8
--- /dev/null
+++ b/packages/server/src/integrations/tests/utils/postgres.ts
@@ -0,0 +1,38 @@
+import { Datasource, SourceName } from "@budibase/types"
+import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
+
+let container: StartedTestContainer | undefined
+
+export async function getDsConfig(): Promise {
+ if (!container) {
+ container = await new GenericContainer("postgres")
+ .withExposedPorts(5432)
+ .withEnv("POSTGRES_PASSWORD", "password")
+ .withWaitStrategy(
+ Wait.forLogMessage(
+ "PostgreSQL init process complete; ready for start up."
+ )
+ )
+ .start()
+ }
+
+ const host = container.getContainerIpAddress()
+ const port = container.getMappedPort(5432)
+
+ return {
+ type: "datasource_plus",
+ source: SourceName.POSTGRES,
+ plus: true,
+ config: {
+ host,
+ port,
+ database: "postgres",
+ user: "postgres",
+ password: "password",
+ schema: "public",
+ ssl: false,
+ rejectUnauthorized: false,
+ ca: false,
+ },
+ }
+}
diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts
index 3f598ce986..2883e4471c 100644
--- a/packages/server/src/integrations/utils.ts
+++ b/packages/server/src/integrations/utils.ts
@@ -182,11 +182,12 @@ export function getSqlQuery(query: SqlQuery | string): SqlQuery {
export const isSQL = helpers.isSQL
export function isIsoDateString(str: string) {
- if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) {
+ const trimmedValue = str.trim()
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(trimmedValue)) {
return false
}
- let d = new Date(str)
- return d.toISOString() === str
+ let d = new Date(trimmedValue)
+ return d.toISOString() === trimmedValue
}
/**
diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts
index b64cad26c1..d83e3cf31e 100644
--- a/packages/server/src/migrations/tests/index.spec.ts
+++ b/packages/server/src/migrations/tests/index.spec.ts
@@ -11,6 +11,7 @@ import { MIGRATIONS } from "../"
import * as helpers from "./helpers"
import tk from "timekeeper"
+import { View } from "@budibase/types"
const timestamp = new Date().toISOString()
tk.freeze(timestamp)
@@ -52,7 +53,9 @@ describe("migrations", () => {
await config.createTable()
await config.createLegacyView()
await config.createTable()
- await config.createLegacyView(structures.view(config.table!._id!))
+ await config.createLegacyView(
+ structures.view(config.table!._id!) as View
+ )
await config.createScreen()
await config.createScreen()
diff --git a/packages/server/src/sdk/app/permissions/index.ts b/packages/server/src/sdk/app/permissions/index.ts
index b2661b0f7e..b62a7fb459 100644
--- a/packages/server/src/sdk/app/permissions/index.ts
+++ b/packages/server/src/sdk/app/permissions/index.ts
@@ -61,11 +61,7 @@ export async function getInheritablePermissions(
export async function allowsExplicitPermissions(resourceId: string) {
if (isViewID(resourceId)) {
const allowed = await features.isViewPermissionEnabled()
- const minPlan = !allowed
- ? env.SELF_HOSTED
- ? PlanType.BUSINESS
- : PlanType.PREMIUM
- : undefined
+ const minPlan = !allowed ? PlanType.BUSINESS : undefined
return {
allowed,
diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts
index 4861f473ea..380521a05a 100644
--- a/packages/server/src/sdk/app/rows/search.ts
+++ b/packages/server/src/sdk/app/rows/search.ts
@@ -30,7 +30,7 @@ export interface ExportRowsParams {
format: Format
rowIds?: string[]
columns?: string[]
- query: SearchFilters
+ query?: SearchFilters
}
export interface ExportRowsResult {
diff --git a/packages/server/src/sdk/app/tables/index.ts b/packages/server/src/sdk/app/tables/index.ts
index 1bf9117837..64fcde4bff 100644
--- a/packages/server/src/sdk/app/tables/index.ts
+++ b/packages/server/src/sdk/app/tables/index.ts
@@ -12,7 +12,7 @@ import {
TableViewsResponse,
} from "@budibase/types"
import datasources from "../datasources"
-import { isEditableColumn, populateExternalTableSchemas } from "./validation"
+import { populateExternalTableSchemas } from "./validation"
import sdk from "../../../sdk"
async function getAllInternalTables(db?: Database): Promise {
@@ -73,12 +73,23 @@ function enrichViewSchemas(table: Table): TableResponse {
}
}
+async function saveTable(table: Table) {
+ const db = context.getAppDB()
+ if (isExternalTable(table._id!)) {
+ const datasource = await sdk.datasources.get(table.sourceId!)
+ datasource.entities![table.name] = table
+ await db.put(datasource)
+ } else {
+ await db.put(table)
+ }
+}
+
export default {
getAllInternalTables,
getAllExternalTables,
getExternalTable,
getTable,
populateExternalTableSchemas,
- isEditableColumn,
enrichViewSchemas,
+ saveTable,
}
diff --git a/packages/server/src/sdk/app/tables/validation.ts b/packages/server/src/sdk/app/tables/validation.ts
index 8dc41107d3..56f3e84c7a 100644
--- a/packages/server/src/sdk/app/tables/validation.ts
+++ b/packages/server/src/sdk/app/tables/validation.ts
@@ -55,13 +55,6 @@ function checkForeignKeysAreAutoColumns(datasource: Datasource) {
return datasource
}
-export function isEditableColumn(column: FieldSchema) {
- const isAutoColumn =
- column.autocolumn && column.autoReason !== AutoReason.FOREIGN_KEY
- const isFormula = column.type === FieldTypes.FORMULA
- return !(isAutoColumn || isFormula)
-}
-
export function populateExternalTableSchemas(datasource: Datasource) {
return checkForeignKeysAreAutoColumns(datasource)
}
diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts
index 4c6d0701f3..da7af8acd7 100644
--- a/packages/server/src/tests/utilities/TestConfiguration.ts
+++ b/packages/server/src/tests/utilities/TestConfiguration.ts
@@ -50,6 +50,11 @@ import {
SearchFilters,
UserRoles,
Automation,
+ View,
+ FieldType,
+ RelationshipType,
+ ViewV2,
+ CreateViewRequest,
} from "@budibase/types"
import API from "./api"
@@ -75,9 +80,8 @@ class TestConfiguration {
globalUserId: any
userMetadataId: any
table?: Table
- linkedTable: any
automation: any
- datasource: any
+ datasource?: Datasource
tenantId?: string
defaultUserValues: DefaultUserValues
api: API
@@ -527,7 +531,7 @@ class TestConfiguration {
// TABLE
async updateTable(
- config?: any,
+ config?: Table,
{ skipReassigning } = { skipReassigning: false }
): Promise {
config = config || basicTable()
@@ -542,33 +546,50 @@ class TestConfiguration {
if (config != null && config._id) {
delete config._id
}
+ config = config || basicTable()
+ if (this.datasource && !config.sourceId) {
+ config.sourceId = this.datasource._id
+ if (this.datasource.plus) {
+ config.type = "external"
+ }
+ }
+
return this.updateTable(config, options)
}
async getTable(tableId?: string) {
- tableId = tableId || this.table?._id
+ tableId = tableId || this.table!._id!
return this._req(null, { tableId }, controllers.table.find)
}
- async createLinkedTable(relationshipType?: string, links: any = ["link"]) {
+ async createLinkedTable(
+ relationshipType = RelationshipType.ONE_TO_MANY,
+ links: any = ["link"],
+ config?: Table
+ ) {
if (!this.table) {
throw "Must have created a table first."
}
- const tableConfig: any = basicTable()
+ const tableConfig = config || basicTable()
tableConfig.primaryDisplay = "name"
for (let link of links) {
tableConfig.schema[link] = {
- type: "link",
+ type: FieldType.LINK,
fieldName: link,
tableId: this.table._id,
name: link,
- }
- if (relationshipType) {
- tableConfig.schema[link].relationshipType = relationshipType
+ relationshipType,
}
}
+
+ if (this.datasource && !tableConfig.sourceId) {
+ tableConfig.sourceId = this.datasource._id
+ if (this.datasource.plus) {
+ tableConfig.type = "external"
+ }
+ }
+
const linkedTable = await this.createTable(tableConfig)
- this.linkedTable = linkedTable
return linkedTable
}
@@ -621,17 +642,36 @@ class TestConfiguration {
// VIEW
- async createLegacyView(config?: any) {
- if (!this.table) {
+ async createLegacyView(config?: View) {
+ if (!this.table && !config) {
throw "Test requires table to be configured."
}
const view = config || {
- tableId: this.table._id,
- name: "ViewTest",
+ tableId: this.table!._id,
+ name: generator.guid(),
}
return this._req(view, null, controllers.view.v1.save)
}
+ async createView(
+ config?: Omit & {
+ name?: string
+ tableId?: string
+ }
+ ) {
+ if (!this.table && !config?.tableId) {
+ throw "Test requires table to be configured."
+ }
+
+ const view: CreateViewRequest = {
+ ...config,
+ tableId: config?.tableId || this.table!._id!,
+ name: config?.name || generator.word(),
+ }
+
+ return await this.api.viewV2.create(view)
+ }
+
// AUTOMATION
async createAutomation(config?: any) {
@@ -677,17 +717,17 @@ class TestConfiguration {
config = config || basicDatasource()
const response = await this._req(config, null, controllers.datasource.save)
this.datasource = response.datasource
- return this.datasource
+ return this.datasource!
}
- async updateDatasource(datasource: any) {
+ async updateDatasource(datasource: Datasource): Promise {
const response = await this._req(
datasource,
{ datasourceId: datasource._id },
controllers.datasource.update
)
this.datasource = response.datasource
- return this.datasource
+ return this.datasource!
}
async restDatasource(cfg?: any) {
@@ -771,7 +811,7 @@ class TestConfiguration {
if (!this.datasource && !config) {
throw "No datasource created for query."
}
- config = config || basicQuery(this.datasource._id)
+ config = config || basicQuery(this.datasource!._id!)
return this._req(config, null, controllers.query.save)
}
diff --git a/packages/server/src/tests/utilities/api/datasource.ts b/packages/server/src/tests/utilities/api/datasource.ts
new file mode 100644
index 0000000000..ee698334f2
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/datasource.ts
@@ -0,0 +1,57 @@
+import {
+ CreateDatasourceRequest,
+ Datasource,
+ VerifyDatasourceRequest,
+ VerifyDatasourceResponse,
+} from "@budibase/types"
+import TestConfiguration from "../TestConfiguration"
+import { TestAPI } from "./base"
+
+export class DatasourceAPI extends TestAPI {
+ constructor(config: TestConfiguration) {
+ super(config)
+ }
+
+ create = async (
+ config: Datasource,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const body: CreateDatasourceRequest = {
+ datasource: config,
+ tablesFilter: [],
+ }
+ const result = await this.request
+ .post(`/api/datasources`)
+ .send(body)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return result.body.datasource as Datasource
+ }
+
+ update = async (
+ datasource: Datasource,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const result = await this.request
+ .put(`/api/datasources/${datasource._id}`)
+ .send(datasource)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return result.body.datasource as Datasource
+ }
+
+ verify = async (
+ data: VerifyDatasourceRequest,
+ { expectStatus } = { expectStatus: 200 }
+ ) => {
+ const result = await this.request
+ .post(`/api/datasources/verify`)
+ .send(data)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return result
+ }
+}
diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts
index 40995b62f2..31c74a0e78 100644
--- a/packages/server/src/tests/utilities/api/index.ts
+++ b/packages/server/src/tests/utilities/api/index.ts
@@ -3,17 +3,23 @@ import { PermissionAPI } from "./permission"
import { RowAPI } from "./row"
import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2"
+import { DatasourceAPI } from "./datasource"
+import { LegacyViewAPI } from "./legacyView"
export default class API {
table: TableAPI
+ legacyView: LegacyViewAPI
viewV2: ViewV2API
row: RowAPI
permission: PermissionAPI
+ datasource: DatasourceAPI
constructor(config: TestConfiguration) {
this.table = new TableAPI(config)
+ this.legacyView = new LegacyViewAPI(config)
this.viewV2 = new ViewV2API(config)
this.row = new RowAPI(config)
this.permission = new PermissionAPI(config)
+ this.datasource = new DatasourceAPI(config)
}
}
diff --git a/packages/server/src/tests/utilities/api/legacyView.ts b/packages/server/src/tests/utilities/api/legacyView.ts
new file mode 100644
index 0000000000..63981cec5e
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/legacyView.ts
@@ -0,0 +1,16 @@
+import TestConfiguration from "../TestConfiguration"
+import { TestAPI } from "./base"
+
+export class LegacyViewAPI extends TestAPI {
+ constructor(config: TestConfiguration) {
+ super(config)
+ }
+
+ get = async (id: string, { expectStatus } = { expectStatus: 200 }) => {
+ return await this.request
+ .get(`/api/views/${id}`)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ }
+}
diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts
index c6ef4606d2..686c8c031b 100644
--- a/packages/server/src/tests/utilities/api/row.ts
+++ b/packages/server/src/tests/utilities/api/row.ts
@@ -1,4 +1,10 @@
-import { PatchRowRequest, SaveRowRequest, Row } from "@budibase/types"
+import {
+ PatchRowRequest,
+ SaveRowRequest,
+ Row,
+ ValidateResponse,
+ ExportRowsRequest,
+} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
@@ -22,6 +28,21 @@ export class RowAPI extends TestAPI {
return request
}
+ getEnriched = async (
+ sourceId: string,
+ rowId: string,
+ { expectStatus } = { expectStatus: 200 }
+ ) => {
+ const request = this.request
+ .get(`/api/${sourceId}/${rowId}/enrich`)
+ .set(this.config.defaultHeaders())
+ .expect(expectStatus)
+ if (expectStatus !== 404) {
+ request.expect("Content-Type", /json/)
+ }
+ return request
+ }
+
save = async (
sourceId: string,
row: SaveRowRequest,
@@ -36,6 +57,20 @@ export class RowAPI extends TestAPI {
return resp.body as Row
}
+ validate = async (
+ sourceId: string,
+ row: SaveRowRequest,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const resp = await this.request
+ .post(`/api/${sourceId}/rows/validate`)
+ .send(row)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return resp.body as ValidateResponse
+ }
+
patch = async (
sourceId: string,
row: PatchRowRequest,
@@ -51,14 +86,40 @@ export class RowAPI extends TestAPI {
delete = async (
sourceId: string,
- rows: Row[],
+ rows: Row | string | (Row | string)[],
{ expectStatus } = { expectStatus: 200 }
) => {
return this.request
.delete(`/api/${sourceId}/rows`)
- .send({ rows })
+ .send(Array.isArray(rows) ? { rows } : rows)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expectStatus)
}
+
+ fetch = async (
+ sourceId: string,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const request = this.request
+ .get(`/api/${sourceId}/rows`)
+ .set(this.config.defaultHeaders())
+ .expect(expectStatus)
+
+ return (await request).body
+ }
+
+ exportRows = async (
+ tableId: string,
+ body: ExportRowsRequest,
+ { expectStatus } = { expectStatus: 200 }
+ ) => {
+ const request = this.request
+ .post(`/api/${tableId}/rows/exportRows?format=json`)
+ .set(this.config.defaultHeaders())
+ .send(body)
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return request
+ }
}
diff --git a/packages/server/src/tests/utilities/api/table.ts b/packages/server/src/tests/utilities/api/table.ts
index 70f0869650..04432a788a 100644
--- a/packages/server/src/tests/utilities/api/table.ts
+++ b/packages/server/src/tests/utilities/api/table.ts
@@ -1,4 +1,4 @@
-import { Table } from "@budibase/types"
+import { SaveTableRequest, SaveTableResponse, Table } from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
@@ -7,6 +7,19 @@ export class TableAPI extends TestAPI {
super(config)
}
+ create = async (
+ data: SaveTableRequest,
+ { expectStatus } = { expectStatus: 200 }
+ ): Promise => {
+ const res = await this.request
+ .post(`/api/tables`)
+ .send(data)
+ .set(this.config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(expectStatus)
+ return res.body
+ }
+
fetch = async (
{ expectStatus } = { expectStatus: 200 }
): Promise => {
diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts
index 0682361e16..92a6d394bf 100644
--- a/packages/server/src/tests/utilities/api/viewV2.ts
+++ b/packages/server/src/tests/utilities/api/viewV2.ts
@@ -23,8 +23,8 @@ export class ViewV2API extends TestAPI {
if (!tableId && !this.config.table) {
throw "Test requires table to be configured."
}
- const table = this.config.table
- tableId = table!._id!
+
+ tableId = tableId || this.config.table!._id!
const view = {
tableId,
name: generator.guid(),
diff --git a/packages/types/src/api/web/app/row.ts b/packages/types/src/api/web/app/row.ts
index f9623a3daf..4ab4090461 100644
--- a/packages/types/src/api/web/app/row.ts
+++ b/packages/types/src/api/web/app/row.ts
@@ -1,5 +1,7 @@
import { Row } from "../../../documents/app/row"
+export interface GetRowResponse extends Row {}
+
export interface DeleteRows {
rows: (Row | string)[]
}
@@ -9,3 +11,8 @@ export interface DeleteRow {
}
export type DeleteRowRequest = DeleteRows | DeleteRow
+
+export interface ValidateResponse {
+ valid: boolean
+ errors: Record
+}
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index a99ef0e837..62ea90a6a4 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -1,5 +1,6 @@
-import { SearchParams } from "../../../sdk"
+import { SearchFilters, SearchParams } from "../../../sdk"
import { Row } from "../../../documents"
+import { ReadStream } from "fs"
export interface SaveRowRequest extends Row {}
@@ -28,3 +29,11 @@ export interface SearchViewRowRequest
export interface SearchRowResponse {
rows: any[]
}
+
+export interface ExportRowsRequest {
+ rows: string[]
+ columns?: string[]
+ query?: SearchFilters
+}
+
+export type ExportRowsResponse = ReadStream
diff --git a/packages/worker/src/api/routes/global/tests/scim.spec.ts b/packages/worker/src/api/routes/global/tests/scim.spec.ts
index 5686e39fa8..fba1523cd4 100644
--- a/packages/worker/src/api/routes/global/tests/scim.spec.ts
+++ b/packages/worker/src/api/routes/global/tests/scim.spec.ts
@@ -10,6 +10,8 @@ import {
import { TestConfiguration } from "../../../../tests"
import { events } from "@budibase/backend-core"
+// this test can 409 - retries reduce issues with this
+jest.retryTimes(2)
jest.setTimeout(30000)
mocks.licenses.useScimIntegration()
diff --git a/qa-core/src/integrations/external-schema/postgres.integration.spec.ts b/qa-core/src/integrations/external-schema/postgres.integration.spec.ts
index 762a16b221..a0812c9677 100644
--- a/qa-core/src/integrations/external-schema/postgres.integration.spec.ts
+++ b/qa-core/src/integrations/external-schema/postgres.integration.spec.ts
@@ -17,8 +17,7 @@ describe("getExternalSchema", () => {
}
beforeAll(async () => {
- // This is left on propose without a tag, so if a new version introduces a breaking change we will be notified
- const container = await new GenericContainer("postgres")
+ const container = await new GenericContainer("postgres:13.12")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password")
.start()
diff --git a/yarn.lock b/yarn.lock
index 00f4dd547c..64b073a5ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6269,6 +6269,14 @@
"@types/tedious" "*"
tarn "^3.0.1"
+"@types/node-fetch@2.6.1":
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
+ integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
+ dependencies:
+ "@types/node" "*"
+ form-data "^3.0.0"
+
"@types/node-fetch@2.6.4":
version "2.6.4"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
@@ -6290,6 +6298,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
+"@types/node@14.18.20":
+ version "14.18.20"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
+ integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
+
"@types/node@16.9.1":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"