Merge branch 'develop' into backmerge-20230919

This commit is contained in:
Adria Navarro 2023-09-19 12:33:04 +02:00 committed by GitHub
commit 235799c07d
48 changed files with 1211 additions and 757 deletions

View File

@ -2,7 +2,7 @@ name: Close stale issues and PRs # https://github.com/actions/stale
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '30 1 * * *' # 1:30 every morning - cron: '*/30 * * * *' # Every 30 mins
jobs: jobs:
stale: stale:

View File

@ -10,3 +10,4 @@ packages/server/client
packages/server/src/definitions/openapi.ts packages/server/src/definitions/openapi.ts
packages/builder/.routify packages/builder/.routify
packages/sdk/sdk packages/sdk/sdk
packages/pro/coverage

View File

@ -82,7 +82,7 @@
"@spectrum-css/typography": "3.0.1", "@spectrum-css/typography": "3.0.1",
"@spectrum-css/underlay": "2.0.9", "@spectrum-css/underlay": "2.0.9",
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.8",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "3.2.3", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0", "svelte-portal": "^1.0.0",

View File

@ -66,6 +66,10 @@
pointer-events: all; pointer-events: all;
width: 100%; width: 100%;
} }
.spectrum-Toast--neutral {
background-color: var(--grey-2);
}
.spectrum-Button { .spectrum-Button {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }

View File

@ -27,7 +27,11 @@
<div class="spectrum-Toast-body" class:actionBody={!!action}> <div class="spectrum-Toast-body" class:actionBody={!!action}>
<div class="wrap spectrum-Toast-content">{message || ""}</div> <div class="wrap spectrum-Toast-content">{message || ""}</div>
{#if action} {#if action}
<ActionButton quiet emphasized on:click={action}> <ActionButton
quiet
emphasized
on:click={() => action(() => dispatch("dismiss"))}
>
<div style="color: white; font-weight: 600;">{actionMessage}</div> <div style="color: white; font-weight: 600;">{actionMessage}</div>
</ActionButton> </ActionButton>
{/if} {/if}

View File

@ -8,7 +8,7 @@
<Portal target=".modal-container"> <Portal target=".modal-container">
<div class="notifications"> <div class="notifications">
{#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)} {#each $notifications as { type, icon, message, id, dismissable, action, actionMessage, wide } (id)}
<div transition:fly={{ y: 30 }}> <div transition:fly={{ y: 30 }}>
<Notification <Notification
{type} {type}
@ -16,6 +16,7 @@
{message} {message}
{dismissable} {dismissable}
{action} {action}
{actionMessage}
{wide} {wide}
on:dismiss={() => notifications.dismiss(id)} on:dismiss={() => notifications.dismiss(id)}
/> />

View File

@ -1,6 +1,7 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
export const BANNER_TYPES = { export const BANNER_TYPES = {
NEUTRAL: "neutral",
INFO: "info", INFO: "info",
NEGATIVE: "negative", NEGATIVE: "negative",
WARNING: "warning", WARNING: "warning",

View File

@ -27,7 +27,9 @@ export const createNotificationStore = () => {
icon = "", icon = "",
autoDismiss = true, autoDismiss = true,
action = null, action = null,
actionMessage = null,
wide = false, wide = false,
dismissTimeout = NOTIFICATION_TIMEOUT,
} }
) => { ) => {
if (block) { if (block) {
@ -44,14 +46,16 @@ export const createNotificationStore = () => {
icon, icon,
dismissable: !autoDismiss, dismissable: !autoDismiss,
action, action,
actionMessage,
wide, wide,
dismissTimeout,
}, },
] ]
}) })
if (autoDismiss) { if (autoDismiss) {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
dismissNotification(_id) dismissNotification(_id)
}, NOTIFICATION_TIMEOUT) }, dismissTimeout)
timeoutIds.add(timeoutId) timeoutIds.add(timeoutId)
} }
} }

View File

@ -126,8 +126,9 @@
transition: top 130ms ease-out, left 130ms ease-out; transition: top 130ms ease-out, left 130ms ease-out;
} }
.spectrum-Tooltip-label { .spectrum-Tooltip-label {
text-overflow: ellipsis; display: -webkit-box;
white-space: nowrap; -webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;

View File

@ -69,7 +69,7 @@
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
"dayjs": "^1.11.2", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"lodash": "4.17.21", "lodash": "4.17.21",

View File

@ -502,7 +502,7 @@
</div> </div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div> <div>
<div> <div class="row">
<Label>Time zones</Label> <Label>Time zones</Label>
<AbsTooltip <AbsTooltip
position="top" position="top"
@ -523,7 +523,7 @@
{:else if editableColumn.type === "number" && !editableColumn.autocolumn} {:else if editableColumn.type === "number" && !editableColumn.autocolumn}
<div class="split-label"> <div class="split-label">
<div class="label-length"> <div class="label-length">
<Label size="M">Max Value</Label> <Label size="M">Min Value</Label>
</div> </div>
<div class="input-length"> <div class="input-length">
<Input <Input
@ -690,18 +690,19 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.tooltip-alignment { .tooltip-alignment {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} }
.label-length { .label-length {
flex-basis: 40%; flex-basis: 40%;
} }
.input-length { .input-length {
flex-grow: 1; flex-grow: 1;
} }
.row {
gap: 8px;
display: flex;
}
</style> </style>

View File

@ -3,6 +3,7 @@
import { admin, auth, licensing } from "stores/portal" import { admin, auth, licensing } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { banner, BANNER_TYPES } from "@budibase/bbui"
import { API } from "api" import { API } from "api"
import Branding from "./Branding.svelte" import Branding from "./Branding.svelte"
@ -16,6 +17,32 @@
$: user = $auth.user $: user = $auth.user
$: useAccountPortal = cloud && !$admin.disableAccountPortal $: useAccountPortal = cloud && !$admin.disableAccountPortal
let showVerificationPrompt = false
const checkVerification = user => {
if (!showVerificationPrompt && user?.account?.verified === false) {
showVerificationPrompt = true
banner.queue([
{
message: `Please verify your account. We've sent the verification link to ${user.email}`,
type: BANNER_TYPES.NEUTRAL,
showCloseButton: false,
extraButtonAction: () => {
fetch(`${$admin.accountPortalUrl}/api/auth/reset`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: user.email }),
})
},
extraButtonText: "Resend email",
},
])
}
}
$: checkVerification(user)
const validateTenantId = async () => { const validateTenantId = async () => {
const host = window.location.host const host = window.location.host

View File

@ -63,8 +63,7 @@
) )
const githubResponse = await githubCheck.json() const githubResponse = await githubCheck.json()
//Get tag and remove the v infront of the tage name e.g. v1.0.0 is 1.0.0 githubVersion = githubResponse.tag_name
githubVersion = githubResponse.tag_name.slice(1)
//Get the release date and output it in the local time format //Get the release date and output it in the local time format
githubPublishedDate = new Date(githubResponse.published_at) githubPublishedDate = new Date(githubResponse.published_at)

View File

@ -33,7 +33,7 @@
"@spectrum-css/typography": "^3.0.2", "@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",
"dayjs": "^1.10.5", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"html5-qrcode": "^2.2.1", "html5-qrcode": "^2.2.1",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",

View File

@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"@budibase/bbui": "0.0.0", "@budibase/bbui": "0.0.0",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"dayjs": "^1.11.7", "dayjs": "^1.10.8",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",
"svelte": "^3.46.2" "svelte": "^3.46.2"

View File

@ -20,6 +20,8 @@
config, config,
ui, ui,
columns, columns,
definition,
datasource,
} = getContext("grid") } = getContext("grid")
const bannedDisplayColumnTypes = [ const bannedDisplayColumnTypes = [
@ -118,6 +120,33 @@
open = false open = false
} }
const duplicateColumn = async () => {
open = false
// Generate new name
let newName = `${column.name} copy`
let attempts = 2
while ($definition.schema[newName]) {
newName = `${column.name} copy ${attempts++}`
}
// Save schema with new column
const existingColumnDefinition = $definition.schema[column.name]
await datasource.actions.saveDefinition({
...$definition,
schema: {
...$definition.schema,
[newName]: {
...existingColumnDefinition,
name: newName,
schema: {
...existingColumnDefinition.schema,
},
},
},
})
}
onMount(() => subscribe("close-edit-column", cancelEdit)) onMount(() => subscribe("close-edit-column", cancelEdit))
</script> </script>
@ -192,6 +221,13 @@
> >
Edit column Edit column
</MenuItem> </MenuItem>
<MenuItem
icon="Duplicate"
on:click={duplicateColumn}
disabled={!$config.canEditColumns}
>
Duplicate column
</MenuItem>
<MenuItem <MenuItem
icon="Label" icon="Label"
on:click={makeDisplayColumn} on:click={makeDisplayColumn}

@ -1 +1 @@
Subproject commit 961d683794442f615468d44b684e5a94633109eb Subproject commit 4638ae916e55ce89166095578cbd01745d0ee9ee

View File

@ -1,4 +1,5 @@
import { import {
AutoReason,
Datasource, Datasource,
FieldSchema, FieldSchema,
FieldType, FieldType,
@ -24,7 +25,7 @@ import {
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils" import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
import { FieldTypes } from "../../../constants" import { AutoFieldSubTypes, FieldTypes } from "../../../constants"
import { processObjectSync } from "@budibase/string-templates" import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { processDates, processFormulas } from "../../../utilities/rowProcessor" import { processDates, processFormulas } from "../../../utilities/rowProcessor"
@ -259,6 +260,15 @@ function isOneSide(field: FieldSchema) {
) )
} }
function isEditableColumn(column: FieldSchema) {
const isExternalAutoColumn =
column.autocolumn &&
column.autoReason !== AutoReason.FOREIGN_KEY &&
column.subtype !== AutoFieldSubTypes.AUTO_ID
const isFormula = column.type === FieldTypes.FORMULA
return !(isExternalAutoColumn || isFormula)
}
export class ExternalRequest { export class ExternalRequest {
private operation: Operation private operation: Operation
private tableId: string private tableId: string
@ -295,11 +305,7 @@ export class ExternalRequest {
manyRelationships: ManyRelationship[] = [] manyRelationships: ManyRelationship[] = []
for (let [key, field] of Object.entries(table.schema)) { for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it // if set already, or not set just skip it
if ( if (row[key] == null || newRow[key] || !isEditableColumn(field)) {
row[key] == null ||
newRow[key] ||
!sdk.tables.isEditableColumn(field)
) {
continue continue
} }
// if its an empty string then it means return the column to null (if possible) // if its an empty string then it means return the column to null (if possible)

View File

@ -18,6 +18,8 @@ import {
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "./utils" import * as utils from "./utils"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import { inputProcessing } from "../../../utilities/rowProcessor"
import { cloneDeep, isEqual } from "lodash"
export async function handleRequest( export async function handleRequest(
operation: Operation, operation: Operation,
@ -76,10 +78,24 @@ export async function save(ctx: UserCtx) {
if (!validateResult.valid) { if (!validateResult.valid) {
throw { validation: validateResult.errors } throw { validation: validateResult.errors }
} }
const table = await sdk.tables.getTable(tableId)
const { table: updatedTable, row } = inputProcessing(
ctx.user,
cloneDeep(table),
inputs
)
const response = await handleRequest(Operation.CREATE, tableId, { const response = await handleRequest(Operation.CREATE, tableId, {
row: inputs, row,
}) })
const responseRow = response as { row: Row } const responseRow = response as { row: Row }
if (!isEqual(table, updatedTable)) {
await sdk.tables.saveTable(updatedTable)
}
const rowId = responseRow.row._id const rowId = responseRow.row._id
if (rowId) { if (rowId) {
const row = await sdk.rows.external.getRow(tableId, rowId, { const row = await sdk.rows.external.getRow(tableId, rowId, {
@ -94,10 +110,18 @@ export async function save(ctx: UserCtx) {
} }
} }
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx): Promise<Row> {
const id = ctx.params.rowId const id = ctx.params.rowId
const tableId = utils.getTableId(ctx) 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) { export async function destroy(ctx: UserCtx) {
@ -107,7 +131,7 @@ export async function destroy(ctx: UserCtx) {
id: breakRowIdField(_id), id: breakRowIdField(_id),
includeSqlRelationships: IncludeRelationship.EXCLUDE, includeSqlRelationships: IncludeRelationship.EXCLUDE,
})) as { row: Row } })) as { row: Row }
return { response: { ok: true }, row } return { response: { ok: true, id: _id }, row }
} }
export async function bulkDestroy(ctx: UserCtx) { export async function bulkDestroy(ctx: UserCtx) {

View File

@ -14,6 +14,10 @@ import {
SearchRowResponse, SearchRowResponse,
SearchRowRequest, SearchRowRequest,
SearchParams, SearchParams,
GetRowResponse,
ValidateResponse,
ExportRowsRequest,
ExportRowsResponse,
} from "@budibase/types" } from "@budibase/types"
import * as utils from "./utils" import * as utils from "./utils"
import { gridSocket } from "../../../websockets" 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<void, GetRowResponse>) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), { ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx), {
datasourceId: tableId, datasourceId: tableId,
@ -214,11 +218,11 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
}) })
} }
export async function validate(ctx: Ctx) { export async function validate(ctx: Ctx<Row, ValidateResponse>) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
// external tables are hard to validate currently // external tables are hard to validate currently
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
ctx.body = { valid: true } ctx.body = { valid: true, errors: {} }
} else { } else {
ctx.body = await sdk.rows.utils.validate({ ctx.body = await sdk.rows.utils.validate({
row: ctx.request.body, 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<ExportRowsRequest, ExportRowsResponse>
) => {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const format = ctx.query.format const format = ctx.query.format

View File

@ -131,7 +131,7 @@ export async function save(ctx: UserCtx) {
}) })
} }
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx): Promise<Row> {
const tableId = utils.getTableId(ctx), const tableId = utils.getTableId(ctx),
rowId = ctx.params.rowId rowId = ctx.params.rowId
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)

View File

@ -34,7 +34,7 @@ validateJs.extend(validateJs.validators.datetime, {
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) { export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
const db = context.getAppDB() const db = context.getAppDB()
let row let row: Row
// TODO remove special user case in future // TODO remove special user case in future
if (tableId === InternalTables.USER_METADATA) { if (tableId === InternalTables.USER_METADATA) {
ctx.params = { ctx.params = {

View File

@ -78,9 +78,9 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
ctx.status = 200 ctx.status = 200
ctx.message = `Table ${table.name} saved successfully.` ctx.message = `Table ${table.name} saved successfully.`
ctx.eventEmitter && ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:save`, appId, savedTable) ctx.eventEmitter.emitTable(`table:save`, appId, { ...savedTable })
ctx.body = savedTable ctx.body = savedTable
builderSocket?.emitTableUpdate(ctx, savedTable) builderSocket?.emitTableUpdate(ctx, { ...savedTable })
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx) {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { events, context } from "@budibase/backend-core" 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 { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities" import * as setup from "./utilities"
const { basicTable } = setup.structures const { basicTable } = setup.structures
@ -90,8 +90,10 @@ describe("/tables", () => {
await config.createLegacyView({ await config.createLegacyView({
name: "TestView", name: "TestView",
field: "Price", field: "Price",
calculation: "stats", calculation: ViewCalculation.STATISTICS,
tableId: testTable._id, tableId: testTable._id!,
schema: {},
filters: [],
}) })
const testRow = await request const testRow = await request
@ -254,7 +256,7 @@ describe("/tables", () => {
})) }))
await config.api.viewV2.create({ tableId }) await config.api.viewV2.create({ tableId })
await config.createLegacyView({ tableId, name: generator.guid() }) await config.createLegacyView()
const res = await config.api.table.fetch() const res = await config.api.table.fetch()
@ -366,7 +368,7 @@ describe("/tables", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`) 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() expect(dependentTable.schema.TestTable).not.toBeDefined()
}) })

View File

@ -6,6 +6,7 @@ import {
SortOrder, SortOrder,
SortType, SortType,
Table, Table,
UIFieldMetadata,
UpdateViewRequest, UpdateViewRequest,
ViewV2, ViewV2,
} from "@budibase/types" } from "@budibase/types"
@ -418,9 +419,12 @@ describe.each([
const res = await config.api.viewV2.create(newView) const res = await config.api.viewV2.create(newView)
const view = await config.api.viewV2.get(res.id) const view = await config.api.viewV2.get(res.id)
expect(view!.schema?.Price).toBeUndefined() expect(view!.schema?.Price).toBeUndefined()
const updatedTable = await config.getTable(table._id!) const updatedTable = await config.api.table.get(table._id!)
const viewSchema = updatedTable.views[view!.name!].schema const viewSchema = updatedTable.views![view!.name!].schema as Record<
expect(viewSchema.Price.visible).toEqual(false) string,
UIFieldMetadata
>
expect(viewSchema.Price?.visible).toEqual(false)
}) })
}) })
}) })

View File

@ -6,7 +6,7 @@ const { RelationshipType } = require("../../constants")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
describe("test the link controller", () => { describe("test the link controller", () => {
let config = new TestConfig(false) let config = new TestConfig()
let table1, table2, appId let table1, table2, appId
beforeAll(async () => { beforeAll(async () => {

View File

@ -4,7 +4,7 @@ const linkUtils = require("../linkedRows/linkUtils")
const { context } = require("@budibase/backend-core") const { context } = require("@budibase/backend-core")
describe("test link functionality", () => { describe("test link functionality", () => {
const config = new TestConfig(false) const config = new TestConfig()
let appId let appId
describe("getLinkedTable", () => { describe("getLinkedTable", () => {

View File

@ -12,18 +12,15 @@ import {
FieldType, FieldType,
RelationshipType, RelationshipType,
Row, Row,
SourceName,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import _ from "lodash" import _ from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core" import { utils } from "@budibase/backend-core"
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" import { databaseTestProviders } from "../integrations/tests/utils"
const config = setup.getConfig()! const config = setup.getConfig()!
jest.setTimeout(30000)
jest.unmock("pg") jest.unmock("pg")
jest.mock("../websockets") jest.mock("../websockets")
@ -35,62 +32,18 @@ describe("postgres integrations", () => {
manyToOneRelationshipInfo: ForeignTableInfo, manyToOneRelationshipInfo: ForeignTableInfo,
manyToManyRelationshipInfo: ForeignTableInfo manyToManyRelationshipInfo: ForeignTableInfo
let host: string
let port: number
const containers: StartedTestContainer[] = []
beforeAll(async () => { 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() await config.init()
const apiKey = await config.generateApiKey() const apiKey = await config.generateApiKey()
containers.push(containerPostgres)
makeRequest = generateMakeRequest(apiKey, true) makeRequest = generateMakeRequest(apiKey, true)
})
afterAll(async () => { postgresDatasource = await config.api.datasource.create(
for (let container of containers) { await databaseTestProviders.postgres.getDsConfig()
await container.stop() )
}
}) })
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 () => { beforeEach(async () => {
postgresDatasource = await config.createDatasource(pgDatasourceConfig())
async function createAuxTable(prefix: string) { async function createAuxTable(prefix: string) {
return await config.createTable({ return await config.createTable({
name: `${prefix}_${generator.word({ length: 6 })}`, name: `${prefix}_${generator.word({ length: 6 })}`,
@ -226,25 +179,6 @@ describe("postgres integrations", () => {
let { rowData } = opts as any let { rowData } = opts as any
let foreignRows: ForeignRowsInfo[] = [] 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) { if (opts?.createForeignRows?.createOneToMany) {
const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}` 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( async function populatePrimaryRows(
count: number, count: number,
opts?: { opts?: {
@ -357,9 +299,9 @@ describe("postgres integrations", () => {
config: { config: {
ca: false, ca: false,
database: "postgres", database: "postgres",
host, host: postgresDatasource.config!.host,
password: "--secret-value--", password: "--secret-value--",
port, port: postgresDatasource.config!.port,
rejectUnauthorized: false, rejectUnauthorized: false,
schema: "public", schema: "public",
ssl: false, ssl: false,
@ -401,12 +343,16 @@ describe("postgres integrations", () => {
it("multiple rows can be persisted", async () => { it("multiple rows can be persisted", async () => {
const numberOfRows = 10 const numberOfRows = 10
const newRows = Array(numberOfRows).fill(generateRandomPrimaryRowData()) const newRows: Row[] = Array(numberOfRows).fill(
generateRandomPrimaryRowData()
)
for (const newRow of newRows) { await Promise.all(
newRows.map(async newRow => {
const res = await createRow(primaryPostgresTable._id, newRow) const res = await createRow(primaryPostgresTable._id, newRow)
expect(res.status).toBe(200) expect(res.status).toBe(200)
} })
)
const persistedRows = await config.getRows(primaryPostgresTable._id!) const persistedRows = await config.getRows(primaryPostgresTable._id!)
expect(persistedRows).toHaveLength(numberOfRows) expect(persistedRows).toHaveLength(numberOfRows)
@ -567,7 +513,7 @@ describe("postgres integrations", () => {
foreignRows = createdRow.foreignRows 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) const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200) expect(res.status).toBe(200)
@ -575,6 +521,12 @@ describe("postgres integrations", () => {
const one2ManyForeignRows = foreignRows.filter( const one2ManyForeignRows = foreignRows.filter(
x => x.relationshipType === RelationshipType.ONE_TO_MANY 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(one2ManyForeignRows).toHaveLength(1)
expect(res.body).toEqual({ expect(res.body).toEqual({
@ -585,9 +537,25 @@ describe("postgres integrations", () => {
_rev: expect.any(String), _rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
one2ManyForeignRows[0].row.id, 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), _rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]: [`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
foreignRows[0].row.id, 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, tableId: row.tableId,
_id: expect.any(String), _id: expect.any(String),
_rev: 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, tableId: row.tableId,
_id: expect.any(String), _id: expect.any(String),
_rev: 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", () => { describe("given than multiple tables have multiple rows", () => {
const rowsCount = 6 const rowsCount = 6
beforeEach(async () => { beforeEach(async () => {
const createRandomTableWithRows = async () =>
await config.createRow({
tableId: (await createDefaultPgTable())._id,
title: generator.name(),
})
await createRandomTableWithRows() await createRandomTableWithRows()
await createRandomTableWithRows() await createRandomTableWithRows()
@ -1023,12 +997,6 @@ describe("postgres integrations", () => {
const rowsCount = 6 const rowsCount = 6
beforeEach(async () => { beforeEach(async () => {
const createRandomTableWithRows = async () =>
await config.createRow({
tableId: (await createDefaultPgTable())._id,
title: generator.name(),
})
await createRandomTableWithRows() await createRandomTableWithRows()
await populatePrimaryRows(rowsCount) await populatePrimaryRows(rowsCount)
await createRandomTableWithRows() await createRandomTableWithRows()
@ -1046,24 +1014,25 @@ describe("postgres integrations", () => {
describe("POST /api/datasources/verify", () => { describe("POST /api/datasources/verify", () => {
it("should be able to verify the connection", async () => { it("should be able to verify the connection", async () => {
const config = pgDatasourceConfig() const response = await config.api.datasource.verify({
const response = await makeRequest( datasource: await databaseTestProviders.postgres.getDsConfig(),
"post", })
"/api/datasources/verify",
config
)
expect(response.status).toBe(200) expect(response.status).toBe(200)
expect(response.body.connected).toBe(true) expect(response.body.connected).toBe(true)
}) })
it("should state an invalid datasource cannot connect", async () => { it("should state an invalid datasource cannot connect", async () => {
const config = pgDatasourceConfig() const dbConfig = await databaseTestProviders.postgres.getDsConfig()
config.datasource.config.password = "wrongpassword" const response = await config.api.datasource.verify({
const response = await makeRequest( datasource: {
"post", ...dbConfig,
"/api/datasources/verify", config: {
config ...dbConfig.config,
) password: "wrongpassword",
},
},
})
expect(response.status).toBe(200) expect(response.status).toBe(200)
expect(response.body.connected).toBe(false) expect(response.body.connected).toBe(false)
expect(response.body.error).toBeDefined() expect(response.body.error).toBeDefined()

View File

@ -158,6 +158,12 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
) { ) {
config.ssl.rejectUnauthorized = config.rejectUnauthorized config.ssl.rejectUnauthorized = config.rejectUnauthorized
} }
// The MySQL library we use doesn't directly document the parameters that can be passed in the ssl
// object, it instead points to an older library that it says it is mostly API compatible with, that
// older library actually documents what parameters can be passed in the ssl object.
// https://github.com/sidorares/node-mysql2#api-and-configuration
// https://github.com/mysqljs/mysql#ssl-options
// @ts-ignore // @ts-ignore
delete config.rejectUnauthorized delete config.rejectUnauthorized
this.config = { this.config = {

View File

@ -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<Datasource>
}
export const databaseTestProviders = {
postgres: pg,
}

View File

@ -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<Datasource> {
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,
},
}
}

View File

@ -11,6 +11,7 @@ import { MIGRATIONS } from "../"
import * as helpers from "./helpers" import * as helpers from "./helpers"
import tk from "timekeeper" import tk from "timekeeper"
import { View } from "@budibase/types"
const timestamp = new Date().toISOString() const timestamp = new Date().toISOString()
tk.freeze(timestamp) tk.freeze(timestamp)
@ -52,7 +53,9 @@ describe("migrations", () => {
await config.createTable() await config.createTable()
await config.createLegacyView() await config.createLegacyView()
await config.createTable() 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()
await config.createScreen() await config.createScreen()

View File

@ -31,7 +31,7 @@ export interface ExportRowsParams {
format: Format format: Format
rowIds?: string[] rowIds?: string[]
columns?: string[] columns?: string[]
query: SearchFilters query?: SearchFilters
} }
export interface ExportRowsResult { export interface ExportRowsResult {

View File

@ -12,7 +12,7 @@ import {
TableViewsResponse, TableViewsResponse,
} from "@budibase/types" } from "@budibase/types"
import datasources from "../datasources" import datasources from "../datasources"
import { isEditableColumn, populateExternalTableSchemas } from "./validation" import { populateExternalTableSchemas } from "./validation"
import sdk from "../../../sdk" import sdk from "../../../sdk"
async function getAllInternalTables(db?: Database): Promise<Table[]> { async function getAllInternalTables(db?: Database): Promise<Table[]> {
@ -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 { export default {
getAllInternalTables, getAllInternalTables,
getAllExternalTables, getAllExternalTables,
getExternalTable, getExternalTable,
getTable, getTable,
populateExternalTableSchemas, populateExternalTableSchemas,
isEditableColumn,
enrichViewSchemas, enrichViewSchemas,
saveTable,
} }

View File

@ -55,13 +55,6 @@ function checkForeignKeysAreAutoColumns(datasource: Datasource) {
return 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) { export function populateExternalTableSchemas(datasource: Datasource) {
return checkForeignKeysAreAutoColumns(datasource) return checkForeignKeysAreAutoColumns(datasource)
} }

View File

@ -50,6 +50,11 @@ import {
SearchFilters, SearchFilters,
UserRoles, UserRoles,
Automation, Automation,
View,
FieldType,
RelationshipType,
ViewV2,
CreateViewRequest,
} from "@budibase/types" } from "@budibase/types"
import API from "./api" import API from "./api"
@ -75,9 +80,8 @@ class TestConfiguration {
globalUserId: any globalUserId: any
userMetadataId: any userMetadataId: any
table?: Table table?: Table
linkedTable: any
automation: any automation: any
datasource: any datasource?: Datasource
tenantId?: string tenantId?: string
defaultUserValues: DefaultUserValues defaultUserValues: DefaultUserValues
api: API api: API
@ -527,7 +531,7 @@ class TestConfiguration {
// TABLE // TABLE
async updateTable( async updateTable(
config?: any, config?: Table,
{ skipReassigning } = { skipReassigning: false } { skipReassigning } = { skipReassigning: false }
): Promise<Table> { ): Promise<Table> {
config = config || basicTable() config = config || basicTable()
@ -542,33 +546,50 @@ class TestConfiguration {
if (config != null && config._id) { if (config != null && config._id) {
delete 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) return this.updateTable(config, options)
} }
async getTable(tableId?: string) { async getTable(tableId?: string) {
tableId = tableId || this.table?._id tableId = tableId || this.table!._id!
return this._req(null, { tableId }, controllers.table.find) 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) { if (!this.table) {
throw "Must have created a table first." throw "Must have created a table first."
} }
const tableConfig: any = basicTable() const tableConfig = config || basicTable()
tableConfig.primaryDisplay = "name" tableConfig.primaryDisplay = "name"
for (let link of links) { for (let link of links) {
tableConfig.schema[link] = { tableConfig.schema[link] = {
type: "link", type: FieldType.LINK,
fieldName: link, fieldName: link,
tableId: this.table._id, tableId: this.table._id,
name: link, name: link,
} relationshipType,
if (relationshipType) {
tableConfig.schema[link].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) const linkedTable = await this.createTable(tableConfig)
this.linkedTable = linkedTable
return linkedTable return linkedTable
} }
@ -621,17 +642,36 @@ class TestConfiguration {
// VIEW // VIEW
async createLegacyView(config?: any) { async createLegacyView(config?: View) {
if (!this.table) { if (!this.table && !config) {
throw "Test requires table to be configured." throw "Test requires table to be configured."
} }
const view = config || { const view = config || {
tableId: this.table._id, tableId: this.table!._id,
name: "ViewTest", name: generator.guid(),
} }
return this._req(view, null, controllers.view.v1.save) return this._req(view, null, controllers.view.v1.save)
} }
async createView(
config?: Omit<CreateViewRequest, "tableId" | "name"> & {
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 // AUTOMATION
async createAutomation(config?: any) { async createAutomation(config?: any) {
@ -677,17 +717,17 @@ class TestConfiguration {
config = config || basicDatasource() config = config || basicDatasource()
const response = await this._req(config, null, controllers.datasource.save) const response = await this._req(config, null, controllers.datasource.save)
this.datasource = response.datasource this.datasource = response.datasource
return this.datasource return this.datasource!
} }
async updateDatasource(datasource: any) { async updateDatasource(datasource: Datasource): Promise<Datasource> {
const response = await this._req( const response = await this._req(
datasource, datasource,
{ datasourceId: datasource._id }, { datasourceId: datasource._id },
controllers.datasource.update controllers.datasource.update
) )
this.datasource = response.datasource this.datasource = response.datasource
return this.datasource return this.datasource!
} }
async restDatasource(cfg?: any) { async restDatasource(cfg?: any) {
@ -771,7 +811,7 @@ class TestConfiguration {
if (!this.datasource && !config) { if (!this.datasource && !config) {
throw "No datasource created for query." 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) return this._req(config, null, controllers.query.save)
} }

View File

@ -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<Datasource> => {
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<Datasource> => {
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
}
}

View File

@ -3,17 +3,23 @@ import { PermissionAPI } from "./permission"
import { RowAPI } from "./row" import { RowAPI } from "./row"
import { TableAPI } from "./table" import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2" import { ViewV2API } from "./viewV2"
import { DatasourceAPI } from "./datasource"
import { LegacyViewAPI } from "./legacyView"
export default class API { export default class API {
table: TableAPI table: TableAPI
legacyView: LegacyViewAPI
viewV2: ViewV2API viewV2: ViewV2API
row: RowAPI row: RowAPI
permission: PermissionAPI permission: PermissionAPI
datasource: DatasourceAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
this.legacyView = new LegacyViewAPI(config)
this.viewV2 = new ViewV2API(config) this.viewV2 = new ViewV2API(config)
this.row = new RowAPI(config) this.row = new RowAPI(config)
this.permission = new PermissionAPI(config) this.permission = new PermissionAPI(config)
this.datasource = new DatasourceAPI(config)
} }
} }

View File

@ -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)
}
}

View File

@ -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 TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
@ -22,6 +28,21 @@ export class RowAPI extends TestAPI {
return request 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 ( save = async (
sourceId: string, sourceId: string,
row: SaveRowRequest, row: SaveRowRequest,
@ -36,6 +57,20 @@ export class RowAPI extends TestAPI {
return resp.body as Row return resp.body as Row
} }
validate = async (
sourceId: string,
row: SaveRowRequest,
{ expectStatus } = { expectStatus: 200 }
): Promise<ValidateResponse> => {
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 ( patch = async (
sourceId: string, sourceId: string,
row: PatchRowRequest, row: PatchRowRequest,
@ -51,14 +86,40 @@ export class RowAPI extends TestAPI {
delete = async ( delete = async (
sourceId: string, sourceId: string,
rows: Row[], rows: Row | string | (Row | string)[],
{ expectStatus } = { expectStatus: 200 } { expectStatus } = { expectStatus: 200 }
) => { ) => {
return this.request return this.request
.delete(`/api/${sourceId}/rows`) .delete(`/api/${sourceId}/rows`)
.send({ rows }) .send(Array.isArray(rows) ? { rows } : rows)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expectStatus) .expect(expectStatus)
} }
fetch = async (
sourceId: string,
{ expectStatus } = { expectStatus: 200 }
): Promise<Row[]> => {
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
}
} }

View File

@ -1,4 +1,4 @@
import { Table } from "@budibase/types" import { SaveTableRequest, SaveTableResponse, Table } from "@budibase/types"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
@ -7,6 +7,19 @@ export class TableAPI extends TestAPI {
super(config) super(config)
} }
create = async (
data: SaveTableRequest,
{ expectStatus } = { expectStatus: 200 }
): Promise<SaveTableResponse> => {
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 ( fetch = async (
{ expectStatus } = { expectStatus: 200 } { expectStatus } = { expectStatus: 200 }
): Promise<Table[]> => { ): Promise<Table[]> => {

View File

@ -23,8 +23,8 @@ export class ViewV2API extends TestAPI {
if (!tableId && !this.config.table) { if (!tableId && !this.config.table) {
throw "Test requires table to be configured." throw "Test requires table to be configured."
} }
const table = this.config.table
tableId = table!._id! tableId = tableId || this.config.table!._id!
const view = { const view = {
tableId, tableId,
name: generator.guid(), name: generator.guid(),

View File

@ -26,7 +26,7 @@
}, },
"dependencies": { "dependencies": {
"@budibase/handlebars-helpers": "^0.11.9", "@budibase/handlebars-helpers": "^0.11.9",
"dayjs": "^1.10.4", "dayjs": "^1.10.8",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"handlebars-utils": "^1.0.6", "handlebars-utils": "^1.0.6",
"lodash": "^4.17.20", "lodash": "^4.17.20",

View File

@ -1,5 +1,7 @@
import { Row } from "../../../documents/app/row" import { Row } from "../../../documents/app/row"
export interface GetRowResponse extends Row {}
export interface DeleteRows { export interface DeleteRows {
rows: (Row | string)[] rows: (Row | string)[]
} }
@ -9,3 +11,8 @@ export interface DeleteRow {
} }
export type DeleteRowRequest = DeleteRows | DeleteRow export type DeleteRowRequest = DeleteRows | DeleteRow
export interface ValidateResponse {
valid: boolean
errors: Record<string, any>
}

View File

@ -1,5 +1,6 @@
import { SearchParams } from "../../../sdk" import { SearchFilters, SearchParams } from "../../../sdk"
import { Row } from "../../../documents" import { Row } from "../../../documents"
import { ReadStream } from "fs"
export interface SaveRowRequest extends Row {} export interface SaveRowRequest extends Row {}
@ -28,3 +29,11 @@ export interface SearchViewRowRequest
export interface SearchRowResponse { export interface SearchRowResponse {
rows: any[] rows: any[]
} }
export interface ExportRowsRequest {
rows: string[]
columns?: string[]
query?: SearchFilters
}
export type ExportRowsResponse = ReadStream

View File

@ -10,6 +10,8 @@ import {
import { TestConfiguration } from "../../../../tests" import { TestConfiguration } from "../../../../tests"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
// this test can 409 - retries reduce issues with this
jest.retryTimes(2)
jest.setTimeout(30000) jest.setTimeout(30000)
mocks.licenses.useScimIntegration() mocks.licenses.useScimIntegration()

View File

@ -6269,14 +6269,6 @@
"@types/tedious" "*" "@types/tedious" "*"
tarn "^3.0.1" 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": "@types/node-fetch@2.6.4":
version "2.6.4" version "2.6.4"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
@ -6298,11 +6290,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== 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": "@types/node@16.9.1":
version "16.9.1" version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
@ -9792,11 +9779,16 @@ dateformat@^4.5.1, dateformat@^4.6.3:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
dayjs@^1.10.4, dayjs@^1.10.5, dayjs@^1.11.2, dayjs@^1.11.7: dayjs@^1.10.4, dayjs@^1.10.5:
version "1.11.7" version "1.11.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
dayjs@^1.10.8:
version "1.11.9"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==
dd-trace@3.13.2: dd-trace@3.13.2:
version "3.13.2" version "3.13.2"
resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd" resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd"