Merge branch 'master' into contrib-expose-fetchData-in-SDK
This commit is contained in:
commit
764cfc04ba
|
@ -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: |
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 },
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 ||
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 || [] }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
type,
|
type,
|
||||||
quiet,
|
quiet,
|
||||||
disabled,
|
disabled,
|
||||||
size,
|
size: size || "M",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
|
@ -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", () => {
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue