1
0
Fork 0
mirror of synced 2024-07-14 10:45:51 +12:00

Merge branch 'master' into feat/automation-naming-ux-updates

This commit is contained in:
Peter Clement 2023-10-31 13:08:17 +00:00 committed by GitHub
commit 8b3eafbede
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 1708 additions and 478 deletions

View file

@ -36,6 +36,7 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 18.x node-version: 18.x
cache: yarn
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- name: Update versions - name: Update versions
@ -66,14 +67,61 @@ jobs:
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build/release Docker images
- name: Docker login
run: | run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker
env: env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }}
- name: Build worker docker
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
build-args: |
BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }}
tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
file: ./packages/worker/Dockerfile.v2
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest
cache-to: type=inline
env:
IMAGE_NAME: budibase/worker
IMAGE_TAG: ${{ steps.currenttag.outputs.version }}
BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }}
- name: Build server docker
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
build-args: |
BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }}
tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
file: ./packages/server/Dockerfile.v2
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest
cache-to: type=inline
env:
IMAGE_NAME: budibase/apps
IMAGE_TAG: ${{ steps.currenttag.outputs.version }}
BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }}
- name: Build proxy docker
uses: docker/build-push-action@v5
with:
context: ./hosting/proxy
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
file: ./hosting/proxy/Dockerfile
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest
cache-to: type=inline
env:
IMAGE_NAME: budibase/proxy
IMAGE_TAG: ${{ steps.currenttag.outputs.version }}
release-helm-chart: release-helm-chart:
needs: [release-images] needs: [release-images]

View file

@ -1,18 +0,0 @@
#!/bin/bash
tag=$1
if [[ ! "$tag" ]]; then
echo "No tag present. You must pass a tag to this script"
exit 1
fi
echo "Tagging images with tag: $tag"
docker tag proxy-service budibase/proxy:$tag
docker tag app-service budibase/apps:$tag
docker tag worker-service budibase/worker:$tag
docker push --all-tags budibase/apps
docker push --all-tags budibase/worker
docker push --all-tags budibase/proxy

View file

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

View file

@ -54,10 +54,6 @@
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"", "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs", "build:specs": "lerna run --stream specs",
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",

View file

@ -21,7 +21,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/nano": "10.1.2", "@budibase/nano": "10.1.3",
"@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",

View file

@ -8,3 +8,7 @@ export const CONSTANT_INTERNAL_ROW_COLS = [
] as const ] as const
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
export function isInternalColumnName(name: string): boolean {
return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
}

View file

@ -6,6 +6,7 @@ import {
ViewName, ViewName,
} from "../constants" } from "../constants"
import { getProdAppID } from "./conversions" import { getProdAppID } from "./conversions"
import { DatabaseQueryOpts } from "@budibase/types"
/** /**
* If creating DB allDocs/query params with only a single top level ID this can be used, this * If creating DB allDocs/query params with only a single top level ID this can be used, this
@ -22,8 +23,8 @@ import { getProdAppID } from "./conversions"
export function getDocParams( export function getDocParams(
docType: string, docType: string,
docId?: string | null, docId?: string | null,
otherProps: any = {} otherProps: Partial<DatabaseQueryOpts> = {}
) { ): DatabaseQueryOpts {
if (docId == null) { if (docId == null) {
docId = "" docId = ""
} }
@ -45,8 +46,8 @@ export function getDocParams(
export function getRowParams( export function getRowParams(
tableId?: string | null, tableId?: string | null,
rowId?: string | null, rowId?: string | null,
otherProps = {} otherProps: Partial<DatabaseQueryOpts> = {}
) { ): DatabaseQueryOpts {
if (tableId == null) { if (tableId == null) {
return getDocParams(DocumentType.ROW, null, otherProps) return getDocParams(DocumentType.ROW, null, otherProps)
} }
@ -88,7 +89,10 @@ export const isDatasourceId = (id: string) => {
/** /**
* Gets parameters for retrieving workspaces. * Gets parameters for retrieving workspaces.
*/ */
export function getWorkspaceParams(id = "", otherProps = {}) { export function getWorkspaceParams(
id = "",
otherProps: Partial<DatabaseQueryOpts> = {}
): DatabaseQueryOpts {
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`, startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`,
@ -99,7 +103,10 @@ export function getWorkspaceParams(id = "", otherProps = {}) {
/** /**
* Gets parameters for retrieving users. * Gets parameters for retrieving users.
*/ */
export function getGlobalUserParams(globalId: any, otherProps: any = {}) { export function getGlobalUserParams(
globalId: any,
otherProps: Partial<DatabaseQueryOpts> = {}
): DatabaseQueryOpts {
if (!globalId) { if (!globalId) {
globalId = "" globalId = ""
} }
@ -117,11 +124,17 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
/** /**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. * Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/ */
export function getUserMetadataParams(userId?: string | null, otherProps = {}) { export function getUserMetadataParams(
userId?: string | null,
otherProps: Partial<DatabaseQueryOpts> = {}
): DatabaseQueryOpts {
return getRowParams(InternalTable.USER_METADATA, userId, otherProps) return getRowParams(InternalTable.USER_METADATA, userId, otherProps)
} }
export function getUsersByAppParams(appId: any, otherProps: any = {}) { export function getUsersByAppParams(
appId: any,
otherProps: Partial<DatabaseQueryOpts> = {}
): DatabaseQueryOpts {
const prodAppId = getProdAppID(appId) const prodAppId = getProdAppID(appId)
return { return {
...otherProps, ...otherProps,

View file

@ -165,13 +165,9 @@ export class UserDB {
} }
static async getUsersByAppAccess(opts: { appId?: string; limit?: number }) { static async getUsersByAppAccess(opts: { appId?: string; limit?: number }) {
const params: any = {
include_docs: true,
limit: opts.limit || 50,
}
let response: User[] = await usersCore.searchGlobalUsersByAppAccess( let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
opts.appId, opts.appId,
params { limit: opts.limit || 50 }
) )
return response return response
} }

View file

@ -20,6 +20,7 @@ import {
User, User,
ContextUser, ContextUser,
DatabaseQueryOpts, DatabaseQueryOpts,
CouchFindOptions,
} from "@budibase/types" } from "@budibase/types"
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import * as context from "../context" import * as context from "../context"
@ -140,7 +141,7 @@ export const getGlobalUserByEmail = async (
export const searchGlobalUsersByApp = async ( export const searchGlobalUsersByApp = async (
appId: any, appId: any,
opts: any, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) => {
if (typeof appId !== "string") { if (typeof appId !== "string") {
@ -166,7 +167,10 @@ export const searchGlobalUsersByApp = async (
Return any user who potentially has access to the application Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async (appId: any, opts: any) => { export const searchGlobalUsersByAppAccess = async (
appId: any,
opts?: { limit?: number }
) => {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -187,7 +191,7 @@ export const searchGlobalUsersByAppAccess = async (appId: any, opts: any) => {
orQuery.push(roleCheck) orQuery.push(roleCheck)
} }
let searchOptions = { let searchOptions: CouchFindOptions = {
selector: { selector: {
$or: orQuery, $or: orQuery,
_id: { _id: {
@ -198,7 +202,7 @@ export const searchGlobalUsersByAppAccess = async (appId: any, opts: any) => {
} }
const resp = await directCouchFind(context.getGlobalDBName(), searchOptions) const resp = await directCouchFind(context.getGlobalDBName(), searchOptions)
return resp?.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export const getGlobalUserByAppPage = (appId: string, user: User) => {

View file

@ -159,8 +159,10 @@
{#if selectedImage.size} {#if selectedImage.size}
<div class="filesize"> <div class="filesize">
{#if selectedImage.size <= BYTES_IN_MB} {#if selectedImage.size <= BYTES_IN_MB}
{`${selectedImage.size / BYTES_IN_KB} KB`} {`${(selectedImage.size / BYTES_IN_KB).toFixed(1)} KB`}
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if} {:else}{`${(selectedImage.size / BYTES_IN_MB).toFixed(
1
)} MB`}{/if}
</div> </div>
{/if} {/if}
{#if !disabled} {#if !disabled}
@ -203,8 +205,8 @@
{#if file.size} {#if file.size}
<div class="filesize"> <div class="filesize">
{#if file.size <= BYTES_IN_MB} {#if file.size <= BYTES_IN_MB}
{`${file.size / BYTES_IN_KB} KB`} {`${(file.size / BYTES_IN_KB).toFixed(1)} KB`}
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if} {:else}{`${(file.size / BYTES_IN_MB).toFixed(1)} MB`}{/if}
</div> </div>
{/if} {/if}
{#if !disabled} {#if !disabled}

View file

@ -580,7 +580,7 @@ export const getFrontendStore = () => {
let table = validTables.find(table => { let table = validTables.find(table => {
return ( return (
table.sourceId !== BUDIBASE_INTERNAL_DB_ID && table.sourceId !== BUDIBASE_INTERNAL_DB_ID &&
table.type === DB_TYPE_INTERNAL table.sourceType === DB_TYPE_INTERNAL
) )
}) })
if (table) { if (table) {
@ -591,7 +591,7 @@ export const getFrontendStore = () => {
table = validTables.find(table => { table = validTables.find(table => {
return ( return (
table.sourceId === BUDIBASE_INTERNAL_DB_ID && table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
table.type === DB_TYPE_INTERNAL table.sourceType === DB_TYPE_INTERNAL
) )
}) })
if (table) { if (table) {
@ -599,7 +599,7 @@ export const getFrontendStore = () => {
} }
// Finally try an external table // Finally try an external table
return validTables.find(table => table.type === DB_TYPE_EXTERNAL) return validTables.find(table => table.sourceType === DB_TYPE_EXTERNAL)
}, },
enrichEmptySettings: (component, opts) => { enrichEmptySettings: (component, opts) => {
if (!component?._component) { if (!component?._component) {

View file

@ -16,7 +16,6 @@
$: linkedTable = $tables.list.find(table => table._id === linkedTableId) $: linkedTable = $tables.list.find(table => table._id === linkedTableId)
$: schema = linkedTable?.schema $: schema = linkedTable?.schema
$: table = $tables.list.find(table => table._id === tableId) $: table = $tables.list.find(table => table._id === tableId)
$: type = table?.type
$: fetchData(tableId, rowId) $: fetchData(tableId, rowId)
$: { $: {
let rowLabel = row?.[table?.primaryDisplay] let rowLabel = row?.[table?.primaryDisplay]
@ -41,5 +40,5 @@
</script> </script>
{#if row && row._id === rowId} {#if row && row._id === rowId}
<Table {title} {schema} {data} {type} /> <Table {title} {schema} {data} />
{/if} {/if}

View file

@ -16,6 +16,7 @@
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte" import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte" import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte" import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
const userSchemaOverrides = { const userSchemaOverrides = {
firstName: { displayName: "First name", disabled: true }, firstName: { displayName: "First name", disabled: true },
@ -27,7 +28,7 @@
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS $: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.type !== "external" $: isInternal = $tables.selected?.sourceType !== DB_TYPE_EXTERNAL
$: gridDatasource = { $: gridDatasource = {
type: "table", type: "table",
tableId: id, tableId: id,
@ -46,10 +47,7 @@
tables.replaceTable(id, e.detail) tables.replaceTable(id, e.detail)
// We need to refresh datasources when an external table changes. // We need to refresh datasources when an external table changes.
// Type "external" may exist - sometimes type is "table" and sometimes it if (e.detail?.sourceType === DB_TYPE_EXTERNAL) {
// is "external" - it has different meanings in different endpoints.
// If we check both these then we hopefully catch all external tables.
if (e.detail?.type === "external" || e.detail?.sql) {
await datasources.fetch() await datasources.fetch()
} }
} }

View file

@ -17,7 +17,6 @@
let hideAutocolumns = true let hideAutocolumns = true
let data = [] let data = []
let loading = false let loading = false
let type = "internal"
$: name = view.name $: name = view.name
$: schema = view.schema $: schema = view.schema
@ -66,7 +65,6 @@
tableId={view.tableId} tableId={view.tableId}
{data} {data}
{loading} {loading}
{type}
rowCount={10} rowCount={10}
allowEditing={false} allowEditing={false}
bind:hideAutocolumns bind:hideAutocolumns

View file

@ -10,6 +10,6 @@
<ImportButton <ImportButton
{disabled} {disabled}
tableId={$datasource?.tableId} tableId={$datasource?.tableId}
tableType={$definition?.type} tableType={$definition?.sourceType}
on:importrows={rows.actions.refreshData} on:importrows={rows.actions.refreshData}
/> />

View file

@ -26,6 +26,7 @@
ALLOWABLE_NUMBER_TYPES, ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES, SWITCHABLE_TYPES,
PrettyRelationshipDefinitions, PrettyRelationshipDefinitions,
DB_TYPE_EXTERNAL,
} from "constants/backend" } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils" import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -254,10 +255,11 @@
!uneditable && !uneditable &&
editableColumn?.type !== AUTO_TYPE && editableColumn?.type !== AUTO_TYPE &&
!editableColumn.autocolumn !editableColumn.autocolumn
$: external = table.type === "external" $: externalTable = table.sourceType === DB_TYPE_EXTERNAL
// in the case of internal tables the sourceId will just be undefined // in the case of internal tables the sourceId will just be undefined
$: tableOptions = $tables.list.filter( $: tableOptions = $tables.list.filter(
opt => opt.type === table.type && table.sourceId === opt.sourceId opt =>
opt.sourceType === table.sourceType && table.sourceId === opt.sourceId
) )
$: typeEnabled = $: typeEnabled =
!originalName || !originalName ||
@ -409,7 +411,7 @@
editableColumn.type === FieldType.BB_REFERENCE && editableColumn.type === FieldType.BB_REFERENCE &&
editableColumn.subtype === FieldSubtype.USERS editableColumn.subtype === FieldSubtype.USERS
if (!external) { if (!externalTable) {
return [ return [
FIELDS.STRING, FIELDS.STRING,
FIELDS.BARCODEQR, FIELDS.BARCODEQR,
@ -441,7 +443,7 @@
isUsers ? FIELDS.USERS : FIELDS.USER, isUsers ? FIELDS.USERS : FIELDS.USER,
] ]
// no-sql or a spreadsheet // no-sql or a spreadsheet
if (!external || table.sql) { if (!externalTable || table.sql) {
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY] fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
} }
return fields return fields
@ -486,7 +488,7 @@
}) })
} }
const newError = {} const newError = {}
if (!external && fieldInfo.name?.startsWith("_")) { if (!externalTable && fieldInfo.name?.startsWith("_")) {
newError.name = `Column name cannot start with an underscore.` newError.name = `Column name cannot start with an underscore.`
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) { } else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
newError.name = `Illegal character; must be alpha-numeric.` newError.name = `Illegal character; must be alpha-numeric.`
@ -498,7 +500,7 @@
newError.name = `Column name already in use.` newError.name = `Column name already in use.`
} }
if (fieldInfo.type == "auto" && !fieldInfo.subtype) { if (fieldInfo.type === "auto" && !fieldInfo.subtype) {
newError.subtype = `Auto Column requires a type` newError.subtype = `Auto Column requires a type`
} }

View file

@ -1,6 +1,6 @@
<script> <script>
import { Select, Toggle, Multiselect } from "@budibase/bbui" import { Select, Toggle, Multiselect } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { DB_TYPE_INTERNAL, FIELDS } from "constants/backend"
import { API } from "api" import { API } from "api"
import { parseFile } from "./utils" import { parseFile } from "./utils"
@ -169,7 +169,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if tableType === "internal"} {#if tableType === DB_TYPE_INTERNAL}
<br /> <br />
<Toggle <Toggle
bind:value={updateExistingRows} bind:value={updateExistingRows}

View file

@ -8,6 +8,7 @@
import { import {
BUDIBASE_INTERNAL_DB_ID, BUDIBASE_INTERNAL_DB_ID,
BUDIBASE_DATASOURCE_TYPE, BUDIBASE_DATASOURCE_TYPE,
DB_TYPE_INTERNAL,
} from "constants/backend" } from "constants/backend"
$: tableNames = $tables.list.map(table => table.name) $: tableNames = $tables.list.map(table => table.name)
@ -55,8 +56,9 @@
name, name,
schema: { ...schema }, schema: { ...schema },
rows, rows,
type: "internal", type: "table",
sourceId: targetDatasourceId, sourceId: targetDatasourceId,
sourceType: DB_TYPE_INTERNAL,
} }
// Only set primary display if defined // Only set primary display if defined

View file

@ -13,6 +13,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
export let table export let table
@ -27,8 +28,8 @@
let willBeDeleted let willBeDeleted
let deleteTableName let deleteTableName
$: external = table?.type === "external" $: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
$: allowDeletion = !external || table?.created $: allowDeletion = !externalTable || table?.created
function showDeleteModal() { function showDeleteModal() {
templateScreens = $store.screens.filter( templateScreens = $store.screens.filter(
@ -48,7 +49,7 @@
for (let screen of templateScreens) { for (let screen of templateScreens) {
await store.actions.screens.delete(screen) await store.actions.screens.delete(screen)
} }
if (table.type === "external") { if (table.sourceType === DB_TYPE_EXTERNAL) {
await datasources.fetch() await datasources.fetch()
} }
notifications.success("Table deleted") notifications.success("Table deleted")
@ -91,7 +92,7 @@
<div slot="control" class="icon"> <div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" /> <Icon s hoverable name="MoreSmallList" />
</div> </div>
{#if !external} {#if !externalTable}
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem> <MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{/if} {/if}
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem> <MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>

View file

@ -23,7 +23,7 @@
try { try {
return await API.uploadBuilderAttachment(data) return await API.uploadBuilderAttachment(data)
} catch (error) { } catch (error) {
notifications.error("Failed to upload attachment") notifications.error(error.message || "Failed to upload attachment")
return [] return []
} }
} }

View file

@ -23,7 +23,7 @@
</script> </script>
<div class="table"> <div class="table">
<Table {schema} data={rowsCopy} type="external" allowEditing={false} /> <Table {schema} data={rowsCopy} allowEditing={false} />
</div> </div>
<style> <style>

View file

@ -2,6 +2,7 @@
import { ModalContent, Body, Input, notifications } from "@budibase/bbui" import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { DB_TYPE_EXTERNAL } from "constants/backend"
export let datasource export let datasource
@ -16,9 +17,10 @@
function buildDefaultTable(tableName, datasourceId) { function buildDefaultTable(tableName, datasourceId) {
return { return {
name: tableName, name: tableName,
type: "external", type: "table",
primary: ["id"], primary: ["id"],
sourceId: datasourceId, sourceId: datasourceId,
sourceType: DB_TYPE_EXTERNAL,
schema: { schema: {
id: { id: {
autocolumn: true, autocolumn: true,

View file

@ -5,7 +5,7 @@
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID, DB_TYPE_EXTERNAL } from "constants/backend"
import { TableNames } from "constants" import { TableNames } from "constants"
import { store } from "builderStore" import { store } from "builderStore"
@ -14,7 +14,7 @@
$: store.actions.websocket.selectResource(BUDIBASE_INTERNAL_DB_ID) $: store.actions.websocket.selectResource(BUDIBASE_INTERNAL_DB_ID)
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && table.sourceType !== DB_TYPE_EXTERNAL &&
table.sourceId === BUDIBASE_INTERNAL_DB_ID && table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
table._id !== TableNames.USERS table._id !== TableNames.USERS
) )

View file

@ -4,7 +4,7 @@
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend" import { DEFAULT_BB_DATASOURCE_ID, DB_TYPE_EXTERNAL } from "constants/backend"
import { onMount } from "svelte" import { onMount } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
@ -13,7 +13,8 @@
$: store.actions.websocket.selectResource(DEFAULT_BB_DATASOURCE_ID) $: store.actions.websocket.selectResource(DEFAULT_BB_DATASOURCE_ID)
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID table.sourceType !== DB_TYPE_EXTERNAL &&
table.sourceId === DEFAULT_BB_DATASOURCE_ID
) )
onMount(() => { onMount(() => {

View file

@ -103,7 +103,6 @@ const fetchRowHandler = async action => {
const deleteRowHandler = async action => { const deleteRowHandler = async action => {
const { tableId, rowId: rowConfig, notificationOverride } = action.parameters const { tableId, rowId: rowConfig, notificationOverride } = action.parameters
if (tableId && rowConfig) { if (tableId && rowConfig) {
try { try {
let requestConfig let requestConfig
@ -129,9 +128,11 @@ const deleteRowHandler = async action => {
requestConfig = [parsedRowConfig] requestConfig = [parsedRowConfig]
} else if (Array.isArray(parsedRowConfig)) { } else if (Array.isArray(parsedRowConfig)) {
requestConfig = parsedRowConfig requestConfig = parsedRowConfig
} else if (Number.isInteger(parsedRowConfig)) {
requestConfig = [String(parsedRowConfig)]
} }
if (!requestConfig.length) { if (!requestConfig && !parsedRowConfig) {
notificationStore.actions.warning("No valid rows were supplied") notificationStore.actions.warning("No valid rows were supplied")
return false return false
} }

View file

@ -140,4 +140,13 @@ export const buildTableEndpoints = API => ({
}, },
}) })
}, },
migrateColumn: async ({ tableId, oldColumn, newColumn }) => {
return await API.post({
url: `/api/tables/${tableId}/migrate`,
body: {
oldColumn,
newColumn,
},
})
},
}) })

View file

@ -55,7 +55,7 @@
try { try {
return await API.uploadBuilderAttachment(data) return await API.uploadBuilderAttachment(data)
} catch (error) { } catch (error) {
$notifications.error("Failed to upload attachment") $notifications.error(error.message || "Failed to upload attachment")
return [] return []
} }
} }

View file

@ -1,11 +1,20 @@
<script> <script>
import { getContext, onMount, tick } from "svelte" import { getContext, onMount, tick } from "svelte"
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core" import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui" import {
Icon,
Popover,
Menu,
MenuItem,
clickOutside,
Modal,
} from "@budibase/bbui"
import GridCell from "./GridCell.svelte" import GridCell from "./GridCell.svelte"
import { getColumnIcon } from "../lib/utils" import { getColumnIcon } from "../lib/utils"
import MigrationModal from "../controls/MigrationModal.svelte"
import { debounce } from "../../../utils/utils" import { debounce } from "../../../utils/utils"
import { FieldType, FormulaTypes } from "@budibase/types" import { FieldType, FormulaTypes } from "@budibase/types"
import { TableNames } from "../../../constants"
export let column export let column
export let idx export let idx
@ -45,6 +54,7 @@
let editIsOpen = false let editIsOpen = false
let timeout let timeout
let popover let popover
let migrationModal
let searchValue let searchValue
let input let input
@ -189,6 +199,11 @@
}) })
} }
const openMigrationModal = () => {
migrationModal.show()
open = false
}
const startSearching = async () => { const startSearching = async () => {
$focusedCellId = null $focusedCellId = null
searchValue = "" searchValue = ""
@ -224,6 +239,10 @@
onMount(() => subscribe("close-edit-column", cancelEdit)) onMount(() => subscribe("close-edit-column", cancelEdit))
</script> </script>
<Modal bind:this={migrationModal}>
<MigrationModal {column} />
</Modal>
<div <div
class="header-cell" class="header-cell"
class:open class:open
@ -363,6 +382,11 @@
> >
Hide column Hide column
</MenuItem> </MenuItem>
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS}
<MenuItem icon="User" on:click={openMigrationModal}>
Migrate to user column
</MenuItem>
{/if}
</Menu> </Menu>
{/if} {/if}
</Popover> </Popover>

View file

@ -0,0 +1,73 @@
<script>
import {
ModalContent,
notifications,
Input,
InlineAlert,
} from "@budibase/bbui"
import { getContext } from "svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
import { FieldSubtype, FieldType, RelationshipType } from "@budibase/types"
const { API, definition, rows } = getContext("grid")
export let column
let newColumnName = `${column.schema.name} migrated`
$: error = checkNewColumnName(newColumnName)
const checkNewColumnName = newColumnName => {
if (newColumnName === "") {
return "Column name can't be empty."
}
if (newColumnName in $definition.schema) {
return "New column name can't be the same as an existing column name."
}
if (newColumnName.match(ValidColumnNameRegex) === null) {
return "Illegal character; must be alpha-numeric."
}
}
const migrateUserColumn = async () => {
let subtype = FieldSubtype.USERS
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
subtype = FieldSubtype.USER
}
try {
await API.migrateColumn({
tableId: $definition._id,
oldColumn: column.schema,
newColumn: {
name: newColumnName,
type: FieldType.BB_REFERENCE,
subtype,
},
})
notifications.success("Column migrated")
} catch (e) {
notifications.error(`Failed to migrate: ${e.message}`)
}
await rows.actions.refreshData()
}
</script>
<ModalContent
title="Migrate column"
confirmText="Continue"
cancelText="Cancel"
onConfirm={migrateUserColumn}
disabled={error !== undefined}
size="M"
>
This operation will kick off a migration of the column "{column.schema.name}"
to a new column, with the name provided - this operation may take a moment to
complete.
<InlineAlert
type="error"
header="Are you sure?"
message="This will leave bindings which utilised the user relationship column in a state where they will need to be updated to use the new column instead."
/>
<Input bind:value={newColumnName} label="New column name" {error} />
</ModalContent>

View file

@ -18,7 +18,6 @@
"test": "bash scripts/test.sh", "test": "bash scripts/test.sh",
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit", "test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"build:docker": "yarn nx build && docker buildx build ../.. -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2 --platform linux/amd64,linux/arm64",
"run:docker": "node dist/index.js", "run:docker": "node dist/index.js",
"run:docker:cluster": "pm2-runtime start pm2.config.js", "run:docker:cluster": "pm2-runtime start pm2.config.js",
"dev:stack:up": "node scripts/dev/manage.js up", "dev:stack:up": "node scripts/dev/manage.js up",

View file

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

View file

@ -12,7 +12,6 @@ import {
CreateDatasourceResponse, CreateDatasourceResponse,
Datasource, Datasource,
DatasourcePlus, DatasourcePlus,
ExternalTable,
FetchDatasourceInfoRequest, FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
IntegrationBase, IntegrationBase,
@ -59,7 +58,7 @@ async function buildSchemaHelper(datasource: Datasource): Promise<Schema> {
const connector = (await getConnector(datasource)) as DatasourcePlus const connector = (await getConnector(datasource)) as DatasourcePlus
return await connector.buildSchema( return await connector.buildSchema(
datasource._id!, datasource._id!,
datasource.entities! as Record<string, ExternalTable> datasource.entities! as Record<string, Table>
) )
} }

View file

@ -1,12 +1,12 @@
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import { APP_PREFIX, DocumentType } from "../../../db/utils" import { APP_PREFIX, DocumentType } from "../../../db/utils"
export async function addRev( export async function addRev(
body: { _id?: string; _rev?: string }, body: { _id?: string; _rev?: string },
tableId?: string tableId?: string
) { ) {
if (!body._id || (tableId && isExternalTable(tableId))) { if (!body._id || (tableId && isExternalTableID(tableId))) {
return body return body
} }
let id = body._id let id = body._id

View file

@ -1,7 +1,7 @@
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import * as internal from "./internal" import * as internal from "./internal"
import * as external from "./external" import * as external from "./external"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import { import {
Ctx, Ctx,
UserCtx, UserCtx,
@ -30,7 +30,7 @@ import { Format } from "../view/exporters"
export * as views from "./views" export * as views from "./views"
function pickApi(tableId: any) { function pickApi(tableId: any) {
if (isExternalTable(tableId)) { if (isExternalTableID(tableId)) {
return external return external
} }
return internal return internal
@ -227,7 +227,7 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
export async function validate(ctx: Ctx<Row, ValidateResponse>) { export async function validate(ctx: Ctx<Row, ValidateResponse>) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
// external tables are hard to validate currently // external tables are hard to validate currently
if (isExternalTable(tableId)) { if (isExternalTableID(tableId)) {
ctx.body = { valid: true, errors: {} } ctx.body = { valid: true, errors: {} }
} else { } else {
ctx.body = await sdk.rows.utils.validate({ ctx.body = await sdk.rows.utils.validate({

View file

@ -1,3 +1,5 @@
import { ValidFileExtensions } from "@budibase/shared-core"
require("svelte/register") require("svelte/register")
import { join } from "../../../utilities/centralPath" import { join } from "../../../utilities/centralPath"
@ -11,34 +13,21 @@ import {
} from "../../../utilities/fileSystem" } from "../../../utilities/fileSystem"
import env from "../../../environment" import env from "../../../environment"
import { DocumentType } from "../../../db/utils" import { DocumentType } from "../../../db/utils"
import { context, objectStore, utils, configs } from "@budibase/backend-core" import {
context,
objectStore,
utils,
configs,
BadRequestError,
} from "@budibase/backend-core"
import AWS from "aws-sdk" import AWS from "aws-sdk"
import fs from "fs" import fs from "fs"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
import { App, Ctx } from "@budibase/types" import { App, Ctx, ProcessAttachmentResponse, Upload } from "@budibase/types"
const send = require("koa-send") const send = require("koa-send")
async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
const response = await objectStore.upload({
bucket,
metadata,
filename: s3Key,
path: file.path,
type: file.type,
})
// don't store a URL, work this out on the way out as the URL could change
return {
size: file.size,
name: file.name,
url: objectStore.getAppFileUrl(s3Key),
extension: [...file.name.split(".")].pop(),
key: response.Key,
}
}
export const toggleBetaUiFeature = async function (ctx: Ctx) { export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}` const cookieName = `beta:${ctx.params.feature}`
@ -72,23 +61,58 @@ export const serveBuilder = async function (ctx: Ctx) {
await send(ctx, ctx.file, { root: builderPath }) await send(ctx, ctx.file, { root: builderPath })
} }
export const uploadFile = async function (ctx: Ctx) { export const uploadFile = async function (
ctx: Ctx<{}, ProcessAttachmentResponse>
) {
const file = ctx.request?.files?.file const file = ctx.request?.files?.file
if (!file) {
throw new BadRequestError("No file provided")
}
let files = file && Array.isArray(file) ? Array.from(file) : [file] let files = file && Array.isArray(file) ? Array.from(file) : [file]
const uploads = files.map(async (file: any) => { ctx.body = await Promise.all(
const fileExtension = [...file.name.split(".")].pop() files.map(async file => {
// filenames converted to UUIDs so they are unique if (!file.name) {
const processedFileName = `${uuid.v4()}.${fileExtension}` throw new BadRequestError(
"Attempted to upload a file without a filename"
)
}
return prepareUpload({ const extension = [...file.name.split(".")].pop()
file, if (!extension) {
s3Key: `${context.getProdAppId()}/attachments/${processedFileName}`, throw new BadRequestError(
bucket: ObjectStoreBuckets.APPS, `File "${file.name}" has no extension, an extension is required to upload a file`
)
}
if (!env.SELF_HOSTED && !ValidFileExtensions.includes(extension)) {
throw new BadRequestError(
`File "${file.name}" has an invalid extension: "${extension}"`
)
}
// filenames converted to UUIDs so they are unique
const processedFileName = `${uuid.v4()}.${extension}`
const s3Key = `${context.getProdAppId()}/attachments/${processedFileName}`
const response = await objectStore.upload({
bucket: ObjectStoreBuckets.APPS,
filename: s3Key,
path: file.path,
type: file.type,
})
return {
size: file.size,
name: file.name,
url: objectStore.getAppFileUrl(s3Key),
extension,
key: response.Key,
}
}) })
}) )
ctx.body = await Promise.all(uploads)
} }
export const deleteObjects = async function (ctx: Ctx) { export const deleteObjects = async function (ctx: Ctx) {

View file

@ -5,18 +5,27 @@ import {
isSchema, isSchema,
validate as validateSchema, validate as validateSchema,
} from "../../../utilities/schema" } from "../../../utilities/schema"
import { isExternalTable, isSQL } from "../../../integrations/utils" import {
isExternalTable,
isExternalTableID,
isSQL,
} from "../../../integrations/utils"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { import {
BulkImportRequest, BulkImportRequest,
BulkImportResponse, BulkImportResponse,
DocumentType,
FetchTablesResponse, FetchTablesResponse,
MigrateRequest,
MigrateResponse,
Row,
SaveTableRequest, SaveTableRequest,
SaveTableResponse, SaveTableResponse,
Table, Table,
TableResponse, TableResponse,
TableSourceType,
UserCtx, UserCtx,
Row, SEPARATOR,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { jsonFromCsvString } from "../../../utilities/csv" import { jsonFromCsvString } from "../../../utilities/csv"
@ -24,12 +33,10 @@ import { builderSocket } from "../../../websockets"
import { cloneDeep, isEqual } from "lodash" import { cloneDeep, isEqual } from "lodash"
function pickApi({ tableId, table }: { tableId?: string; table?: Table }) { function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
if (table && !tableId) { if (table && isExternalTable(table)) {
tableId = table._id
}
if (table && table.type === "external") {
return external return external
} else if (tableId && isExternalTable(tableId)) { }
if (tableId && isExternalTableID(tableId)) {
return external return external
} }
return internal return internal
@ -46,8 +53,8 @@ export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) {
if (entities) { if (entities) {
return Object.values(entities).map<Table>((entity: Table) => ({ return Object.values(entities).map<Table>((entity: Table) => ({
...entity, ...entity,
type: "external", sourceType: TableSourceType.EXTERNAL,
sourceId: datasource._id, sourceId: datasource._id!,
sql: isSQL(datasource), sql: isSQL(datasource),
})) }))
} else { } else {
@ -158,3 +165,19 @@ export async function validateExistingTableImport(ctx: UserCtx) {
ctx.status = 422 ctx.status = 422
} }
} }
export async function migrate(ctx: UserCtx<MigrateRequest, MigrateResponse>) {
const { oldColumn, newColumn } = ctx.request.body
let tableId = ctx.params.tableId as string
const table = await sdk.tables.getTable(tableId)
let result = await sdk.tables.migrate(table, oldColumn, newColumn)
for (let table of result.tablesUpdated) {
builderSocket?.emitTableUpdate(ctx, table, {
includeOriginator: true,
})
}
ctx.status = 200
ctx.body = { message: `Column ${oldColumn.name} migrated.` }
}

View file

@ -7,6 +7,7 @@ import {
SaveTableRequest, SaveTableRequest,
SaveTableResponse, SaveTableResponse,
Table, Table,
TableSourceType,
UserCtx, UserCtx,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
@ -16,10 +17,11 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
let tableToSave: Table & { let tableToSave: Table & {
_rename?: RenameColumn _rename?: RenameColumn
} = { } = {
type: "table",
_id: generateTableID(), _id: generateTableID(),
views: {},
...rest, ...rest,
type: "table",
sourceType: TableSourceType.INTERNAL,
views: {},
} }
const renaming = tableToSave._rename const renaming = tableToSave._rename
delete tableToSave._rename delete tableToSave._rename

View file

@ -52,4 +52,11 @@ router
tableController.bulkImport tableController.bulkImport
) )
.post(
"/api/tables/:tableId/migrate",
paramResource("tableId"),
authorized(BUILDER),
tableController.migrate
)
export default router export default router

View file

@ -7,7 +7,7 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"entities": [ "entities": [
{ {
"_id": "ta_users", "_id": "ta_users",
"_rev": "1-2375e1bc58aeec664dc1b1f04ad43e44", "_rev": "1-73b7912e6cbdd3d696febc60f3715844",
"createdAt": "2020-01-01T00:00:00.000Z", "createdAt": "2020-01-01T00:00:00.000Z",
"name": "Users", "name": "Users",
"primaryDisplay": "email", "primaryDisplay": "email",
@ -21,7 +21,6 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"presence": true, "presence": true,
"type": "string", "type": "string",
}, },
"fieldName": "email",
"name": "email", "name": "email",
"type": "string", "type": "string",
}, },
@ -30,7 +29,6 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"presence": false, "presence": false,
"type": "string", "type": "string",
}, },
"fieldName": "firstName",
"name": "firstName", "name": "firstName",
"type": "string", "type": "string",
}, },
@ -39,7 +37,6 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"presence": false, "presence": false,
"type": "string", "type": "string",
}, },
"fieldName": "lastName",
"name": "lastName", "name": "lastName",
"type": "string", "type": "string",
}, },
@ -54,7 +51,6 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"presence": false, "presence": false,
"type": "string", "type": "string",
}, },
"fieldName": "roleId",
"name": "roleId", "name": "roleId",
"type": "options", "type": "options",
}, },
@ -67,11 +63,12 @@ exports[`/datasources fetch returns all the datasources from the server 1`] = `
"presence": false, "presence": false,
"type": "string", "type": "string",
}, },
"fieldName": "status",
"name": "status", "name": "status",
"type": "options", "type": "options",
}, },
}, },
"sourceId": "bb_internal",
"sourceType": "internal",
"type": "table", "type": "table",
"updatedAt": "2020-01-01T00:00:00.000Z", "updatedAt": "2020-01-01T00:00:00.000Z",
"views": {}, "views": {},

View file

@ -0,0 +1,49 @@
import * as setup from "./utilities"
import { APIError } from "@budibase/types"
describe("/api/applications/:appId/sync", () => {
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
describe("/api/attachments/process", () => {
it("should accept an image file upload", async () => {
let resp = await config.api.attachment.process(
"1px.jpg",
Buffer.from([0])
)
expect(resp.length).toBe(1)
let upload = resp[0]
expect(upload.url.endsWith(".jpg")).toBe(true)
expect(upload.extension).toBe("jpg")
expect(upload.size).toBe(1)
expect(upload.name).toBe("1px.jpg")
})
it("should reject an upload with a malicious file extension", async () => {
await config.withEnv({ SELF_HOSTED: undefined }, async () => {
let resp = (await config.api.attachment.process(
"ohno.exe",
Buffer.from([0]),
{ expectStatus: 400 }
)) as unknown as APIError
expect(resp.message).toContain("invalid extension")
})
})
it("should reject an upload with no file", async () => {
let resp = (await config.api.attachment.process(
undefined as any,
undefined as any,
{
expectStatus: 400,
}
)) as unknown as APIError
expect(resp.message).toContain("No file provided")
})
})
})

View file

@ -10,6 +10,7 @@ import {
FieldSchema, FieldSchema,
FieldType, FieldType,
FieldTypeSubtypes, FieldTypeSubtypes,
INTERNAL_TABLE_SOURCE_ID,
MonthlyQuotaName, MonthlyQuotaName,
PermissionLevel, PermissionLevel,
QuotaUsageType, QuotaUsageType,
@ -21,6 +22,7 @@ import {
SortType, SortType,
StaticQuotaName, StaticQuotaName,
Table, Table,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { import {
expectAnyExternalColsAttributes, expectAnyExternalColsAttributes,
@ -65,6 +67,8 @@ describe.each([
type: "table", type: "table",
primary: ["id"], primary: ["id"],
primaryDisplay: "name", primaryDisplay: "name",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: { schema: {
id: { id: {
type: FieldType.AUTO, type: FieldType.AUTO,
@ -134,9 +138,22 @@ describe.each([
} }
: undefined : undefined
async function createTable(
cfg: Omit<SaveTableRequest, "sourceId" | "sourceType">,
opts?: { skipReassigning: boolean }
) {
let table
if (dsProvider) {
table = await config.createExternalTable(cfg, opts)
} else {
table = await config.createTable(cfg, opts)
}
return table
}
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
const table = await config.createTable(tableConfig) let table = await createTable(tableConfig)
tableId = table._id! tableId = table._id!
}) })
@ -165,7 +182,7 @@ describe.each([
const queryUsage = await getQueryUsage() const queryUsage = await getQueryUsage()
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
const newTable = await config.createTable( const newTable = await createTable(
{ {
...tableConfig, ...tableConfig,
name: "TestTableAuto", name: "TestTableAuto",
@ -242,7 +259,7 @@ describe.each([
}) })
it("should list all rows for given tableId", async () => { it("should list all rows for given tableId", async () => {
const table = await config.createTable(generateTableConfig(), { const table = await createTable(generateTableConfig(), {
skipReassigning: true, skipReassigning: true,
}) })
const tableId = table._id! const tableId = table._id!
@ -323,7 +340,7 @@ describe.each([
inclusion: ["Alpha", "Beta", "Gamma"], inclusion: ["Alpha", "Beta", "Gamma"],
}, },
} }
const table = await config.createTable({ const table = await createTable({
name: "TestTable2", name: "TestTable2",
type: "table", type: "table",
schema: { schema: {
@ -438,7 +455,8 @@ describe.each([
describe("view save", () => { describe("view save", () => {
it("views have extra data trimmed", async () => { it("views have extra data trimmed", async () => {
const table = await config.createTable({ const table = await createTable({
type: "table",
name: "orders", name: "orders",
primary: ["OrderID"], primary: ["OrderID"],
schema: { schema: {
@ -494,7 +512,7 @@ describe.each([
describe("patch", () => { describe("patch", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should update only the fields that are supplied", async () => { it("should update only the fields that are supplied", async () => {
@ -548,7 +566,7 @@ describe.each([
describe("destroy", () => { describe("destroy", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should be able to delete a row", async () => { it("should be able to delete a row", async () => {
@ -566,7 +584,7 @@ describe.each([
describe("validate", () => { describe("validate", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should return no errors on valid row", async () => { it("should return no errors on valid row", async () => {
@ -603,7 +621,7 @@ describe.each([
describe("bulkDelete", () => { describe("bulkDelete", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should be able to delete a bulk set of rows", async () => { it("should be able to delete a bulk set of rows", async () => {
@ -687,7 +705,7 @@ describe.each([
describe("fetchView", () => { describe("fetchView", () => {
beforeEach(async () => { beforeEach(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should be able to fetch tables contents via 'view'", async () => { it("should be able to fetch tables contents via 'view'", async () => {
@ -735,7 +753,7 @@ describe.each([
describe("fetchEnrichedRows", () => { describe("fetchEnrichedRows", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should allow enriching some linked rows", async () => { it("should allow enriching some linked rows", async () => {
@ -808,7 +826,7 @@ describe.each([
describe("attachments", () => { describe("attachments", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should allow enriching attachment rows", async () => { it("should allow enriching attachment rows", async () => {
@ -839,7 +857,7 @@ describe.each([
describe("exportData", () => { describe("exportData", () => {
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
table = await config.createTable(tableConfig) table = await createTable(tableConfig)
}) })
it("should allow exporting all columns", async () => { it("should allow exporting all columns", async () => {
@ -880,6 +898,8 @@ describe.each([
async function userTable(): Promise<Table> { async function userTable(): Promise<Table> {
return { return {
name: `users_${generator.word()}`, name: `users_${generator.word()}`,
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
type: "table", type: "table",
primary: ["id"], primary: ["id"],
schema: { schema: {
@ -925,7 +945,7 @@ describe.each([
describe("create", () => { describe("create", () => {
it("should persist a new row with only the provided view fields", async () => { it("should persist a new row with only the provided view fields", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const view = await config.createView({ const view = await config.createView({
schema: { schema: {
name: { visible: true }, name: { visible: true },
@ -960,7 +980,7 @@ describe.each([
describe("patch", () => { describe("patch", () => {
it("should update only the view fields for a row", async () => { it("should update only the view fields for a row", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const tableId = table._id! const tableId = table._id!
const view = await config.createView({ const view = await config.createView({
schema: { schema: {
@ -1001,7 +1021,7 @@ describe.each([
describe("destroy", () => { describe("destroy", () => {
it("should be able to delete a row", async () => { it("should be able to delete a row", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const tableId = table._id! const tableId = table._id!
const view = await config.createView({ const view = await config.createView({
schema: { schema: {
@ -1025,7 +1045,7 @@ describe.each([
}) })
it("should be able to delete multiple rows", async () => { it("should be able to delete multiple rows", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const tableId = table._id! const tableId = table._id!
const view = await config.createView({ const view = await config.createView({
schema: { schema: {
@ -1062,6 +1082,8 @@ describe.each([
async function userTable(): Promise<Table> { async function userTable(): Promise<Table> {
return { return {
name: `users_${generator.word()}`, name: `users_${generator.word()}`,
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
type: "table", type: "table",
primary: ["id"], primary: ["id"],
schema: { schema: {
@ -1088,7 +1110,7 @@ describe.each([
} }
it("returns empty rows from view when no schema is passed", async () => { it("returns empty rows from view when no schema is passed", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const rows = await Promise.all( const rows = await Promise.all(
Array.from({ length: 10 }, () => Array.from({ length: 10 }, () =>
config.api.row.save(table._id!, { tableId: table._id }) config.api.row.save(table._id!, { tableId: table._id })
@ -1119,7 +1141,7 @@ describe.each([
}) })
it("searching respects the view filters", async () => { it("searching respects the view filters", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
await Promise.all( await Promise.all(
Array.from({ length: 10 }, () => Array.from({ length: 10 }, () =>
@ -1243,7 +1265,7 @@ describe.each([
describe("sorting", () => { describe("sorting", () => {
beforeAll(async () => { beforeAll(async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const users = [ const users = [
{ name: "Alice", age: 25 }, { name: "Alice", age: 25 },
{ name: "Bob", age: 30 }, { name: "Bob", age: 30 },
@ -1310,7 +1332,7 @@ describe.each([
}) })
it("when schema is defined, defined columns and row attributes are returned", async () => { it("when schema is defined, defined columns and row attributes are returned", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const rows = await Promise.all( const rows = await Promise.all(
Array.from({ length: 10 }, () => Array.from({ length: 10 }, () =>
config.api.row.save(table._id!, { config.api.row.save(table._id!, {
@ -1341,7 +1363,7 @@ describe.each([
}) })
it("views without data can be returned", async () => { it("views without data can be returned", async () => {
const table = await config.createTable(await userTable()) const table = await createTable(await userTable())
const createViewResponse = await config.createView() const createViewResponse = await config.createView()
const response = await config.api.viewV2.search(createViewResponse.id) const response = await config.api.viewV2.search(createViewResponse.id)
@ -1350,7 +1372,7 @@ describe.each([
}) })
it("respects the limit parameter", async () => { it("respects the limit parameter", async () => {
await config.createTable(await userTable()) await createTable(await userTable())
await Promise.all(Array.from({ length: 10 }, () => config.createRow())) await Promise.all(Array.from({ length: 10 }, () => config.createRow()))
const limit = generator.integer({ min: 1, max: 8 }) const limit = generator.integer({ min: 1, max: 8 })
@ -1365,7 +1387,7 @@ describe.each([
}) })
it("can handle pagination", async () => { it("can handle pagination", async () => {
await config.createTable(await userTable()) await createTable(await userTable())
await Promise.all(Array.from({ length: 10 }, () => config.createRow())) await Promise.all(Array.from({ length: 10 }, () => config.createRow()))
const createViewResponse = await config.createView() const createViewResponse = await config.createView()
@ -1443,7 +1465,7 @@ describe.each([
let tableId: string let tableId: string
beforeAll(async () => { beforeAll(async () => {
await config.createTable(await userTable()) await createTable(await userTable())
await Promise.all( await Promise.all(
Array.from({ length: 10 }, () => config.createRow()) Array.from({ length: 10 }, () => config.createRow())
) )
@ -1521,13 +1543,13 @@ describe.each([
let o2mTable: Table let o2mTable: Table
let m2mTable: Table let m2mTable: Table
beforeAll(async () => { beforeAll(async () => {
o2mTable = await config.createTable( o2mTable = await createTable(
{ ...generateTableConfig(), name: "o2m" }, { ...generateTableConfig(), name: "o2m" },
{ {
skipReassigning: true, skipReassigning: true,
} }
) )
m2mTable = await config.createTable( m2mTable = await createTable(
{ ...generateTableConfig(), name: "m2m" }, { ...generateTableConfig(), name: "m2m" },
{ {
skipReassigning: true, skipReassigning: true,
@ -1597,9 +1619,9 @@ describe.each([
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
if (config.datasource) { if (config.datasource) {
tableConfig.sourceId = config.datasource._id tableConfig.sourceId = config.datasource._id!
if (config.datasource.plus) { if (config.datasource.plus) {
tableConfig.type = "external" tableConfig.sourceType = TableSourceType.EXTERNAL
} }
} }
const table = await config.api.table.create({ const table = await config.api.table.create({

View file

@ -5,11 +5,15 @@ describe("/static", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let app let app
let cleanupEnv
afterAll(setup.afterAll) afterAll(() => {
setup.afterAll()
cleanupEnv()
})
beforeAll(async () => { beforeAll(async () => {
config.modeSelf() cleanupEnv = config.setEnv({ SELF_HOSTED: "true" })
app = await config.init() app = await config.init()
}) })

View file

@ -1,16 +1,24 @@
import { events, context } from "@budibase/backend-core" import { context, events } from "@budibase/backend-core"
import { import {
FieldType,
SaveTableRequest,
RelationshipType,
Table,
ViewCalculation,
AutoFieldSubTypes, AutoFieldSubTypes,
FieldSubtype,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
InternalTable,
RelationshipType,
Row,
SaveTableRequest,
Table,
TableSourceType,
User,
ViewCalculation,
} from "@budibase/types" } from "@budibase/types"
import { checkBuilderEndpoint } from "./utilities/TestFunctions" import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities" import * as setup from "./utilities"
const { basicTable } = setup.structures
import sdk from "../../../sdk" import sdk from "../../../sdk"
import uuid from "uuid"
const { basicTable } = setup.structures
describe("/tables", () => { describe("/tables", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -239,7 +247,8 @@ describe("/tables", () => {
.expect(200) .expect(200)
const fetchedTable = res.body[0] const fetchedTable = res.body[0]
expect(fetchedTable.name).toEqual(testTable.name) expect(fetchedTable.name).toEqual(testTable.name)
expect(fetchedTable.type).toEqual("internal") expect(fetchedTable.type).toEqual("table")
expect(fetchedTable.sourceType).toEqual("internal")
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
@ -417,4 +426,281 @@ describe("/tables", () => {
}) })
}) })
}) })
describe("migrate", () => {
let users: User[]
beforeAll(async () => {
users = await Promise.all([
config.createUser({ email: `${uuid.v4()}@example.com` }),
config.createUser({ email: `${uuid.v4()}@example.com` }),
config.createUser({ email: `${uuid.v4()}@example.com` }),
])
})
it("should successfully migrate a one-to-many user relationship to a user column", async () => {
const table = await config.api.table.create({
name: "table",
type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
},
},
})
const rows = await Promise.all(
users.map(u =>
config.api.row.save(table._id!, { "user relationship": [u] })
)
)
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
newColumn: {
name: "user column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USER,
},
})
const migratedTable = await config.api.table.get(table._id!)
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const migratedRows = await config.api.row.fetch(table._id!)
rows.sort((a, b) => a._id!.localeCompare(b._id!))
migratedRows.sort((a, b) => a._id!.localeCompare(b._id!))
for (const [i, row] of rows.entries()) {
const migratedRow = migratedRows[i]
expect(migratedRow["user column"]).toBeDefined()
expect(migratedRow["user relationship"]).not.toBeDefined()
expect(row["user relationship"][0]._id).toEqual(
migratedRow["user column"][0]._id
)
}
})
it("should successfully migrate a many-to-many user relationship to a users column", async () => {
const table = await config.api.table.create({
name: "table",
type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.MANY_TO_MANY,
tableId: InternalTable.USER_METADATA,
},
},
})
const row1 = await config.api.row.save(table._id!, {
"user relationship": [users[0], users[1]],
})
const row2 = await config.api.row.save(table._id!, {
"user relationship": [users[1], users[2]],
})
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
newColumn: {
name: "user column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
})
const migratedTable = await config.api.table.get(table._id!)
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const row1Migrated = (await config.api.row.get(table._id!, row1._id!))
.body as Row
expect(row1Migrated["user relationship"]).not.toBeDefined()
expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual(
expect.arrayContaining([users[0]._id, users[1]._id])
)
const row2Migrated = (await config.api.row.get(table._id!, row2._id!))
.body as Row
expect(row2Migrated["user relationship"]).not.toBeDefined()
expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual(
expect.arrayContaining([users[1]._id, users[2]._id])
)
})
it("should successfully migrate a many-to-one user relationship to a users column", async () => {
const table = await config.api.table.create({
name: "table",
type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.MANY_TO_ONE,
tableId: InternalTable.USER_METADATA,
},
},
})
const row1 = await config.api.row.save(table._id!, {
"user relationship": [users[0], users[1]],
})
const row2 = await config.api.row.save(table._id!, {
"user relationship": [users[2]],
})
await config.api.table.migrate(table._id!, {
oldColumn: table.schema["user relationship"],
newColumn: {
name: "user column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
})
const migratedTable = await config.api.table.get(table._id!)
expect(migratedTable.schema["user column"]).toBeDefined()
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
const row1Migrated = (await config.api.row.get(table._id!, row1._id!))
.body as Row
expect(row1Migrated["user relationship"]).not.toBeDefined()
expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual(
expect.arrayContaining([users[0]._id, users[1]._id])
)
const row2Migrated = (await config.api.row.get(table._id!, row2._id!))
.body as Row
expect(row2Migrated["user relationship"]).not.toBeDefined()
expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual([
users[2]._id,
])
})
describe("unhappy paths", () => {
let table: Table
beforeAll(async () => {
table = await config.api.table.create({
name: "table",
type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: {
"user relationship": {
type: FieldType.LINK,
fieldName: "test",
name: "user relationship",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.MANY_TO_ONE,
tableId: InternalTable.USER_METADATA,
},
num: {
type: FieldType.NUMBER,
name: "num",
constraints: {
type: "number",
presence: false,
},
},
},
})
})
it("should fail if the new column name is blank", async () => {
await config.api.table.migrate(
table._id!,
{
oldColumn: table.schema["user relationship"],
newColumn: {
name: "",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
},
{ expectStatus: 400 }
)
})
it("should fail if the new column name is a reserved name", async () => {
await config.api.table.migrate(
table._id!,
{
oldColumn: table.schema["user relationship"],
newColumn: {
name: "_id",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
},
{ expectStatus: 400 }
)
})
it("should fail if the new column name is the same as an existing column", async () => {
await config.api.table.migrate(
table._id!,
{
oldColumn: table.schema["user relationship"],
newColumn: {
name: "num",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
},
{ expectStatus: 400 }
)
})
it("should fail if the old column name isn't a column in the table", async () => {
await config.api.table.migrate(
table._id!,
{
oldColumn: {
name: "not a column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
newColumn: {
name: "new column",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
},
},
{ expectStatus: 400 }
)
})
})
})
}) })

View file

@ -3,10 +3,12 @@ import {
CreateViewRequest, CreateViewRequest,
FieldSchema, FieldSchema,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID,
SearchQueryOperators, SearchQueryOperators,
SortOrder, SortOrder,
SortType, SortType,
Table, Table,
TableSourceType,
UIFieldMetadata, UIFieldMetadata,
UpdateViewRequest, UpdateViewRequest,
ViewV2, ViewV2,
@ -18,6 +20,8 @@ function priceTable(): Table {
return { return {
name: "table", name: "table",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
Price: { Price: {
type: FieldType.NUMBER, type: FieldType.NUMBER,
@ -54,10 +58,10 @@ describe.each([
}, },
}) })
return config.createTable({ return config.createExternalTable({
...priceTable(), ...priceTable(),
sourceId: datasource._id, sourceId: datasource._id,
type: "external", sourceType: TableSourceType.EXTERNAL,
}) })
}, },
], ],

View file

@ -8,11 +8,15 @@ describe("/webhooks", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let webhook: Webhook let webhook: Webhook
let cleanupEnv: () => void
afterAll(setup.afterAll) afterAll(() => {
setup.afterAll()
cleanupEnv()
})
const setupTest = async () => { const setupTest = async () => {
config.modeSelf() cleanupEnv = config.setEnv({ SELF_HOSTED: "true" })
await config.init() await config.init()
const autoConfig = basicAutomation() const autoConfig = basicAutomation()
autoConfig.definition.trigger.schema = { autoConfig.definition.trigger.schema = {

View file

@ -1,5 +1,11 @@
import { objectStore, roles, constants } from "@budibase/backend-core" import { constants, objectStore, roles } from "@budibase/backend-core"
import { FieldType as FieldTypes } from "@budibase/types" import {
FieldType as FieldTypes,
INTERNAL_TABLE_SOURCE_ID,
Table,
TableSourceType,
} from "@budibase/types"
export { export {
FieldType as FieldTypes, FieldType as FieldTypes,
RelationshipType, RelationshipType,
@ -70,9 +76,11 @@ export enum SortDirection {
DESCENDING = "DESCENDING", DESCENDING = "DESCENDING",
} }
export const USERS_TABLE_SCHEMA = { export const USERS_TABLE_SCHEMA: Table = {
_id: "ta_users", _id: "ta_users",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
views: {}, views: {},
name: "Users", name: "Users",
// 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
@ -87,12 +95,10 @@ export const USERS_TABLE_SCHEMA = {
}, },
presence: true, presence: true,
}, },
fieldName: "email",
name: "email", name: "email",
}, },
firstName: { firstName: {
name: "firstName", name: "firstName",
fieldName: "firstName",
type: FieldTypes.STRING, type: FieldTypes.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldTypes.STRING,
@ -101,7 +107,6 @@ export const USERS_TABLE_SCHEMA = {
}, },
lastName: { lastName: {
name: "lastName", name: "lastName",
fieldName: "lastName",
type: FieldTypes.STRING, type: FieldTypes.STRING,
constraints: { constraints: {
type: FieldTypes.STRING, type: FieldTypes.STRING,
@ -109,7 +114,6 @@ export const USERS_TABLE_SCHEMA = {
}, },
}, },
roleId: { roleId: {
fieldName: "roleId",
name: "roleId", name: "roleId",
type: FieldTypes.OPTIONS, type: FieldTypes.OPTIONS,
constraints: { constraints: {
@ -119,7 +123,6 @@ export const USERS_TABLE_SCHEMA = {
}, },
}, },
status: { status: {
fieldName: "status",
name: "status", name: "status",
type: FieldTypes.OPTIONS, type: FieldTypes.OPTIONS,
constraints: { constraints: {

View file

@ -1,4 +1,4 @@
import { FieldTypes, AutoFieldSubTypes } from "../../constants" import { AutoFieldSubTypes, FieldTypes } from "../../constants"
import { importToRows } from "../../api/controllers/table/utils" import { importToRows } from "../../api/controllers/table/utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import LinkDocument from "../linkedRows/LinkDocument" import LinkDocument from "../linkedRows/LinkDocument"
@ -8,11 +8,12 @@ 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 {
Table,
Row,
RelationshipType,
FieldType, FieldType,
RelationshipType,
Row,
Table,
TableSchema, TableSchema,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs" export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
@ -89,9 +90,10 @@ const AUTO_COLUMNS: TableSchema = {
export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = { export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
_id: DEFAULT_INVENTORY_TABLE_ID, _id: DEFAULT_INVENTORY_TABLE_ID,
type: "internal", type: "table",
views: {}, views: {},
sourceId: DEFAULT_BB_DATASOURCE_ID, sourceId: DEFAULT_BB_DATASOURCE_ID,
sourceType: TableSourceType.INTERNAL,
primaryDisplay: "Item Name", primaryDisplay: "Item Name",
name: "Inventory", name: "Inventory",
schema: { schema: {
@ -198,10 +200,11 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = { export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
_id: DEFAULT_EMPLOYEE_TABLE_ID, _id: DEFAULT_EMPLOYEE_TABLE_ID,
type: "internal", type: "table",
views: {}, views: {},
name: "Employees", name: "Employees",
sourceId: DEFAULT_BB_DATASOURCE_ID, sourceId: DEFAULT_BB_DATASOURCE_ID,
sourceType: TableSourceType.INTERNAL,
primaryDisplay: "First Name", primaryDisplay: "First Name",
schema: { schema: {
"First Name": { "First Name": {
@ -346,9 +349,10 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
export const DEFAULT_JOBS_TABLE_SCHEMA: Table = { export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
_id: DEFAULT_JOBS_TABLE_ID, _id: DEFAULT_JOBS_TABLE_ID,
type: "internal", type: "table",
name: "Jobs", name: "Jobs",
sourceId: DEFAULT_BB_DATASOURCE_ID, sourceId: DEFAULT_BB_DATASOURCE_ID,
sourceType: TableSourceType.INTERNAL,
primaryDisplay: "Job ID", primaryDisplay: "Job ID",
schema: { schema: {
"Job ID": { "Job ID": {
@ -503,10 +507,11 @@ export const DEFAULT_JOBS_TABLE_SCHEMA: Table = {
export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = { export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
_id: DEFAULT_EXPENSES_TABLE_ID, _id: DEFAULT_EXPENSES_TABLE_ID,
type: "internal", type: "table",
views: {}, views: {},
name: "Expenses", name: "Expenses",
sourceId: DEFAULT_BB_DATASOURCE_ID, sourceId: DEFAULT_BB_DATASOURCE_ID,
sourceType: TableSourceType.INTERNAL,
primaryDisplay: "Expense ID", primaryDisplay: "Expense ID",
schema: { schema: {
"Expense ID": { "Expense ID": {

View file

@ -2,7 +2,12 @@ import { ViewName, getQueryIndex, isRelationshipColumn } from "../utils"
import { FieldTypes } from "../../constants" 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 { LinkDocument, LinkDocumentValue, Table } from "@budibase/types" import {
DatabaseQueryOpts,
LinkDocument,
LinkDocumentValue,
Table,
} from "@budibase/types"
export { createLinkView } from "../views/staticViews" export { createLinkView } from "../views/staticViews"
@ -36,13 +41,13 @@ export async function getLinkDocuments(args: {
}): Promise<LinkDocumentValue[] | LinkDocument[]> { }): Promise<LinkDocumentValue[] | LinkDocument[]> {
const { tableId, rowId, fieldName, includeDocs } = args const { tableId, rowId, fieldName, includeDocs } = args
const db = context.getAppDB() const db = context.getAppDB()
let params: any let params: DatabaseQueryOpts
if (rowId) { if (rowId) {
params = { key: [tableId, rowId] } params = { key: [tableId, rowId] }
} }
// only table is known // only table is known
else { else {
params = { startKey: [tableId], endKey: [tableId, {}] } params = { startkey: [tableId], endkey: [tableId, {}] }
} }
if (includeDocs) { if (includeDocs) {
params.include_docs = true params.include_docs = true

View file

@ -5,6 +5,7 @@ import {
FieldSchema, FieldSchema,
RelationshipFieldMetadata, RelationshipFieldMetadata,
VirtualDocumentType, VirtualDocumentType,
INTERNAL_TABLE_SOURCE_ID,
} from "@budibase/types" } from "@budibase/types"
import { FieldTypes } from "../constants" import { FieldTypes } from "../constants"
export { DocumentType, VirtualDocumentType } from "@budibase/types" export { DocumentType, VirtualDocumentType } from "@budibase/types"
@ -18,7 +19,7 @@ export const enum AppStatus {
} }
export const BudibaseInternalDB = { export const BudibaseInternalDB = {
_id: "bb_internal", _id: INTERNAL_TABLE_SOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE, type: dbCore.BUDIBASE_DATASOURCE_TYPE,
name: "Budibase DB", name: "Budibase DB",
source: "BUDIBASE", source: "BUDIBASE",

View file

@ -75,7 +75,6 @@ const environment = {
}, },
isTest: coreEnv.isTest, isTest: coreEnv.isTest,
isJest: coreEnv.isJest, isJest: coreEnv.isJest,
isDev: coreEnv.isDev, isDev: coreEnv.isDev,
isProd: () => { isProd: () => {
return !coreEnv.isDev() return !coreEnv.isDev()

View file

@ -1,6 +1,4 @@
import fetch from "node-fetch" import fetch from "node-fetch"
// @ts-ignore
fetch.mockSearch()
import { import {
generateMakeRequest, generateMakeRequest,
MakeRequestResponse, MakeRequestResponse,
@ -13,12 +11,15 @@ import {
RelationshipType, RelationshipType,
Row, Row,
Table, Table,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import _ from "lodash" import _ from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core" import { utils } from "@budibase/backend-core"
import { databaseTestProviders } from "../integrations/tests/utils" import { databaseTestProviders } from "../integrations/tests/utils"
import { Client } from "pg" import { Client } from "pg"
// @ts-ignore
fetch.mockSearch()
const config = setup.getConfig()! const config = setup.getConfig()!
@ -52,7 +53,7 @@ describe("postgres integrations", () => {
async function createAuxTable(prefix: string) { async function createAuxTable(prefix: string) {
return await config.createTable({ return await config.createTable({
name: `${prefix}_${generator.word({ length: 6 })}`, name: `${prefix}_${generator.word({ length: 6 })}`,
type: "external", type: "table",
primary: ["id"], primary: ["id"],
primaryDisplay: "title", primaryDisplay: "title",
schema: { schema: {
@ -67,6 +68,7 @@ describe("postgres integrations", () => {
}, },
}, },
sourceId: postgresDatasource._id, sourceId: postgresDatasource._id,
sourceType: TableSourceType.EXTERNAL,
}) })
} }
@ -88,7 +90,7 @@ describe("postgres integrations", () => {
primaryPostgresTable = await config.createTable({ primaryPostgresTable = await config.createTable({
name: `p_${generator.word({ length: 6 })}`, name: `p_${generator.word({ length: 6 })}`,
type: "external", type: "table",
primary: ["id"], primary: ["id"],
schema: { schema: {
id: { id: {
@ -143,6 +145,7 @@ describe("postgres integrations", () => {
}, },
}, },
sourceId: postgresDatasource._id, sourceId: postgresDatasource._id,
sourceType: TableSourceType.EXTERNAL,
}) })
}) })
@ -249,7 +252,7 @@ describe("postgres integrations", () => {
async function createDefaultPgTable() { async function createDefaultPgTable() {
return await config.createTable({ return await config.createTable({
name: generator.word({ length: 10 }), name: generator.word({ length: 10 }),
type: "external", type: "table",
primary: ["id"], primary: ["id"],
schema: { schema: {
id: { id: {
@ -259,6 +262,7 @@ describe("postgres integrations", () => {
}, },
}, },
sourceId: postgresDatasource._id, sourceId: postgresDatasource._id,
sourceType: TableSourceType.EXTERNAL,
}) })
} }

View file

@ -10,11 +10,12 @@ import {
QueryJson, QueryJson,
QueryType, QueryType,
Row, Row,
Schema,
SearchFilters, SearchFilters,
SortJson, SortJson,
ExternalTable, Table,
TableRequest, TableRequest,
Schema, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { OAuth2Client } from "google-auth-library" import { OAuth2Client } from "google-auth-library"
import { import {
@ -262,11 +263,13 @@ class GoogleSheetsIntegration implements DatasourcePlus {
id?: string id?: string
) { ) {
// base table // base table
const table: ExternalTable = { const table: Table = {
type: "table",
name: title, name: title,
primary: [GOOGLE_SHEETS_PRIMARY_KEY], primary: [GOOGLE_SHEETS_PRIMARY_KEY],
schema: {}, schema: {},
sourceId: datasourceId, sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
} }
if (id) { if (id) {
table._id = id table._id = id
@ -283,7 +286,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Promise<Schema> { ): Promise<Schema> {
// not fully configured yet // not fully configured yet
if (!this.config.auth) { if (!this.config.auth) {
@ -291,7 +294,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
} }
await this.connect() await this.connect()
const sheets = this.client.sheetsByIndex const sheets = this.client.sheetsByIndex
const tables: Record<string, ExternalTable> = {} const tables: Record<string, Table> = {}
let errors: Record<string, string> = {} let errors: Record<string, string> = {}
await utils.parallelForeach( await utils.parallelForeach(
sheets, sheets,

View file

@ -2,7 +2,7 @@ import {
DatasourceFieldType, DatasourceFieldType,
Integration, Integration,
Operation, Operation,
ExternalTable, Table,
TableSchema, TableSchema,
QueryJson, QueryJson,
QueryType, QueryType,
@ -12,6 +12,7 @@ import {
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema, Schema,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -380,7 +381,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Promise<Schema> { ): Promise<Schema> {
await this.connect() await this.connect()
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL) let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
@ -394,7 +395,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
.map((record: any) => record.TABLE_NAME) .map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1) .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
const tables: Record<string, ExternalTable> = {} const tables: Record<string, Table> = {}
for (let tableName of tableNames) { for (let tableName of tableNames) {
// get the column definition (type) // get the column definition (type)
const definition = await this.runSQL( const definition = await this.runSQL(
@ -439,7 +440,9 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
tables[tableName] = { tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName), _id: buildExternalTableId(datasourceId, tableName),
type: "table",
sourceId: datasourceId, sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
primary: primaryKeys, primary: primaryKeys,
name: tableName, name: tableName,
schema, schema,

View file

@ -4,13 +4,14 @@ import {
QueryType, QueryType,
QueryJson, QueryJson,
SqlQuery, SqlQuery,
ExternalTable, Table,
TableSchema, TableSchema,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema, Schema,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -278,9 +279,9 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Promise<Schema> { ): Promise<Schema> {
const tables: { [key: string]: ExternalTable } = {} const tables: { [key: string]: Table } = {}
await this.connect() await this.connect()
try { try {
@ -317,8 +318,10 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} }
if (!tables[tableName]) { if (!tables[tableName]) {
tables[tableName] = { tables[tableName] = {
type: "table",
_id: buildExternalTableId(datasourceId, tableName), _id: buildExternalTableId(datasourceId, tableName),
sourceId: datasourceId, sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
primary: primaryKeys, primary: primaryKeys,
name: tableName, name: tableName,
schema, schema,

View file

@ -5,11 +5,12 @@ import {
QueryJson, QueryJson,
QueryType, QueryType,
SqlQuery, SqlQuery,
ExternalTable, Table,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
Schema, Schema,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { import {
buildExternalTableId, buildExternalTableId,
@ -263,25 +264,27 @@ class OracleIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Promise<Schema> { ): Promise<Schema> {
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL, sql: this.COLUMNS_SQL,
}) })
const oracleTables = this.mapColumns(columnsResponse) const oracleTables = this.mapColumns(columnsResponse)
const tables: { [key: string]: ExternalTable } = {} const tables: { [key: string]: Table } = {}
// iterate each table // iterate each table
Object.values(oracleTables).forEach(oracleTable => { Object.values(oracleTables).forEach(oracleTable => {
let table = tables[oracleTable.name] let table = tables[oracleTable.name]
if (!table) { if (!table) {
table = { table = {
type: "table",
_id: buildExternalTableId(datasourceId, oracleTable.name), _id: buildExternalTableId(datasourceId, oracleTable.name),
primary: [], primary: [],
name: oracleTable.name, name: oracleTable.name,
schema: {}, schema: {},
sourceId: datasourceId, sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
} }
tables[oracleTable.name] = table tables[oracleTable.name] = table
} }

View file

@ -5,12 +5,13 @@ import {
QueryType, QueryType,
QueryJson, QueryJson,
SqlQuery, SqlQuery,
ExternalTable, Table,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema, Schema,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -273,7 +274,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Promise<Schema> { ): Promise<Schema> {
let tableKeys: { [key: string]: string[] } = {} let tableKeys: { [key: string]: string[] } = {}
await this.openConnection() await this.openConnection()
@ -300,7 +301,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
const columnsResponse: { rows: PostgresColumn[] } = const columnsResponse: { rows: PostgresColumn[] } =
await this.client.query(this.COLUMNS_SQL) await this.client.query(this.COLUMNS_SQL)
const tables: { [key: string]: ExternalTable } = {} const tables: { [key: string]: Table } = {}
for (let column of columnsResponse.rows) { for (let column of columnsResponse.rows) {
const tableName: string = column.table_name const tableName: string = column.table_name
@ -309,11 +310,13 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
// table key doesn't exist yet // table key doesn't exist yet
if (!tables[tableName] || !tables[tableName].schema) { if (!tables[tableName] || !tables[tableName].schema) {
tables[tableName] = { tables[tableName] = {
type: "table",
_id: buildExternalTableId(datasourceId, tableName), _id: buildExternalTableId(datasourceId, tableName),
primary: tableKeys[tableName] || [], primary: tableKeys[tableName] || [],
name: tableName, name: tableName,
schema: {}, schema: {},
sourceId: datasourceId, sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
} }
} }

View file

@ -30,18 +30,24 @@ GoogleSpreadsheet.mockImplementation(() => mockGoogleIntegration)
import { structures } from "@budibase/backend-core/tests" import { structures } from "@budibase/backend-core/tests"
import TestConfiguration from "../../tests/utilities/TestConfiguration" import TestConfiguration from "../../tests/utilities/TestConfiguration"
import GoogleSheetsIntegration from "../googlesheets" import GoogleSheetsIntegration from "../googlesheets"
import { FieldType, Table, TableSchema } from "@budibase/types" import { FieldType, Table, TableSchema, TableSourceType } from "@budibase/types"
import { generateDatasourceID } from "../../db/utils"
describe("Google Sheets Integration", () => { describe("Google Sheets Integration", () => {
let integration: any, let integration: any,
config = new TestConfiguration() config = new TestConfiguration()
let cleanupEnv: () => void
beforeAll(() => { beforeAll(() => {
config.setGoogleAuth("test") cleanupEnv = config.setEnv({
GOOGLE_CLIENT_ID: "test",
GOOGLE_CLIENT_SECRET: "test",
})
}) })
afterAll(async () => { afterAll(async () => {
await config.end() cleanupEnv()
config.end()
}) })
beforeEach(async () => { beforeEach(async () => {
@ -60,7 +66,10 @@ describe("Google Sheets Integration", () => {
function createBasicTable(name: string, columns: string[]): Table { function createBasicTable(name: string, columns: string[]): Table {
return { return {
type: "table",
name, name,
sourceId: generateDatasourceID(),
sourceType: TableSourceType.EXTERNAL,
schema: { schema: {
...columns.reduce((p, c) => { ...columns.reduce((p, c) => {
p[c] = { p[c] = {

View file

@ -1,39 +1,47 @@
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> {
if (!container) { try {
container = await new GenericContainer("postgres") if (!container) {
.withExposedPorts(5432) // postgres 15-bullseye safer bet on Linux
.withEnv("POSTGRES_PASSWORD", "password") const version = isMac ? undefined : "15-bullseye"
.withWaitStrategy( container = await new GenericContainer("postgres", version)
Wait.forLogMessage( .withExposedPorts(5432)
"PostgreSQL init process complete; ready for start up." .withEnv("POSTGRES_PASSWORD", "password")
.withWaitStrategy(
Wait.forLogMessage(
"PostgreSQL init process complete; ready for start up."
)
) )
) .start()
.start() }
} const host = container.getContainerIpAddress()
const port = container.getMappedPort(5432)
const host = container.getContainerIpAddress() return {
const port = container.getMappedPort(5432) type: "datasource_plus",
source: SourceName.POSTGRES,
return { plus: true,
type: "datasource_plus", config: {
source: SourceName.POSTGRES, host,
plus: true, port,
config: { database: "postgres",
host, user: "postgres",
port, password: "password",
database: "postgres", schema: "public",
user: "postgres", ssl: false,
password: "password", rejectUnauthorized: false,
schema: "public", ca: false,
ssl: false, },
rejectUnauthorized: false, }
ca: false, } catch (err) {
}, throw new Error("**UNABLE TO CREATE TO POSTGRES CONTAINER**")
} }
} }

View file

@ -4,11 +4,13 @@ import {
SearchFilters, SearchFilters,
Datasource, Datasource,
FieldType, FieldType,
ExternalTable, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { DocumentType, SEPARATOR } from "../db/utils" import { DocumentType, SEPARATOR } from "../db/utils"
import { InvalidColumns, NoEmptyFilterStrings } from "../constants" import { InvalidColumns, NoEmptyFilterStrings } from "../constants"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import * as external from "../api/controllers/table/external"
import * as internal from "../api/controllers/table/internal"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g const ROW_ID_REGEX = /^\[.*]$/g
@ -83,10 +85,28 @@ export enum SqlClient {
ORACLE = "oracledb", ORACLE = "oracledb",
} }
export function isExternalTable(tableId: string) { export function isExternalTableID(tableId: string) {
return tableId.includes(DocumentType.DATASOURCE) return tableId.includes(DocumentType.DATASOURCE)
} }
export function isInternalTableID(tableId: string) {
return !isExternalTableID(tableId)
}
export function isExternalTable(table: Table) {
if (
table?.sourceId &&
table.sourceId.includes(DocumentType.DATASOURCE + SEPARATOR)
) {
return true
} else if (table?.sourceType === TableSourceType.EXTERNAL) {
return true
} else if (table?._id && isExternalTableID(table._id)) {
return true
}
return false
}
export function buildExternalTableId(datasourceId: string, tableName: string) { export function buildExternalTableId(datasourceId: string, tableName: string) {
// encode spaces // encode spaces
if (tableName.includes(" ")) { if (tableName.includes(" ")) {
@ -297,9 +317,9 @@ function copyExistingPropsOver(
* @param entities The old list of tables, if there was any to look for definitions in. * @param entities The old list of tables, if there was any to look for definitions in.
*/ */
export function finaliseExternalTables( export function finaliseExternalTables(
tables: Record<string, ExternalTable>, tables: Record<string, Table>,
entities: Record<string, ExternalTable> entities: Record<string, Table>
): Record<string, ExternalTable> { ): Record<string, Table> {
let finalTables: Record<string, Table> = {} let finalTables: Record<string, Table> = {}
const tableIds = Object.values(tables).map(table => table._id!) const tableIds = Object.values(tables).map(table => table._id!)
for (let [name, table] of Object.entries(tables)) { for (let [name, table] of Object.entries(tables)) {
@ -312,7 +332,7 @@ export function finaliseExternalTables(
} }
export function checkExternalTables( export function checkExternalTables(
tables: Record<string, ExternalTable> tables: Record<string, Table>
): Record<string, string> { ): Record<string, string> {
const invalidColumns = Object.values(InvalidColumns) as string[] const invalidColumns = Object.values(InvalidColumns) as string[]
const errors: Record<string, string> = {} const errors: Record<string, string> = {}

View file

@ -1,5 +1,12 @@
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { BBRequest, FieldType, Row, Table } from "@budibase/types" import {
BBRequest,
FieldType,
Row,
Table,
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
} from "@budibase/types"
import * as utils from "../../db/utils" import * as utils from "../../db/utils"
import trimViewRowInfoMiddleware from "../trimViewRowInfo" import trimViewRowInfoMiddleware from "../trimViewRowInfo"
@ -73,6 +80,8 @@ describe("trimViewRowInfo middleware", () => {
const table: Table = { const table: Table = {
_id: tableId, _id: tableId,
name: generator.word(), name: generator.word(),
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
type: "table", type: "table",
schema: { schema: {
name: { name: {

View file

@ -1,7 +1,6 @@
import { Ctx, Row } from "@budibase/types" import { Ctx, Row } from "@budibase/types"
import * as utils from "../db/utils" import * as utils from "../db/utils"
import sdk from "../sdk" import sdk from "../sdk"
import { db } from "@budibase/backend-core"
import { Next } from "koa" import { Next } from "koa"
import { getTableId } from "../api/controllers/row/utils" import { getTableId } from "../api/controllers/row/utils"

View file

@ -11,10 +11,11 @@ import {
isOIDCConfig, isOIDCConfig,
isSettingsConfig, isSettingsConfig,
ConfigType, ConfigType,
DatabaseQueryOpts,
} from "@budibase/types" } from "@budibase/types"
import env from "./../../../../environment" import env from "./../../../../environment"
export const getConfigParams = () => { export function getConfigParams(): DatabaseQueryOpts {
return { return {
include_docs: true, include_docs: true,
startkey: `${DocumentType.CONFIG}${SEPARATOR}`, startkey: `${DocumentType.CONFIG}${SEPARATOR}`,

View file

@ -0,0 +1,5 @@
import * as links from "./links"
export default {
...links,
}

View file

@ -0,0 +1,39 @@
import { context } from "@budibase/backend-core"
import { isTableId } from "@budibase/backend-core/src/docIds"
import {
DatabaseQueryOpts,
LinkDocument,
LinkDocumentValue,
} from "@budibase/types"
import { ViewName, getQueryIndex } from "../../../../src/db/utils"
export async function fetch(tableId: string): Promise<LinkDocumentValue[]> {
if (!isTableId(tableId)) {
throw new Error(`Invalid tableId: ${tableId}`)
}
const db = context.getAppDB()
const params: DatabaseQueryOpts = {
startkey: [tableId],
endkey: [tableId, {}],
}
const linkRows = (await db.query(getQueryIndex(ViewName.LINK), params)).rows
return linkRows.map(row => row.value as LinkDocumentValue)
}
export async function fetchWithDocument(
tableId: string
): Promise<LinkDocument[]> {
if (!isTableId(tableId)) {
throw new Error(`Invalid tableId: ${tableId}`)
}
const db = context.getAppDB()
const params: DatabaseQueryOpts = {
startkey: [tableId],
endkey: [tableId, {}],
include_docs: true,
}
const linkRows = (await db.query(getQueryIndex(ViewName.LINK), params)).rows
return linkRows.map(row => row.doc as LinkDocument)
}

View file

@ -1,4 +1,4 @@
import { IncludeRelationship, Operation, Row } from "@budibase/types" import { IncludeRelationship, Operation } from "@budibase/types"
import { handleRequest } from "../../../api/controllers/row/external" import { handleRequest } from "../../../api/controllers/row/external"
import { breakRowIdField } from "../../../integrations/utils" import { breakRowIdField } from "../../../integrations/utils"

View file

@ -1,5 +1,5 @@
import { SearchFilters, SearchParams, Row } from "@budibase/types" import { Row, SearchFilters, SearchParams } from "@budibase/types"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./search/internal" import * as internal from "./search/internal"
import * as external from "./search/external" import * as external from "./search/external"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
@ -12,7 +12,7 @@ export interface ViewParams {
} }
function pickApi(tableId: any) { function pickApi(tableId: any) {
if (isExternalTable(tableId)) { if (isExternalTableID(tableId)) {
return external return external
} }
return internal return internal
@ -49,6 +49,10 @@ export async function fetch(tableId: string): Promise<Row[]> {
return pickApi(tableId).fetch(tableId) return pickApi(tableId).fetch(tableId)
} }
export async function fetchRaw(tableId: string): Promise<Row[]> {
return pickApi(tableId).fetchRaw(tableId)
}
export async function fetchView( export async function fetchView(
tableId: string, tableId: string,
viewName: string, viewName: string,

View file

@ -186,6 +186,12 @@ export async function fetch(tableId: string): Promise<Row[]> {
}) })
} }
export async function fetchRaw(tableId: string): Promise<Row[]> {
return await handleRequest<Operation.READ>(Operation.READ, tableId, {
includeSqlRelationships: IncludeRelationship.INCLUDE,
})
}
export async function fetchView(viewName: string) { export async function fetchView(viewName: string) {
// there are no views in external datasources, shouldn't ever be called // there are no views in external datasources, shouldn't ever be called
// for now just fetch // for now just fetch

View file

@ -140,14 +140,13 @@ export async function exportRows(
} }
export async function fetch(tableId: string): Promise<Row[]> { export async function fetch(tableId: string): Promise<Row[]> {
const db = context.getAppDB()
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
const rows = await getRawTableData(db, tableId) const rows = await fetchRaw(tableId)
return await outputProcessing(table, rows) return await outputProcessing(table, rows)
} }
async function getRawTableData(db: Database, tableId: string) { export async function fetchRaw(tableId: string): Promise<Row[]> {
const db = context.getAppDB()
let rows let rows
if (tableId === InternalTables.USER_METADATA) { if (tableId === InternalTables.USER_METADATA) {
rows = await sdk.users.fetchMetadata() rows = await sdk.users.fetchMetadata()
@ -182,7 +181,7 @@ export async function fetchView(
}) })
} else { } else {
const tableId = viewInfo.meta.tableId const tableId = viewInfo.meta.tableId
const data = await getRawTableData(db, tableId) const data = await fetchRaw(tableId)
response = await inMemoryViews.runView( response = await inMemoryViews.runView(
viewInfo, viewInfo,
calculation as string, calculation as string,
@ -198,11 +197,7 @@ export async function fetchView(
try { try {
table = await sdk.tables.getTable(viewInfo.meta.tableId) table = await sdk.tables.getTable(viewInfo.meta.tableId)
} catch (err) { } catch (err) {
/* istanbul ignore next */ throw new Error("Unable to retrieve view table.")
table = {
name: "",
schema: {},
}
} }
rows = await outputProcessing(table, response.rows) rows = await outputProcessing(table, response.rows)
} }

View file

@ -7,6 +7,7 @@ import {
SourceName, SourceName,
Table, Table,
SearchParams, SearchParams,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
@ -15,6 +16,7 @@ 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")
@ -23,36 +25,7 @@ jest.setTimeout(30000)
describe.skip("external", () => { describe.skip("external", () => {
const config = new TestConfiguration() const config = new TestConfiguration()
let externalDatasource: Datasource let externalDatasource: Datasource, tableData: Table
const tableData: Table = {
name: generator.word(),
type: "external",
primary: ["id"],
schema: {
id: {
name: "id",
type: FieldType.AUTO,
autocolumn: true,
},
name: {
name: "name",
type: FieldType.STRING,
},
surname: {
name: "surname",
type: FieldType.STRING,
},
age: {
name: "age",
type: FieldType.NUMBER,
},
address: {
name: "address",
type: FieldType.STRING,
},
},
}
beforeAll(async () => { beforeAll(async () => {
const container = await new GenericContainer("mysql") const container = await new GenericContainer("mysql")
@ -84,12 +57,43 @@ describe.skip("external", () => {
}, },
}, },
}) })
tableData = {
name: generator.word(),
type: "table",
primary: ["id"],
sourceId: externalDatasource._id!,
sourceType: TableSourceType.EXTERNAL,
schema: {
id: {
name: "id",
type: FieldType.AUTO,
autocolumn: true,
},
name: {
name: "name",
type: FieldType.STRING,
},
surname: {
name: "surname",
type: FieldType.STRING,
},
age: {
name: "age",
type: FieldType.NUMBER,
},
address: {
name: "address",
type: FieldType.STRING,
},
},
}
}) })
describe("search", () => { describe("search", () => {
const rows: Row[] = [] const rows: Row[] = []
beforeAll(async () => { beforeAll(async () => {
const table = await config.createTable({ const table = await config.createExternalTable({
...tableData, ...tableData,
sourceId: externalDatasource._id, sourceId: externalDatasource._id,
}) })

View file

@ -1,4 +1,11 @@
import { FieldType, Row, Table, SearchParams } from "@budibase/types" import {
FieldType,
Row,
Table,
SearchParams,
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
} from "@budibase/types"
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
import { search } from "../internal" import { search } from "../internal"
import { import {
@ -12,6 +19,8 @@ describe("internal", () => {
const tableData: Table = { const tableData: Table = {
name: generator.word(), name: generator.word(),
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
name: "name", name: "name",

View file

@ -3,14 +3,19 @@ import { db as dbCore } from "@budibase/backend-core"
import { import {
FieldType, FieldType,
FieldTypeSubtypes, FieldTypeSubtypes,
Table, INTERNAL_TABLE_SOURCE_ID,
SearchParams, SearchParams,
Table,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
const tableId = "ta_a" const tableId = "ta_a"
const tableWithUserCol: Table = { const tableWithUserCol: Table = {
type: "table",
_id: tableId, _id: tableId,
name: "table", name: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
user: { user: {
name: "user", name: "user",
@ -21,8 +26,11 @@ const tableWithUserCol: Table = {
} }
const tableWithUsersCol: Table = { const tableWithUsersCol: Table = {
type: "table",
_id: tableId, _id: tableId,
name: "table", name: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
user: { user: {
name: "user", name: "user",

View file

@ -35,10 +35,10 @@ export async function save(
opts?: { tableId?: string; renaming?: RenameColumn } opts?: { tableId?: string; renaming?: RenameColumn }
) { ) {
let tableToSave: TableRequest = { let tableToSave: TableRequest = {
...update,
type: "table", type: "table",
_id: buildExternalTableId(datasourceId, update.name), _id: buildExternalTableId(datasourceId, update.name),
sourceId: datasourceId, sourceId: datasourceId,
...update,
} }
const tableId = opts?.tableId || update._id const tableId = opts?.tableId || update._id

View file

@ -6,6 +6,7 @@ import {
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipType, RelationshipType,
Table, Table,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { FieldTypes } from "../../../../constants" import { FieldTypes } from "../../../../constants"
import { import {
@ -76,12 +77,16 @@ export function generateManyLinkSchema(
const primary = table.name + table.primary[0] const primary = table.name + table.primary[0]
const relatedPrimary = relatedTable.name + relatedTable.primary[0] const relatedPrimary = relatedTable.name + relatedTable.primary[0]
const jcTblName = generateJunctionTableName(column, table, relatedTable) const jcTblName = generateJunctionTableName(column, table, relatedTable)
const datasourceId = datasource._id!
// first create the new table // first create the new table
const junctionTable = { const junctionTable: Table = {
_id: buildExternalTableId(datasource._id!, jcTblName), type: "table",
_id: buildExternalTableId(datasourceId, jcTblName),
name: jcTblName, name: jcTblName,
primary: [primary, relatedPrimary], primary: [primary, relatedPrimary],
constrained: [primary, relatedPrimary], constrained: [primary, relatedPrimary],
sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
schema: { schema: {
[primary]: foreignKeyStructure(primary, { [primary]: foreignKeyStructure(primary, {
toTable: table.name, toTable: table.name,

View file

@ -1,30 +1,47 @@
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { import { getMultiIDParams, getTableParams } from "../../../db/utils"
BudibaseInternalDB,
getMultiIDParams,
getTableParams,
} from "../../../db/utils"
import { import {
breakExternalTableId, breakExternalTableId,
isExternalTable, isExternalTableID,
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
import { import {
AllDocsResponse,
Database, Database,
INTERNAL_TABLE_SOURCE_ID,
Table, Table,
TableResponse, TableResponse,
TableSourceType,
TableViewsResponse, TableViewsResponse,
} from "@budibase/types" } from "@budibase/types"
import datasources from "../datasources" import datasources from "../datasources"
import sdk from "../../../sdk" import sdk from "../../../sdk"
function processInternalTables(docs: AllDocsResponse<Table[]>): Table[] { export function processTable(table: Table): Table {
return docs.rows.map((tableDoc: any) => ({ if (table._id && isExternalTableID(table._id)) {
...tableDoc.doc, return {
type: "internal", ...table,
sourceId: tableDoc.doc.sourceId || BudibaseInternalDB._id, type: "table",
})) sourceType: TableSourceType.EXTERNAL,
}
} else {
return {
...table,
type: "table",
sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
}
}
}
export function processTables(tables: Table[]): Table[] {
return tables.map(table => processTable(table))
}
function processEntities(tables: Record<string, Table>) {
for (let key of Object.keys(tables)) {
tables[key] = processTable(tables[key])
}
return tables
} }
export async function getAllInternalTables(db?: Database): Promise<Table[]> { export async function getAllInternalTables(db?: Database): Promise<Table[]> {
@ -36,7 +53,7 @@ export async function getAllInternalTables(db?: Database): Promise<Table[]> {
include_docs: true, include_docs: true,
}) })
) )
return processInternalTables(internalTables) return processTables(internalTables.rows.map(row => row.doc!))
} }
async function getAllExternalTables(): Promise<Table[]> { async function getAllExternalTables(): Promise<Table[]> {
@ -48,7 +65,7 @@ async function getAllExternalTables(): Promise<Table[]> {
final = final.concat(Object.values(entities)) final = final.concat(Object.values(entities))
} }
} }
return final return processTables(final)
} }
export async function getExternalTable( export async function getExternalTable(
@ -56,19 +73,21 @@ export async function getExternalTable(
tableName: string tableName: string
): Promise<Table> { ): Promise<Table> {
const entities = await getExternalTablesInDatasource(datasourceId) const entities = await getExternalTablesInDatasource(datasourceId)
return entities[tableName] return processTable(entities[tableName])
} }
export async function getTable(tableId: string): Promise<Table> { export async function getTable(tableId: string): Promise<Table> {
const db = context.getAppDB() const db = context.getAppDB()
if (isExternalTable(tableId)) { let output: Table
if (isExternalTableID(tableId)) {
let { datasourceId, tableName } = breakExternalTableId(tableId) let { datasourceId, tableName } = breakExternalTableId(tableId)
const datasource = await datasources.get(datasourceId!) const datasource = await datasources.get(datasourceId!)
const table = await getExternalTable(datasourceId!, tableName!) const table = await getExternalTable(datasourceId!, tableName!)
return { ...table, sql: isSQL(datasource) } output = { ...table, sql: isSQL(datasource) }
} else { } else {
return db.get(tableId) output = await db.get<Table>(tableId)
} }
return processTable(output)
} }
export async function getAllTables() { export async function getAllTables() {
@ -76,7 +95,7 @@ export async function getAllTables() {
getAllInternalTables(), getAllInternalTables(),
getAllExternalTables(), getAllExternalTables(),
]) ])
return [...internal, ...external] return processTables([...internal, ...external])
} }
export async function getExternalTablesInDatasource( export async function getExternalTablesInDatasource(
@ -86,12 +105,14 @@ export async function getExternalTablesInDatasource(
if (!datasource || !datasource.entities) { if (!datasource || !datasource.entities) {
throw new Error("Datasource is not configured fully.") throw new Error("Datasource is not configured fully.")
} }
return datasource.entities return processEntities(datasource.entities)
} }
export async function getTables(tableIds: string[]): Promise<Table[]> { export async function getTables(tableIds: string[]): Promise<Table[]> {
const externalTableIds = tableIds.filter(tableId => isExternalTable(tableId)), const externalTableIds = tableIds.filter(tableId =>
internalTableIds = tableIds.filter(tableId => !isExternalTable(tableId)) isExternalTableID(tableId)
),
internalTableIds = tableIds.filter(tableId => !isExternalTableID(tableId))
let tables: Table[] = [] let tables: Table[] = []
if (externalTableIds.length) { if (externalTableIds.length) {
const externalTables = await getAllExternalTables() const externalTables = await getAllExternalTables()
@ -106,9 +127,9 @@ export async function getTables(tableIds: string[]): Promise<Table[]> {
const internalTableDocs = await db.allDocs<Table[]>( const internalTableDocs = await db.allDocs<Table[]>(
getMultiIDParams(internalTableIds) getMultiIDParams(internalTableIds)
) )
tables = tables.concat(processInternalTables(internalTableDocs)) tables = tables.concat(internalTableDocs.rows.map(row => row.doc!))
} }
return tables return processTables(tables)
} }
export function enrichViewSchemas(table: Table): TableResponse { export function enrichViewSchemas(table: Table): TableResponse {

View file

@ -2,10 +2,12 @@ import { populateExternalTableSchemas } from "./validation"
import * as getters from "./getters" import * as getters from "./getters"
import * as updates from "./update" import * as updates from "./update"
import * as utils from "./utils" import * as utils from "./utils"
import { migrate } from "./migration"
export default { export default {
populateExternalTableSchemas, populateExternalTableSchemas,
...updates, ...updates,
...getters, ...getters,
...utils, ...utils,
migrate,
} }

View file

@ -0,0 +1,194 @@
import { BadRequestError, context, db as dbCore } from "@budibase/backend-core"
import {
BBReferenceFieldMetadata,
FieldSchema,
FieldSubtype,
InternalTable,
isBBReferenceField,
isRelationshipField,
LinkDocument,
RelationshipFieldMetadata,
RelationshipType,
Row,
Table,
} from "@budibase/types"
import sdk from "../../../sdk"
import { isExternalTableID } from "../../../integrations/utils"
import { EventType, updateLinks } from "../../../db/linkedRows"
import { cloneDeep } from "lodash"
import { isInternalColumnName } from "@budibase/backend-core/src/db"
export interface MigrationResult {
tablesUpdated: Table[]
}
export async function migrate(
table: Table,
oldColumn: FieldSchema,
newColumn: FieldSchema
): Promise<MigrationResult> {
if (newColumn.name in table.schema) {
throw new BadRequestError(`Column "${newColumn.name}" already exists`)
}
if (newColumn.name === "") {
throw new BadRequestError(`Column name cannot be empty`)
}
if (isInternalColumnName(newColumn.name)) {
throw new BadRequestError(`Column name cannot be a reserved column name`)
}
table.schema[newColumn.name] = newColumn
table = await sdk.tables.saveTable(table)
let migrator = getColumnMigrator(table, oldColumn, newColumn)
try {
return await migrator.doMigration()
} catch (e) {
// If the migration fails then we need to roll back the table schema
// change.
delete table.schema[newColumn.name]
await sdk.tables.saveTable(table)
throw e
}
}
interface ColumnMigrator {
doMigration(): Promise<MigrationResult>
}
function getColumnMigrator(
table: Table,
oldColumn: FieldSchema,
newColumn: FieldSchema
): ColumnMigrator {
// For now, we're only supporting migrations of user relationships to user
// columns in internal tables. In the future, we may want to support other
// migrations but for now return an error if we aren't migrating a user
// relationship.
if (isExternalTableID(table._id!)) {
throw new BadRequestError("External tables cannot be migrated")
}
if (!(oldColumn.name in table.schema)) {
throw new BadRequestError(`Column "${oldColumn.name}" does not exist`)
}
if (!isBBReferenceField(newColumn)) {
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
}
if (newColumn.subtype !== "user" && newColumn.subtype !== "users") {
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
}
if (!isRelationshipField(oldColumn)) {
throw new BadRequestError(
`Column "${oldColumn.name}" is not a user relationship`
)
}
if (oldColumn.tableId !== InternalTable.USER_METADATA) {
throw new BadRequestError(
`Column "${oldColumn.name}" is not a user relationship`
)
}
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
if (newColumn.subtype !== FieldSubtype.USER) {
throw new BadRequestError(
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
)
}
return new SingleUserColumnMigrator(table, oldColumn, newColumn)
}
if (
oldColumn.relationshipType === RelationshipType.MANY_TO_MANY ||
oldColumn.relationshipType === RelationshipType.MANY_TO_ONE
) {
if (newColumn.subtype !== FieldSubtype.USERS) {
throw new BadRequestError(
`Column "${oldColumn.name}" is a ${oldColumn.relationshipType} column but "${newColumn.name}" is not a multi user column`
)
}
return new MultiUserColumnMigrator(table, oldColumn, newColumn)
}
throw new BadRequestError(`Unknown migration type`)
}
abstract class UserColumnMigrator implements ColumnMigrator {
constructor(
protected table: Table,
protected oldColumn: RelationshipFieldMetadata,
protected newColumn: BBReferenceFieldMetadata
) {}
abstract updateRow(row: Row, link: LinkDocument): void
async doMigration(): Promise<MigrationResult> {
let oldTable = cloneDeep(this.table)
let rows = await sdk.rows.fetchRaw(this.table._id!)
let rowsById = rows.reduce((acc, row) => {
acc[row._id!] = row
return acc
}, {} as Record<string, Row>)
let links = await sdk.links.fetchWithDocument(this.table._id!)
for (let link of links) {
if (
link.doc1.tableId !== this.table._id ||
link.doc1.fieldName !== this.oldColumn.name ||
link.doc2.tableId !== InternalTable.USER_METADATA
) {
continue
}
let row = rowsById[link.doc1.rowId]
if (!row) {
// This can happen if the row has been deleted but the link hasn't,
// which was a state that was found during the initial testing of this
// feature. Not sure exactly what can cause it, but best to be safe.
continue
}
this.updateRow(row, link)
}
let db = context.getAppDB()
await db.bulkDocs(rows)
delete this.table.schema[this.oldColumn.name]
this.table = await sdk.tables.saveTable(this.table)
await updateLinks({
eventType: EventType.TABLE_UPDATED,
table: this.table,
oldTable,
})
let otherTable = await sdk.tables.getTable(this.oldColumn.tableId)
return {
tablesUpdated: [this.table, otherTable],
}
}
}
class SingleUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void {
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
link.doc2.rowId
)
}
}
class MultiUserColumnMigrator extends UserColumnMigrator {
updateRow(row: Row, link: LinkDocument): void {
if (!row[this.newColumn.name]) {
row[this.newColumn.name] = []
}
row[this.newColumn.name].push(
dbCore.getGlobalIDFromUserMetadataID(link.doc2.rowId)
)
}
}

View file

@ -1,4 +1,10 @@
import { FieldType, Table, ViewV2 } from "@budibase/types" import {
FieldType,
INTERNAL_TABLE_SOURCE_ID,
Table,
TableSourceType,
ViewV2,
} from "@budibase/types"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import sdk from "../../.." import sdk from "../../.."
@ -13,6 +19,8 @@ describe("table sdk", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,

View file

@ -1,73 +1,92 @@
import { populateExternalTableSchemas } from "../validation" import { populateExternalTableSchemas } from "../validation"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { AutoReason, Datasource, Table } from "@budibase/types" import {
AutoReason,
Datasource,
FieldType,
RelationshipType,
SourceName,
Table,
TableSourceType,
} from "@budibase/types"
import { isEqual } from "lodash" import { isEqual } from "lodash"
import { generateDatasourceID } from "../../../../db/utils"
const SCHEMA = { const datasourceId = generateDatasourceID()
const SCHEMA: Datasource = {
source: SourceName.POSTGRES,
type: "datasource",
_id: datasourceId,
entities: { entities: {
client: { client: {
type: "table",
_id: "tableA", _id: "tableA",
name: "client", name: "client",
primary: ["idC"], primary: ["idC"],
primaryDisplay: "Name", primaryDisplay: "Name",
sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
schema: { schema: {
idC: { idC: {
autocolumn: true, autocolumn: true,
externalType: "int unsigned", externalType: "int unsigned",
name: "idC", name: "idC",
type: "number", type: FieldType.NUMBER,
}, },
Name: { Name: {
autocolumn: false, autocolumn: false,
externalType: "varchar(255)", externalType: "varchar(255)",
name: "Name", name: "Name",
type: "string", type: FieldType.STRING,
}, },
project: { project: {
fieldName: "idC", fieldName: "idC",
foreignKey: "idC", foreignKey: "idC",
main: true, main: true,
name: "project", name: "project",
relationshipType: "many-to-one", relationshipType: RelationshipType.MANY_TO_ONE,
tableId: "tableB", tableId: "tableB",
type: "link", type: FieldType.LINK,
}, },
}, },
}, },
project: { project: {
type: "table",
_id: "tableB", _id: "tableB",
name: "project", name: "project",
primary: ["idP"], primary: ["idP"],
primaryDisplay: "Name", primaryDisplay: "Name",
sourceId: datasourceId,
sourceType: TableSourceType.EXTERNAL,
schema: { schema: {
idC: { idC: {
externalType: "int unsigned", externalType: "int unsigned",
name: "idC", name: "idC",
type: "number", type: FieldType.NUMBER,
}, },
idP: { idP: {
autocolumn: true, autocolumn: true,
externalType: "int unsigned", externalType: "int unsigned",
name: "idProject", name: "idProject",
type: "number", type: FieldType.NUMBER,
}, },
Name: { Name: {
autocolumn: false, autocolumn: false,
externalType: "varchar(255)", externalType: "varchar(255)",
name: "Name", name: "Name",
type: "string", type: FieldType.STRING,
}, },
client: { client: {
fieldName: "idC", fieldName: "idC",
foreignKey: "idC", foreignKey: "idC",
name: "client", name: "client",
relationshipType: "one-to-many", relationshipType: RelationshipType.ONE_TO_MANY,
tableId: "tableA", tableId: "tableA",
type: "link", type: FieldType.LINK,
}, },
}, },
sql: true, sql: true,
type: "table",
}, },
}, },
} }
@ -95,12 +114,12 @@ describe("validation and update of external table schemas", () => {
function noOtherTableChanges(response: any) { function noOtherTableChanges(response: any) {
checkOtherColumns( checkOtherColumns(
response.entities!.client!, response.entities!.client!,
SCHEMA.entities.client as Table, SCHEMA.entities!.client,
OTHER_CLIENT_COLS OTHER_CLIENT_COLS
) )
checkOtherColumns( checkOtherColumns(
response.entities!.project!, response.entities!.project!,
SCHEMA.entities.project as Table, SCHEMA.entities!.project,
OTHER_PROJECT_COLS OTHER_PROJECT_COLS
) )
} }

View file

@ -1,23 +1,30 @@
import { Table, RenameColumn } from "@budibase/types" import { Table, RenameColumn } from "@budibase/types"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import sdk from "../../index" import sdk from "../../index"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { isExternal } from "./utils" import { isExternal } from "./utils"
import { DocumentInsertResponse } from "@budibase/nano"
import * as external from "./external" import * as external from "./external"
import * as internal from "./internal" import * as internal from "./internal"
import { cloneDeep } from "lodash"
export * as external from "./external" export * as external from "./external"
export * as internal from "./internal" export * as internal from "./internal"
export async function saveTable(table: Table) { export async function saveTable(table: Table): Promise<Table> {
const db = context.getAppDB() const db = context.getAppDB()
if (isExternalTable(table._id!)) { let resp: DocumentInsertResponse
if (isExternalTableID(table._id!)) {
const datasource = await sdk.datasources.get(table.sourceId!) const datasource = await sdk.datasources.get(table.sourceId!)
datasource.entities![table.name] = table datasource.entities![table.name] = table
await db.put(datasource) resp = await db.put(datasource)
} else { } else {
await db.put(table) resp = await db.put(table)
} }
let tableClone = cloneDeep(table)
tableClone._rev = resp.rev
return tableClone
} }
export async function update(table: Table, renaming?: RenameColumn) { export async function update(table: Table, renaming?: RenameColumn) {

View file

@ -1,10 +1,10 @@
import { Table } from "@budibase/types" import { Table, TableSourceType } from "@budibase/types"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
export function isExternal(opts: { table?: Table; tableId?: string }): boolean { export function isExternal(opts: { table?: Table; tableId?: string }): boolean {
if (opts.table && opts.table.type === "external") { if (opts.table && opts.table.sourceType === TableSourceType.EXTERNAL) {
return true return true
} else if (opts.tableId && isExternalTable(opts.tableId)) { } else if (opts.tableId && isExternalTableID(opts.tableId)) {
return true return true
} }
return false return false

View file

@ -4,13 +4,13 @@ import { cloneDeep } from "lodash"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "../../../db/utils" import * as utils from "../../../db/utils"
import { isExternalTable } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./internal" import * as internal from "./internal"
import * as external from "./external" import * as external from "./external"
function pickApi(tableId: any) { function pickApi(tableId: any) {
if (isExternalTable(tableId)) { if (isExternalTableID(tableId)) {
return external return external
} }
return internal return internal

View file

@ -2,8 +2,10 @@ import _ from "lodash"
import { import {
FieldSchema, FieldSchema,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID,
Table, Table,
TableSchema, TableSchema,
TableSourceType,
ViewV2, ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
@ -14,6 +16,8 @@ describe("table sdk", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,

View file

@ -5,6 +5,7 @@ import { default as applications } from "./app/applications"
import { default as datasources } from "./app/datasources" import { default as datasources } from "./app/datasources"
import { default as queries } from "./app/queries" import { default as queries } from "./app/queries"
import { default as rows } from "./app/rows" import { default as rows } from "./app/rows"
import { default as links } from "./app/links"
import { default as users } from "./users" import { default as users } from "./users"
import { default as plugins } from "./plugins" import { default as plugins } from "./plugins"
import * as views from "./app/views" import * as views from "./app/views"
@ -22,6 +23,7 @@ const sdk = {
plugins, plugins,
views, views,
permissions, permissions,
links,
} }
// default export for TS // default export for TS

View file

@ -39,12 +39,12 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(3) expect(metadata).toHaveLength(3)
expect(metadata).toContainEqual( expect(metadata).toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user1._id), _id: db.generateUserMetadataID(user1._id!),
}) })
) )
expect(metadata).toContainEqual( expect(metadata).toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user2._id), _id: db.generateUserMetadataID(user2._id!),
}) })
) )
}) })
@ -59,7 +59,7 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(1) expect(metadata).toHaveLength(1)
expect(metadata).not.toContainEqual( expect(metadata).not.toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user._id), _id: db.generateUserMetadataID(user._id!),
}) })
) )
}) })
@ -70,7 +70,7 @@ describe("syncGlobalUsers", () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup()) const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false }) const user1 = await config.createUser({ admin: false, builder: false })
const user2 = await config.createUser({ admin: false, builder: false }) const user2 = await config.createUser({ admin: false, builder: false })
await proSdk.groups.addUsers(group.id, [user1._id, user2._id]) await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
await syncGlobalUsers() await syncGlobalUsers()
@ -87,12 +87,12 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(3) expect(metadata).toHaveLength(3)
expect(metadata).toContainEqual( expect(metadata).toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user1._id), _id: db.generateUserMetadataID(user1._id!),
}) })
) )
expect(metadata).toContainEqual( expect(metadata).toContainEqual(
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(user2._id), _id: db.generateUserMetadataID(user2._id!),
}) })
) )
}) })
@ -109,7 +109,7 @@ describe("syncGlobalUsers", () => {
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
], ],
}) })
await proSdk.groups.addUsers(group.id, [user1._id, user2._id]) await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
await syncGlobalUsers() await syncGlobalUsers()

View file

@ -2,37 +2,31 @@ import { generator, mocks, structures } from "@budibase/backend-core/tests"
// init the licensing mock // init the licensing mock
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
import { init as dbInit } from "../../db" import { init as dbInit } from "../../db"
dbInit()
import env from "../../environment" import env from "../../environment"
import { import {
basicTable,
basicRow,
basicRole,
basicAutomation, basicAutomation,
basicDatasource,
basicQuery,
basicScreen,
basicLayout,
basicWebhook,
basicAutomationResults, basicAutomationResults,
basicDatasource,
basicLayout,
basicQuery,
basicRole,
basicRow,
basicScreen,
basicTable,
basicWebhook,
} from "./structures" } from "./structures"
import { import {
constants, auth,
tenancy,
sessions,
cache, cache,
constants,
context, context,
db as dbCore, db as dbCore,
encryption, encryption,
auth,
roles,
env as coreEnv, env as coreEnv,
roles,
sessions,
tenancy,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import * as controllers from "./controllers" import * as controllers from "./controllers"
import { cleanup } from "../../utilities/fileSystem" import { cleanup } from "../../utilities/fileSystem"
@ -43,21 +37,32 @@ import supertest from "supertest"
import { import {
App, App,
AuthToken, AuthToken,
Automation,
CreateViewRequest,
Datasource, Datasource,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
RelationshipFieldMetadata,
RelationshipType,
Row, Row,
SearchFilters,
SourceName, SourceName,
Table, Table,
SearchFilters, TableSourceType,
User,
UserRoles, UserRoles,
Automation,
View, View,
FieldType,
RelationshipType,
CreateViewRequest,
RelationshipFieldMetadata,
} from "@budibase/types" } from "@budibase/types"
import API from "./api" import API from "./api"
import { cloneDeep } from "lodash"
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
dbInit()
type DefaultUserValues = { type DefaultUserValues = {
globalUserId: string globalUserId: string
@ -67,6 +72,11 @@ type DefaultUserValues = {
csrfToken: string csrfToken: string
} }
interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
sourceId?: string
sourceType?: TableSourceType
}
class TestConfiguration { class TestConfiguration {
server: any server: any
request: supertest.SuperTest<supertest.Test> | undefined request: supertest.SuperTest<supertest.Test> | undefined
@ -188,30 +198,38 @@ class TestConfiguration {
} }
} }
// MODES async withEnv(newEnvVars: Partial<typeof env>, f: () => Promise<void>) {
setMultiTenancy = (value: boolean) => { let cleanup = this.setEnv(newEnvVars)
env._set("MULTI_TENANCY", value) try {
coreEnv._set("MULTI_TENANCY", value) await f()
} finally {
cleanup()
}
} }
setSelfHosted = (value: boolean) => { /*
env._set("SELF_HOSTED", value) * Sets the environment variables to the given values and returns a function
coreEnv._set("SELF_HOSTED", value) * that can be called to reset the environment variables to their original values.
} */
setEnv(newEnvVars: Partial<typeof env>): () => void {
const oldEnv = cloneDeep(env)
const oldCoreEnv = cloneDeep(coreEnv)
setGoogleAuth = (value: string) => { let key: keyof typeof newEnvVars
env._set("GOOGLE_CLIENT_ID", value) for (key in newEnvVars) {
env._set("GOOGLE_CLIENT_SECRET", value) env._set(key, newEnvVars[key])
coreEnv._set("GOOGLE_CLIENT_ID", value) coreEnv._set(key, newEnvVars[key])
coreEnv._set("GOOGLE_CLIENT_SECRET", value) }
}
modeCloud = () => { return () => {
this.setSelfHosted(false) for (const [key, value] of Object.entries(oldEnv)) {
} env._set(key, value)
}
modeSelf = () => { for (const [key, value] of Object.entries(oldCoreEnv)) {
this.setSelfHosted(true) coreEnv._set(key, value)
}
}
} }
// UTILS // UTILS
@ -254,7 +272,7 @@ class TestConfiguration {
} catch (err) { } catch (err) {
existing = { email } existing = { email }
} }
const user = { const user: User = {
_id: id, _id: id,
...existing, ...existing,
roles: roles || {}, roles: roles || {},
@ -294,7 +312,7 @@ class TestConfiguration {
admin?: boolean admin?: boolean
roles?: UserRoles roles?: UserRoles
} = {} } = {}
) { ): Promise<User> {
let { id, firstName, lastName, email, builder, admin, roles } = user let { id, firstName, lastName, email, builder, admin, roles } = user
firstName = firstName || this.defaultUserValues.firstName firstName = firstName || this.defaultUserValues.firstName
lastName = lastName || this.defaultUserValues.lastName lastName = lastName || this.defaultUserValues.lastName
@ -314,10 +332,7 @@ class TestConfiguration {
roles, roles,
}) })
await cache.user.invalidateUser(globalId) await cache.user.invalidateUser(globalId)
return { return resp
...resp,
globalId,
}
} }
async createGroup(roleId: string = roles.BUILTIN_ROLE_IDS.BASIC) { async createGroup(roleId: string = roles.BUILTIN_ROLE_IDS.BASIC) {
@ -540,10 +555,12 @@ class TestConfiguration {
// TABLE // TABLE
async updateTable( async updateTable(
config?: Table, config?: TableToBuild,
{ skipReassigning } = { skipReassigning: false } { skipReassigning } = { skipReassigning: false }
): Promise<Table> { ): Promise<Table> {
config = config || basicTable() config = config || basicTable()
config.sourceType = config.sourceType || TableSourceType.INTERNAL
config.sourceId = config.sourceId || INTERNAL_TABLE_SOURCE_ID
const response = await this._req(config, null, controllers.table.save) const response = await this._req(config, null, controllers.table.save)
if (!skipReassigning) { if (!skipReassigning) {
this.table = response this.table = response
@ -551,18 +568,32 @@ class TestConfiguration {
return response return response
} }
async createTable(config?: Table, options = { skipReassigning: false }) { async createTable(
config?: TableToBuild,
options = { skipReassigning: false }
) {
if (config != null && config._id) { if (config != null && config._id) {
delete config._id delete config._id
} }
config = config || basicTable() config = config || basicTable()
if (this.datasource && !config.sourceId) { if (!config.sourceId) {
config.sourceId = this.datasource._id config.sourceId = INTERNAL_TABLE_SOURCE_ID
if (this.datasource.plus) {
config.type = "external"
}
} }
return this.updateTable(config, options)
}
async createExternalTable(
config?: TableToBuild,
options = { skipReassigning: false }
) {
if (config != null && config._id) {
delete config._id
}
config = config || basicTable()
if (this.datasource?._id) {
config.sourceId = this.datasource._id
config.sourceType = TableSourceType.EXTERNAL
}
return this.updateTable(config, options) return this.updateTable(config, options)
} }
@ -574,12 +605,15 @@ class TestConfiguration {
async createLinkedTable( async createLinkedTable(
relationshipType = RelationshipType.ONE_TO_MANY, relationshipType = RelationshipType.ONE_TO_MANY,
links: any = ["link"], links: any = ["link"],
config?: Table config?: TableToBuild
) { ) {
if (!this.table) { if (!this.table) {
throw "Must have created a table first." throw "Must have created a table first."
} }
const tableConfig = config || basicTable() const tableConfig = config || basicTable()
if (!tableConfig.sourceId) {
tableConfig.sourceId = INTERNAL_TABLE_SOURCE_ID
}
tableConfig.primaryDisplay = "name" tableConfig.primaryDisplay = "name"
for (let link of links) { for (let link of links) {
tableConfig.schema[link] = { tableConfig.schema[link] = {
@ -591,15 +625,12 @@ class TestConfiguration {
} as RelationshipFieldMetadata } as RelationshipFieldMetadata
} }
if (this.datasource && !tableConfig.sourceId) { if (this.datasource?._id) {
tableConfig.sourceId = this.datasource._id tableConfig.sourceId = this.datasource._id
if (this.datasource.plus) { tableConfig.sourceType = TableSourceType.EXTERNAL
tableConfig.type = "external"
}
} }
const linkedTable = await this.createTable(tableConfig) return await this.createTable(tableConfig)
return linkedTable
} }
async createAttachmentTable() { async createAttachmentTable() {

View file

@ -0,0 +1,35 @@
import {
APIError,
Datasource,
ProcessAttachmentResponse,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
import fs from "fs"
export class AttachmentAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
process = async (
name: string,
file: Buffer | fs.ReadStream | string,
{ expectStatus } = { expectStatus: 200 }
): Promise<ProcessAttachmentResponse> => {
const result = await this.request
.post(`/api/attachments/process`)
.attach("file", file, name)
.set(this.config.defaultHeaders())
if (result.statusCode !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
result.statusCode
}, body: ${JSON.stringify(result.body)}`
)
}
return result.body
}
}

View file

@ -7,6 +7,7 @@ import { DatasourceAPI } from "./datasource"
import { LegacyViewAPI } from "./legacyView" import { LegacyViewAPI } from "./legacyView"
import { ScreenAPI } from "./screen" import { ScreenAPI } from "./screen"
import { ApplicationAPI } from "./application" import { ApplicationAPI } from "./application"
import { AttachmentAPI } from "./attachment"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -17,6 +18,7 @@ export default class API {
datasource: DatasourceAPI datasource: DatasourceAPI
screen: ScreenAPI screen: ScreenAPI
application: ApplicationAPI application: ApplicationAPI
attachment: AttachmentAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -27,5 +29,6 @@ export default class API {
this.datasource = new DatasourceAPI(config) this.datasource = new DatasourceAPI(config)
this.screen = new ScreenAPI(config) this.screen = new ScreenAPI(config)
this.application = new ApplicationAPI(config) this.application = new ApplicationAPI(config)
this.attachment = new AttachmentAPI(config)
} }
} }

View file

@ -1,4 +1,10 @@
import { SaveTableRequest, SaveTableResponse, Table } from "@budibase/types" import {
MigrateRequest,
MigrateResponse,
SaveTableRequest,
SaveTableResponse,
Table,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
@ -42,4 +48,23 @@ export class TableAPI extends TestAPI {
.expect(expectStatus) .expect(expectStatus)
return res.body return res.body
} }
migrate = async (
tableId: string,
data: MigrateRequest,
{ expectStatus } = { expectStatus: 200 }
): Promise<MigrateResponse> => {
const res = await this.request
.post(`/api/tables/${tableId}/migrate`)
.send(data)
.set(this.config.defaultHeaders())
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body
}
} }

View file

@ -19,6 +19,8 @@ import {
FieldType, FieldType,
SourceName, SourceName,
Table, Table,
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
const { BUILTIN_ROLE_IDS } = roles const { BUILTIN_ROLE_IDS } = roles
@ -26,6 +28,8 @@ export function basicTable(): Table {
return { return {
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,

View file

@ -17,7 +17,7 @@ import {
processInputBBReferences, processInputBBReferences,
processOutputBBReferences, processOutputBBReferences,
} from "./bbReferenceProcessor" } from "./bbReferenceProcessor"
import { isExternalTable } from "../../integrations/utils" import { isExternalTableID } from "../../integrations/utils"
export * from "./utils" export * from "./utils"
type AutoColumnProcessingOpts = { type AutoColumnProcessingOpts = {
@ -51,7 +51,7 @@ function getRemovedAttachmentKeys(
/** /**
* This will update any auto columns that are found on the row/table with the correct information based on * This will update any auto columns that are found on the row/table with the correct information based on
* time now and the current logged in user making the request. * time now and the current logged in user making the request.
* @param user The user to be used for an appId as well as the createdBy and createdAt fields. * @param userId The user to be used for an appId as well as the createdBy and createdAt fields.
* @param table The table which is to be used for the schema, as well as handling auto IDs incrementing. * @param table The table which is to be used for the schema, as well as handling auto IDs incrementing.
* @param row The row which is to be updated with information for the auto columns. * @param row The row which is to be updated with information for the auto columns.
* @param opts specific options for function to carry out optional features. * @param opts specific options for function to carry out optional features.
@ -241,7 +241,7 @@ export async function outputProcessing<T extends Row[] | Row>(
continue continue
} }
row[property].forEach((attachment: RowAttachment) => { row[property].forEach((attachment: RowAttachment) => {
attachment.url = objectStore.getAppFileUrl(attachment.key) attachment.url ??= objectStore.getAppFileUrl(attachment.key)
}) })
} }
} else if ( } else if (
@ -267,7 +267,7 @@ export async function outputProcessing<T extends Row[] | Row>(
)) as Row[] )) as Row[]
} }
// remove null properties to match internal API // remove null properties to match internal API
if (isExternalTable(table._id!)) { if (isExternalTableID(table._id!)) {
for (let row of enriched) { for (let row of enriched) {
for (let key of Object.keys(row)) { for (let key of Object.keys(row)) {
if (row[key] === null) { if (row[key] === null) {

View file

@ -1,6 +1,12 @@
import { inputProcessing } from ".." import { inputProcessing } from ".."
import { generator, structures } from "@budibase/backend-core/tests" import { generator, structures } from "@budibase/backend-core/tests"
import { FieldType, FieldTypeSubtypes, Table } from "@budibase/types" import {
FieldType,
FieldTypeSubtypes,
INTERNAL_TABLE_SOURCE_ID,
Table,
TableSourceType,
} from "@budibase/types"
import * as bbReferenceProcessor from "../bbReferenceProcessor" import * as bbReferenceProcessor from "../bbReferenceProcessor"
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({ jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
@ -20,6 +26,8 @@ describe("rowProcessor - inputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,
@ -70,6 +78,8 @@ describe("rowProcessor - inputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,
@ -110,6 +120,8 @@ describe("rowProcessor - inputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,
@ -150,6 +162,8 @@ describe("rowProcessor - inputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,

View file

@ -2,7 +2,9 @@ import {
FieldSubtype, FieldSubtype,
FieldType, FieldType,
FieldTypeSubtypes, FieldTypeSubtypes,
INTERNAL_TABLE_SOURCE_ID,
Table, Table,
TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { outputProcessing } from ".." import { outputProcessing } from ".."
import { generator, structures } from "@budibase/backend-core/tests" import { generator, structures } from "@budibase/backend-core/tests"
@ -26,6 +28,8 @@ describe("rowProcessor - outputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,
@ -71,6 +75,8 @@ describe("rowProcessor - outputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,
@ -108,6 +114,8 @@ describe("rowProcessor - outputProcessing", () => {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL,
schema: { schema: {
name: { name: {
type: FieldType.STRING, type: FieldType.STRING,

View file

@ -1,5 +1,5 @@
import authorized from "../middleware/authorized" import authorized from "../middleware/authorized"
import { BaseSocket } from "./websocket" import { BaseSocket, EmitOptions } from "./websocket"
import { permissions, events, context } from "@budibase/backend-core" import { permissions, events, context } from "@budibase/backend-core"
import http from "http" import http from "http"
import Koa from "koa" import Koa from "koa"
@ -16,6 +16,7 @@ import { gridSocket } from "./index"
import { clearLock, updateLock } from "../utilities/redis" import { clearLock, updateLock } from "../utilities/redis"
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent } from "@budibase/shared-core"
import { processTable } from "../sdk/app/tables/getters"
export default class BuilderSocket extends BaseSocket { export default class BuilderSocket extends BaseSocket {
constructor(app: Koa, server: http.Server) { constructor(app: Koa, server: http.Server) {
@ -100,11 +101,22 @@ export default class BuilderSocket extends BaseSocket {
}) })
} }
emitTableUpdate(ctx: any, table: Table) { emitTableUpdate(ctx: any, table: Table, options?: EmitOptions) {
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, { // This was added to make sure that sourceId is always present when
id: table._id, // sending this message to clients. Without this, tables without a
table, // sourceId (e.g. ta_users) won't get correctly updated client-side.
}) table = processTable(table)
this.emitToRoom(
ctx,
ctx.appId,
BuilderSocketEvent.TableChange,
{
id: table._id,
table,
},
options
)
gridSocket?.emitTableUpdate(ctx, table) gridSocket?.emitTableUpdate(ctx, table)
} }

View file

@ -11,6 +11,14 @@ import { SocketSession } from "@budibase/types"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { createContext, runMiddlewares } from "./middleware" import { createContext, runMiddlewares } from "./middleware"
export interface EmitOptions {
// Whether to include the originator of the request from the broadcast,
// defaults to false because it is assumed that the user who triggered
// an action will already have the changes of that action reflected in their
// own UI, so there is no need to send them again.
includeOriginator?: boolean
}
const anonUser = () => ({ const anonUser = () => ({
_id: uuid(), _id: uuid(),
email: "user@mail.com", email: "user@mail.com",
@ -270,10 +278,17 @@ export class BaseSocket {
// Emit an event to everyone in a room, including metadata of whom // Emit an event to everyone in a room, including metadata of whom
// the originator of the request was // the originator of the request was
emitToRoom(ctx: any, room: string | string[], event: string, payload: any) { emitToRoom(
this.io.in(room).emit(event, { ctx: any,
...payload, room: string | string[],
apiSessionId: ctx.headers?.[Header.SESSION_ID], event: string,
}) payload: any,
options?: EmitOptions
) {
let emitPayload = { ...payload }
if (!options?.includeOriginator) {
emitPayload.apiSessionId = ctx.headers?.[Header.SESSION_ID]
}
this.io.in(room).emit(event, emitPayload)
} }
} }

View file

@ -96,3 +96,45 @@ export enum BuilderSocketEvent {
export const SocketSessionTTL = 60 export const SocketSessionTTL = 60
export const ValidQueryNameRegex = /^[^()]*$/ export const ValidQueryNameRegex = /^[^()]*$/
export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g
export const ValidFileExtensions = [
"avif",
"css",
"csv",
"docx",
"drawio",
"editorconfig",
"edl",
"enc",
"export",
"geojson",
"gif",
"htm",
"html",
"ics",
"iqy",
"jfif",
"jpeg",
"jpg",
"json",
"log",
"md",
"mid",
"odt",
"pdf",
"png",
"ris",
"rtf",
"svg",
"tex",
"toml",
"twig",
"txt",
"url",
"wav",
"webp",
"xls",
"xlsx",
"xml",
"yaml",
"yml",
]

View file

@ -15,7 +15,7 @@
}, },
"jest": {}, "jest": {},
"devDependencies": { "devDependencies": {
"@budibase/nano": "10.1.2", "@budibase/nano": "10.1.3",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/node": "18.17.0", "@types/node": "18.17.0",
"@types/pouchdb": "6.4.0", "@types/pouchdb": "6.4.0",

View file

@ -0,0 +1,9 @@
export interface Upload {
size: number
name: string
url: string
extension: string
key: string
}
export type ProcessAttachmentResponse = Upload[]

View file

@ -5,3 +5,4 @@ export * from "./view"
export * from "./rows" export * from "./rows"
export * from "./table" export * from "./table"
export * from "./permission" export * from "./permission"
export * from "./attachment"

View file

@ -1,4 +1,5 @@
import { import {
FieldSchema,
Row, Row,
Table, Table,
TableRequest, TableRequest,
@ -33,3 +34,12 @@ export interface BulkImportRequest {
export interface BulkImportResponse { export interface BulkImportResponse {
message: string message: string
} }
export interface MigrateRequest {
oldColumn: FieldSchema
newColumn: FieldSchema
}
export interface MigrateResponse {
message: string
}

View file

@ -164,3 +164,33 @@ export type FieldSchema =
export interface TableSchema { export interface TableSchema {
[key: string]: FieldSchema [key: string]: FieldSchema
} }
export function isRelationshipField(
field: FieldSchema
): field is RelationshipFieldMetadata {
return field.type === FieldType.LINK
}
export function isManyToMany(
field: RelationshipFieldMetadata
): field is ManyToManyRelationshipFieldMetadata {
return field.relationshipType === RelationshipType.MANY_TO_MANY
}
export function isOneToMany(
field: RelationshipFieldMetadata
): field is OneToManyRelationshipFieldMetadata {
return field.relationshipType === RelationshipType.ONE_TO_MANY
}
export function isManyToOne(
field: RelationshipFieldMetadata
): field is ManyToOneRelationshipFieldMetadata {
return field.relationshipType === RelationshipType.MANY_TO_ONE
}
export function isBBReferenceField(
field: FieldSchema
): field is BBReferenceFieldMetadata {
return field.type === FieldType.BB_REFERENCE
}

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