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
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",
"packages": [
"packages/*"

View File

@ -51,9 +51,9 @@
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && yarn nx run-many --target=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:server": "yarn run kill-server && yarn nx run-many --target=dev:builder --projects=@budibase/worker,@budibase/server",
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder",
"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 && 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: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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,14 +101,14 @@
"@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.5",
"@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",
"babel-jest": "^26.6.3",
"babel-jest": "29.6.2",
"cypress": "^9.3.1",
"cypress-multi-reporters": "^1.6.0",
"cypress-terminal-report": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"jest": "29.6.2",
"jsdom": "^21.1.1",
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.2.1",

View File

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

View File

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

View File

@ -53,9 +53,9 @@
"yaml": "^2.1.1"
},
"devDependencies": {
"@swc/core": "^1.3.25",
"@swc/jest": "^0.2.24",
"@types/jest": "^29.4.0",
"@swc/core": "1.3.71",
"@swc/jest": "0.2.27",
"@types/jest": "29.5.3",
"@types/node-fetch": "2.6.1",
"@types/pouchdb": "^6.4.0",
"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": {
"name": "Form",
"icon": "Form",
@ -3965,6 +4106,10 @@
"label": "Bar",
"value": "bar"
},
{
"label": "Histogram",
"value": "histogram"
},
{
"label": "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,
"name": "Line Chart",

View File

@ -46,6 +46,9 @@
export let lowColumn
export let dateColumn
// Histogram
export let bucketCount
let dataProviderId
$: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null
@ -92,6 +95,7 @@
highColumn,
lowColumn,
dateColumn,
bucketCount,
}}
/>
{/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 area } from "./AreaChart.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 { join } from "path"
const preset = require("ts-jest/jest-preset")
const baseConfig: Config.InitialProjectOptions = {
...preset,
preset: "@trendyol/jest-testcontainers",
setupFiles: ["./src/tests/jestEnv.ts"],
setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],

View File

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

View File

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

View File

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

View File

@ -15,19 +15,26 @@ import * as utils from "./utils"
import { cloneDeep } from "lodash/fp"
import { context, db as dbCore } from "@budibase/backend-core"
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"
export async function patch(ctx: UserCtx) {
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const inputs = ctx.request.body
const tableId = inputs.tableId
const isUserTable = tableId === InternalTables.USER_METADATA
let oldRow
const dbTable = await sdk.tables.getTable(tableId)
try {
let dbTable = await sdk.tables.getTable(tableId)
oldRow = await outputProcessing(
dbTable,
await utils.findRow(ctx, tableId, inputs._id)
await utils.findRow(ctx, tableId, inputs._id!)
)
} catch (err) {
if (isUserTable) {
@ -40,7 +47,7 @@ export async function patch(ctx: UserCtx) {
throw "Row does not exist"
}
}
let dbTable = await sdk.tables.getTable(tableId)
// need to build up full patch fields before coerce
let combinedRow: any = cloneDeep(oldRow)
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
let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
const validateResult = await utils.validate({
const validateResult = await sdk.rows.utils.validate({
row,
table,
})
@ -74,7 +81,7 @@ export async function patch(ctx: UserCtx) {
if (isUserTable) {
// 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)
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)
const validateResult = await utils.validate({
const validateResult = await sdk.rows.utils.validate({
row,
table,
})

View File

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

View File

@ -2,7 +2,8 @@ import { FieldTypes, FormulaTypes } from "../../../constants"
import { clearColumns } from "./utils"
import { doesContainStrings } from "@budibase/string-templates"
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 { context } from "@budibase/backend-core"
import { FieldSchema, Table } from "@budibase/types"

View File

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

View File

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

View File

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

View File

@ -17,7 +17,8 @@ import {
} from "@budibase/types"
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) {
ctx.body = await getViews()

View File

@ -5,7 +5,7 @@ tk.freeze(timestamp)
import { outputProcessing } from "../../../utilities/rowProcessor"
import * as setup from "./utilities"
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 {
QuotaUsageType,
@ -16,6 +16,7 @@ import {
FieldType,
SortType,
SortOrder,
PatchRowRequest,
} from "@budibase/types"
import {
expectAnyInternalColsAttributes,
@ -399,17 +400,12 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request
.patch(`/api/${table._id}/rows`)
.send({
_id: existing._id,
_rev: existing._rev,
tableId: table._id,
name: "Updated Name",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
const res = await config.api.row.patch(table._id!, {
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
name: "Updated Name",
})
expect((res as any).res.statusMessage).toEqual(
`${table.name} updated successfully.`
@ -430,16 +426,16 @@ describe("/rows", () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await request
.patch(`/api/${table._id}/rows`)
.send({
_id: existing._id,
_rev: existing._rev,
tableId: table._id,
await config.api.row.patch(
table._id!,
{
_id: existing._id!,
_rev: existing._rev!,
tableId: table._id!,
name: 1,
})
.set(config.defaultHeaders())
.expect(400)
},
{ expectStatus: 400 }
)
await assertRowUsage(rowUsage)
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 } },
})
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).toEqual(
expect.arrayContaining(
rows.map(r => ({
...expectAnyInternalColsAttributes,
_viewId: view.id,
name: r.name,
}))
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import {
import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils"
import { getDefinitions, getDefinition } from "../../../integrations"
import _ from "lodash"
import merge from "lodash/merge"
import {
BudibaseInternalDB,
getDatasourceParams,
@ -227,7 +227,7 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
}
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

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 search from "./search"
import * as utils from "./utils"
import * as external from "./external"
export default {
...attachments,
...rows,
...search,
utils: utils,
utils,
external,
}

View File

@ -147,8 +147,8 @@ export async function exportRows(
export async function fetch(tableId: string) {
const db = context.getAppDB()
let table = await sdk.tables.getTable(tableId)
let rows = await getRawTableData(db, tableId)
const table = await sdk.tables.getTable(tableId)
const rows = await getRawTableData(db, tableId)
const result = await outputProcessing(table, rows)
return result
}
@ -171,7 +171,7 @@ async function getRawTableData(db: Database, tableId: string) {
export async function fetchView(
viewName: string,
options: { calculation: string; group: string; field: string }
) {
): Promise<Row[]> {
// if this is a table view being looked for just transfer to that
if (viewName.startsWith(DocumentType.TABLE)) {
return fetch(viewName)
@ -197,7 +197,7 @@ export async function fetchView(
)
}
let rows
let rows: Row[] = []
if (!calculation) {
response.rows = response.rows.map(row => row.doc)
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 { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters"
@ -46,3 +48,90 @@ export function cleanExportRows(
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 { populateExternalTableSchemas, isEditableColumn } from "./validation"
import sdk from "../../../sdk"
import _ from "lodash"
async function getAllInternalTables(db?: Database): Promise<Table[]> {
if (!db) {

View File

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

View File

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

View File

@ -1,13 +1,16 @@
import TestConfiguration from "../TestConfiguration"
import { RowAPI } from "./row"
import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2"
export default class API {
table: TableAPI
viewV2: ViewV2API
row: RowAPI
constructor(config: TestConfiguration) {
this.table = new TableAPI(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 { groups } from "@budibase/pro"
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
import { cloneDeep } from "lodash"
import cloneDeep from "lodash/cloneDeep"
export function updateAppRole(
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.
* @returns {object[]|object} the enriched rows will be returned.
*/
export async function outputProcessing(
export async function outputProcessing<T extends Row[] | Row>(
table: Table,
rows: Row[] | Row,
rows: T,
opts = { squash: true }
) {
): Promise<T> {
let safeRows: Row[]
let wasArray = true
if (!(rows instanceof Array)) {
rows = [rows]
safeRows = [rows]
wasArray = false
} else {
safeRows = rows
}
// attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(table, rows as Row[])
let enriched = await linkRows.attachFullLinkedDocs(table, safeRows)
// process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
@ -221,7 +224,7 @@ export async function outputProcessing(
enriched
)) 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-json": "^4.1.0",
"doctrine": "^3.0.0",
"jest": "^26.6.3",
"jest-environment-node": "^26.6.2",
"jest": "29.6.2",
"jest-environment-node": "29.6.2",
"marked": "^4.0.10",
"rollup": "^2.36.2",
"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 {
rows: any[]
}

View File

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

View File

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

View File

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

View File

@ -314,7 +314,7 @@ describe("scim", () => {
const user = await config.getUser(email)
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 () => {
@ -345,7 +345,7 @@ describe("scim", () => {
const user = await config.getUser(email)
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 () => {

View File

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