Merge branch 'master' into contrib-expose-fetchData-in-SDK

This commit is contained in:
Andrew Kingston 2024-01-29 08:57:00 +00:00 committed by GitHub
commit 764cfc04ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
119 changed files with 1519 additions and 697 deletions

View File

@ -33,13 +33,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -50,14 +50,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -92,14 +92,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -116,14 +116,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -140,14 +140,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -165,14 +165,14 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -189,13 +189,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: yarn cache: yarn
@ -219,7 +219,7 @@ jobs:
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
@ -249,7 +249,7 @@ jobs:
- name: Check submodule merged to base branch - name: Check submodule merged to base branch
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }} if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
uses: actions/github-script@v4 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
@ -269,7 +269,7 @@ jobs:
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo and submodules
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
@ -299,7 +299,7 @@ jobs:
- name: Check submodule merged to base branch - name: Check submodule merged to base branch
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }} if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
uses: actions/github-script@v4 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View File

@ -17,7 +17,7 @@ jobs:
github.event.label.name == 'feature-branch' github.event.label.name == 'feature-branch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }} PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}

View File

@ -17,7 +17,7 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'feature-branch') contains(github.event.pull_request.labels.*.name, 'feature-branch')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
PAYLOAD_BRANCH: ${{ github.head_ref }} PAYLOAD_BRANCH: ${{ github.head_ref }}

View File

@ -28,7 +28,7 @@ jobs:
run: | run: |
echo "Ref is not master, you must run this job from master." echo "Ref is not master, you must run this job from master."
exit 1 exit 1
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
@ -53,7 +53,7 @@ jobs:
needs: [tag-release] needs: [tag-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: peter-evans/repository-dispatch@v2 - uses: peter-evans/repository-dispatch@v2
with: with:

View File

@ -1,5 +1,5 @@
{ {
"version": "2.15.2", "version": "2.15.7",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

@ -1 +1 @@
Subproject commit 05c90ce55144e260da6688335c16783eab79bf96 Subproject commit dd9cec22751405e042ba0fe58e3c05f7223c3723

View File

@ -179,6 +179,7 @@ const environment = {
...getPackageJsonFields(), ...getPackageJsonFields(),
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
OFFLINE_MODE: process.env.OFFLINE_MODE, OFFLINE_MODE: process.env.OFFLINE_MODE,
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
_set(key: any, value: any) { _set(key: any, value: any) {
process.env[key] = value process.env[key] = value
// @ts-ignore // @ts-ignore

View File

@ -2,6 +2,7 @@ export * as configs from "./configs"
export * as events from "./events" export * as events from "./events"
export * as migrations from "./migrations" export * as migrations from "./migrations"
export * as users from "./users" export * as users from "./users"
export * as userUtils from "./users/utils"
export * as roles from "./security/roles" export * as roles from "./security/roles"
export * as permissions from "./security/permissions" export * as permissions from "./security/permissions"
export * as accounts from "./accounts" export * as accounts from "./accounts"

View File

@ -1,8 +1,8 @@
const redis = require("../redis/init") import * as redis from "../redis/init"
const { v4: uuidv4 } = require("uuid") import { v4 as uuidv4 } from "uuid"
const { logWarn } = require("../logging") import { logWarn } from "../logging"
import env from "../environment" import env from "../environment"
import { Duration } from "../utils"
import { import {
Session, Session,
ScannedSession, ScannedSession,
@ -10,8 +10,10 @@ import {
CreateSession, CreateSession,
} from "@budibase/types" } from "@budibase/types"
// a week in seconds // a week expiry is the default
const EXPIRY_SECONDS = 86400 * 7 const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
? parseInt(env.SESSION_EXPIRY_SECONDS)
: Duration.fromDays(7).toSeconds()
function makeSessionID(userId: string, sessionId: string) { function makeSessionID(userId: string, sessionId: string) {
return `${userId}/${sessionId}` return `${userId}/${sessionId}`

View File

@ -251,7 +251,8 @@ export class UserDB {
} }
const change = dbUser ? 0 : 1 // no change if there is existing user const change = dbUser ? 0 : 1 // no change if there is existing user
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0 const creatorsChange =
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
return UserDB.quotas.addUsers(change, creatorsChange, async () => { return UserDB.quotas.addUsers(change, creatorsChange, async () => {
await validateUniqueUser(email, tenantId) await validateUniqueUser(email, tenantId)
@ -335,7 +336,7 @@ export class UserDB {
} }
newUser.userGroups = groups || [] newUser.userGroups = groups || []
newUsers.push(newUser) newUsers.push(newUser)
if (isCreator(newUser)) { if (await isCreator(newUser)) {
newCreators.push(newUser) newCreators.push(newUser)
} }
} }
@ -432,12 +433,16 @@ export class UserDB {
_deleted: true, _deleted: true,
})) }))
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
const creatorsToDelete = usersToDelete.filter(isCreator)
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
const creatorsToDeleteCount = creatorsEval.filter(
creator => !!creator
).length
for (let user of usersToDelete) { for (let user of usersToDelete) {
await bulkDeleteProcessing(user) await bulkDeleteProcessing(user)
} }
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length) await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
// Build Response // Build Response
// index users by id // index users by id
@ -486,7 +491,7 @@ export class UserDB {
await db.remove(userId, dbUser._rev) await db.remove(userId, dbUser._rev)
const creatorsToDelete = isCreator(dbUser) ? 1 : 0 const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
await UserDB.quotas.removeUsers(1, creatorsToDelete) await UserDB.quotas.removeUsers(1, creatorsToDelete)
await eventHelpers.handleDeleteEvents(dbUser) await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId) await cache.user.invalidateUser(userId)

View File

@ -0,0 +1,67 @@
import { User, UserGroup } from "@budibase/types"
import { generator, structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra"
import { getGlobalDB } from "../../context"
import { isCreator } from "../utils"
const config = new DBTestConfiguration()
describe("Users", () => {
it("User is a creator if it is configured as a global builder", async () => {
const user: User = structures.users.user({ builder: { global: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is configured as a global admin", async () => {
const user: User = structures.users.user({ admin: { global: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is configured with creator permission", async () => {
const user: User = structures.users.user({ builder: { creator: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is a builder in some application", async () => {
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it has CREATOR permission in some application", async () => {
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it has ADMIN permission in some application", async () => {
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
const usersInGroup = 10
const groupId = "gr_17abffe89e0b40268e755b952f101a59"
const group: UserGroup = {
...structures.userGroups.userGroup(),
...{ _id: groupId, roles: { app1: "ADMIN" } },
}
const users: User[] = []
for (const _ of Array.from({ length: usersInGroup })) {
const userId = `us_${generator.guid()}`
const user: User = structures.users.user({
_id: userId,
userGroups: [groupId],
})
users.push(user)
}
await config.doInTenant(async () => {
const db = getGlobalDB()
await db.put(group)
for (let user of users) {
await db.put(user)
const creator = await isCreator(user)
expect(creator).toBe(true)
}
})
})
})

View File

@ -309,7 +309,8 @@ export async function getCreatorCount() {
let creators = 0 let creators = 0
async function iterate(startPage?: string) { async function iterate(startPage?: string) {
const page = await paginatedUsers({ bookmark: startPage }) const page = await paginatedUsers({ bookmark: startPage })
creators += page.data.filter(isCreator).length const creatorsEval = await Promise.all(page.data.map(isCreator))
creators += creatorsEval.filter(creator => !!creator).length
if (page.hasNextPage) { if (page.hasNextPage) {
await iterate(page.nextPage) await iterate(page.nextPage)
} }

View File

@ -1,4 +1,4 @@
import { CloudAccount } from "@budibase/types" import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import env from "../environment" import env from "../environment"
import { getPlatformUser } from "./lookup" import { getPlatformUser } from "./lookup"
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
import { getTenantId } from "../context" import { getTenantId } from "../context"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { getAccountByTenantId } from "../accounts" import { getAccountByTenantId } from "../accounts"
import { BUILTIN_ROLE_IDS } from "../security/roles"
import * as context from "../context"
// extract from shared-core to make easily accessible from backend-core // extract from shared-core to make easily accessible from backend-core
export const isBuilder = sdk.users.isBuilder export const isBuilder = sdk.users.isBuilder
export const isAdmin = sdk.users.isAdmin export const isAdmin = sdk.users.isAdmin
export const isCreator = sdk.users.isCreator
export const isGlobalBuilder = sdk.users.isGlobalBuilder export const isGlobalBuilder = sdk.users.isGlobalBuilder
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasAdminPermissions = sdk.users.hasAdminPermissions
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
export async function isCreator(user?: User | ContextUser) {
const isCreatorByUserDefinition = sdk.users.isCreator(user)
if (!isCreatorByUserDefinition && user) {
return await isCreatorByGroupMembership(user)
}
return isCreatorByUserDefinition
}
async function isCreatorByGroupMembership(user?: User | ContextUser) {
const userGroups = user?.userGroups || []
if (userGroups.length > 0) {
const db = context.getGlobalDB()
const groups: UserGroup[] = []
for (let groupId of userGroups) {
try {
const group = await db.get<UserGroup>(groupId)
groups.push(group)
} catch (e: any) {
if (e.error !== "not_found") {
throw e
}
}
}
return groups.some(group =>
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
)
}
return false
}
export async function validateUniqueUser(email: string, tenantId: string) { export async function validateUniqueUser(email: string, tenantId: string) {
// check budibase users in other tenants // check budibase users in other tenants
if (env.MULTI_TENANCY) { if (env.MULTI_TENANCY) {

View File

@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
useAnchorWidth, useAnchorWidth,
offset = 5, offset = 5,
customUpdate, customUpdate,
offsetBelow,
} = opts } = opts
if (!anchor) { if (!anchor) {
return return
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
styles.top = anchorBounds.top - elementBounds.height - offset styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240 styles.maxHeight = maxHeight || 240
} else { } else {
styles.top = anchorBounds.bottom + (offsetBelow || offset) styles.top = anchorBounds.bottom + offset
styles.maxHeight = styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20 maxHeight || window.innerHeight - anchorBounds.bottom - 20
} }

View File

@ -15,8 +15,6 @@
export let autoWidth = false export let autoWidth = false
export let searchTerm = null export let searchTerm = null
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let open = false export let open = false
export let loading export let loading
@ -98,7 +96,5 @@
{sort} {sort}
{autoWidth} {autoWidth}
{customPopoverHeight} {customPopoverHeight}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
{loading} {loading}
/> />

View File

@ -37,8 +37,6 @@
export let sort = false export let sort = false
export let searchTerm = null export let searchTerm = null
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let align = "left" export let align = "left"
export let footer = null export let footer = null
export let customAnchor = null export let customAnchor = null
@ -156,9 +154,7 @@
on:close={() => (open = false)} on:close={() => (open = false)}
useAnchorWidth={!autoWidth} useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null} maxWidth={autoWidth ? 400 : null}
maxHeight={customPopoverMaxHeight}
customHeight={customPopoverHeight} customHeight={customPopoverHeight}
offsetBelow={customPopoverOffsetBelow}
> >
<div <div
class="popover-content" class="popover-content"

View File

@ -12,6 +12,7 @@
export let getOptionIcon = () => null export let getOptionIcon = () => null
export let getOptionColour = () => null export let getOptionColour = () => null
export let getOptionSubtitle = () => null export let getOptionSubtitle = () => null
export let compare = null
export let useOptionIconImage = false export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled
export let readonly = false export let readonly = false
@ -23,8 +24,6 @@
export let footer = null export let footer = null
export let open = false export let open = false
export let tag = null export let tag = null
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let searchTerm = null export let searchTerm = null
export let loading export let loading
@ -34,13 +33,19 @@
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
$: fieldColour = getFieldAttribute(getOptionColour, value, options) $: fieldColour = getFieldAttribute(getOptionColour, value, options)
function compareOptionAndValue(option, value) {
return typeof compare === "function"
? compare(option, value)
: option === value
}
const getFieldAttribute = (getAttribute, value, options) => { const getFieldAttribute = (getAttribute, value, options) => {
// Wait for options to load if there is a value but no options // Wait for options to load if there is a value but no options
if (!options?.length) { if (!options?.length) {
return "" return ""
} }
const index = options.findIndex( const index = options.findIndex((option, idx) =>
(option, idx) => getOptionValue(option, idx) === value compareOptionAndValue(getOptionValue(option, idx), value)
) )
return index !== -1 ? getAttribute(options[index], index) : null return index !== -1 ? getAttribute(options[index], index) : null
} }
@ -90,11 +95,9 @@
{autocomplete} {autocomplete}
{sort} {sort}
{tag} {tag}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}
placeholderOption={placeholder === false ? null : placeholder} placeholderOption={placeholder === false ? null : placeholder}
isOptionSelected={option => option === value} isOptionSelected={option => compareOptionAndValue(option, value)}
onSelectOption={selectOption} onSelectOption={selectOption}
{loading} {loading}
/> />

View File

@ -28,6 +28,7 @@
export let footer = null export let footer = null
export let tag = null export let tag = null
export let helpText = null export let helpText = null
export let compare
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
value = e.detail value = e.detail
@ -65,6 +66,7 @@
{autocomplete} {autocomplete}
{customPopoverHeight} {customPopoverHeight}
{tag} {tag}
{compare}
on:change={onChange} on:change={onChange}
on:click on:click
/> />

View File

@ -18,7 +18,6 @@
export let useAnchorWidth = false export let useAnchorWidth = false
export let dismissible = true export let dismissible = true
export let offset = 5 export let offset = 5
export let offsetBelow
export let customHeight export let customHeight
export let animate = true export let animate = true
export let customZindex export let customZindex
@ -89,7 +88,6 @@
maxWidth, maxWidth,
useAnchorWidth, useAnchorWidth,
offset, offset,
offsetBelow,
customUpdate: handlePostionUpdate, customUpdate: handlePostionUpdate,
}} }}
use:clickOutside={{ use:clickOutside={{

View File

@ -1035,11 +1035,48 @@ export const getAllStateVariables = () => {
getAllAssets().forEach(asset => { getAllAssets().forEach(asset => {
findAllMatchingComponents(asset.props, component => { findAllMatchingComponents(asset.props, component => {
const settings = getComponentSettings(component._component) const settings = getComponentSettings(component._component)
settings
.filter(setting => setting.type === "event") const parseEventSettings = (settings, comp) => {
.forEach(setting => { settings
eventSettings.push(component[setting.key]) .filter(setting => setting.type === "event")
}) .forEach(setting => {
eventSettings.push(comp[setting.key])
})
}
const parseComponentSettings = (settings, component) => {
// Parse the nested button configurations
settings
.filter(setting => setting.type === "buttonConfiguration")
.forEach(setting => {
const buttonConfig = component[setting.key]
if (Array.isArray(buttonConfig)) {
buttonConfig.forEach(button => {
const nestedSettings = getComponentSettings(button._component)
parseEventSettings(nestedSettings, button)
})
}
})
parseEventSettings(settings, component)
}
// Parse the base component settings
parseComponentSettings(settings, component)
// Parse step configuration
const stepSetting = settings.find(
setting => setting.type === "stepConfiguration"
)
const steps = stepSetting ? component[stepSetting.key] : []
const stepDefinition = getComponentSettings(
"@budibase/standard-components/multistepformblockstep"
)
steps.forEach(step => {
parseComponentSettings(stepDefinition, step)
})
}) })
}) })

View File

@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history" import { createHistoryStore } from "builderStore/store/history"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { getHoverStore } from "./store/hover"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
@ -16,6 +17,7 @@ export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore() export const temporalStore = getTemporalStore()
export const userStore = getUserStore() export const userStore = getUserStore()
export const deploymentStore = getDeploymentStore() export const deploymentStore = getDeploymentStore()
export const hoverStore = getHoverStore()
// Setup history for screens // Setup history for screens
export const screenHistoryStore = createHistoryStore({ export const screenHistoryStore = createHistoryStore({

View File

@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = {
// Onboarding // Onboarding
onboarding: false, onboarding: false,
tourNodes: null, tourNodes: null,
// UI state
hoveredComponentId: null,
} }
export const getFrontendStore = () => { export const getFrontendStore = () => {
@ -1415,18 +1412,6 @@ export const getFrontendStore = () => {
return state return state
}) })
}, },
hover: (componentId, notifyClient = true) => {
if (componentId === get(store).hoveredComponentId) {
return
}
store.update(state => {
state.hoveredComponentId = componentId
return state
})
if (notifyClient) {
store.actions.preview.sendEvent("hover-component", componentId)
}
},
}, },
links: { links: {
save: async (url, title) => { save: async (url, title) => {

View File

@ -0,0 +1,27 @@
import { get, writable } from "svelte/store"
import { store as builder } from "builderStore"
export const getHoverStore = () => {
const initialValue = {
componentId: null,
}
const store = writable(initialValue)
const update = (componentId, notifyClient = true) => {
if (componentId === get(store).componentId) {
return
}
store.update(state => {
state.componentId = componentId
return state
})
if (notifyClient) {
builder.actions.preview.sendEvent("hover-component", componentId)
}
}
return {
subscribe: store.subscribe,
actions: { update },
}
}

View File

@ -184,8 +184,9 @@
} }
if ( if (
(idx === 0 && automation.trigger?.event === "row:update") || idx === 0 &&
automation.trigger?.event === "row:save" (automation.trigger?.event === "row:update" ||
automation.trigger?.event === "row:save")
) { ) {
if (name !== "id" && name !== "revision") return `trigger.row.${name}` if (name !== "id" && name !== "revision") return `trigger.row.${name}`
} }

View File

@ -13,6 +13,7 @@
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { getFormattedPlanName } from "helpers/planTitle"
import { get } from "svelte/store" import { get } from "svelte/store"
export let resourceId export let resourceId
@ -99,7 +100,9 @@
{#if requiresPlanToModify} {#if requiresPlanToModify}
<span class="lock-tag"> <span class="lock-tag">
<Tags> <Tags>
<Tag icon="LockClosed">{capitalise(requiresPlanToModify)}</Tag> <Tag icon="LockClosed"
>{getFormattedPlanName(requiresPlanToModify)}</Tag
>
</Tags> </Tags>
</span> </span>
{/if} {/if}

View File

@ -88,8 +88,12 @@
hasValidated = false hasValidated = false
}) })
} }
$: valid = $: valid =
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType) getErrorCount(errors) === 0 &&
allRequiredAttributesSet(relationshipType) &&
fromId &&
toId
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = $: isManyToOne =
relationshipType === RelationshipType.MANY_TO_ONE || relationshipType === RelationshipType.MANY_TO_ONE ||

View File

@ -5,6 +5,7 @@
import { store } from "builderStore" import { store } from "builderStore"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getEventContextBindings } from "builderStore/dataBinding" import { getEventContextBindings } from "builderStore/dataBinding"
import { cloneDeep, isEqual } from "lodash/fp"
export let componentInstance export let componentInstance
export let componentBindings export let componentBindings
@ -17,8 +18,13 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focusItem let focusItem
let cachedValue
$: buttonList = sanitizeValue(value) || [] $: if (!isEqual(value, cachedValue)) {
cachedValue = cloneDeep(value)
}
$: buttonList = sanitizeValue(cachedValue) || []
$: buttonCount = buttonList.length $: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({ $: eventContextBindings = getEventContextBindings({
componentInstance, componentInstance,

View File

@ -35,6 +35,7 @@
export let bindingDrawerLeft export let bindingDrawerLeft
export let allowHelpers = true export let allowHelpers = true
export let customButtonText = null export let customButtonText = null
export let compare = (option, value) => option === value
let fields = Object.entries(object || {}).map(([name, value]) => ({ let fields = Object.entries(object || {}).map(([name, value]) => ({
name, name,
@ -112,7 +113,12 @@
on:blur={changed} on:blur={changed}
/> />
{#if options} {#if options}
<Select bind:value={field.value} on:change={changed} {options} /> <Select
bind:value={field.value}
{compare}
on:change={changed}
{options}
/>
{:else if bindings && bindings.length} {:else if bindings && bindings.length}
<DrawerBindableInput <DrawerBindableInput
{bindings} {bindings}

View File

@ -1,6 +1,6 @@
<script> <script>
import KeyValueBuilder from "../KeyValueBuilder.svelte" import KeyValueBuilder from "../KeyValueBuilder.svelte"
import { SchemaTypeOptions } from "constants/backend" import { SchemaTypeOptionsExpanded } from "constants/backend"
export let schema export let schema
export let onSchemaChange = () => {} export let onSchemaChange = () => {}
@ -24,6 +24,7 @@
object={schema} object={schema}
name="field" name="field"
headings headings
options={SchemaTypeOptions} options={SchemaTypeOptionsExpanded}
compare={(option, value) => option.type === value.type}
/> />
{/key} {/key}

View File

@ -33,7 +33,7 @@
PaginationTypes, PaginationTypes,
RawRestBodyTypes, RawRestBodyTypes,
RestBodyTypes as bodyTypes, RestBodyTypes as bodyTypes,
SchemaTypeOptions, SchemaTypeOptionsExpanded,
} from "constants/backend" } from "constants/backend"
import JSONPreview from "components/integration/JSONPreview.svelte" import JSONPreview from "components/integration/JSONPreview.svelte"
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
@ -97,9 +97,7 @@
$: schemaReadOnly = !responseSuccess $: schemaReadOnly = !responseSuccess
$: variablesReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
$: hasSchema = $: hasSchema = Object.keys(schema || {}).length !== 0
Object.keys(schema || {}).length !== 0 ||
Object.keys(query?.schema || {}).length !== 0
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs) $: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
@ -161,7 +159,7 @@
newQuery.fields.queryString = queryString newQuery.fields.queryString = queryString
newQuery.fields.authConfigId = authConfigId newQuery.fields.authConfigId = authConfigId
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders) newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
newQuery.schema = restUtils.fieldsToSchema(schema) newQuery.schema = schema
return newQuery return newQuery
} }
@ -231,6 +229,14 @@
notifications.info("Request did not return any data") notifications.info("Request did not return any data")
} else { } else {
response.info = response.info || { code: 200 } response.info = response.info || { code: 200 }
// if existing schema, copy over what it is
if (schema) {
for (let [name, field] of Object.entries(schema)) {
if (response.schema[name]) {
response.schema[name] = field
}
}
}
schema = response.schema schema = response.schema
notifications.success("Request sent successfully") notifications.success("Request sent successfully")
} }
@ -386,6 +392,7 @@
onMount(async () => { onMount(async () => {
query = getSelectedQuery() query = getSelectedQuery()
schema = query.schema
try { try {
// Clear any unsaved changes to the datasource // Clear any unsaved changes to the datasource
@ -416,7 +423,6 @@
query.fields.path = `${datasource.config.url}/${path ? path : ""}` query.fields.path = `${datasource.config.url}/${path ? path : ""}`
} }
url = buildUrl(query.fields.path, breakQs) url = buildUrl(query.fields.path, breakQs)
schema = restUtils.schemaToFields(query.schema)
requestBindings = restUtils.queryParametersToKeyValue(query.parameters) requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
authConfigId = getAuthConfigId() authConfigId = getAuthConfigId()
if (!query.fields.disabledHeaders) { if (!query.fields.disabledHeaders) {
@ -682,10 +688,11 @@
bind:object={schema} bind:object={schema}
name="schema" name="schema"
headings headings
options={SchemaTypeOptions} options={SchemaTypeOptionsExpanded}
menuItems={schemaMenuItems} menuItems={schemaMenuItems}
showMenu={!schemaReadOnly} showMenu={!schemaReadOnly}
readOnly={schemaReadOnly} readOnly={schemaReadOnly}
compare={(option, value) => option.type === value.type}
/> />
</Tab> </Tab>
{/if} {/if}

View File

@ -271,6 +271,11 @@ export const SchemaTypeOptions = [
{ label: "Datetime", value: "datetime" }, { label: "Datetime", value: "datetime" },
] ]
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
...el,
value: { type: el.value },
}))
export const RawRestBodyTypes = { export const RawRestBodyTypes = {
NONE: "none", NONE: "none",
FORM: "form", FORM: "form",

View File

@ -1,26 +1,6 @@
import { IntegrationTypes } from "constants/backend" import { IntegrationTypes } from "constants/backend"
import { findHBSBlocks } from "@budibase/string-templates" import { findHBSBlocks } from "@budibase/string-templates"
export function schemaToFields(schema) {
const response = {}
if (schema && typeof schema === "object") {
for (let [field, value] of Object.entries(schema)) {
response[field] = value?.type || "string"
}
}
return response
}
export function fieldsToSchema(fields) {
const response = {}
if (fields && typeof fields === "object") {
for (let [name, type] of Object.entries(fields)) {
response[name] = { name, type }
}
}
return response
}
export function breakQueryString(qs) { export function breakQueryString(qs) {
if (!qs) { if (!qs) {
return {} return {}
@ -184,10 +164,8 @@ export const parseToCsv = (headers, rows) => {
export default { export default {
breakQueryString, breakQueryString,
buildQueryString, buildQueryString,
fieldsToSchema,
flipHeaderState, flipHeaderState,
keyValueToQueryParameters, keyValueToQueryParameters,
parseToCsv, parseToCsv,
queryParametersToKeyValue, queryParametersToKeyValue,
schemaToFields,
} }

View File

@ -0,0 +1,27 @@
import { PlanType } from "@budibase/types"
export function getFormattedPlanName(userPlanType) {
let planName
switch (userPlanType) {
case PlanType.PRO:
planName = "Pro"
break
case PlanType.TEAM:
planName = "Team"
break
case PlanType.PREMIUM:
case PlanType.PREMIUM_PLUS:
planName = "Premium"
break
case PlanType.BUSINESS:
planName = "Business"
break
case PlanType.ENTERPRISE_BASIC:
case PlanType.ENTERPRISE:
planName = "Enterprise"
break
default:
planName = "Free" // Default to "Free" if the type is not explicitly handled
}
return `${planName} Plan`
}

View File

@ -392,6 +392,10 @@
} }
const openInviteFlow = () => { const openInviteFlow = () => {
// prevent email from getting overwritten if changes are made
if (!email) {
email = query
}
$licensing.userLimitReached $licensing.userLimitReached
? userLimitReachedModal.show() ? userLimitReachedModal.show()
: (invitingFlow = true) : (invitingFlow = true)

View File

@ -1,7 +1,7 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { store, selectedScreen, currentAsset } from "builderStore" import { store, selectedScreen, currentAsset, hoverStore } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import {
ProgressCircle, ProgressCircle,
@ -118,7 +118,7 @@
} else if (type === "select-component" && data.id) { } else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id $store.selectedComponentId = data.id
} else if (type === "hover-component") { } else if (type === "hover-component") {
store.actions.components.hover(data.id, false) hoverStore.actions.update(data.id, false)
} else if (type === "update-prop") { } else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value) await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") { } else if (type === "update-styles") {

View File

@ -5,6 +5,7 @@
selectedComponentPath, selectedComponentPath,
selectedComponent, selectedComponent,
selectedScreen, selectedScreen,
hoverStore,
} from "builderStore" } from "builderStore"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte" import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
@ -90,7 +91,7 @@
return findComponentPath($selectedComponent, component._id)?.length > 0 return findComponentPath($selectedComponent, component._id)?.length > 0
} }
const hover = store.actions.components.hover const hover = hoverStore.actions.update
</script> </script>
<ul> <ul>
@ -111,7 +112,7 @@
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)} on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={onDrop} on:drop={onDrop}
hovering={$store.hoveredComponentId === component._id} hovering={$hoverStore.componentId === component._id}
on:mouseenter={() => hover(component._id)} on:mouseenter={() => hover(component._id)}
on:mouseleave={() => hover(null)} on:mouseleave={() => hover(null)}
text={getComponentText(component)} text={getComponentText(component)}

View File

@ -1,7 +1,12 @@
<script> <script>
import { notifications, Icon, Body } from "@budibase/bbui" import { notifications, Icon, Body } from "@budibase/bbui"
import { isActive, goto } from "@roxi/routify" import { isActive, goto } from "@roxi/routify"
import { store, selectedScreen, userSelectedResourceMap } from "builderStore" import {
store,
selectedScreen,
userSelectedResourceMap,
hoverStore,
} from "builderStore"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import ComponentTree from "./ComponentTree.svelte" import ComponentTree from "./ComponentTree.svelte"
import { dndStore, DropPosition } from "./dndStore.js" import { dndStore, DropPosition } from "./dndStore.js"
@ -36,7 +41,7 @@
scrolling = e.target.scrollTop !== 0 scrolling = e.target.scrollTop !== 0
} }
const hover = store.actions.components.hover const hover = hoverStore.actions.update
</script> </script>
<div class="components"> <div class="components">
@ -60,7 +65,7 @@
icon="WebPage" icon="WebPage"
on:drop={onDrop} on:drop={onDrop}
on:click={() => ($store.selectedComponentId = screenComponentId)} on:click={() => ($store.selectedComponentId = screenComponentId)}
hovering={$store.hoveredComponentId === screenComponentId} hovering={$hoverStore.componentId === screenComponentId}
on:mouseenter={() => hover(screenComponentId)} on:mouseenter={() => hover(screenComponentId)}
on:mouseleave={() => hover(null)} on:mouseleave={() => hover(null)}
id="component-screen" id="component-screen"
@ -79,7 +84,7 @@
: "VisibilityOff"} : "VisibilityOff"}
on:drop={onDrop} on:drop={onDrop}
on:click={() => ($store.selectedComponentId = navComponentId)} on:click={() => ($store.selectedComponentId = navComponentId)}
hovering={$store.hoveredComponentId === navComponentId} hovering={$hoverStore.componentId === navComponentId}
on:mouseenter={() => hover(navComponentId)} on:mouseenter={() => hover(navComponentId)}
on:mouseleave={() => hover(null)} on:mouseleave={() => hover(null)}
id="component-nav" id="component-nav"

View File

@ -15,9 +15,9 @@
<Content showMobileNav> <Content showMobileNav>
<SideNav slot="side-nav"> <SideNav slot="side-nav">
<SideNavItem <SideNavItem
text="Automation History" text="Automations"
url={$url("./automation-history")} url={$url("./automations")}
active={$isActive("./automation-history")} active={$isActive("./automations")}
/> />
<SideNavItem <SideNavItem
text="Backups" text="Backups"

View File

@ -8,6 +8,8 @@
Body, Body,
Heading, Heading,
Divider, Divider,
Toggle,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte" import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import StatusRenderer from "./_components/StatusRenderer.svelte" import StatusRenderer from "./_components/StatusRenderer.svelte"
@ -16,7 +18,7 @@
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { getContext, onDestroy, onMount } from "svelte" import { getContext, onDestroy, onMount } from "svelte"
import dayjs from "dayjs" import dayjs from "dayjs"
import { auth, licensing, admin } from "stores/portal" import { auth, licensing, admin, apps } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import Portal from "svelte-portal" import Portal from "svelte-portal"
@ -35,9 +37,13 @@
let timeRange = null let timeRange = null
let loaded = false let loaded = false
$: app = $apps.find(app => app.devId === $store.appId?.includes(app.appId))
$: licensePlan = $auth.user?.license?.plan $: licensePlan = $auth.user?.license?.plan
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchLogs(automationId, status, page, timeRange) $: fetchLogs(automationId, status, page, timeRange)
$: isCloud = $admin.cloud
$: chainAutomations = app?.automations?.chainAutomations ?? !isCloud
const timeOptions = [ const timeOptions = [
{ value: "90-d", label: "Past 90 days" }, { value: "90-d", label: "Past 90 days" },
@ -124,6 +130,18 @@
sidePanel.open() sidePanel.open()
} }
async function save({ detail }) {
try {
await apps.update($store.appId, {
automations: {
chainAutomations: detail,
},
})
} catch (error) {
notifications.error("Error updating automation chaining setting")
}
}
onMount(async () => { onMount(async () => {
await automationStore.actions.fetch() await automationStore.actions.fetch()
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
@ -150,11 +168,30 @@
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading>Automation History</Heading> <Heading>Automations</Heading>
<Body>View the automations your app has executed</Body> <Body size="S">See your automation history and edit advanced settings</Body>
</Layout> </Layout>
<Divider /> <Divider />
<Layout gap="XS" noPadding>
<Heading size="XS">Chain automations</Heading>
<Body size="S">Allow automations to trigger from other automations</Body>
<div class="setting-spacing">
<Toggle
text={"Enable chaining"}
on:change={e => {
save(e)
}}
value={chainAutomations}
/>
</div>
</Layout>
<Divider />
<Layout gap="XS" noPadding>
<Heading size="XS">History</Heading>
<Body size="S">Free plan stores up to 1 day of automation history</Body>
</Layout>
<div class="controls"> <div class="controls">
<div class="search"> <div class="search">
<div class="select"> <div class="select">
@ -237,6 +274,9 @@
{/if} {/if}
<style> <style>
.setting-spacing {
padding-top: var(--spacing-s);
}
.controls { .controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,5 +1,5 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
$redirect("../settings/automation-history") $redirect("../settings/automations")
</script> </script>

View File

@ -15,7 +15,7 @@
import { DashCard, Usage } from "components/usage" import { DashCard, Usage } from "components/usage"
import { PlanModel } from "constants" import { PlanModel } from "constants"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { PlanType } from "@budibase/types" import { getFormattedPlanName } from "helpers/planTitle"
let staticUsage = [] let staticUsage = []
let monthlyUsage = [] let monthlyUsage = []
@ -100,23 +100,6 @@
cancelAt = license?.billing?.subscription?.cancelAt cancelAt = license?.billing?.subscription?.cancelAt
} }
const capitalise = string => {
if (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
}
const planTitle = () => {
const planType = license?.plan.type
let planName = license?.plan.type
if (planType === PlanType.PREMIUM_PLUS) {
planName = "Premium"
} else if (planType === PlanType.ENTERPRISE_BASIC) {
planName = "Enterprise"
}
return `${capitalise(planName)} Plan`
}
const getDaysRemaining = timestamp => { const getDaysRemaining = timestamp => {
if (!timestamp) { if (!timestamp) {
return return
@ -227,7 +210,7 @@
<DashCard <DashCard
description="YOUR CURRENT PLAN" description="YOUR CURRENT PLAN"
title={planTitle()} title={getFormattedPlanName(license?.plan.type)}
{primaryActionText} {primaryActionText}
primaryAction={showButton ? goToAccountPortal : undefined} primaryAction={showButton ? goToAccountPortal : undefined}
{textRows} {textRows}

View File

@ -89,8 +89,8 @@ export function createQueriesStore() {
// Assume all the fields are strings and create a basic schema from the // Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server // unique fields returned by the server
const schema = {} const schema = {}
for (let [field, type] of Object.entries(result.schemaFields)) { for (let [field, metadata] of Object.entries(result.schema)) {
schema[field] = type || "string" schema[field] = metadata || { type: "string" }
} }
return { ...result, schema, rows: result.rows || [] } return { ...result, schema, rows: result.rows || [] }
} }

View File

@ -3969,6 +3969,12 @@
"key": "allowManualEntry", "key": "allowManualEntry",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Auto confirm",
"key": "autoConfirm",
"defaultValue": false
},
{ {
"type": "boolean", "type": "boolean",
"label": "Play sound on scan", "label": "Play sound on scan",
@ -6098,23 +6104,6 @@
} }
] ]
}, },
{
"tag": "style",
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{ {
"tag": "style", "tag": "style",
"type": "select", "type": "select",
@ -6131,6 +6120,23 @@
} }
], ],
"defaultValue": "bottom" "defaultValue": "bottom"
},
{
"tag": "style",
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
} }
], ],
"actions": [ "actions": [

View File

@ -29,7 +29,7 @@
type, type,
quiet, quiet,
disabled, disabled,
size, size: size || "M",
}} }}
/> />
{/each} {/each}

View File

@ -14,11 +14,13 @@
export let value export let value
export let disabled = false export let disabled = false
export let allowManualEntry = false export let allowManualEntry = false
export let autoConfirm = false
export let scanButtonText = "Scan code" export let scanButtonText = "Scan code"
export let beepOnScan = false export let beepOnScan = false
export let beepFrequency = 2637 export let beepFrequency = 2637
export let customFrequency = 1046 export let customFrequency = 1046
export let preferredCamera = "environment" export let preferredCamera = "environment"
export let validator
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -41,6 +43,9 @@
beep() beep()
} }
dispatch("change", decodedText) dispatch("change", decodedText)
if (autoConfirm && !validator?.(decodedText)) {
camModal?.hide()
}
} }
} }
@ -127,7 +132,11 @@
<div class="scanner-video-wrapper"> <div class="scanner-video-wrapper">
{#if value && !manualMode} {#if value && !manualMode}
<div class="scanner-value field-display"> <div class="scanner-value field-display">
<StatusLight positive /> {#if validator?.(value)}
<StatusLight negative />
{:else}
<StatusLight positive />
{/if}
{value} {value}
</div> </div>
{/if} {/if}
@ -183,11 +192,16 @@
</div> </div>
{#if cameraEnabled === true} {#if cameraEnabled === true}
<div class="code-wrap"> <div class="code-wrap">
{#if value} {#if value && !validator?.(value)}
<div class="scanner-value"> <div class="scanner-value">
<StatusLight positive /> <StatusLight positive />
{value} {value}
</div> </div>
{:else if value && validator?.(value)}
<div class="scanner-value">
<StatusLight negative />
{value}
</div>
{:else} {:else}
<div class="scanner-value"> <div class="scanner-value">
<StatusLight neutral /> <StatusLight neutral />

View File

@ -11,6 +11,7 @@
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
export let allowManualEntry export let allowManualEntry
export let autoConfirm
export let scanButtonText export let scanButtonText
export let beepOnScan export let beepOnScan
export let beepFrequency export let beepFrequency
@ -49,11 +50,13 @@
on:change={handleUpdate} on:change={handleUpdate}
disabled={fieldState.disabled || fieldState.readonly} disabled={fieldState.disabled || fieldState.readonly}
{allowManualEntry} {allowManualEntry}
{autoConfirm}
scanButtonText={scanText} scanButtonText={scanText}
{beepOnScan} {beepOnScan}
{beepFrequency} {beepFrequency}
{customFrequency} {customFrequency}
{preferredCamera} {preferredCamera}
validator={fieldState.validator}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -108,8 +108,16 @@
} }
} }
$: forceFetchRows(filter)
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue) $: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
const forceFetchRows = async () => {
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
optionsObj = {}
fieldApi?.setValue([])
selectedValue = []
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
}
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => { const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
const allRowsFetched = const allRowsFetched =
$fetch.loaded && $fetch.loaded &&
@ -228,7 +236,6 @@
bind:searchTerm bind:searchTerm
loading={$fetch.loading} loading={$fetch.loading}
bind:open bind:open
customPopoverMaxHeight={400}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -13,7 +13,7 @@
import { getColumnIcon } from "../lib/utils" import { getColumnIcon } from "../lib/utils"
import MigrationModal from "../controls/MigrationModal.svelte" import MigrationModal from "../controls/MigrationModal.svelte"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
import { FieldType, FormulaTypes } from "@budibase/types" import { FieldType, FormulaType } from "@budibase/types"
import { TableNames } from "../../../constants" import { TableNames } from "../../../constants"
export let column export let column
@ -96,7 +96,7 @@
const { type, formulaType } = col.schema const { type, formulaType } = col.schema
return ( return (
searchableTypes.includes(type) || searchableTypes.includes(type) ||
(type === FieldType.FORMULA && formulaType === FormulaTypes.STATIC) (type === FieldType.FORMULA && formulaType === FormulaType.STATIC)
) )
} }

@ -1 +1 @@
Subproject commit ce7722ed4474718596b465dcfd49bef36cab2e42 Subproject commit eb9565f568cfef14b336b14eee753119acfdd43b

View File

@ -119,8 +119,8 @@
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8",
"@types/koa-send": "^4.1.6", "@types/koa-send": "^4.1.6",
"@types/koa__router": "8.0.8",
"@types/lodash": "4.14.200", "@types/lodash": "4.14.200",
"@types/mssql": "9.1.4", "@types/mssql": "9.1.4",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",
@ -142,6 +142,7 @@
"rimraf": "3.0.2", "rimraf": "3.0.2",
"supertest": "6.3.3", "supertest": "6.3.3",
"swagger-jsdoc": "6.1.0", "swagger-jsdoc": "6.1.0",
"testcontainers": "10.6.0",
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",

View File

@ -2,7 +2,7 @@ version: "3.8"
services: services:
db: db:
container_name: postgres container_name: postgres
image: postgres:15-bullseye image: postgres:16.1-bullseye
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root

View File

@ -1,4 +1,4 @@
import { FieldTypes, RelationshipType, FormulaTypes } from "../../src/constants" import { FieldType, FormulaType, RelationshipType } from "@budibase/types"
import { object } from "./utils" import { object } from "./utils"
import Resource from "./utils/Resource" import Resource from "./utils/Resource"
@ -27,7 +27,7 @@ const table = {
const baseColumnDef = { const baseColumnDef = {
type: { type: {
type: "string", type: "string",
enum: Object.values(FieldTypes), enum: Object.values(FieldType),
description: description:
"Defines the type of the column, most explain themselves, a link column is a relationship.", "Defines the type of the column, most explain themselves, a link column is a relationship.",
}, },
@ -81,7 +81,7 @@ const tableSchema = {
...baseColumnDef, ...baseColumnDef,
type: { type: {
type: "string", type: "string",
enum: [FieldTypes.LINK], enum: [FieldType.LINK],
description: "A relationship column.", description: "A relationship column.",
}, },
fieldName: { fieldName: {
@ -128,7 +128,7 @@ const tableSchema = {
...baseColumnDef, ...baseColumnDef,
type: { type: {
type: "string", type: "string",
enum: [FieldTypes.FORMULA], enum: [FieldType.FORMULA],
description: "A formula column.", description: "A formula column.",
}, },
formula: { formula: {
@ -138,7 +138,7 @@ const tableSchema = {
}, },
formulaType: { formulaType: {
type: "string", type: "string",
enum: Object.values(FormulaTypes), enum: Object.values(FormulaType),
description: description:
"Defines whether this is a static or dynamic formula.", "Defines whether this is a static or dynamic formula.",
}, },

View File

@ -9,8 +9,11 @@ import {
CreateDatasourceResponse, CreateDatasourceResponse,
Datasource, Datasource,
DatasourcePlus, DatasourcePlus,
Document,
FetchDatasourceInfoRequest, FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
FieldType,
RelationshipFieldMetadata,
SourceName, SourceName,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UserCtx, UserCtx,
@ -218,9 +221,26 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
[] []
) )
function updateRevisions(deletedLinks: RelationshipFieldMetadata[]) {
for (const link of deletedLinks) {
datasourceTableDocs.forEach((doc: Document) => {
if (doc._id === link.tableId) {
doc._rev = link.tableRev
}
})
}
}
// Destroy the tables. // Destroy the tables.
for (const table of datasourceTableDocs) { for (const table of datasourceTableDocs) {
await sdk.tables.internal.destroy(table) const deleted = await sdk.tables.internal.destroy(table)
// Update the revisions of any tables that remain to be deleted
const deletedLinks: RelationshipFieldMetadata[] = Object.values(
deleted.table.schema
)
.filter(field => field.type === FieldType.LINK)
.map(field => field as RelationshipFieldMetadata)
updateRevisions(deletedLinks)
} }
} }

View File

@ -1,15 +1,21 @@
import { generateQueryID } from "../../../db/utils" import { generateQueryID } from "../../../db/utils"
import { BaseQueryVerbs, FieldTypes } from "../../../constants" import { BaseQueryVerbs } from "../../../constants"
import { Thread, ThreadType } from "../../../threads" import { Thread, ThreadType } from "../../../threads"
import { save as saveDatasource } from "../datasource" import { save as saveDatasource } from "../datasource"
import { RestImporter } from "./import" import { RestImporter } from "./import"
import { invalidateDynamicVariables } from "../../../threads/utils" import { invalidateDynamicVariables } from "../../../threads/utils"
import env from "../../../environment" import env from "../../../environment"
import { quotas } from "@budibase/pro"
import { events, context, utils, constants } from "@budibase/backend-core" import { events, context, utils, constants } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { QueryEvent } from "../../../threads/definitions" import { QueryEvent, QueryResponse } from "../../../threads/definitions"
import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types" import {
ConfigType,
Query,
UserCtx,
SessionCookie,
QuerySchema,
FieldType,
} from "@budibase/types"
import { ValidQueryNameRegex } from "@budibase/shared-core" import { ValidQueryNameRegex } from "@budibase/shared-core"
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
@ -162,39 +168,43 @@ export async function preview(ctx: UserCtx) {
}, },
} }
const { rows, keys, info, extra } = (await Runner.run(inputs)) as any const { rows, keys, info, extra } = await Runner.run<QueryResponse>(inputs)
const schemaFields: any = {} const previewSchema: Record<string, QuerySchema> = {}
const makeQuerySchema = (type: FieldType, name: string): QuerySchema => ({
type,
name,
})
if (rows?.length > 0) { if (rows?.length > 0) {
for (let key of [...new Set(keys)] as string[]) { for (let key of [...new Set(keys)] as string[]) {
const field = rows[0][key] const field = rows[0][key]
let type = typeof field, let type = typeof field,
fieldType = FieldTypes.STRING fieldMetadata = makeQuerySchema(FieldType.STRING, key)
if (field) if (field)
switch (type) { switch (type) {
case "boolean": case "boolean":
schemaFields[key] = FieldTypes.BOOLEAN fieldMetadata = makeQuerySchema(FieldType.BOOLEAN, key)
break break
case "object": case "object":
if (field instanceof Date) { if (field instanceof Date) {
fieldType = FieldTypes.DATETIME fieldMetadata = makeQuerySchema(FieldType.DATETIME, key)
} else if (Array.isArray(field)) { } else if (Array.isArray(field)) {
fieldType = FieldTypes.ARRAY fieldMetadata = makeQuerySchema(FieldType.ARRAY, key)
} else { } else {
fieldType = FieldTypes.JSON fieldMetadata = makeQuerySchema(FieldType.JSON, key)
} }
break break
case "number": case "number":
fieldType = FieldTypes.NUMBER fieldMetadata = makeQuerySchema(FieldType.NUMBER, key)
break break
} }
schemaFields[key] = fieldType previewSchema[key] = fieldMetadata
} }
} }
// if existing schema, update to include any previous schema keys // if existing schema, update to include any previous schema keys
if (existingSchema) { if (existingSchema) {
for (let key of Object.keys(schemaFields)) { for (let key of Object.keys(previewSchema)) {
if (existingSchema[key]?.type) { if (existingSchema[key]) {
schemaFields[key] = existingSchema[key].type previewSchema[key] = existingSchema[key]
} }
} }
} }
@ -203,7 +213,7 @@ export async function preview(ctx: UserCtx) {
await events.query.previewed(datasource, query) await events.query.previewed(datasource, query)
ctx.body = { ctx.body = {
rows, rows,
schemaFields, schema: previewSchema,
info, info,
extra, extra,
} }
@ -257,7 +267,9 @@ async function execute(
schema: query.schema, schema: query.schema,
} }
const { rows, pagination, extra, info } = (await Runner.run(inputs)) as any const { rows, pagination, extra, info } = await Runner.run<QueryResponse>(
inputs
)
// remove the raw from execution incase transformer being used to hide data // remove the raw from execution incase transformer being used to hide data
if (extra?.raw) { if (extra?.raw) {
delete extra.raw delete extra.raw

View File

@ -1,4 +1,5 @@
import { import {
AutoFieldSubType,
AutoReason, AutoReason,
Datasource, Datasource,
FieldSchema, FieldSchema,
@ -27,7 +28,6 @@ import {
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils" import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
import { AutoFieldSubTypes, FieldTypes } from "../../../constants"
import { processObjectSync } from "@budibase/string-templates" import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { processDates, processFormulas } from "../../../utilities/rowProcessor" import { processDates, processFormulas } from "../../../utilities/rowProcessor"
@ -111,10 +111,10 @@ function buildFilters(
*/ */
function cleanupConfig(config: RunConfig, table: Table): RunConfig { function cleanupConfig(config: RunConfig, table: Table): RunConfig {
const primaryOptions = [ const primaryOptions = [
FieldTypes.STRING, FieldType.STRING,
FieldTypes.LONGFORM, FieldType.LONGFORM,
FieldTypes.OPTIONS, FieldType.OPTIONS,
FieldTypes.NUMBER, FieldType.NUMBER,
] ]
// filter out fields which cannot be keys // filter out fields which cannot be keys
const fieldNames = Object.entries(table.schema) const fieldNames = Object.entries(table.schema)
@ -241,10 +241,7 @@ function basicProcessing({
function fixArrayTypes(row: Row, table: Table) { function fixArrayTypes(row: Row, table: Table) {
for (let [fieldName, schema] of Object.entries(table.schema)) { for (let [fieldName, schema] of Object.entries(table.schema)) {
if ( if (schema.type === FieldType.ARRAY && typeof row[fieldName] === "string") {
schema.type === FieldTypes.ARRAY &&
typeof row[fieldName] === "string"
) {
try { try {
row[fieldName] = JSON.parse(row[fieldName]) row[fieldName] = JSON.parse(row[fieldName])
} catch (err) { } catch (err) {
@ -274,8 +271,8 @@ function isEditableColumn(column: FieldSchema) {
const isExternalAutoColumn = const isExternalAutoColumn =
column.autocolumn && column.autocolumn &&
column.autoReason !== AutoReason.FOREIGN_KEY && column.autoReason !== AutoReason.FOREIGN_KEY &&
column.subtype !== AutoFieldSubTypes.AUTO_ID column.subtype !== AutoFieldSubType.AUTO_ID
const isFormula = column.type === FieldTypes.FORMULA const isFormula = column.type === FieldType.FORMULA
return !(isExternalAutoColumn || isFormula) return !(isExternalAutoColumn || isFormula)
} }
@ -322,11 +319,11 @@ export class ExternalRequest<T extends Operation> {
continue continue
} }
// parse floats/numbers // parse floats/numbers
if (field.type === FieldTypes.NUMBER && !isNaN(parseFloat(row[key]))) { if (field.type === FieldType.NUMBER && !isNaN(parseFloat(row[key]))) {
newRow[key] = parseFloat(row[key]) newRow[key] = parseFloat(row[key])
} }
// if its not a link then just copy it over // if its not a link then just copy it over
if (field.type !== FieldTypes.LINK) { if (field.type !== FieldType.LINK) {
newRow[key] = row[key] newRow[key] = row[key]
continue continue
} }
@ -532,7 +529,7 @@ export class ExternalRequest<T extends Operation> {
buildRelationships(table: Table): RelationshipsJson[] { buildRelationships(table: Table): RelationshipsJson[] {
const relationships = [] const relationships = []
for (let [fieldName, field] of Object.entries(table.schema)) { for (let [fieldName, field] of Object.entries(table.schema)) {
if (field.type !== FieldTypes.LINK) { if (field.type !== FieldType.LINK) {
continue continue
} }
const { tableName: linkTableName } = breakExternalTableId(field.tableId) const { tableName: linkTableName } = breakExternalTableId(field.tableId)
@ -586,7 +583,7 @@ export class ExternalRequest<T extends Operation> {
// we need this to work out if any relationships need removed // we need this to work out if any relationships need removed
for (const field of Object.values(table.schema)) { for (const field of Object.values(table.schema)) {
if ( if (
field.type !== FieldTypes.LINK || field.type !== FieldType.LINK ||
!field.fieldName || !field.fieldName ||
isOneSide(field) isOneSide(field)
) { ) {
@ -730,15 +727,15 @@ export class ExternalRequest<T extends Operation> {
return Object.entries(table.schema) return Object.entries(table.schema)
.filter( .filter(
column => column =>
column[1].type !== FieldTypes.LINK && column[1].type !== FieldType.LINK &&
column[1].type !== FieldTypes.FORMULA && column[1].type !== FieldType.FORMULA &&
!existing.find((field: string) => field === column[0]) !existing.find((field: string) => field === column[0])
) )
.map(column => `${table.name}.${column[0]}`) .map(column => `${table.name}.${column[0]}`)
} }
let fields = extractRealFields(table) let fields = extractRealFields(table)
for (let field of Object.values(table.schema)) { for (let field of Object.values(table.schema)) {
if (field.type !== FieldTypes.LINK || !includeRelations) { if (field.type !== FieldType.LINK || !includeRelations) {
continue continue
} }
const { tableName: linkTableName } = breakExternalTableId(field.tableId) const { tableName: linkTableName } = breakExternalTableId(field.tableId)

View File

@ -1,4 +1,3 @@
import { FieldTypes } from "../../../constants"
import { import {
breakExternalTableId, breakExternalTableId,
breakRowIdField, breakRowIdField,
@ -9,6 +8,7 @@ import {
RunConfig, RunConfig,
} from "./ExternalRequest" } from "./ExternalRequest"
import { import {
FieldType,
Datasource, Datasource,
IncludeRelationship, IncludeRelationship,
Operation, Operation,
@ -154,7 +154,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
// for a single row, there is probably a better way to do this with some smart multi-layer joins // for a single row, there is probably a better way to do this with some smart multi-layer joins
for (let [fieldName, field] of Object.entries(table.schema)) { for (let [fieldName, field] of Object.entries(table.schema)) {
if ( if (
field.type !== FieldTypes.LINK || field.type !== FieldType.LINK ||
!row[fieldName] || !row[fieldName] ||
row[fieldName].length === 0 row[fieldName].length === 0
) { ) {

View File

@ -6,12 +6,12 @@ import {
inputProcessing, inputProcessing,
outputProcessing, outputProcessing,
} from "../../../utilities/rowProcessor" } from "../../../utilities/rowProcessor"
import { FieldTypes } from "../../../constants"
import * as utils from "./utils" import * as utils from "./utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { finaliseRow, updateRelatedFormula } from "./staticFormula" import { finaliseRow, updateRelatedFormula } from "./staticFormula"
import { import {
FieldType,
LinkDocumentValue, LinkDocumentValue,
PatchRowRequest, PatchRowRequest,
PatchRowResponse, PatchRowResponse,
@ -225,7 +225,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
// insert the link rows in the correct place throughout the main row // insert the link rows in the correct place throughout the main row
for (let fieldName of Object.keys(table.schema)) { for (let fieldName of Object.keys(table.schema)) {
let field = table.schema[fieldName] let field = table.schema[fieldName]
if (field.type === FieldTypes.LINK) { if (field.type === FieldType.LINK) {
// find the links that pertain to this field // find the links that pertain to this field
const links = linkVals.filter(link => link.fieldName === fieldName) const links = linkVals.filter(link => link.fieldName === fieldName)
// find the rows that the links state are linked to this field // find the rows that the links state are linked to this field

View File

@ -4,9 +4,15 @@ import {
processAutoColumn, processAutoColumn,
processFormulas, processFormulas,
} from "../../../utilities/rowProcessor" } from "../../../utilities/rowProcessor"
import { FieldTypes, FormulaTypes } from "../../../constants"
import { context, locks } from "@budibase/backend-core" import { context, locks } from "@budibase/backend-core"
import { Table, Row, LockType, LockName } from "@budibase/types" import {
Table,
Row,
LockType,
LockName,
FormulaType,
FieldType,
} 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/isEqual" import isEqual from "lodash/isEqual"
@ -35,7 +41,7 @@ export async function updateRelatedFormula(
let relatedRows: Record<string, Row[]> = {} let relatedRows: Record<string, Row[]> = {}
for (let [key, field] of Object.entries(enrichedRow)) { for (let [key, field] of Object.entries(enrichedRow)) {
const columnDefinition = table.schema[key] const columnDefinition = table.schema[key]
if (columnDefinition && columnDefinition.type === FieldTypes.LINK) { if (columnDefinition && columnDefinition.type === FieldType.LINK) {
const relatedTableId = columnDefinition.tableId! const relatedTableId = columnDefinition.tableId!
if (!relatedRows[relatedTableId]) { if (!relatedRows[relatedTableId]) {
relatedRows[relatedTableId] = [] relatedRows[relatedTableId] = []
@ -63,8 +69,8 @@ export async function updateRelatedFormula(
for (let column of Object.values(relatedTable!.schema)) { for (let column of Object.values(relatedTable!.schema)) {
// needs updated in related rows // needs updated in related rows
if ( if (
column.type === FieldTypes.FORMULA && column.type === FieldType.FORMULA &&
column.formulaType === FormulaTypes.STATIC column.formulaType === FormulaType.STATIC
) { ) {
// re-enrich rows for all the related, don't update the related formula for them // re-enrich rows for all the related, don't update the related formula for them
promises = promises.concat( promises = promises.concat(

View File

@ -1,4 +1,3 @@
import { 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"
@ -7,6 +6,7 @@ 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 { import {
FormulaType,
FieldSchema, FieldSchema,
FieldType, FieldType,
FormulaFieldMetadata, FormulaFieldMetadata,
@ -17,10 +17,10 @@ import { isRelationshipColumn } from "../../../db/utils"
function isStaticFormula( function isStaticFormula(
column: FieldSchema column: FieldSchema
): column is FormulaFieldMetadata & { formulaType: FormulaTypes.STATIC } { ): column is FormulaFieldMetadata & { formulaType: FormulaType.STATIC } {
return ( return (
column.type === FieldType.FORMULA && column.type === FieldType.FORMULA &&
column.formulaType === FormulaTypes.STATIC column.formulaType === FormulaType.STATIC
) )
} }

View File

@ -1,5 +1,4 @@
import { FieldType } from "@budibase/types" import { AutoFieldSubType, FieldType } from "@budibase/types"
import { AutoFieldSubTypes } from "../../../../constants"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import { importToRows } from "../utils" import { importToRows } from "../utils"
@ -22,7 +21,7 @@ describe("utils", () => {
autoId: { autoId: {
name: "autoId", name: "autoId",
type: FieldType.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldType.NUMBER, type: FieldType.NUMBER,
@ -69,7 +68,7 @@ describe("utils", () => {
autoId: { autoId: {
name: "autoId", name: "autoId",
type: FieldType.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldType.NUMBER, type: FieldType.NUMBER,

View File

@ -2,8 +2,6 @@ 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/isEqual" import isEqual from "lodash/isEqual"
import { import {
AutoFieldSubTypes,
FieldTypes,
GOOGLE_SHEETS_PRIMARY_KEY, GOOGLE_SHEETS_PRIMARY_KEY,
USERS_TABLE_SCHEMA, USERS_TABLE_SCHEMA,
SwitchableTypes, SwitchableTypes,
@ -19,6 +17,7 @@ import { cloneDeep } from "lodash/fp"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { events, context } from "@budibase/backend-core" import { events, context } from "@budibase/backend-core"
import { import {
AutoFieldSubType,
ContextUser, ContextUser,
Datasource, Datasource,
Row, Row,
@ -106,7 +105,7 @@ export function makeSureTableUpToDate(table: Table, tableToSave: Table) {
for ([field, column] of Object.entries(table.schema)) { for ([field, column] of Object.entries(table.schema)) {
if ( if (
column.autocolumn && column.autocolumn &&
column.subtype === AutoFieldSubTypes.AUTO_ID && column.subtype === AutoFieldSubType.AUTO_ID &&
tableToSave.schema[field] tableToSave.schema[field]
) { ) {
const tableCol = tableToSave.schema[field] as NumberFieldMetadata const tableCol = tableToSave.schema[field] as NumberFieldMetadata
@ -144,8 +143,8 @@ export async function importToRows(
? row[fieldName] ? row[fieldName]
: [row[fieldName]] : [row[fieldName]]
if ( if (
(schema.type === FieldTypes.OPTIONS || (schema.type === FieldType.OPTIONS ||
schema.type === FieldTypes.ARRAY) && schema.type === FieldType.ARRAY) &&
row[fieldName] row[fieldName]
) { ) {
let merged = [...schema.constraints!.inclusion!, ...rowVal] let merged = [...schema.constraints!.inclusion!, ...rowVal]
@ -403,7 +402,7 @@ export async function checkForViewUpdates(
) )
const newViewTemplate = viewTemplate( const newViewTemplate = viewTemplate(
viewMetadata, viewMetadata,
groupByField?.type === FieldTypes.ARRAY groupByField?.type === FieldType.ARRAY
) )
const viewName = view.name! const viewName = view.name!
await saveView(null, viewName, newViewTemplate) await saveView(null, viewName, newViewTemplate)
@ -434,7 +433,7 @@ export function generateJunctionTableName(
export function foreignKeyStructure(keyName: string, meta?: any) { export function foreignKeyStructure(keyName: string, meta?: any) {
const structure: any = { const structure: any = {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
constraints: {}, constraints: {},
name: keyName, name: keyName,
} }

View File

@ -6,8 +6,8 @@ import { fetchView } from "../row"
import { context, events } from "@budibase/backend-core" import { context, events } from "@budibase/backend-core"
import { DocumentType } from "../../../db/utils" import { DocumentType } from "../../../db/utils"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { FieldTypes } from "../../../constants"
import { import {
FieldType,
Ctx, Ctx,
Row, Row,
Table, Table,
@ -37,7 +37,7 @@ export async function save(ctx: Ctx) {
(field: any) => field.name == viewToSave.groupBy (field: any) => field.name == viewToSave.groupBy
) )
const view = viewTemplate(viewToSave, groupByField?.type === FieldTypes.ARRAY) const view = viewTemplate(viewToSave, groupByField?.type === FieldType.ARRAY)
const viewName = viewToSave.name const viewName = viewToSave.name
if (!viewName) { if (!viewName) {

View File

@ -235,9 +235,9 @@ describe("/queries", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
// these responses come from the mock // these responses come from the mock
expect(res.body.schemaFields).toEqual({ expect(res.body.schema).toEqual({
a: "string", a: { type: "string", name: "a" },
b: "number", b: { type: "number", name: "b" },
}) })
expect(res.body.rows.length).toEqual(1) expect(res.body.rows.length).toEqual(1)
expect(events.query.previewed).toBeCalledTimes(1) expect(events.query.previewed).toBeCalledTimes(1)
@ -300,10 +300,10 @@ describe("/queries", () => {
queryString: "test={{ variable2 }}", queryString: "test={{ variable2 }}",
}) })
// these responses come from the mock // these responses come from the mock
expect(res.body.schemaFields).toEqual({ expect(res.body.schema).toEqual({
opts: "json", opts: { type: "json", name: "opts" },
url: "string", url: { type: "string", name: "url" },
value: "string", value: { type: "string", name: "value" },
}) })
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1") expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
}) })
@ -314,10 +314,10 @@ describe("/queries", () => {
path: "www.google.com", path: "www.google.com",
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
expect(res.body.schemaFields).toEqual({ expect(res.body.schema).toEqual({
opts: "json", opts: { type: "json", name: "opts" },
url: "string", url: { type: "string", name: "url" },
value: "string", value: { type: "string", name: "value" },
}) })
expect(res.body.rows[0].url).toContain("doctype%20html") expect(res.body.rows[0].url).toContain("doctype%20html")
}) })
@ -337,10 +337,10 @@ describe("/queries", () => {
path: "www.failonce.com", path: "www.failonce.com",
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
expect(res.body.schemaFields).toEqual({ expect(res.body.schema).toEqual({
fails: "number", fails: { type: "number", name: "fails" },
opts: "json", opts: { type: "json", name: "opts" },
url: "string", url: { type: "string", name: "url" },
}) })
expect(res.body.rows[0].fails).toEqual(1) expect(res.body.rows[0].fails).toEqual(1)
}) })

View File

@ -6,11 +6,11 @@ import * as setup from "./utilities"
import { context, InternalTable, roles, tenancy } from "@budibase/backend-core" import { context, InternalTable, roles, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
AutoFieldSubTypes, AutoFieldSubType,
FieldSchema, FieldSchema,
FieldType, FieldType,
FieldTypeSubtypes, FieldTypeSubtypes,
FormulaTypes, FormulaType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
MonthlyQuotaName, MonthlyQuotaName,
PermissionLevel, PermissionLevel,
@ -192,7 +192,7 @@ describe.each([
"Row ID": { "Row ID": {
name: "Row ID", name: "Row ID",
type: FieldType.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
@ -2032,7 +2032,7 @@ describe.each([
name: "formula", name: "formula",
type: FieldType.FORMULA, type: FieldType.FORMULA,
formula: "{{ links.0.name }}", formula: "{{ links.0.name }}",
formulaType: FormulaTypes.DYNAMIC, formulaType: FormulaType.DYNAMIC,
}, },
}, },
} }
@ -2086,7 +2086,7 @@ describe.each([
name: "formula", name: "formula",
type: FieldType.FORMULA, type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`, formula: `{{ js "${js}"}}`,
formulaType: FormulaTypes.DYNAMIC, formulaType: FormulaType.DYNAMIC,
}, },
}, },
}) })
@ -2129,7 +2129,7 @@ describe.each([
name: "formula", name: "formula",
type: FieldType.FORMULA, type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`, formula: `{{ js "${js}"}}`,
formulaType: FormulaTypes.DYNAMIC, formulaType: FormulaType.DYNAMIC,
}, },
}, },
}) })

View File

@ -1,6 +1,6 @@
import { context, events } from "@budibase/backend-core" import { context, events } from "@budibase/backend-core"
import { import {
AutoFieldSubTypes, AutoFieldSubType,
FieldSubtype, FieldSubtype,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
@ -205,7 +205,7 @@ describe("/tables", () => {
autoId: { autoId: {
name: "id", name: "id",
type: FieldType.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: "number", type: "number",

View File

@ -1,9 +1,9 @@
import * as rowController from "../../api/controllers/row" import * as rowController from "../../api/controllers/row"
import * as tableController from "../../api/controllers/table" import * as tableController from "../../api/controllers/table"
import { FieldTypes } from "../../constants"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { import {
FieldType,
AutomationActionStepId, AutomationActionStepId,
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
@ -115,7 +115,7 @@ function typeCoercion(filters: SearchFilters, table: Table) {
if (!column || typeof value !== "string") { if (!column || typeof value !== "string") {
continue continue
} }
if (column.type === FieldTypes.NUMBER) { if (column.type === FieldType.NUMBER) {
if (key === "oneOf") { if (key === "oneOf") {
searchParam[property] = value searchParam[property] = value
.split(",") .split(",")
@ -148,11 +148,11 @@ export async function run({ inputs, appId }: AutomationStepInput) {
} }
} }
const table = await getTable(appId, tableId) const table = await getTable(appId, tableId)
let sortType = FieldTypes.STRING let sortType = FieldType.STRING
if (table && table.schema && table.schema[sortColumn] && sortColumn) { if (table && table.schema && table.schema[sortColumn] && sortColumn) {
const fieldType = table.schema[sortColumn].type const fieldType = table.schema[sortColumn].type
sortType = sortType =
fieldType === FieldTypes.NUMBER ? FieldTypes.NUMBER : FieldTypes.STRING fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING
} }
const ctx: any = buildCtx(appId, null, { const ctx: any = buildCtx(appId, null, {
params: { params: {

View File

@ -1,18 +1,11 @@
import { constants, objectStore, roles } from "@budibase/backend-core" import { constants, objectStore, roles } from "@budibase/backend-core"
import { import {
FieldType as FieldTypes, FieldType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
Table, Table,
TableSourceType, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
export {
FieldType as FieldTypes,
RelationshipType,
AutoFieldSubTypes,
FormulaTypes,
} from "@budibase/types"
export enum FilterTypes { export enum FilterTypes {
STRING = "string", STRING = "string",
FUZZY = "fuzzy", FUZZY = "fuzzy",
@ -36,14 +29,14 @@ export const NoEmptyFilterStrings = [
] ]
export const CanSwitchTypes = [ export const CanSwitchTypes = [
[FieldTypes.JSON, FieldTypes.ARRAY], [FieldType.JSON, FieldType.ARRAY],
[ [
FieldTypes.STRING, FieldType.STRING,
FieldTypes.OPTIONS, FieldType.OPTIONS,
FieldTypes.LONGFORM, FieldType.LONGFORM,
FieldTypes.BARCODEQR, FieldType.BARCODEQR,
], ],
[FieldTypes.BOOLEAN, FieldTypes.NUMBER], [FieldType.BOOLEAN, FieldType.NUMBER],
] ]
export const SwitchableTypes = CanSwitchTypes.reduce((prev, current) => export const SwitchableTypes = CanSwitchTypes.reduce((prev, current) =>
@ -86,9 +79,9 @@ export const USERS_TABLE_SCHEMA: Table = {
// TODO: ADMIN PANEL - when implemented this doesn't need to be carried out // TODO: ADMIN PANEL - when implemented this doesn't need to be carried out
schema: { schema: {
email: { email: {
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
email: true, email: true,
length: { length: {
maximum: "", maximum: "",
@ -99,34 +92,34 @@ export const USERS_TABLE_SCHEMA: Table = {
}, },
firstName: { firstName: {
name: "firstName", name: "firstName",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
presence: false, presence: false,
}, },
}, },
lastName: { lastName: {
name: "lastName", name: "lastName",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
presence: false, presence: false,
}, },
}, },
roleId: { roleId: {
name: "roleId", name: "roleId",
type: FieldTypes.OPTIONS, type: FieldType.OPTIONS,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
presence: false, presence: false,
inclusion: Object.values(roles.BUILTIN_ROLE_IDS), inclusion: Object.values(roles.BUILTIN_ROLE_IDS),
}, },
}, },
status: { status: {
name: "status", name: "status",
type: FieldTypes.OPTIONS, type: FieldType.OPTIONS,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
presence: false, presence: false,
inclusion: Object.values(constants.UserStatus), inclusion: Object.values(constants.UserStatus),
}, },

View File

@ -1,6 +1,4 @@
import { import {
AutoFieldSubTypes,
FieldTypes,
DEFAULT_BB_DATASOURCE_ID, DEFAULT_BB_DATASOURCE_ID,
DEFAULT_INVENTORY_TABLE_ID, DEFAULT_INVENTORY_TABLE_ID,
DEFAULT_EMPLOYEE_TABLE_ID, DEFAULT_EMPLOYEE_TABLE_ID,
@ -16,6 +14,7 @@ import { jobsImport } from "./jobsImport"
import { expensesImport } from "./expensesImport" import { expensesImport } from "./expensesImport"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { import {
AutoFieldSubType,
FieldType, FieldType,
RelationshipType, RelationshipType,
Row, Row,
@ -40,7 +39,7 @@ function syncLastIds(table: Table, rowCount: number) {
if ( if (
entry.autocolumn && entry.autocolumn &&
entry.type === FieldType.NUMBER && entry.type === FieldType.NUMBER &&
entry.subtype == AutoFieldSubTypes.AUTO_ID entry.subtype == AutoFieldSubType.AUTO_ID
) { ) {
entry.lastID = rowCount entry.lastID = rowCount
} }
@ -58,12 +57,12 @@ async function tableImport(table: Table, data: Row[]) {
const AUTO_COLUMNS: TableSchema = { const AUTO_COLUMNS: TableSchema = {
"Created At": { "Created At": {
name: "Created At", name: "Created At",
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
subtype: AutoFieldSubTypes.CREATED_AT, subtype: AutoFieldSubType.CREATED_AT,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -74,12 +73,12 @@ const AUTO_COLUMNS: TableSchema = {
}, },
"Updated At": { "Updated At": {
name: "Updated At", name: "Updated At",
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
subtype: AutoFieldSubTypes.UPDATED_AT, subtype: AutoFieldSubType.UPDATED_AT,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -101,12 +100,12 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
schema: { schema: {
"Item ID": { "Item ID": {
name: "Item ID", name: "Item ID",
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: false, presence: false,
numericality: { numericality: {
greaterThanOrEqualTo: "", greaterThanOrEqualTo: "",
@ -115,9 +114,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
}, },
}, },
"Item Name": { "Item Name": {
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: { length: {
maximum: null, maximum: null,
}, },
@ -128,9 +127,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
name: "Item Name", name: "Item Name",
}, },
"Item Tags": { "Item Tags": {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: { presence: {
allowEmpty: false, allowEmpty: false,
}, },
@ -140,9 +139,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
sortable: false, sortable: false,
}, },
Notes: { Notes: {
type: FieldTypes.LONGFORM, type: FieldType.LONGFORM,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
@ -150,9 +149,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
useRichText: null, useRichText: null,
}, },
Status: { Status: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: { presence: {
allowEmpty: false, allowEmpty: false,
}, },
@ -162,18 +161,18 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
sortable: false, sortable: false,
}, },
SKU: { SKU: {
type: FieldTypes.BARCODEQR, type: FieldType.BARCODEQR,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
name: "SKU", name: "SKU",
}, },
"Purchase Date": { "Purchase Date": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -185,9 +184,9 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
"Purchase Price": { "Purchase Price": {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: false, presence: false,
numericality: { numericality: {
greaterThanOrEqualTo: null, greaterThanOrEqualTo: null,
@ -211,75 +210,75 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
schema: { schema: {
"First Name": { "First Name": {
name: "First Name", name: "First Name",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
"Last Name": { "Last Name": {
name: "Last Name", name: "Last Name",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
Email: { Email: {
name: "Email", name: "Email",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
Address: { Address: {
name: "Address", name: "Address",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
City: { City: {
name: "City", name: "City",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
Postcode: { Postcode: {
name: "Postcode", name: "Postcode",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
Phone: { Phone: {
name: "Phone", name: "Phone",
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
}, },
"EMPLOYEE ID": { "EMPLOYEE ID": {
name: "EMPLOYEE ID", name: "EMPLOYEE ID",
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: false, presence: false,
numericality: { numericality: {
greaterThanOrEqualTo: "", greaterThanOrEqualTo: "",
@ -288,9 +287,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
}, },
}, },
"Employee Level": { "Employee Level": {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: false, presence: false,
inclusion: ["Manager", "Junior", "Senior", "Apprentice", "Contractor"], inclusion: ["Manager", "Junior", "Senior", "Apprentice", "Contractor"],
}, },
@ -298,18 +297,18 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
sortable: false, sortable: false,
}, },
"Badge Photo": { "Badge Photo": {
type: FieldTypes.ATTACHMENT, type: FieldType.ATTACHMENT,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: false, presence: false,
}, },
name: "Badge Photo", name: "Badge Photo",
sortable: false, sortable: false,
}, },
Jobs: { Jobs: {
type: FieldTypes.LINK, type: FieldType.LINK,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: false, presence: false,
}, },
fieldName: "Assigned", fieldName: "Assigned",
@ -318,9 +317,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
tableId: DEFAULT_JOBS_TABLE_ID, tableId: DEFAULT_JOBS_TABLE_ID,
}, },
"Start Date": { "Start Date": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -332,9 +331,9 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
"End Date": { "End Date": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -359,12 +358,12 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
schema: { schema: {
"Job ID": { "Job ID": {
name: "Job ID", name: "Job ID",
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: false, presence: false,
numericality: { numericality: {
greaterThanOrEqualTo: "", greaterThanOrEqualTo: "",
@ -373,9 +372,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
}, },
}, },
"Quote Date": { "Quote Date": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: { presence: {
allowEmpty: false, allowEmpty: false,
@ -389,9 +388,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
"Quote Price": { "Quote Price": {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: { presence: {
allowEmpty: false, allowEmpty: false,
}, },
@ -403,9 +402,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
name: "Quote Price", name: "Quote Price",
}, },
"Works Start": { "Works Start": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -417,9 +416,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
Address: { Address: {
type: FieldTypes.LONGFORM, type: FieldType.LONGFORM,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
@ -427,9 +426,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
useRichText: null, useRichText: null,
}, },
"Customer Name": { "Customer Name": {
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: { length: {
maximum: null, maximum: null,
}, },
@ -438,9 +437,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
name: "Customer Name", name: "Customer Name",
}, },
Notes: { Notes: {
type: FieldTypes.LONGFORM, type: FieldType.LONGFORM,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
@ -448,9 +447,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
useRichText: null, useRichText: null,
}, },
"Customer Phone": { "Customer Phone": {
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: { length: {
maximum: null, maximum: null,
}, },
@ -459,9 +458,9 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
name: "Customer Phone", name: "Customer Phone",
}, },
"Customer Email": { "Customer Email": {
type: FieldTypes.STRING, type: FieldType.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: { length: {
maximum: null, maximum: null,
}, },
@ -471,14 +470,14 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
}, },
Assigned: { Assigned: {
name: "Assigned", name: "Assigned",
type: FieldTypes.LINK, type: FieldType.LINK,
tableId: DEFAULT_EMPLOYEE_TABLE_ID, tableId: DEFAULT_EMPLOYEE_TABLE_ID,
fieldName: "Jobs", fieldName: "Jobs",
relationshipType: RelationshipType.MANY_TO_MANY, relationshipType: RelationshipType.MANY_TO_MANY,
// sortable: true, // sortable: true,
}, },
"Works End": { "Works End": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: "string", type: "string",
length: {}, length: {},
@ -492,7 +491,7 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
"Updated Price": { "Updated Price": {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
constraints: { constraints: {
type: "number", type: "number",
presence: false, presence: false,
@ -518,12 +517,12 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
schema: { schema: {
"Expense ID": { "Expense ID": {
name: "Expense ID", name: "Expense ID",
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: false, presence: false,
numericality: { numericality: {
greaterThanOrEqualTo: "", greaterThanOrEqualTo: "",
@ -532,9 +531,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
}, },
}, },
"Expense Tags": { "Expense Tags": {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: { presence: {
allowEmpty: false, allowEmpty: false,
}, },
@ -554,9 +553,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
sortable: false, sortable: false,
}, },
Cost: { Cost: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
constraints: { constraints: {
type: FieldTypes.NUMBER, type: FieldType.NUMBER,
presence: { presence: {
allowEmpty: false, allowEmpty: false,
}, },
@ -568,9 +567,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
name: "Cost", name: "Cost",
}, },
Notes: { Notes: {
type: FieldTypes.LONGFORM, type: FieldType.LONGFORM,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
}, },
@ -578,9 +577,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
useRichText: null, useRichText: null,
}, },
"Payment Due": { "Payment Due": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -592,9 +591,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
"Date Paid": { "Date Paid": {
type: FieldTypes.DATETIME, type: FieldType.DATETIME,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldType.STRING,
length: {}, length: {},
presence: false, presence: false,
datetime: { datetime: {
@ -606,9 +605,9 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
ignoreTimezones: true, ignoreTimezones: true,
}, },
Attachment: { Attachment: {
type: FieldTypes.ATTACHMENT, type: FieldType.ATTACHMENT,
constraints: { constraints: {
type: FieldTypes.ARRAY, type: FieldType.ARRAY,
presence: false, presence: false,
}, },
name: "Attachment", name: "Attachment",

View File

@ -13,6 +13,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Senior"], "Employee Level": ["Senior"],
"Start Date": "2015-02-12T12:00:00.000", "Start Date": "2015-02-12T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Mandy", "First Name": "Mandy",
@ -28,6 +29,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Senior"], "Employee Level": ["Senior"],
"Start Date": "2017-09-10T12:00:00.000", "Start Date": "2017-09-10T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Holly", "First Name": "Holly",
@ -43,6 +45,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Senior"], "Employee Level": ["Senior"],
"Start Date": "2022-02-12T12:00:00.000", "Start Date": "2022-02-12T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Francis", "First Name": "Francis",
@ -58,6 +61,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Apprentice"], "Employee Level": ["Apprentice"],
"Start Date": "2021-03-10T12:00:00.000", "Start Date": "2021-03-10T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Richard", "First Name": "Richard",
@ -73,6 +77,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Apprentice"], "Employee Level": ["Apprentice"],
"Start Date": "2020-07-09T12:00:00.000", "Start Date": "2020-07-09T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Donald", "First Name": "Donald",
@ -88,6 +93,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Junior"], "Employee Level": ["Junior"],
"Start Date": "2018-04-13T12:00:00.000", "Start Date": "2018-04-13T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Maria", "First Name": "Maria",
@ -103,6 +109,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Manager"], "Employee Level": ["Manager"],
"Start Date": "2016-05-22T12:00:00.000", "Start Date": "2016-05-22T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Suzy", "First Name": "Suzy",
@ -118,6 +125,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Senior", "Manager"], "Employee Level": ["Senior", "Manager"],
"Start Date": "2019-05-01T12:00:00.000", "Start Date": "2019-05-01T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Patrick", "First Name": "Patrick",
@ -133,6 +141,7 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Apprentice"], "Employee Level": ["Apprentice"],
"Start Date": "2014-08-30T12:00:00.000", "Start Date": "2014-08-30T12:00:00.000",
"Badge Photo": [],
}, },
{ {
"First Name": "Brayden", "First Name": "Brayden",
@ -148,5 +157,6 @@ export const employeeImport = [
type: "row", type: "row",
"Employee Level": ["Contractor"], "Employee Level": ["Contractor"],
"Start Date": "2022-11-09T12:00:00.000", "Start Date": "2022-11-09T12:00:00.000",
"Badge Photo": [],
}, },
] ]

View File

@ -1,6 +1,5 @@
import { IncludeDocs, getLinkDocuments } from "./linkUtils" import { IncludeDocs, getLinkDocuments } from "./linkUtils"
import { InternalTables, getUserMetadataParams } from "../utils" import { InternalTables, getUserMetadataParams } from "../utils"
import { FieldTypes } from "../../constants"
import { context, logging } from "@budibase/backend-core" import { context, logging } from "@budibase/backend-core"
import LinkDocument from "./LinkDocument" import LinkDocument from "./LinkDocument"
import { import {
@ -62,7 +61,7 @@ class LinkController {
} }
for (let fieldName of Object.keys(table.schema)) { for (let fieldName of Object.keys(table.schema)) {
const { type } = table.schema[fieldName] const { type } = table.schema[fieldName]
if (type === FieldTypes.LINK) { if (type === FieldType.LINK) {
return true return true
} }
} }
@ -96,7 +95,7 @@ class LinkController {
validateTable(table: Table) { validateTable(table: Table) {
const usedAlready = [] const usedAlready = []
for (let schema of Object.values(table.schema)) { for (let schema of Object.values(table.schema)) {
if (schema.type !== FieldTypes.LINK) { if (schema.type !== FieldType.LINK) {
continue continue
} }
const unique = schema.tableId! + schema?.fieldName const unique = schema.tableId! + schema?.fieldName
@ -172,7 +171,7 @@ class LinkController {
// get the links this row wants to make // get the links this row wants to make
const rowField = row[fieldName] const rowField = row[fieldName]
const field = table.schema[fieldName] const field = table.schema[fieldName]
if (field.type === FieldTypes.LINK && rowField != null) { if (field.type === FieldType.LINK && rowField != null) {
// check which links actual pertain to the update in this row // check which links actual pertain to the update in this row
const thisFieldLinkDocs = linkDocs.filter( const thisFieldLinkDocs = linkDocs.filter(
linkDoc => linkDoc =>
@ -353,7 +352,7 @@ class LinkController {
const schema = table.schema const schema = table.schema
for (let fieldName of Object.keys(schema)) { for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName] const field = schema[fieldName]
if (field.type === FieldTypes.LINK && field.fieldName) { if (field.type === FieldType.LINK && field.fieldName) {
// handle this in a separate try catch, want // handle this in a separate try catch, want
// the put to bubble up as an error, if can't update // the put to bubble up as an error, if can't update
// table for some reason // table for some reason
@ -366,7 +365,7 @@ class LinkController {
} }
const fields = this.handleRelationshipType(field, { const fields = this.handleRelationshipType(field, {
name: field.fieldName, name: field.fieldName,
type: FieldTypes.LINK, type: FieldType.LINK,
// these are the props of the table that initiated the link // these are the props of the table that initiated the link
tableId: table._id!, tableId: table._id!,
fieldName: fieldName, fieldName: fieldName,
@ -413,10 +412,7 @@ class LinkController {
for (let fieldName of Object.keys(oldTable?.schema || {})) { for (let fieldName of Object.keys(oldTable?.schema || {})) {
const field = oldTable?.schema[fieldName] as FieldSchema const field = oldTable?.schema[fieldName] as FieldSchema
// this field has been removed from the table schema // this field has been removed from the table schema
if ( if (field.type === FieldType.LINK && newTable.schema[fieldName] == null) {
field.type === FieldTypes.LINK &&
newTable.schema[fieldName] == null
) {
await this.removeFieldFromTable(fieldName) await this.removeFieldFromTable(fieldName)
} }
} }
@ -437,10 +433,10 @@ class LinkController {
for (let fieldName of Object.keys(schema)) { for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName] const field = schema[fieldName]
try { try {
if (field.type === FieldTypes.LINK && field.fieldName) { if (field.type === FieldType.LINK && field.fieldName) {
const linkedTable = await this._db.get<Table>(field.tableId) const linkedTable = await this._db.get<Table>(field.tableId)
delete linkedTable.schema[field.fieldName] delete linkedTable.schema[field.fieldName]
await this._db.put(linkedTable) field.tableRev = (await this._db.put(linkedTable)).rev
} }
} catch (err: any) { } catch (err: any) {
logging.logWarn(err?.message, err) logging.logWarn(err?.message, err)

View File

@ -1,6 +1,5 @@
import { generateLinkID } from "../utils" import { generateLinkID } from "../utils"
import { FieldTypes } from "../../constants" import { FieldType, LinkDocument } from "@budibase/types"
import { LinkDocument } from "@budibase/types"
/** /**
* Creates a new link document structure which can be put to the database. It is important to * Creates a new link document structure which can be put to the database. It is important to
@ -43,7 +42,7 @@ class LinkDocumentImpl implements LinkDocument {
fieldName1, fieldName1,
fieldName2 fieldName2
) )
this.type = FieldTypes.LINK this.type = FieldType.LINK
this.doc1 = { this.doc1 = {
tableId: tableId1, tableId: tableId1,
fieldName: fieldName1, fieldName: fieldName1,

View File

@ -1,8 +1,8 @@
import { ViewName, getQueryIndex, isRelationshipColumn } from "../utils" import { ViewName, getQueryIndex, isRelationshipColumn } from "../utils"
import { FieldTypes } from "../../constants"
import { createLinkView } from "../views/staticViews" import { createLinkView } from "../views/staticViews"
import { context, logging } from "@budibase/backend-core" import { context, logging } from "@budibase/backend-core"
import { import {
FieldType,
DatabaseQueryOpts, DatabaseQueryOpts,
LinkDocument, LinkDocument,
LinkDocumentValue, LinkDocumentValue,
@ -131,11 +131,11 @@ export async function getLinkedTable(id: string, tables: Table[]) {
export function getRelatedTableForField(table: Table, fieldName: string) { export function getRelatedTableForField(table: Table, fieldName: string) {
// look to see if its on the table, straight in the schema // look to see if its on the table, straight in the schema
const field = table.schema[fieldName] const field = table.schema[fieldName]
if (field?.type === FieldTypes.LINK) { if (field?.type === FieldType.LINK) {
return field.tableId return field.tableId
} }
for (let column of Object.values(table.schema)) { for (let column of Object.values(table.schema)) {
if (column.type === FieldTypes.LINK && column.fieldName === fieldName) { if (column.type === FieldType.LINK && column.fieldName === fieldName) {
return column.tableId return column.tableId
} }
} }

View File

@ -1,17 +1,57 @@
const TestConfig = require("../../tests/utilities/TestConfiguration") import TestConfig from "../../tests/utilities/TestConfiguration"
const { import {
basicRow,
basicLinkedRow, basicLinkedRow,
basicRow,
basicTable, basicTable,
} = require("../../tests/utilities/structures") } from "../../tests/utilities/structures"
const LinkController = require("../linkedRows/LinkController").default import LinkController from "../linkedRows/LinkController"
const { context } = require("@budibase/backend-core") import { context } from "@budibase/backend-core"
const { RelationshipType } = require("../../constants") import {
const { cloneDeep } = require("lodash/fp") FieldType,
ManyToManyRelationshipFieldMetadata,
ManyToOneRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata,
RelationshipFieldMetadata,
RelationshipType,
Row,
Table,
} from "@budibase/types"
import { cloneDeep } from "lodash"
const baseColumn = {
type: FieldType.LINK,
fieldName: "",
tableId: "",
name: "",
}
function mockManyToManyColumn(): ManyToManyRelationshipFieldMetadata {
return <ManyToManyRelationshipFieldMetadata>{
...baseColumn,
through: "",
throughFrom: "",
throughTo: "",
relationshipType: RelationshipType.MANY_TO_MANY,
}
}
function mockManyToOneColumn(): ManyToOneRelationshipFieldMetadata {
return <ManyToOneRelationshipFieldMetadata>{
...baseColumn,
relationshipType: RelationshipType.MANY_TO_ONE,
}
}
function mockOneToManyColumn(): OneToManyRelationshipFieldMetadata {
return <OneToManyRelationshipFieldMetadata>{
...baseColumn,
relationshipType: RelationshipType.ONE_TO_MANY,
}
}
describe("test the link controller", () => { describe("test the link controller", () => {
let config = new TestConfig() let config = new TestConfig()
let table1, table2, appId let table1: Table, table2: Table, appId: string
beforeAll(async () => { beforeAll(async () => {
const app = await config.init() const app = await config.init()
@ -30,9 +70,18 @@ describe("test the link controller", () => {
afterAll(config.end) afterAll(config.end)
async function createLinkController(table, row = null, oldTable = null) { async function createLinkController(
table: Table,
row?: Row,
oldTable?: Table
) {
return context.doInAppContext(appId, () => { return context.doInAppContext(appId, () => {
const linkConfig = { const linkConfig: {
tableId?: string
table: Table
row?: Row
oldTable?: Table
} = {
tableId: table._id, tableId: table._id,
table, table,
} }
@ -47,11 +96,11 @@ describe("test the link controller", () => {
} }
async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) { async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) {
const row = await config.createRow(basicRow(t2._id)) const row = await config.createRow(basicRow(t2._id!))
const { _id } = await config.createRow( const { _id } = await config.createRow(
basicLinkedRow(t1._id, row._id, linkField) basicLinkedRow(t1._id!, row._id!, linkField)
) )
return config.getRow(t1._id, _id) return config.getRow(t1._id!, _id!)
} }
it("should be able to confirm if two table schemas are equal", async () => { it("should be able to confirm if two table schemas are equal", async () => {
@ -71,6 +120,7 @@ describe("test the link controller", () => {
it("should be able to check the relationship types across two fields", async () => { it("should be able to check the relationship types across two fields", async () => {
const controller = await createLinkController(table1) const controller = await createLinkController(table1)
// empty case // empty case
//@ts-ignore
let output = controller.handleRelationshipType({}, {}) let output = controller.handleRelationshipType({}, {})
expect(output.linkedField.relationshipType).toEqual( expect(output.linkedField.relationshipType).toEqual(
RelationshipType.MANY_TO_MANY RelationshipType.MANY_TO_MANY
@ -79,8 +129,8 @@ describe("test the link controller", () => {
RelationshipType.MANY_TO_MANY RelationshipType.MANY_TO_MANY
) )
output = controller.handleRelationshipType( output = controller.handleRelationshipType(
{ relationshipType: RelationshipType.MANY_TO_MANY }, mockManyToManyColumn(),
{} {} as any
) )
expect(output.linkedField.relationshipType).toEqual( expect(output.linkedField.relationshipType).toEqual(
RelationshipType.MANY_TO_MANY RelationshipType.MANY_TO_MANY
@ -88,20 +138,14 @@ describe("test the link controller", () => {
expect(output.linkerField.relationshipType).toEqual( expect(output.linkerField.relationshipType).toEqual(
RelationshipType.MANY_TO_MANY RelationshipType.MANY_TO_MANY
) )
output = controller.handleRelationshipType( output = controller.handleRelationshipType(mockManyToOneColumn(), {} as any)
{ relationshipType: RelationshipType.MANY_TO_ONE },
{}
)
expect(output.linkedField.relationshipType).toEqual( expect(output.linkedField.relationshipType).toEqual(
RelationshipType.ONE_TO_MANY RelationshipType.ONE_TO_MANY
) )
expect(output.linkerField.relationshipType).toEqual( expect(output.linkerField.relationshipType).toEqual(
RelationshipType.MANY_TO_ONE RelationshipType.MANY_TO_ONE
) )
output = controller.handleRelationshipType( output = controller.handleRelationshipType(mockOneToManyColumn(), {} as any)
{ relationshipType: RelationshipType.ONE_TO_MANY },
{}
)
expect(output.linkedField.relationshipType).toEqual( expect(output.linkedField.relationshipType).toEqual(
RelationshipType.MANY_TO_ONE RelationshipType.MANY_TO_ONE
) )
@ -115,16 +159,16 @@ describe("test the link controller", () => {
const controller = await createLinkController(table1, row) const controller = await createLinkController(table1, row)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
// get initial count // get initial count
const beforeLinks = await controller.getRowLinkDocs(row._id) const beforeLinks = await controller.getRowLinkDocs(row._id!)
await controller.rowDeleted() await controller.rowDeleted()
let afterLinks = await controller.getRowLinkDocs(row._id) let afterLinks = await controller.getRowLinkDocs(row._id!)
expect(beforeLinks.length).toEqual(1) expect(beforeLinks.length).toEqual(1)
expect(afterLinks.length).toEqual(0) expect(afterLinks.length).toEqual(0)
}) })
}) })
it("shouldn't throw an error when deleting a row with no links", async () => { it("shouldn't throw an error when deleting a row with no links", async () => {
const row = await config.createRow(basicRow(table1._id)) const row = await config.createRow(basicRow(table1._id!))
const controller = await createLinkController(table1, row) const controller = await createLinkController(table1, row)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
let error let error
@ -142,12 +186,13 @@ describe("test the link controller", () => {
const copyTable = { const copyTable = {
...table1, ...table1,
} }
//@ts-ignore
copyTable.schema.otherTableLink = { copyTable.schema.otherTableLink = {
type: "link", type: FieldType.LINK,
fieldName: "link", fieldName: "link",
tableId: table2._id, tableId: table2._id!,
} }
let error let error: any
try { try {
controller.validateTable(copyTable) controller.validateTable(copyTable)
} catch (err) { } catch (err) {
@ -166,7 +211,7 @@ describe("test the link controller", () => {
const controller = await createLinkController(table1, row) const controller = await createLinkController(table1, row)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
await controller.rowSaved() await controller.rowSaved()
let links = await controller.getRowLinkDocs(row._id) let links = await controller.getRowLinkDocs(row._id!)
expect(links.length).toEqual(0) expect(links.length).toEqual(0)
}) })
}) })
@ -186,7 +231,7 @@ describe("test the link controller", () => {
it("should be able to remove a linked field from a table", async () => { it("should be able to remove a linked field from a table", async () => {
await createLinkedRow() await createLinkedRow()
await createLinkedRow("link2") await createLinkedRow("link2")
const controller = await createLinkController(table1, null, table1) const controller = await createLinkController(table1, undefined, table1)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
let before = await controller.getTableLinkDocs() let before = await controller.getTableLinkDocs()
await controller.removeFieldFromTable("link") await controller.removeFieldFromTable("link")
@ -199,7 +244,8 @@ describe("test the link controller", () => {
it("should throw an error when overwriting a link column", async () => { it("should throw an error when overwriting a link column", async () => {
const update = cloneDeep(table1) const update = cloneDeep(table1)
update.schema.link.relationshipType = RelationshipType.MANY_TO_ONE const linkSchema = update.schema.link as ManyToOneRelationshipFieldMetadata
linkSchema.relationshipType = RelationshipType.MANY_TO_ONE
let error let error
try { try {
const controller = await createLinkController(update) const controller = await createLinkController(update)
@ -215,7 +261,7 @@ describe("test the link controller", () => {
await createLinkedRow() await createLinkedRow()
const newTable = cloneDeep(table1) const newTable = cloneDeep(table1)
delete newTable.schema.link delete newTable.schema.link
const controller = await createLinkController(newTable, null, table1) const controller = await createLinkController(newTable, undefined, table1)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
await controller.tableUpdated() await controller.tableUpdated()
const links = await controller.getTableLinkDocs() const links = await controller.getTableLinkDocs()
@ -235,7 +281,7 @@ describe("test the link controller", () => {
let error let error
try { try {
// create another row to initiate the error // create another row to initiate the error
await config.createRow(basicLinkedRow(row.tableId, row.link[0])) await config.createRow(basicLinkedRow(row.tableId!, row.link[0]))
} catch (err) { } catch (err) {
error = err error = err
} }
@ -245,7 +291,7 @@ describe("test the link controller", () => {
it("should not error if a link being created doesn't exist", async () => { it("should not error if a link being created doesn't exist", async () => {
let error let error
try { try {
await config.createRow(basicLinkedRow(table1._id, "invalid")) await config.createRow(basicLinkedRow(table1._id!, "invalid"))
} catch (err) { } catch (err) {
error = err error = err
} }
@ -255,10 +301,11 @@ describe("test the link controller", () => {
it("make sure auto column goes onto other row too", async () => { it("make sure auto column goes onto other row too", async () => {
const table = await config.createTable() const table = await config.createTable()
const tableCfg = basicTable() const tableCfg = basicTable()
//@ts-ignore
tableCfg.schema.link = { tableCfg.schema.link = {
type: "link", type: FieldType.LINK,
fieldName: "link", fieldName: "link",
tableId: table._id, tableId: table._id!,
name: "link", name: "link",
autocolumn: true, autocolumn: true,
} }
@ -269,10 +316,11 @@ describe("test the link controller", () => {
it("should be able to link to self", async () => { it("should be able to link to self", async () => {
const table = await config.createTable() const table = await config.createTable()
//@ts-ignore
table.schema.link = { table.schema.link = {
type: "link", type: FieldType.LINK,
fieldName: "link", fieldName: "link",
tableId: table._id, tableId: table._id!,
name: "link", name: "link",
autocolumn: true, autocolumn: true,
} }
@ -282,8 +330,9 @@ describe("test the link controller", () => {
it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => { it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => {
await createLinkedRow() await createLinkedRow()
await createLinkedRow("link2") await createLinkedRow("link2")
table1.schema["link"].tableId = "not_found" const linkSchema = table1.schema["link"] as RelationshipFieldMetadata
const controller = await createLinkController(table1, null, table1) linkSchema.tableId = "not_found"
const controller = await createLinkController(table1, undefined, table1)
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
let before = await controller.getTableLinkDocs() let before = await controller.getTableLinkDocs()
await controller.removeFieldFromTable("link") await controller.removeFieldFromTable("link")

View File

@ -1,14 +1,15 @@
const TestConfig = require("../../tests/utilities/TestConfiguration") import TestConfig from "../../tests/utilities/TestConfiguration"
const { basicTable } = require("../../tests/utilities/structures") import { basicTable } from "../../tests/utilities/structures"
const linkUtils = require("../linkedRows/linkUtils") import * as linkUtils from "../linkedRows/linkUtils"
const { context } = require("@budibase/backend-core") import { context } from "@budibase/backend-core"
import { FieldType, RelationshipType, Table } from "@budibase/types"
describe("test link functionality", () => { describe("test link functionality", () => {
const config = new TestConfig() const config = new TestConfig()
let appId let appId: string
describe("getLinkedTable", () => { describe("getLinkedTable", () => {
let table let table: Table
beforeAll(async () => { beforeAll(async () => {
const app = await config.init() const app = await config.init()
appId = app.appId appId = app.appId
@ -17,15 +18,15 @@ describe("test link functionality", () => {
it("should be able to retrieve a linked table from a list", async () => { it("should be able to retrieve a linked table from a list", async () => {
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
const retrieved = await linkUtils.getLinkedTable(table._id, [table]) const retrieved = await linkUtils.getLinkedTable(table._id!, [table])
expect(retrieved._id).toBe(table._id) expect(retrieved._id).toBe(table._id)
}) })
}) })
it("should be able to retrieve a table from DB and update list", async () => { it("should be able to retrieve a table from DB and update list", async () => {
const tables = [] const tables: Table[] = []
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
const retrieved = await linkUtils.getLinkedTable(table._id, tables) const retrieved = await linkUtils.getLinkedTable(table._id!, tables)
expect(retrieved._id).toBe(table._id) expect(retrieved._id).toBe(table._id)
expect(tables[0]).toBeDefined() expect(tables[0]).toBeDefined()
}) })
@ -35,9 +36,11 @@ describe("test link functionality", () => {
describe("getRelatedTableForField", () => { describe("getRelatedTableForField", () => {
let link = basicTable() let link = basicTable()
link.schema.link = { link.schema.link = {
name: "link",
relationshipType: RelationshipType.ONE_TO_MANY,
fieldName: "otherLink", fieldName: "otherLink",
tableId: "tableID", tableId: "tableID",
type: "link", type: FieldType.LINK,
} }
it("should get the field from the table directly", () => { it("should get the field from the table directly", () => {

View File

@ -1,6 +1,7 @@
import newid from "./newid" import newid from "./newid"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { import {
FieldType,
DocumentType, DocumentType,
FieldSchema, FieldSchema,
RelationshipFieldMetadata, RelationshipFieldMetadata,
@ -8,7 +9,6 @@ import {
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
DatabaseQueryOpts, DatabaseQueryOpts,
} from "@budibase/types" } from "@budibase/types"
import { FieldTypes } from "../constants"
export { DocumentType, VirtualDocumentType } from "@budibase/types" export { DocumentType, VirtualDocumentType } from "@budibase/types"
@ -315,5 +315,5 @@ export function extractViewInfoFromID(viewId: string) {
export function isRelationshipColumn( export function isRelationshipColumn(
column: FieldSchema column: FieldSchema
): column is RelationshipFieldMetadata { ): column is RelationshipFieldMetadata {
return column.type === FieldTypes.LINK return column.type === FieldType.LINK
} }

View File

@ -60,6 +60,7 @@ const environment = {
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins", PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
OPENAI_API_KEY: process.env.OPENAI_API_KEY, OPENAI_API_KEY: process.env.OPENAI_API_KEY,
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB, MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
// flags // flags
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING, DISABLE_THREADING: process.env.DISABLE_THREADING,

View File

@ -1,18 +1,11 @@
import { rowEmission, tableEmission } from "./utils" import { rowEmission, tableEmission } from "./utils"
import mainEmitter from "./index" import mainEmitter from "./index"
import env from "../environment" import env from "../environment"
import { Table, Row } from "@budibase/types" import { Table, Row, DocumentType, App } from "@budibase/types"
import { context } from "@budibase/backend-core"
// max number of automations that can chain on top of each other const MAX_AUTOMATIONS_ALLOWED = 5
// TODO: in future make this configurable at the automation level
const MAX_AUTOMATION_CHAIN = env.SELF_HOSTED ? 5 : 0
/**
* Special emitter which takes the count of automation runs which have occurred and blocks an
* automation from running if it has reached the maximum number of chained automations runs.
* This essentially "fakes" the normal emitter to add some functionality in-between to stop automations
* from getting stuck endlessly chaining.
*/
class AutomationEmitter { class AutomationEmitter {
chainCount: number chainCount: number
metadata: { automationChainCount: number } metadata: { automationChainCount: number }
@ -24,7 +17,23 @@ class AutomationEmitter {
} }
} }
emitRow(eventName: string, appId: string, row: Row, table?: Table) { async getMaxAutomationChain() {
const db = context.getAppDB()
const appMetadata = await db.get<App>(DocumentType.APP_METADATA)
let chainAutomations = appMetadata?.automations?.chainAutomations
if (chainAutomations === true) {
return MAX_AUTOMATIONS_ALLOWED
} else if (chainAutomations === undefined && env.SELF_HOSTED) {
return MAX_AUTOMATIONS_ALLOWED
} else {
return 0
}
}
async emitRow(eventName: string, appId: string, row: Row, table?: Table) {
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
// don't emit even if we've reached max automation chain // don't emit even if we've reached max automation chain
if (this.chainCount >= MAX_AUTOMATION_CHAIN) { if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
return return
@ -39,9 +48,11 @@ class AutomationEmitter {
}) })
} }
emitTable(eventName: string, appId: string, table?: Table) { async emitTable(eventName: string, appId: string, table?: Table) {
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
// don't emit even if we've reached max automation chain // don't emit even if we've reached max automation chain
if (this.chainCount > MAX_AUTOMATION_CHAIN) { if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
return return
} }

View File

@ -1,5 +1,6 @@
import { Knex, knex } from "knex" import { Knex, knex } from "knex"
import { import {
RelationshipType,
FieldSubtype, FieldSubtype,
NumberFieldMetadata, NumberFieldMetadata,
Operation, Operation,
@ -11,7 +12,6 @@ import {
import { breakExternalTableId } from "../utils" import { breakExternalTableId } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder import CreateTableBuilder = Knex.CreateTableBuilder
import { RelationshipType } from "../../constants"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
function isIgnoredType(type: FieldType) { function isIgnoredType(type: FieldType) {

View File

@ -1,4 +1,5 @@
import { import {
FieldType,
DatasourceFieldType, DatasourceFieldType,
Integration, Integration,
Operation, Operation,
@ -21,7 +22,6 @@ import {
SqlClient, SqlClient,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { FieldTypes } from "../constants"
import { import {
BindParameters, BindParameters,
Connection, Connection,
@ -302,7 +302,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
}) })
if (this.isBooleanType(oracleColumn)) { if (this.isBooleanType(oracleColumn)) {
fieldSchema.type = FieldTypes.BOOLEAN fieldSchema.type = FieldType.BOOLEAN
} }
table.schema[columnName] = fieldSchema table.schema[columnName] = fieldSchema

View File

@ -1,27 +1,23 @@
import { Datasource, SourceName } from "@budibase/types" import { Datasource, SourceName } from "@budibase/types"
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
import env from "../../../environment"
let container: StartedTestContainer | undefined let container: StartedTestContainer | undefined
const isMac = process.platform === "darwin"
export async function getDsConfig(): Promise<Datasource> { export async function getDsConfig(): Promise<Datasource> {
try { try {
if (!container) { if (!container) {
// postgres 15-bullseye safer bet on Linux container = await new GenericContainer("postgres:16.1-bullseye")
const version = isMac ? undefined : "15-bullseye"
container = await new GenericContainer("postgres", version)
.withExposedPorts(5432) .withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password") .withEnvironment({ POSTGRES_PASSWORD: "password" })
.withWaitStrategy( .withWaitStrategy(
Wait.forLogMessage( Wait.forLogMessage(
"PostgreSQL init process complete; ready for start up." "database system is ready to accept connections",
2
) )
) )
.start() .start()
} }
const host = container.getContainerIpAddress() const host = container.getHost()
const port = container.getMappedPort(5432) const port = container.getMappedPort(5432)
return { return {

View File

@ -376,8 +376,8 @@ export function checkExternalTables(
errors[name] = "Table must have a primary key." errors[name] = "Table must have a primary key."
} }
const schemaFields = Object.keys(table.schema) const columnNames = Object.keys(table.schema)
if (schemaFields.find(f => invalidColumns.includes(f))) { if (columnNames.find(f => invalidColumns.includes(f))) {
errors[name] = "Table contains invalid columns." errors[name] = "Table contains invalid columns."
} }
} }

View File

@ -34,7 +34,7 @@ const checkAuthorized = async (
const isCreatorApi = permType === PermissionType.CREATOR const isCreatorApi = permType === PermissionType.CREATOR
const isBuilderApi = permType === PermissionType.BUILDER const isBuilderApi = permType === PermissionType.BUILDER
const isGlobalBuilder = users.isGlobalBuilder(ctx.user) const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
const isCreator = users.isCreator(ctx.user) const isCreator = await users.isCreator(ctx.user)
const isBuilder = appId const isBuilder = appId
? users.isBuilder(ctx.user, appId) ? users.isBuilder(ctx.user, appId)
: users.hasBuilderPermissions(ctx.user) : users.hasBuilderPermissions(ctx.user)

View File

@ -61,7 +61,7 @@ export async function getInheritablePermissions(
export async function allowsExplicitPermissions(resourceId: string) { export async function allowsExplicitPermissions(resourceId: string) {
if (isViewID(resourceId)) { if (isViewID(resourceId)) {
const allowed = await features.isViewPermissionEnabled() const allowed = await features.isViewPermissionEnabled()
const minPlan = !allowed ? PlanType.BUSINESS : undefined const minPlan = !allowed ? PlanType.PREMIUM_PLUS : undefined
return { return {
allowed, allowed,

View File

@ -3,6 +3,27 @@ import { processStringSync } from "@budibase/string-templates"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { getQueryParams, isProdAppID } from "../../../db/utils" import { getQueryParams, isProdAppID } from "../../../db/utils"
import { BaseQueryVerbs } from "../../../constants" import { BaseQueryVerbs } from "../../../constants"
import { Query, QuerySchema } from "@budibase/types"
function updateSchema(query: Query): Query {
if (!query.schema) {
return query
}
const schema: Record<string, QuerySchema> = {}
for (let key of Object.keys(query.schema)) {
if (typeof query.schema[key] === "string") {
schema[key] = { type: query.schema[key] as string, name: key }
} else {
schema[key] = query.schema[key] as QuerySchema
}
}
query.schema = schema
return query
}
function updateSchemas(queries: Query[]): Query[] {
return queries.map(query => updateSchema(query))
}
// simple function to append "readable" to all read queries // simple function to append "readable" to all read queries
function enrichQueries(input: any) { function enrichQueries(input: any) {
@ -25,7 +46,7 @@ export async function find(queryId: string) {
delete query.fields delete query.fields
delete query.parameters delete query.parameters
} }
return query return updateSchema(query)
} }
export async function fetch(opts: { enrich: boolean } = { enrich: true }) { export async function fetch(opts: { enrich: boolean } = { enrich: true }) {
@ -37,12 +58,11 @@ export async function fetch(opts: { enrich: boolean } = { enrich: true }) {
}) })
) )
const queries = body.rows.map((row: any) => row.doc) let queries = body.rows.map((row: any) => row.doc)
if (opts.enrich) { if (opts.enrich) {
return enrichQueries(queries) queries = await enrichQueries(queries)
} else {
return queries
} }
return updateSchemas(queries)
} }
export async function enrichContext( export async function enrichContext(

View File

@ -1,7 +1,6 @@
import { CouchFindOptions, Table, Row } from "@budibase/types" import { FieldType, CouchFindOptions, Table, Row } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { DocumentType, SEPARATOR } from "../../../db/utils" import { DocumentType, SEPARATOR } from "../../../db/utils"
import { FieldTypes } from "../../../constants"
// default limit - seems to work well for performance // default limit - seems to work well for performance
export const FIND_LIMIT = 25 export const FIND_LIMIT = 25
@ -31,7 +30,7 @@ export async function getRowsWithAttachments(appId: string, table: Table) {
const db = dbCore.getDB(appId) const db = dbCore.getDB(appId)
const attachmentCols: string[] = [] const attachmentCols: string[] = []
for (let [key, column] of Object.entries(table.schema)) { for (let [key, column] of Object.entries(table.schema)) {
if (column.type === FieldTypes.ATTACHMENT) { if (column.type === FieldType.ATTACHMENT) {
attachmentCols.push(key) attachmentCols.push(key)
} }
} }

View File

@ -16,7 +16,6 @@ import {
expectAnyExternalColsAttributes, expectAnyExternalColsAttributes,
generator, generator,
} from "@budibase/backend-core/tests" } from "@budibase/backend-core/tests"
import datasource from "../../../../../api/routes/datasource"
jest.unmock("mysql2/promise") jest.unmock("mysql2/promise")
@ -30,13 +29,15 @@ describe.skip("external", () => {
beforeAll(async () => { beforeAll(async () => {
const container = await new GenericContainer("mysql") const container = await new GenericContainer("mysql")
.withExposedPorts(3306) .withExposedPorts(3306)
.withEnv("MYSQL_ROOT_PASSWORD", "admin") .withEnvironment({
.withEnv("MYSQL_DATABASE", "db") MYSQL_ROOT_PASSWORD: "admin",
.withEnv("MYSQL_USER", "user") MYSQL_DATABASE: "db",
.withEnv("MYSQL_PASSWORD", "password") MYSQL_USER: "user",
MYSQL_PASSWORD: "password",
})
.start() .start()
const host = container.getContainerIpAddress() const host = container.getHost()
const port = container.getMappedPort(3306) const port = container.getMappedPort(3306)
await config.init() await config.init()

View File

@ -7,7 +7,7 @@ import {
TableSourceType, TableSourceType,
FieldType, FieldType,
Table, Table,
AutoFieldSubTypes, AutoFieldSubType,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
@ -117,7 +117,7 @@ describe("sdk >> rows >> internal", () => {
id: { id: {
name: "id", name: "id",
type: FieldType.AUTO, type: FieldType.AUTO,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true, autocolumn: true,
lastID: 0, lastID: 0,
}, },
@ -181,7 +181,7 @@ describe("sdk >> rows >> internal", () => {
id: { id: {
name: "id", name: "id",
type: FieldType.AUTO, type: FieldType.AUTO,
subtype: AutoFieldSubTypes.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
autocolumn: true, autocolumn: true,
lastID: 0, lastID: 0,
}, },

View File

@ -1,7 +1,6 @@
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js" import validateJs from "validate.js"
import { Row, Table, TableSchema } from "@budibase/types" import { FieldType, Row, Table, TableSchema } from "@budibase/types"
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"
import sdk from "../.." import sdk from "../.."
@ -22,7 +21,7 @@ export function cleanExportRows(
let cleanRows = [...rows] let cleanRows = [...rows]
const relationships = Object.entries(schema) const relationships = Object.entries(schema)
.filter((entry: any[]) => entry[1].type === FieldTypes.LINK) .filter((entry: any[]) => entry[1].type === FieldType.LINK)
.map(entry => entry[0]) .map(entry => entry[0])
relationships.forEach(column => { relationships.forEach(column => {
@ -88,17 +87,17 @@ export async function validate({
continue continue
} }
// formulas shouldn't validated, data will be deleted anyway // formulas shouldn't validated, data will be deleted anyway
if (type === FieldTypes.FORMULA || column.autocolumn) { if (type === FieldType.FORMULA || column.autocolumn) {
continue continue
} }
// special case for options, need to always allow unselected (empty) // special case for options, need to always allow unselected (empty)
if (type === FieldTypes.OPTIONS && constraints?.inclusion) { if (type === FieldType.OPTIONS && constraints?.inclusion) {
constraints.inclusion.push(null as any, "") constraints.inclusion.push(null as any, "")
} }
let res let res
// Validate.js doesn't seem to handle array // Validate.js doesn't seem to handle array
if (type === FieldTypes.ARRAY && row[fieldName]) { if (type === FieldType.ARRAY && row[fieldName]) {
if (row[fieldName].length) { if (row[fieldName].length) {
if (!Array.isArray(row[fieldName])) { if (!Array.isArray(row[fieldName])) {
row[fieldName] = row[fieldName].split(",") row[fieldName] = row[fieldName].split(",")
@ -116,13 +115,13 @@ export async function validate({
errors[fieldName] = [`${fieldName} is required`] errors[fieldName] = [`${fieldName} is required`]
} }
} else if ( } else if (
(type === FieldTypes.ATTACHMENT || type === FieldTypes.JSON) && (type === FieldType.ATTACHMENT || type === FieldType.JSON) &&
typeof row[fieldName] === "string" typeof row[fieldName] === "string"
) { ) {
// this should only happen if there is an error // this should only happen if there is an error
try { try {
const json = JSON.parse(row[fieldName]) const json = JSON.parse(row[fieldName])
if (type === FieldTypes.ATTACHMENT) { if (type === FieldType.ATTACHMENT) {
if (Array.isArray(json)) { if (Array.isArray(json)) {
row[fieldName] = json row[fieldName] = json
} else { } else {

View File

@ -1,4 +1,5 @@
import { import {
FieldType,
Operation, Operation,
RelationshipType, RelationshipType,
RenameColumn, RenameColumn,
@ -14,7 +15,6 @@ import {
setStaticSchemas, setStaticSchemas,
} from "../../../../api/controllers/table/utils" } from "../../../../api/controllers/table/utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { FieldTypes } from "../../../../constants"
import { makeTableRequest } from "../../../../api/controllers/table/ExternalRequest" import { makeTableRequest } from "../../../../api/controllers/table/ExternalRequest"
import { import {
isRelationshipSetup, isRelationshipSetup,
@ -78,7 +78,7 @@ export async function save(
// check if relations need setup // check if relations need setup
for (let schema of Object.values(tableToSave.schema)) { for (let schema of Object.values(tableToSave.schema)) {
if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) { if (schema.type !== FieldType.LINK || isRelationshipSetup(schema)) {
continue continue
} }
const schemaTableId = schema.tableId const schemaTableId = schema.tableId

View File

@ -9,7 +9,6 @@ import {
Table, Table,
TableSourceType, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { FieldTypes } from "../../../../constants"
import { import {
foreignKeyStructure, foreignKeyStructure,
generateForeignKey, generateForeignKey,
@ -27,7 +26,7 @@ export function cleanupRelationships(
// clean up relationships in couch table schemas // clean up relationships in couch table schemas
for (let [key, schema] of Object.entries(tableToIterate.schema)) { for (let [key, schema] of Object.entries(tableToIterate.schema)) {
if ( if (
schema.type === FieldTypes.LINK && schema.type === FieldType.LINK &&
(!oldTable || table.schema[key] == null) (!oldTable || table.schema[key] == null)
) { ) {
const schemaTableId = schema.tableId const schemaTableId = schema.tableId

View File

@ -1,4 +1,5 @@
import { import {
FieldType,
RenameColumn, RenameColumn,
Table, Table,
ViewStatisticsSchema, ViewStatisticsSchema,
@ -10,7 +11,6 @@ import {
hasTypeChanged, hasTypeChanged,
TableSaveFunctions, TableSaveFunctions,
} from "../../../../api/controllers/table/utils" } from "../../../../api/controllers/table/utils"
import { FieldTypes } from "../../../../constants"
import { EventType, updateLinks } from "../../../../db/linkedRows" import { EventType, updateLinks } from "../../../../db/linkedRows"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
@ -63,7 +63,7 @@ export async function save(
} }
// rename row fields when table column is renamed // rename row fields when table column is renamed
if (renaming && table.schema[renaming.updated].type === FieldTypes.LINK) { if (renaming && table.schema[renaming.updated].type === FieldType.LINK) {
throw new Error("Cannot rename a linked column.") throw new Error("Cannot rename a linked column.")
} }

View File

@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => {
await syncGlobalUsers() await syncGlobalUsers()
const metadata = await rawUserMetadata() const metadata = await rawUserMetadata()
expect(metadata).toHaveLength(3) expect(metadata).toHaveLength(2)
expect(metadata).toContainEqual( expect(metadata).toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user1._id!), _id: db.generateUserMetadataID(user1._id!),
@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => {
await syncGlobalUsers() await syncGlobalUsers()
const metadata = await rawUserMetadata() const metadata = await rawUserMetadata()
expect(metadata).toHaveLength(0) expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application
}) })
}) })
}) })

View File

@ -278,6 +278,9 @@ class TestConfiguration {
if (params) { if (params) {
request.params = params request.params = params
} }
request.throw = (status: number, message: string) => {
throw new Error(`Error ${status} - ${message}`)
}
return this.doInContext(appId, async () => { return this.doInContext(appId, async () => {
await controlFunc(request) await controlFunc(request)
return request.body return request.body

View File

@ -1,3 +1,5 @@
import { QuerySchema, Row } from "@budibase/types"
export type WorkerCallback = (error: any, response?: any) => void export type WorkerCallback = (error: any, response?: any) => void
export interface QueryEvent { export interface QueryEvent {
@ -11,7 +13,15 @@ export interface QueryEvent {
queryId: string queryId: string
environmentVariables?: Record<string, string> environmentVariables?: Record<string, string>
ctx?: any ctx?: any
schema?: Record<string, { name?: string; type: string }> schema?: Record<string, QuerySchema | string>
}
export interface QueryResponse {
rows: Row[]
keys: string[]
info: any
extra: any
pagination: any
} }
export interface QueryVariable { export interface QueryVariable {

View File

@ -74,7 +74,7 @@ export class Thread {
) )
} }
run(job: AutomationJob | QueryEvent) { run<T>(job: AutomationJob | QueryEvent): Promise<T> {
const timeout = this.timeoutMs const timeout = this.timeoutMs
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
function fire(worker: any) { function fire(worker: any) {

View File

@ -1,7 +1,12 @@
import { default as threadUtils } from "./utils" import { default as threadUtils } from "./utils"
threadUtils.threadSetup() threadUtils.threadSetup()
import { WorkerCallback, QueryEvent, QueryVariable } from "./definitions" import {
WorkerCallback,
QueryEvent,
QueryVariable,
QueryResponse,
} from "./definitions"
import ScriptRunner from "../utilities/scriptRunner" import ScriptRunner from "../utilities/scriptRunner"
import { getIntegration } from "../integrations" import { getIntegration } from "../integrations"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
@ -9,7 +14,7 @@ import { context, cache, auth } from "@budibase/backend-core"
import { getGlobalIDFromUserMetadataID } from "../db/utils" import { getGlobalIDFromUserMetadataID } from "../db/utils"
import sdk from "../sdk" import sdk from "../sdk"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { SourceName, Query } from "@budibase/types" import { Query } from "@budibase/types"
import { isSQL } from "../integrations/utils" import { isSQL } from "../integrations/utils"
import { interpolateSQL } from "../integrations/queries/sql" import { interpolateSQL } from "../integrations/queries/sql"
@ -53,7 +58,7 @@ class QueryRunner {
this.hasDynamicVariables = false this.hasDynamicVariables = false
} }
async execute(): Promise<any> { async execute(): Promise<QueryResponse> {
let { datasource, fields, queryVerb, transformer, schema } = this let { datasource, fields, queryVerb, transformer, schema } = this
let datasourceClone = cloneDeep(datasource) let datasourceClone = cloneDeep(datasource)
let fieldsClone = cloneDeep(fields) let fieldsClone = cloneDeep(fields)

View File

@ -1,6 +1,12 @@
import { FieldTypes, ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import { context, db as dbCore, objectStore } from "@budibase/backend-core" import { context, db as dbCore, objectStore } from "@budibase/backend-core"
import { RenameColumn, Row, RowAttachment, Table } from "@budibase/types" import {
FieldType,
RenameColumn,
Row,
RowAttachment,
Table,
} from "@budibase/types"
export class AttachmentCleanup { export class AttachmentCleanup {
static async coreCleanup(fileListFn: () => string[]): Promise<void> { static async coreCleanup(fileListFn: () => string[]): Promise<void> {
@ -28,7 +34,7 @@ export class AttachmentCleanup {
let files: string[] = [] let files: string[] = []
const tableSchema = opts.oldTable?.schema || table.schema const tableSchema = opts.oldTable?.schema || table.schema
for (let [key, schema] of Object.entries(tableSchema)) { for (let [key, schema] of Object.entries(tableSchema)) {
if (schema.type !== FieldTypes.ATTACHMENT) { if (schema.type !== FieldType.ATTACHMENT) {
continue continue
} }
const columnRemoved = opts.oldTable && !table.schema[key] const columnRemoved = opts.oldTable && !table.schema[key]
@ -62,7 +68,7 @@ export class AttachmentCleanup {
return AttachmentCleanup.coreCleanup(() => { return AttachmentCleanup.coreCleanup(() => {
let files: string[] = [] let files: string[] = []
for (let [key, schema] of Object.entries(table.schema)) { for (let [key, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldTypes.ATTACHMENT) { if (schema.type !== FieldType.ATTACHMENT) {
continue continue
} }
rows.forEach(row => { rows.forEach(row => {
@ -79,7 +85,7 @@ export class AttachmentCleanup {
return AttachmentCleanup.coreCleanup(() => { return AttachmentCleanup.coreCleanup(() => {
let files: string[] = [] let files: string[] = []
for (let [key, schema] of Object.entries(table.schema)) { for (let [key, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldTypes.ATTACHMENT) { if (schema.type !== FieldType.ATTACHMENT) {
continue continue
} }
const oldKeys = const oldKeys =

View File

@ -1,10 +1,16 @@
import * as linkRows from "../../db/linkedRows" import * as linkRows from "../../db/linkedRows"
import { FieldTypes, AutoFieldSubTypes } from "../../constants"
import { processFormulas, fixAutoColumnSubType } from "./utils" import { processFormulas, fixAutoColumnSubType } from "./utils"
import { objectStore, utils } from "@budibase/backend-core" import { objectStore, utils } from "@budibase/backend-core"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { TYPE_TRANSFORM_MAP } from "./map" import { TYPE_TRANSFORM_MAP } from "./map"
import { FieldSubtype, Row, RowAttachment, Table } from "@budibase/types" import {
FieldType,
AutoFieldSubType,
FieldSubtype,
Row,
RowAttachment,
Table,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { import {
processInputBBReferences, processInputBBReferences,
@ -54,25 +60,25 @@ export function processAutoColumn(
schema = fixAutoColumnSubType(schema) schema = fixAutoColumnSubType(schema)
} }
switch (schema.subtype) { switch (schema.subtype) {
case AutoFieldSubTypes.CREATED_BY: case AutoFieldSubType.CREATED_BY:
if (creating && shouldUpdateUserFields && userId) { if (creating && shouldUpdateUserFields && userId) {
row[key] = [userId] row[key] = [userId]
} }
break break
case AutoFieldSubTypes.CREATED_AT: case AutoFieldSubType.CREATED_AT:
if (creating) { if (creating) {
row[key] = now row[key] = now
} }
break break
case AutoFieldSubTypes.UPDATED_BY: case AutoFieldSubType.UPDATED_BY:
if (shouldUpdateUserFields && userId) { if (shouldUpdateUserFields && userId) {
row[key] = [userId] row[key] = [userId]
} }
break break
case AutoFieldSubTypes.UPDATED_AT: case AutoFieldSubType.UPDATED_AT:
row[key] = now row[key] = now
break break
case AutoFieldSubTypes.AUTO_ID: case AutoFieldSubType.AUTO_ID:
if (creating) { if (creating) {
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1 schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
row[key] = schema.lastID row[key] = schema.lastID
@ -134,7 +140,7 @@ export async function inputProcessing(
continue continue
} }
// remove any formula values, they are to be generated // remove any formula values, they are to be generated
if (field.type === FieldTypes.FORMULA) { if (field.type === FieldType.FORMULA) {
delete clonedRow[key] delete clonedRow[key]
} }
// otherwise coerce what is there to correct types // otherwise coerce what is there to correct types
@ -143,7 +149,7 @@ export async function inputProcessing(
} }
// remove any attachment urls, they are generated on read // remove any attachment urls, they are generated on read
if (field.type === FieldTypes.ATTACHMENT) { if (field.type === FieldType.ATTACHMENT) {
const attachments = clonedRow[key] const attachments = clonedRow[key]
if (attachments?.length) { if (attachments?.length) {
attachments.forEach((attachment: RowAttachment) => { attachments.forEach((attachment: RowAttachment) => {
@ -152,7 +158,7 @@ export async function inputProcessing(
} }
} }
if (field.type === FieldTypes.BB_REFERENCE && value) { if (field.type === FieldType.BB_REFERENCE && value) {
clonedRow[key] = await processInputBBReferences( clonedRow[key] = await processInputBBReferences(
value, value,
field.subtype as FieldSubtype field.subtype as FieldSubtype
@ -214,7 +220,7 @@ export async function outputProcessing<T extends Row[] | Row>(
// process complex types: attachements, bb references... // process complex types: attachements, bb references...
for (let [property, column] of Object.entries(table.schema)) { for (let [property, column] of Object.entries(table.schema)) {
if (column.type === FieldTypes.ATTACHMENT) { if (column.type === FieldType.ATTACHMENT) {
for (let row of enriched) { for (let row of enriched) {
if (row[property] == null || !Array.isArray(row[property])) { if (row[property] == null || !Array.isArray(row[property])) {
continue continue
@ -227,7 +233,7 @@ export async function outputProcessing<T extends Row[] | Row>(
} }
} else if ( } else if (
!opts.skipBBReferences && !opts.skipBBReferences &&
column.type == FieldTypes.BB_REFERENCE column.type == FieldType.BB_REFERENCE
) { ) {
for (let row of enriched) { for (let row of enriched) {
row[property] = await processOutputBBReferences( row[property] = await processOutputBBReferences(

Some files were not shown because too many files have changed in this diff Show More