Merge branch 'develop' into backmerge-20230919
This commit is contained in:
commit
235799c07d
|
@ -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:
|
||||||
|
|
|
@ -9,4 +9,5 @@ packages/backend-core/coverage
|
||||||
packages/server/client
|
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
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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(
|
||||||
const res = await createRow(primaryPostgresTable._id, newRow)
|
newRows.map(async newRow => {
|
||||||
expect(res.status).toBe(200)
|
const res = await createRow(primaryPostgresTable._id, newRow)
|
||||||
}
|
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()
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]> => {
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue