Merge branch 'develop' into pipeline/fail-if-changes-in-master

This commit is contained in:
Martin McKeaveney 2023-07-31 11:41:15 +01:00 committed by GitHub
commit d7fb75d10c
60 changed files with 1568 additions and 2302 deletions

View File

@ -1,2 +1,3 @@
nodejs 14.21.3 nodejs 14.21.3
python 3.10.0 python 3.10.0
yarn 1.22.19

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.28-alpha.1", "version": "2.8.29-alpha.4",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -51,9 +51,9 @@
"kill-builder": "kill-port 3000", "kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002", "kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server", "kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && yarn nx run-many --target=dev:builder", "dev": "yarn run kill-all && lerna run --stream --parallel dev:builder",
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && yarn nx run-many --target=dev:builder --exclude=@budibase/backend-core,@budibase/server,@budibase/worker", "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && yarn nx run-many --target=dev:builder --projects=@budibase/worker,@budibase/server", "dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", "dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream", "test": "lerna run --stream test --stream",

View File

@ -1,8 +1,6 @@
import { Config } from "@jest/types" import { Config } from "@jest/types"
const preset = require("ts-jest/jest-preset")
const baseConfig: Config.InitialProjectOptions = { const baseConfig: Config.InitialProjectOptions = {
...preset,
preset: "@trendyol/jest-testcontainers", preset: "@trendyol/jest-testcontainers",
setupFiles: ["./tests/jestEnv.ts"], setupFiles: ["./tests/jestEnv.ts"],
setupFilesAfterEnv: ["./tests/jestSetup.ts"], setupFilesAfterEnv: ["./tests/jestSetup.ts"],

View File

@ -23,7 +23,6 @@
"@budibase/nano": "10.1.2", "@budibase/nano": "10.1.2",
"@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0", "aws-cloudfront-sign": "2.2.0",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
@ -58,12 +57,13 @@
"uuid": "8.3.2" "uuid": "8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/test-sequencer": "29.5.0", "@jest/test-sequencer": "29.6.2",
"@swc/core": "^1.3.25", "@shopify/jest-koa-mocks": "5.1.1",
"@swc/jest": "^0.2.24", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3", "@types/chance": "1.1.3",
"@types/jest": "29.5.0", "@types/jest": "29.5.3",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.180",
"@types/node": "14.18.20", "@types/node": "14.18.20",
@ -75,15 +75,14 @@
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"chance": "1.1.8", "chance": "1.1.8",
"ioredis-mock": "8.7.0", "ioredis-mock": "8.7.0",
"jest": "29.5.0", "jest": "29.6.2",
"jest-environment-node": "29.5.0", "jest-environment-node": "29.6.2",
"jest-serial-runner": "^1.2.1", "jest-serial-runner": "1.2.1",
"koa": "2.13.4", "koa": "2.13.4",
"nodemon": "2.0.16", "nodemon": "2.0.16",
"pino-pretty": "10.0.0", "pino-pretty": "10.0.0",
"pouchdb-adapter-memory": "7.2.2", "pouchdb-adapter-memory": "7.2.2",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-jest": "29.0.5",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3" "typescript": "4.7.3"

View File

@ -8,6 +8,6 @@ then
jest --coverage --runInBand --forceExit jest --coverage --runInBand --forceExit
else else
# --maxWorkers performs better in development # --maxWorkers performs better in development
echo "jest --coverage --forceExit" echo "jest --coverage --detectOpenHandles"
jest --coverage --forceExit jest --coverage --detectOpenHandles
fi fi

View File

@ -1,5 +1,5 @@
const { flatten } = require("lodash") import flatten from "lodash/flatten"
const { cloneDeep } = require("lodash/fp") import cloneDeep from "lodash/fp/cloneDeep"
export type RoleHierarchy = { export type RoleHierarchy = {
permissionId: string permissionId: string

View File

@ -3,7 +3,7 @@ import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
import { getAppDB } from "../context" import { getAppDB } from "../context"
import { doWithDB } from "../db" import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types" import { Screen, Role as RoleDoc } from "@budibase/types"
const { cloneDeep } = require("lodash/fp") import cloneDeep from "lodash/fp/cloneDeep"
export const BUILTIN_ROLE_IDS = { export const BUILTIN_ROLE_IDS = {
ADMIN: "ADMIN", ADMIN: "ADMIN",

View File

@ -1,4 +1,4 @@
import { cloneDeep } from "lodash" import cloneDeep from "lodash/cloneDeep"
import * as permissions from "../permissions" import * as permissions from "../permissions"
import { BUILTIN_ROLE_IDS } from "../roles" import { BUILTIN_ROLE_IDS } from "../roles"

View File

@ -1,5 +1,5 @@
import { Feature, License, Quotas } from "@budibase/types" import { Feature, License, Quotas } from "@budibase/types"
import _ from "lodash" import cloneDeep from "lodash/cloneDeep"
let CLOUD_FREE_LICENSE: License let CLOUD_FREE_LICENSE: License
let UNLIMITED_LICENSE: License let UNLIMITED_LICENSE: License
@ -58,7 +58,7 @@ export const useCloudFree = () => {
// FEATURES // FEATURES
const useFeature = (feature: Feature) => { const useFeature = (feature: Feature) => {
const license = _.cloneDeep(UNLIMITED_LICENSE) const license = cloneDeep(UNLIMITED_LICENSE)
const opts: UseLicenseOpts = { const opts: UseLicenseOpts = {
features: [feature], features: [feature],
} }
@ -97,7 +97,7 @@ export const useSyncAutomations = () => {
// QUOTAS // QUOTAS
export const setAutomationLogsQuota = (value: number) => { export const setAutomationLogsQuota = (value: number) => {
const license = _.cloneDeep(UNLIMITED_LICENSE) const license = cloneDeep(UNLIMITED_LICENSE)
license.quotas.constant.automationLogRetentionDays.value = value license.quotas.constant.automationLogRetentionDays.value = value
return useLicense(license) return useLicense(license)
} }

View File

@ -11,7 +11,7 @@ import {
CreateAccount, CreateAccount,
CreatePassswordAccount, CreatePassswordAccount,
} from "@budibase/types" } from "@budibase/types"
import _ from "lodash" import sample from "lodash/sample"
export const account = (partial: Partial<Account> = {}): Account => { export const account = (partial: Partial<Account> = {}): Account => {
return { return {
@ -46,13 +46,11 @@ export const cloudAccount = (): CloudAccount => {
} }
function providerType(): AccountSSOProviderType { function providerType(): AccountSSOProviderType {
return _.sample( return sample(Object.values(AccountSSOProviderType)) as AccountSSOProviderType
Object.values(AccountSSOProviderType)
) as AccountSSOProviderType
} }
function provider(): AccountSSOProvider { function provider(): AccountSSOProvider {
return _.sample(Object.values(AccountSSOProvider)) as AccountSSOProvider return sample(Object.values(AccountSSOProvider)) as AccountSSOProvider
} }
export function ssoAccount(account: Account = cloudAccount()): SSOAccount { export function ssoAccount(account: Account = cloudAccount()): SSOAccount {

View File

@ -1,7 +1,6 @@
import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types" import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types"
import { uuid } from "./common" import { uuid } from "./common"
import { generator } from "./generator" import { generator } from "./generator"
import _ from "lodash"
interface CreateUserRequestFields { interface CreateUserRequestFields {
externalId: string externalId: string
@ -20,10 +19,10 @@ export function createUserRequest(userData?: Partial<CreateUserRequestFields>) {
username: generator.name(), username: generator.name(),
} }
const { externalId, email, firstName, lastName, username } = _.assign( const { externalId, email, firstName, lastName, username } = {
defaultValues, ...defaultValues,
userData ...userData,
) }
let user: ScimCreateUserRequest = { let user: ScimCreateUserRequest = {
schemas: [ schemas: [

View File

@ -15,7 +15,7 @@ import { generator } from "./generator"
import { email, uuid } from "./common" import { email, uuid } from "./common"
import * as shared from "./shared" import * as shared from "./shared"
import { user } from "./shared" import { user } from "./shared"
import _ from "lodash" import sample from "lodash/sample"
export function OAuth(): OAuth2 { export function OAuth(): OAuth2 {
return { return {
@ -47,7 +47,7 @@ export function authDetails(userDoc?: User): SSOAuthDetails {
} }
export function providerType(): SSOProviderType { export function providerType(): SSOProviderType {
return _.sample(Object.values(SSOProviderType)) as SSOProviderType return sample(Object.values(SSOProviderType)) as SSOProviderType
} }
export function ssoProfile(user?: User): SSOProfile { export function ssoProfile(user?: User): SSOProfile {

View File

@ -101,14 +101,14 @@
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.5", "@roxi/routify": "2.18.5",
"@sveltejs/vite-plugin-svelte": "1.0.1", "@sveltejs/vite-plugin-svelte": "1.0.1",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "5.17.0",
"@testing-library/svelte": "^3.2.2", "@testing-library/svelte": "^3.2.2",
"babel-jest": "^26.6.3", "babel-jest": "29.6.2",
"cypress": "^9.3.1", "cypress": "^9.3.1",
"cypress-multi-reporters": "^1.6.0", "cypress-multi-reporters": "^1.6.0",
"cypress-terminal-report": "^1.4.1", "cypress-terminal-report": "^1.4.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3", "jest": "29.6.2",
"jsdom": "^21.1.1", "jsdom": "^21.1.1",
"mochawesome": "^7.1.3", "mochawesome": "^7.1.3",
"mochawesome-merge": "^4.2.1", "mochawesome-merge": "^4.2.1",

View File

@ -75,6 +75,14 @@
{ {
"name": "Chart", "name": "Chart",
"icon": "GraphBarVertical", "icon": "GraphBarVertical",
"children": ["bar", "line", "area", "candlestick", "pie", "donut"] "children": [
"bar",
"line",
"area",
"candlestick",
"pie",
"donut",
"histogram"
]
} }
] ]

View File

@ -1,5 +1,5 @@
<script> <script>
import { Layout, Body, Button } from "@budibase/bbui" import { Layout, Heading, Body, Button } from "@budibase/bbui"
import { downloadStream } from "@budibase/frontend-core" import { downloadStream } from "@budibase/frontend-core"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
@ -18,6 +18,7 @@
</script> </script>
<Layout noPadding> <Layout noPadding>
<Heading>System logs</Heading>
<Body>Download your latest logs to share with the Budibase team</Body> <Body>Download your latest logs to share with the Budibase team</Body>
<div class="download-button"> <div class="download-button">
<Button cta on:click={download} disabled={loading}> <Button cta on:click={download} disabled={loading}>
@ -25,7 +26,7 @@
{#if loading} {#if loading}
<Spinner size="10" /> <Spinner size="10" />
{/if} {/if}
Download system logs Download
</div> </div>
</Button> </Button>
</div> </div>

View File

@ -53,9 +53,9 @@
"yaml": "^2.1.1" "yaml": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.3.25", "@swc/core": "1.3.71",
"@swc/jest": "^0.2.24", "@swc/jest": "0.2.27",
"@types/jest": "^29.4.0", "@types/jest": "29.5.3",
"@types/node-fetch": "2.6.1", "@types/node-fetch": "2.6.1",
"@types/pouchdb": "^6.4.0", "@types/pouchdb": "^6.4.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",

View File

@ -2212,6 +2212,147 @@
} }
] ]
}, },
"histogram": {
"name": "Histogram Chart",
"description": "Histogram chart",
"icon": "Histogram",
"size": {
"width": 600,
"height": 400
},
"requiredAncestors": ["dataprovider"],
"settings": [
{
"type": "text",
"label": "Title",
"key": "title"
},
{
"type": "dataProvider",
"label": "Provider",
"key": "dataProvider",
"required": true
},
{
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "text",
"label": "Y axis label",
"key": "yAxisLabel",
"defaultValue": "Frequency"
},
{
"type": "text",
"label": "X axis label",
"key": "xAxisLabel"
},
{
"type": "number",
"label": "Bucket count",
"key": "bucketCount",
"defaultValue": 10,
"min": 2
},
{
"type": "boolean",
"label": "Data labels",
"key": "dataLabels",
"defaultValue": false
},
{
"type": "text",
"label": "Width",
"key": "width"
},
{
"type": "text",
"label": "Height",
"key": "height",
"defaultValue": "400"
},
{
"type": "select",
"label": "Colors",
"key": "palette",
"defaultValue": "Palette 1",
"options": [
"Custom",
"Palette 1",
"Palette 2",
"Palette 3",
"Palette 4",
"Palette 5",
"Palette 6",
"Palette 7",
"Palette 8",
"Palette 9",
"Palette 10"
]
},
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "boolean",
"label": "Animate",
"key": "animate",
"defaultValue": true
},
{
"type": "boolean",
"label": "Horizontal",
"key": "horizontal",
"defaultValue": false
}
]
},
"form": { "form": {
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
@ -3965,6 +4106,10 @@
"label": "Bar", "label": "Bar",
"value": "bar" "value": "bar"
}, },
{
"label": "Histogram",
"value": "histogram"
},
{ {
"label": "Line", "label": "Line",
"value": "line" "value": "line"
@ -4215,6 +4360,47 @@
} }
] ]
}, },
{
"section": true,
"name": "Histogram Chart",
"icon": "Histogram",
"dependsOn": {
"setting": "chartType",
"value": "histogram"
},
"settings": [
{
"type": "field",
"label": "Value column",
"key": "valueColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "text",
"label": "Y axis label",
"key": "yAxisLabel"
},
{
"type": "text",
"label": "X axis label",
"key": "xAxisLabel"
},
{
"type": "boolean",
"label": "Horizontal",
"key": "horizontal",
"defaultValue": false
},
{
"type": "number",
"label": "Bucket count",
"key": "bucketCount",
"defaultValue": 10,
"min": 2
}
]
},
{ {
"section": true, "section": true,
"name": "Line Chart", "name": "Line Chart",

View File

@ -46,6 +46,9 @@
export let lowColumn export let lowColumn
export let dateColumn export let dateColumn
// Histogram
export let bucketCount
let dataProviderId let dataProviderId
$: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null $: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null
@ -92,6 +95,7 @@
highColumn, highColumn,
lowColumn, lowColumn,
dateColumn, dateColumn,
bucketCount,
}} }}
/> />
{/if} {/if}

View File

@ -0,0 +1,136 @@
<script>
import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte"
export let title
export let dataProvider
export let valueColumn
export let xAxisLabel
export let yAxisLabel
export let height
export let width
export let dataLabels
export let animate
export let palette
export let c1, c2, c3, c4, c5
export let horizontal
export let bucketCount = 10
$: options = setUpChart(
title,
dataProvider,
valueColumn,
xAxisLabel || valueColumn,
yAxisLabel,
height,
width,
dataLabels,
animate,
palette,
horizontal,
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
customColor,
bucketCount
)
$: customColor = palette === "Custom"
const setUpChart = (
title,
dataProvider,
valueColumn,
xAxisLabel, //freqAxisLabel
yAxisLabel, //valueAxisLabel
height,
width,
dataLabels,
animate,
palette,
horizontal,
colors,
customColor,
bucketCount
) => {
const allCols = [valueColumn]
if (
!dataProvider ||
!dataProvider.rows?.length ||
allCols.find(x => x == null)
) {
return null
}
// Fetch data
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
if (!schema || !data.length) {
return null
}
// Initialise default chart
let builder = new ApexOptionsBuilder()
.type("bar")
.title(title)
.width(width)
.height(height)
.xLabel(horizontal ? yAxisLabel : xAxisLabel)
.yLabel(horizontal ? xAxisLabel : yAxisLabel)
.dataLabels(dataLabels)
.animate(animate)
.palette(palette)
.horizontal(horizontal)
.colors(customColor ? colors : null)
if (horizontal) {
builder = builder.setOption(["plotOptions", "bar", "barHeight"], "90%")
} else {
builder = builder.setOption(["plotOptions", "bar", "columnWidth"], "99%")
}
// Pull occurences of the value.
let flatlist = data.map(row => {
return row[valueColumn]
})
// Build range buckets
let interval = Math.max(...flatlist) / bucketCount
let counts = Array(bucketCount).fill(0)
// Assign row data to a bucket
let buckets = flatlist.reduce((acc, val) => {
let dest = Math.min(Math.floor(val / interval), bucketCount - 1)
acc[dest] = acc[dest] + 1
return acc
}, counts)
const rangeLabel = bucketIdx => {
return `${Math.floor(interval * bucketIdx)} - ${Math.floor(
interval * (bucketIdx + 1)
)}`
}
const series = [
{
name: yAxisLabel,
data: Array.from({ length: buckets.length }, (_, i) => ({
x: rangeLabel(i),
y: buckets[i],
})),
},
]
builder = builder.setOption(["xaxis", "labels"], {
formatter: x => {
return x + ""
},
})
builder = builder.series(series)
return builder.getOptions()
}
</script>
<ApexChart {options} />

View File

@ -4,3 +4,4 @@ export { default as pie } from "./PieChart.svelte"
export { default as donut } from "./DonutChart.svelte" export { default as donut } from "./DonutChart.svelte"
export { default as area } from "./AreaChart.svelte" export { default as area } from "./AreaChart.svelte"
export { default as candlestick } from "./CandleStickChart.svelte" export { default as candlestick } from "./CandleStickChart.svelte"
export { default as histogram } from "./HistogramChart.svelte"

@ -1 +1 @@
Subproject commit a60183319f410d05aaa1c2f2718b772978b54d64 Subproject commit 63fa1b15f6e2afa8a264d597157fd798c9ce031c

View File

@ -2,10 +2,8 @@ import { Config } from "@jest/types"
import * as fs from "fs" import * as fs from "fs"
import { join } from "path" import { join } from "path"
const preset = require("ts-jest/jest-preset")
const baseConfig: Config.InitialProjectOptions = { const baseConfig: Config.InitialProjectOptions = {
...preset,
preset: "@trendyol/jest-testcontainers", preset: "@trendyol/jest-testcontainers",
setupFiles: ["./src/tests/jestEnv.ts"], setupFiles: ["./src/tests/jestEnv.ts"],
setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],

View File

@ -131,15 +131,15 @@
"@babel/core": "7.17.4", "@babel/core": "7.17.4",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@jest/test-sequencer": "29.5.0", "@jest/test-sequencer": "29.6.2",
"@swc/core": "^1.3.25", "@swc/core": "1.3.71",
"@swc/jest": "^0.2.24", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "2.1.1",
"@types/apidoc": "0.50.0", "@types/apidoc": "0.50.0",
"@types/bson": "4.2.0", "@types/bson": "4.2.0",
"@types/global-agent": "2.1.1", "@types/global-agent": "2.1.1",
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.0", "@types/jest": "29.5.3",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.180",
@ -155,15 +155,15 @@
"@types/tar": "6.1.5", "@types/tar": "6.1.5",
"@typescript-eslint/parser": "5.45.0", "@typescript-eslint/parser": "5.45.0",
"apidoc": "0.50.4", "apidoc": "0.50.4",
"babel-jest": "29.5.0", "babel-jest": "29.6.2",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"docker-compose": "0.23.17", "docker-compose": "0.23.17",
"eslint": "6.8.0", "eslint": "6.8.0",
"is-wsl": "2.2.0", "is-wsl": "2.2.0",
"jest": "29.5.0", "jest": "29.6.2",
"jest-openapi": "0.14.2", "jest-openapi": "0.14.2",
"jest-runner": "29.5.0", "jest-runner": "29.6.2",
"jest-serial-runner": "^1.2.1", "jest-serial-runner": "1.2.1",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"openapi-types": "9.3.1", "openapi-types": "9.3.1",
"openapi-typescript": "5.2.0", "openapi-typescript": "5.2.0",
@ -172,7 +172,6 @@
"supertest": "6.2.2", "supertest": "6.2.2",
"swagger-jsdoc": "6.1.0", "swagger-jsdoc": "6.1.0",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-jest": "29.0.5",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "4.7.3",

View File

@ -8,26 +8,13 @@ import {
Datasource, Datasource,
IncludeRelationship, IncludeRelationship,
Operation, Operation,
PatchRowRequest,
PatchRowResponse,
Row, Row,
Table, Table,
UserCtx, UserCtx,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "./utils"
async function getRow(
tableId: string,
rowId: string,
opts?: { relationships?: boolean }
) {
const response = (await handleRequest(Operation.READ, tableId, {
id: breakRowIdField(rowId),
includeSqlRelationships: opts?.relationships
? IncludeRelationship.INCLUDE
: IncludeRelationship.EXCLUDE,
})) as Row[]
return response ? response[0] : response
}
export async function handleRequest( export async function handleRequest(
operation: Operation, operation: Operation,
@ -55,14 +42,12 @@ export async function handleRequest(
) )
} }
export async function patch(ctx: UserCtx) { export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const inputs = ctx.request.body
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
const id = inputs._id const { id, ...rowData } = ctx.request.body
// don't save the ID to db
delete inputs._id const validateResult = await sdk.rows.utils.validate({
const validateResult = await utils.validate({ row: rowData,
row: inputs,
tableId, tableId,
}) })
if (!validateResult.valid) { if (!validateResult.valid) {
@ -70,9 +55,11 @@ export async function patch(ctx: UserCtx) {
} }
const response = await handleRequest(Operation.UPDATE, tableId, { const response = await handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(id), id: breakRowIdField(id),
row: inputs, row: rowData,
})
const row = await sdk.rows.external.getRow(tableId, id, {
relationships: true,
}) })
const row = await getRow(tableId, id, { relationships: true })
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
return { return {
...response, ...response,
@ -84,7 +71,7 @@ export async function patch(ctx: UserCtx) {
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx) {
const inputs = ctx.request.body const inputs = ctx.request.body
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row: inputs, row: inputs,
tableId, tableId,
}) })
@ -97,7 +84,9 @@ export async function save(ctx: UserCtx) {
const responseRow = response as { row: Row } const responseRow = response as { row: Row }
const rowId = responseRow.row._id const rowId = responseRow.row._id
if (rowId) { if (rowId) {
const row = await getRow(tableId, rowId, { relationships: true }) const row = await sdk.rows.external.getRow(tableId, rowId, {
relationships: true,
})
return { return {
...response, ...response,
row, row,
@ -110,7 +99,7 @@ export async function save(ctx: UserCtx) {
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx) {
const id = ctx.params.rowId const id = ctx.params.rowId
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
return getRow(tableId, id) return sdk.rows.external.getRow(tableId, id)
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx) {

View File

@ -9,6 +9,8 @@ import {
DeleteRow, DeleteRow,
DeleteRows, DeleteRows,
Row, Row,
PatchRowRequest,
PatchRowResponse,
SearchResponse, SearchResponse,
SortOrder, SortOrder,
SortType, SortType,
@ -29,7 +31,9 @@ function pickApi(tableId: any) {
return internal return internal
} }
export async function patch(ctx: any): Promise<any> { export async function patch(
ctx: UserCtx<PatchRowRequest, PatchRowResponse>
): Promise<any> {
const appId = ctx.appId const appId = ctx.appId
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
@ -38,7 +42,7 @@ export async function patch(ctx: any): Promise<any> {
return save(ctx) return save(ctx)
} }
try { try {
const { row, table } = await quotas.addQuery<any>( const { row, table } = await quotas.addQuery(
() => pickApi(tableId).patch(ctx), () => pickApi(tableId).patch(ctx),
{ {
datasourceId: tableId, datasourceId: tableId,
@ -53,7 +57,7 @@ export async function patch(ctx: any): Promise<any> {
ctx.message = `${table.name} updated successfully.` ctx.message = `${table.name} updated successfully.`
ctx.body = row ctx.body = row
gridSocket?.emitRowUpdate(ctx, row) gridSocket?.emitRowUpdate(ctx, row)
} catch (err) { } catch (err: any) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
@ -78,6 +82,7 @@ export const save = async (ctx: any) => {
ctx.body = row || squashed ctx.body = row || squashed
gridSocket?.emitRowUpdate(ctx, row || squashed) gridSocket?.emitRowUpdate(ctx, row || squashed)
} }
export async function fetchView(ctx: any) { export async function fetchView(ctx: any) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const viewName = decodeURIComponent(ctx.params.viewName) const viewName = decodeURIComponent(ctx.params.viewName)
@ -267,7 +272,7 @@ export async function searchView(ctx: Ctx<void, SearchResponse>) {
undefined undefined
ctx.status = 200 ctx.status = 200
ctx.body = await quotas.addQuery( const result = await quotas.addQuery(
() => () =>
sdk.rows.search({ sdk.rows.search({
tableId: view.tableId, tableId: view.tableId,
@ -279,6 +284,9 @@ export async function searchView(ctx: Ctx<void, SearchResponse>) {
datasourceId: view.tableId, datasourceId: view.tableId,
} }
) )
result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result
} }
export async function validate(ctx: Ctx) { export async function validate(ctx: Ctx) {
@ -287,7 +295,7 @@ export async function validate(ctx: Ctx) {
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
ctx.body = { valid: true } ctx.body = { valid: true }
} else { } else {
ctx.body = await utils.validate({ ctx.body = await sdk.rows.utils.validate({
row: ctx.request.body, row: ctx.request.body,
tableId, tableId,
}) })

View File

@ -15,19 +15,26 @@ import * as utils from "./utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { context, db as dbCore } from "@budibase/backend-core" import { context, db as dbCore } from "@budibase/backend-core"
import { finaliseRow, updateRelatedFormula } from "./staticFormula" import { finaliseRow, updateRelatedFormula } from "./staticFormula"
import { UserCtx, LinkDocumentValue, Row, Table } from "@budibase/types" import {
UserCtx,
LinkDocumentValue,
Row,
Table,
PatchRowRequest,
PatchRowResponse,
} from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
export async function patch(ctx: UserCtx) { export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const inputs = ctx.request.body const inputs = ctx.request.body
const tableId = inputs.tableId const tableId = inputs.tableId
const isUserTable = tableId === InternalTables.USER_METADATA const isUserTable = tableId === InternalTables.USER_METADATA
let oldRow let oldRow
const dbTable = await sdk.tables.getTable(tableId)
try { try {
let dbTable = await sdk.tables.getTable(tableId)
oldRow = await outputProcessing( oldRow = await outputProcessing(
dbTable, dbTable,
await utils.findRow(ctx, tableId, inputs._id) await utils.findRow(ctx, tableId, inputs._id!)
) )
} catch (err) { } catch (err) {
if (isUserTable) { if (isUserTable) {
@ -40,7 +47,7 @@ export async function patch(ctx: UserCtx) {
throw "Row does not exist" throw "Row does not exist"
} }
} }
let dbTable = await sdk.tables.getTable(tableId)
// need to build up full patch fields before coerce // need to build up full patch fields before coerce
let combinedRow: any = cloneDeep(oldRow) let combinedRow: any = cloneDeep(oldRow)
for (let key of Object.keys(inputs)) { for (let key of Object.keys(inputs)) {
@ -53,7 +60,7 @@ export async function patch(ctx: UserCtx) {
// this returns the table and row incase they have been updated // this returns the table and row incase they have been updated
let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow) let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, table,
}) })
@ -74,7 +81,7 @@ export async function patch(ctx: UserCtx) {
if (isUserTable) { if (isUserTable) {
// the row has been updated, need to put it into the ctx // the row has been updated, need to put it into the ctx
ctx.request.body = row ctx.request.body = row as any
await userController.updateMetadata(ctx) await userController.updateMetadata(ctx)
return { row: ctx.body as Row, table } return { row: ctx.body as Row, table }
} }
@ -102,7 +109,7 @@ export async function save(ctx: UserCtx) {
let { table, row } = inputProcessing(ctx.user, tableClone, inputs) let { table, row } = inputProcessing(ctx.user, tableClone, inputs)
const validateResult = await utils.validate({ const validateResult = await sdk.rows.utils.validate({
row, row,
table, table,
}) })

View File

@ -9,7 +9,7 @@ import { context } from "@budibase/backend-core"
import { Table, Row } from "@budibase/types" import { Table, Row } from "@budibase/types"
import * as linkRows from "../../../db/linkedRows" import * as linkRows from "../../../db/linkedRows"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { isEqual } from "lodash" import isEqual from "lodash/isEqual"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
/** /**

View File

@ -2,7 +2,8 @@ import { FieldTypes, FormulaTypes } from "../../../constants"
import { clearColumns } from "./utils" import { clearColumns } from "./utils"
import { doesContainStrings } from "@budibase/string-templates" import { doesContainStrings } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { isEqual, uniq } from "lodash" import isEqual from "lodash/isEqual"
import uniq from "lodash/uniq"
import { updateAllFormulasInTable } from "../row/staticFormula" import { updateAllFormulasInTable } from "../row/staticFormula"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { FieldSchema, Table } from "@budibase/types" import { FieldSchema, Table } from "@budibase/types"

View File

@ -11,7 +11,7 @@ import {
import { runStaticFormulaChecks } from "./bulkFormula" import { runStaticFormulaChecks } from "./bulkFormula"
import { Table } from "@budibase/types" import { Table } from "@budibase/types"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { isEqual } from "lodash" import isEqual from "lodash/isEqual"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import sdk from "../../../sdk" import sdk from "../../../sdk"

View File

@ -1,6 +1,6 @@
import { parse, isSchema, isRows } from "../../../utilities/schema" import { parse, isSchema, isRows } from "../../../utilities/schema"
import { getRowParams, generateRowID, InternalTables } from "../../../db/utils" import { getRowParams, generateRowID, InternalTables } from "../../../db/utils"
import { isEqual } from "lodash" import isEqual from "lodash/isEqual"
import { import {
AutoFieldSubTypes, AutoFieldSubTypes,
FieldTypes, FieldTypes,

View File

@ -1,6 +1,5 @@
import { generateUserMetadataID, generateUserFlagID } from "../../db/utils" import { generateUserFlagID } from "../../db/utils"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { getGlobalUsers } from "../../utilities/global"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { Ctx, UserCtx } from "@budibase/types" import { Ctx, UserCtx } from "@budibase/types"

View File

@ -17,7 +17,8 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
const { cloneDeep, isEqual } = require("lodash") const cloneDeep = require("lodash/cloneDeep")
import isEqual from "lodash/isEqual"
export async function fetch(ctx: Ctx) { export async function fetch(ctx: Ctx) {
ctx.body = await getViews() ctx.body = await getViews()

View File

@ -5,7 +5,7 @@ tk.freeze(timestamp)
import { outputProcessing } from "../../../utilities/rowProcessor" import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities" import * as setup from "./utilities"
const { basicRow } = setup.structures const { basicRow } = setup.structures
import { context, db, tenancy } from "@budibase/backend-core" import { context, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
QuotaUsageType, QuotaUsageType,
@ -16,6 +16,7 @@ import {
FieldType, FieldType,
SortType, SortType,
SortOrder, SortOrder,
PatchRowRequest,
} from "@budibase/types" } from "@budibase/types"
import { import {
expectAnyInternalColsAttributes, expectAnyInternalColsAttributes,
@ -399,17 +400,12 @@ describe("/rows", () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
const res = await request const res = await config.api.row.patch(table._id!, {
.patch(`/api/${table._id}/rows`) _id: existing._id!,
.send({ _rev: existing._rev!,
_id: existing._id, tableId: table._id!,
_rev: existing._rev, name: "Updated Name",
tableId: table._id, })
name: "Updated Name",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect((res as any).res.statusMessage).toEqual( expect((res as any).res.statusMessage).toEqual(
`${table.name} updated successfully.` `${table.name} updated successfully.`
@ -430,16 +426,16 @@ describe("/rows", () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
await request await config.api.row.patch(
.patch(`/api/${table._id}/rows`) table._id!,
.send({ {
_id: existing._id, _id: existing._id!,
_rev: existing._rev, _rev: existing._rev!,
tableId: table._id, tableId: table._id!,
name: 1, name: 1,
}) },
.set(config.defaultHeaders()) { expectStatus: 400 }
.expect(400) )
await assertRowUsage(rowUsage) await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage) await assertQueryUsage(queryUsage)
@ -986,16 +982,17 @@ describe("/rows", () => {
) )
} }
const createViewResponse = await config.api.viewV2.create({ const view = await config.api.viewV2.create({
columns: { name: { visible: true } }, columns: { name: { visible: true } },
}) })
const response = await config.api.viewV2.search(createViewResponse.id) const response = await config.api.viewV2.search(view.id)
expect(response.body.rows).toHaveLength(10) expect(response.body.rows).toHaveLength(10)
expect(response.body.rows).toEqual( expect(response.body.rows).toEqual(
expect.arrayContaining( expect.arrayContaining(
rows.map(r => ({ rows.map(r => ({
...expectAnyInternalColsAttributes, ...expectAnyInternalColsAttributes,
_viewId: view.id,
name: r.name, name: r.name,
})) }))
) )

View File

@ -4,6 +4,7 @@ jest.mock("../../utilities/redis", () => ({
checkTestFlag: () => { checkTestFlag: () => {
return false return false
}, },
shutdown: jest.fn(),
})) }))
jest.spyOn(global.console, "error") jest.spyOn(global.console, "error")

View File

@ -1,4 +1,4 @@
import { merge } from "lodash" import merge from "lodash/merge"
import env from "../environment" import env from "../environment"
export const AWS_REGION = env.AWS_REGION ? env.AWS_REGION : "eu-west-1" export const AWS_REGION = env.AWS_REGION ? env.AWS_REGION : "eu-west-1"

View File

@ -8,10 +8,10 @@ import {
getLinkedTableIDs, getLinkedTableIDs,
getLinkedTable, getLinkedTable,
} from "./linkUtils" } from "./linkUtils"
import { flatten } from "lodash" import flatten from "lodash/flatten"
import { FieldTypes } from "../../constants" import { FieldTypes } from "../../constants"
import { getMultiIDParams, USER_METDATA_PREFIX } from "../utils" import { getMultiIDParams, USER_METDATA_PREFIX } from "../utils"
import { partition } from "lodash" import partition from "lodash/partition"
import { getGlobalUsersFromMetadata } from "../../utilities/global" import { getGlobalUsersFromMetadata } from "../../utilities/global"
import { processFormulas } from "../../utilities/rowProcessor" import { processFormulas } from "../../utilities/rowProcessor"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"

View File

@ -17,7 +17,7 @@ import oracle from "./oracle"
import { SourceName, Integration, PluginType } from "@budibase/types" import { SourceName, Integration, PluginType } from "@budibase/types"
import { getDatasourcePlugin } from "../utilities/fileSystem" import { getDatasourcePlugin } from "../utilities/fileSystem"
import env from "../environment" import env from "../environment"
import { cloneDeep } from "lodash" import cloneDeep from "lodash/cloneDeep"
import sdk from "../sdk" import sdk from "../sdk"
const DEFINITIONS: Record<SourceName, Integration | undefined> = { const DEFINITIONS: Record<SourceName, Integration | undefined> = {
@ -54,7 +54,6 @@ const INTEGRATIONS: Record<SourceName, any> = {
[SourceName.FIRESTORE]: firebase.integration, [SourceName.FIRESTORE]: firebase.integration,
[SourceName.GOOGLE_SHEETS]: googlesheets.integration, [SourceName.GOOGLE_SHEETS]: googlesheets.integration,
[SourceName.REDIS]: redis.integration, [SourceName.REDIS]: redis.integration,
[SourceName.FIRESTORE]: firebase.integration,
[SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: undefined, [SourceName.ORACLE]: undefined,
} }

View File

@ -11,7 +11,7 @@ import {
RestBasicAuthConfig, RestBasicAuthConfig,
RestBearerAuthConfig, RestBearerAuthConfig,
} from "@budibase/types" } from "@budibase/types"
import { get } from "lodash" import get from "lodash/get"
import * as https from "https" import * as https from "https"
import qs from "querystring" import qs from "querystring"
import fetch from "node-fetch" import fetch from "node-fetch"

View File

@ -15,7 +15,7 @@ import {
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils" import { getEnvironmentVariables } from "../../utils"
import { getDefinitions, getDefinition } from "../../../integrations" import { getDefinitions, getDefinition } from "../../../integrations"
import _ from "lodash" import merge from "lodash/merge"
import { import {
BudibaseInternalDB, BudibaseInternalDB,
getDatasourceParams, getDatasourceParams,
@ -227,7 +227,7 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
} }
if (old.config?.auth) { if (old.config?.auth) {
update.config = _.merge(old.config, update.config) update.config = merge(old.config, update.config)
} }
// update back to actual passwords for everything else // update back to actual passwords for everything else

View File

@ -0,0 +1,17 @@
import { IncludeRelationship, Operation, Row } from "@budibase/types"
import { handleRequest } from "../../../api/controllers/row/external"
import { breakRowIdField } from "../../../integrations/utils"
export async function getRow(
tableId: string,
rowId: string,
opts?: { relationships?: boolean }
) {
const response = (await handleRequest(Operation.READ, tableId, {
id: breakRowIdField(rowId),
includeSqlRelationships: opts?.relationships
? IncludeRelationship.INCLUDE
: IncludeRelationship.EXCLUDE,
})) as Row[]
return response ? response[0] : response
}

View File

@ -2,10 +2,12 @@ import * as attachments from "./attachments"
import * as rows from "./rows" import * as rows from "./rows"
import * as search from "./search" import * as search from "./search"
import * as utils from "./utils" import * as utils from "./utils"
import * as external from "./external"
export default { export default {
...attachments, ...attachments,
...rows, ...rows,
...search, ...search,
utils: utils, utils,
external,
} }

View File

@ -147,8 +147,8 @@ export async function exportRows(
export async function fetch(tableId: string) { export async function fetch(tableId: string) {
const db = context.getAppDB() const db = context.getAppDB()
let table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
let rows = await getRawTableData(db, tableId) const rows = await getRawTableData(db, tableId)
const result = await outputProcessing(table, rows) const result = await outputProcessing(table, rows)
return result return result
} }
@ -171,7 +171,7 @@ async function getRawTableData(db: Database, tableId: string) {
export async function fetchView( export async function fetchView(
viewName: string, viewName: string,
options: { calculation: string; group: string; field: string } options: { calculation: string; group: string; field: string }
) { ): Promise<Row[]> {
// if this is a table view being looked for just transfer to that // if this is a table view being looked for just transfer to that
if (viewName.startsWith(DocumentType.TABLE)) { if (viewName.startsWith(DocumentType.TABLE)) {
return fetch(viewName) return fetch(viewName)
@ -197,7 +197,7 @@ export async function fetchView(
) )
} }
let rows let rows: Row[] = []
if (!calculation) { if (!calculation) {
response.rows = response.rows.map(row => row.doc) response.rows = response.rows.map(row => row.doc)
let table: Table let table: Table

View File

@ -1,4 +1,6 @@
import { TableSchema } from "@budibase/types" import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js"
import { FieldType, Row, Table, TableSchema } from "@budibase/types"
import { FieldTypes } from "../../../constants" import { FieldTypes } from "../../../constants"
import { makeExternalQuery } from "../../../integrations/base/query" import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
@ -46,3 +48,90 @@ export function cleanExportRows(
return cleanRows return cleanRows
} }
function isForeignKey(key: string, table: Table) {
const relationships = Object.values(table.schema).filter(
column => column.type === FieldType.LINK
)
return relationships.some(relationship => relationship.foreignKey === key)
}
export async function validate({
tableId,
row,
table,
}: {
tableId?: string
row: Row
table?: Table
}): Promise<{
valid: boolean
errors: Record<string, any>
}> {
let fetchedTable: Table
if (!table) {
fetchedTable = await sdk.tables.getTable(tableId)
} else {
fetchedTable = table
}
const errors: Record<string, any> = {}
for (let fieldName of Object.keys(fetchedTable.schema)) {
const column = fetchedTable.schema[fieldName]
const constraints = cloneDeep(column.constraints)
const type = column.type
// foreign keys are likely to be enriched
if (isForeignKey(fieldName, fetchedTable)) {
continue
}
// formulas shouldn't validated, data will be deleted anyway
if (type === FieldTypes.FORMULA || column.autocolumn) {
continue
}
// special case for options, need to always allow unselected (empty)
if (type === FieldTypes.OPTIONS && constraints?.inclusion) {
constraints.inclusion.push(null as any, "")
}
let res
// Validate.js doesn't seem to handle array
if (type === FieldTypes.ARRAY && row[fieldName]) {
if (row[fieldName].length) {
if (!Array.isArray(row[fieldName])) {
row[fieldName] = row[fieldName].split(",")
}
row[fieldName].map((val: any) => {
if (
!constraints?.inclusion?.includes(val) &&
constraints?.inclusion?.length !== 0
) {
errors[fieldName] = "Field not in list"
}
})
} else if (constraints?.presence && row[fieldName].length === 0) {
// non required MultiSelect creates an empty array, which should not throw errors
errors[fieldName] = [`${fieldName} is required`]
}
} else if (
(type === FieldTypes.ATTACHMENT || type === FieldTypes.JSON) &&
typeof row[fieldName] === "string"
) {
// this should only happen if there is an error
try {
const json = JSON.parse(row[fieldName])
if (type === FieldTypes.ATTACHMENT) {
if (Array.isArray(json)) {
row[fieldName] = json
} else {
errors[fieldName] = [`Must be an array`]
}
}
} catch (err) {
errors[fieldName] = [`Contains invalid JSON`]
}
} else {
res = validateJs.single(row[fieldName], constraints)
}
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}

View File

@ -14,7 +14,6 @@ import {
import datasources from "../datasources" import datasources from "../datasources"
import { populateExternalTableSchemas, isEditableColumn } from "./validation" import { populateExternalTableSchemas, isEditableColumn } from "./validation"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import _ from "lodash"
async function getAllInternalTables(db?: Database): Promise<Table[]> { async function getAllInternalTables(db?: Database): Promise<Table[]> {
if (!db) { if (!db) {

View File

@ -3,7 +3,7 @@ import { TableSchema, UIFieldMetadata, View, ViewV2 } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "../../../db/utils" import * as utils from "../../../db/utils"
import _ from "lodash" import merge from "lodash/merge"
export async function get(viewId: string): Promise<ViewV2 | undefined> { export async function get(viewId: string): Promise<ViewV2 | undefined> {
const { tableId } = utils.extractViewInfoFromID(viewId) const { tableId } = utils.extractViewInfoFromID(viewId)
@ -103,7 +103,7 @@ function enrichViewV2Schema(
delete tableFieldSchema.order delete tableFieldSchema.order
} }
result[columnName] = _.merge(tableFieldSchema, columnUIMetadata) result[columnName] = merge(tableFieldSchema, columnUIMetadata)
} }
return result return result
} }

View File

@ -6,7 +6,7 @@ import {
getUserMetadataParams, getUserMetadataParams,
InternalTables, InternalTables,
} from "../../db/utils" } from "../../db/utils"
import { isEqual } from "lodash" import isEqual from "lodash/isEqual"
import { ContextUser, UserMetadata, User, Database } from "@budibase/types" import { ContextUser, UserMetadata, User, Database } from "@budibase/types"
export function combineMetadataAndUser( export function combineMetadataAndUser(

View File

@ -1,13 +1,16 @@
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { RowAPI } from "./row"
import { TableAPI } from "./table" import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2" import { ViewV2API } from "./viewV2"
export default class API { export default class API {
table: TableAPI table: TableAPI
viewV2: ViewV2API viewV2: ViewV2API
row: RowAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
this.viewV2 = new ViewV2API(config) this.viewV2 = new ViewV2API(config)
this.row = new RowAPI(config)
} }
} }

View File

@ -0,0 +1,22 @@
import { PatchRowRequest } from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class RowAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
patch = async (
tableId: string,
row: PatchRowRequest,
{ expectStatus } = { expectStatus: 200 }
) => {
return this.request
.patch(`/api/${tableId}/rows`)
.send(row)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expectStatus)
}
}

View File

@ -9,7 +9,7 @@ import {
import env from "../environment" import env from "../environment"
import { groups } from "@budibase/pro" import { groups } from "@budibase/pro"
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types" import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
import { cloneDeep } from "lodash" import cloneDeep from "lodash/cloneDeep"
export function updateAppRole( export function updateAppRole(
user: ContextUser, user: ContextUser,

View File

@ -186,18 +186,21 @@ export function inputProcessing(
* @param {object} opts used to set some options for the output, such as disabling relationship squashing. * @param {object} opts used to set some options for the output, such as disabling relationship squashing.
* @returns {object[]|object} the enriched rows will be returned. * @returns {object[]|object} the enriched rows will be returned.
*/ */
export async function outputProcessing( export async function outputProcessing<T extends Row[] | Row>(
table: Table, table: Table,
rows: Row[] | Row, rows: T,
opts = { squash: true } opts = { squash: true }
) { ): Promise<T> {
let safeRows: Row[]
let wasArray = true let wasArray = true
if (!(rows instanceof Array)) { if (!(rows instanceof Array)) {
rows = [rows] safeRows = [rows]
wasArray = false wasArray = false
} else {
safeRows = rows
} }
// attach any linked row information // attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(table, rows as Row[]) let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
// process formulas // process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[] enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
@ -221,7 +224,7 @@ export async function outputProcessing(
enriched enriched
)) as Row[] )) as Row[]
} }
return wasArray ? enriched : enriched[0] return (wasArray ? enriched : enriched[0]) as T
} }
/** /**

View File

@ -36,8 +36,8 @@
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"jest": "^26.6.3", "jest": "29.6.2",
"jest-environment-node": "^26.6.2", "jest-environment-node": "29.6.2",
"marked": "^4.0.10", "marked": "^4.0.10",
"rollup": "^2.36.2", "rollup": "^2.36.2",
"rollup-plugin-inject-process-env": "^1.3.1", "rollup-plugin-inject-process-env": "^1.3.1",

View File

@ -1,3 +1,13 @@
import { Row } from "../../../documents"
export interface PatchRowRequest extends Row {
_id: string
_rev: string
tableId: string
}
export interface PatchRowResponse extends Row {}
export interface SearchResponse { export interface SearchResponse {
rows: any[] rows: any[]
} }

View File

@ -30,5 +30,6 @@ export interface RowAttachment {
export interface Row extends Document { export interface Row extends Document {
type?: string type?: string
tableId?: string tableId?: string
_viewId?: string
[key: string]: any [key: string]: any
} }

View File

@ -1,9 +1,7 @@
import { Config } from "@jest/types" import { Config } from "@jest/types"
import * as fs from "fs" import * as fs from "fs"
const preset = require("ts-jest/jest-preset")
const config: Config.InitialOptions = { const config: Config.InitialOptions = {
...preset,
preset: "@trendyol/jest-testcontainers", preset: "@trendyol/jest-testcontainers",
setupFiles: ["./src/tests/jestEnv.ts"], setupFiles: ["./src/tests/jestEnv.ts"],
setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],

View File

@ -74,10 +74,10 @@
"server-destroy": "1.0.1" "server-destroy": "1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.3.25", "@swc/core": "1.3.71",
"@swc/jest": "^0.2.24", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "2.1.1",
"@types/jest": "28.1.1", "@types/jest": "29.5.3",
"@types/jsonwebtoken": "8.5.1", "@types/jsonwebtoken": "8.5.1",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
@ -91,14 +91,13 @@
"@typescript-eslint/parser": "5.45.0", "@typescript-eslint/parser": "5.45.0",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"eslint": "6.8.0", "eslint": "6.8.0",
"jest": "28.1.1", "jest": "29.6.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"pouchdb-adapter-memory": "7.2.2", "pouchdb-adapter-memory": "7.2.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"supertest": "6.2.2", "supertest": "6.2.2",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-jest": "28.0.4",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "4.7.3",

View File

@ -314,7 +314,7 @@ describe("scim", () => {
const user = await config.getUser(email) const user = await config.getUser(email)
expect(user).toBeDefined() expect(user).toBeDefined()
expect(user.email).toEqual(email) expect(user!.email).toEqual(email)
}) })
it("if multiple emails are provided, the first primary one is used as email", async () => { it("if multiple emails are provided, the first primary one is used as email", async () => {
@ -345,7 +345,7 @@ describe("scim", () => {
const user = await config.getUser(email) const user = await config.getUser(email)
expect(user).toBeDefined() expect(user).toBeDefined()
expect(user.email).toEqual(email) expect(user!.email).toEqual(email)
}) })
it("if no email is provided and the user name is not an email, an exception is thrown", async () => { it("if no email is provided and the user name is not an email, an exception is thrown", async () => {

View File

@ -24,18 +24,18 @@
}, },
"devDependencies": { "devDependencies": {
"@budibase/types": "^2.3.17", "@budibase/types": "^2.3.17",
"@trendyol/jest-testcontainers": "^2.1.1", "@trendyol/jest-testcontainers": "2.1.1",
"@types/jest": "29.0.0", "@types/jest": "29.5.3",
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"chance": "1.1.8", "chance": "1.1.8",
"dotenv": "16.0.1", "dotenv": "16.0.1",
"jest": "29.0.0", "jest": "29.6.2",
"prettier": "2.7.1", "prettier": "2.7.1",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"@swc/core": "^1.3.25", "@swc/core": "1.3.71",
"@swc/jest": "^0.2.24", "@swc/jest": "0.2.27",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-jest": "29.0.0", "ts-jest": "29.1.1",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3" "typescript": "4.7.3"

File diff suppressed because it is too large Load Diff

2241
yarn.lock

File diff suppressed because it is too large Load Diff