Merge branch 'master' into fix-grid-single-char-changes-v2

This commit is contained in:
Andrew Kingston 2024-02-29 17:05:35 +00:00 committed by GitHub
commit 613105d370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 611 additions and 819 deletions

View File

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

@ -1 +1 @@
Subproject commit de6d44c372a7f48ca0ce8c6c0c19311d4bc21646
Subproject commit 19f7a5829f4d23cbc694136e45d94482a59a475a

View File

@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
export function generateTemplateID(ownerId: any) {
export function generateTemplateID(ownerId: string) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
* Generates a new dev info document ID - this is scoped to a user.
* @returns The new dev info ID which info for dev (like api key) can be stored under.
*/
export const generateDevInfoID = (userId: any) => {
export const generateDevInfoID = (userId: string) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}

View File

@ -116,7 +116,6 @@
$: pagerText = `Page ${currentPage} of ${totalPages}`
</script>
a11y-click-events-have-key-events
<div bind:this={buttonAnchor}>
<ActionButton on:click={dropdown.show}>
{displayValue}

View File

@ -69,11 +69,12 @@
// brought back to the same screen.
const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path))
if (!activeTopNav) return
builderStore.setPreviousTopNavPath(
activeTopNav.path,
window.location.pathname
)
if (activeTopNav) {
builderStore.setPreviousTopNavPath(
activeTopNav.path,
window.location.pathname
)
}
$goto($builderStore.previousTopNavPath[path] || path)
}

View File

@ -12,11 +12,17 @@
hoverStore,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
import {
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe
let layout
@ -234,16 +240,8 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container">
{#if loading}
<div
class={`loading ${$builderStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$builderStore.navigation?.navigation === "Left"}
hideFooter
hideDevTools
/>
<div class="center">
<ProgressCircle />
</div>
{:else if error}
<div class="center error">
@ -260,6 +258,8 @@
bind:this={iframe}
src="/app/preview"
class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/>
<div
class="add-component"
@ -279,25 +279,6 @@
/>
<style>
.loading {
position: absolute;
container-type: inline-size;
width: 100%;
height: 100%;
border: 2px solid transparent;
box-sizing: border-box;
}
.loading.tablet {
width: calc(1024px + 6px);
max-height: calc(768px + 6px);
}
.loading.mobile {
width: calc(390px + 6px);
max-height: calc(844px + 6px);
}
.component-container {
grid-row-start: middle;
grid-column-start: middle;

View File

@ -1,22 +1,16 @@
<script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify"
import { licensing, apps, auth, sideBarCollapsed } from "stores/portal"
import { apps, auth, sideBarCollapsed } from "stores/portal"
import { Link, Body, ActionButton } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core"
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
import { ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $apps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => {
loading = true
if (app.status === "published") {
return `/app${app.url}`
}
@ -34,20 +28,6 @@
}
$: fetchScreens(app?.devId)
const receiveMessage = async message => {
if (message.data.type === "docLoaded") {
loading = false
}
}
onMount(() => {
window.addEventListener("message", receiveMessage)
})
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
</script>
<div class="container">
@ -98,17 +78,7 @@
</Body>
</div>
{:else}
<div class:hide={!loading} class="loading">
<div class={`loadingThemeWrapper ${app.theme}`}>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe class:hide={loading} src={iframeUrl} title={app.name} />
<iframe src={iframeUrl} title={app.name} />
{/if}
</div>
@ -130,23 +100,6 @@
flex: 0 0 50px;
}
.loading {
height: 100%;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--spacing-s);
overflow: hidden;
}
.loadingThemeWrapper {
height: 100%;
container-type: inline-size;
}
.hide {
visibility: hidden;
height: 0;
border: none;
}
iframe {
flex: 1 1 auto;
border-radius: var(--spacing-s);

View File

@ -80,18 +80,11 @@
}
}
let fontsLoaded = false
// Load app config
onMount(async () => {
document.fonts.ready.then(() => {
fontsLoaded = true
})
await initialise()
await authStore.actions.fetchUser()
dataLoaded = true
if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded()
} else {
@ -100,12 +93,6 @@
})
}
})
$: {
if (dataLoaded && fontsLoaded) {
document.getElementById("clientAppSkeletonLoader")?.remove()
}
}
</script>
<svelte:head>
@ -116,140 +103,140 @@
{/if}
</svelte:head>
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
class:show={fontsLoaded && dataLoaded}
>
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
{#if dataLoaded}
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
>
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
{/if}
{#if showDevTools}
<DevTools />
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
{#if showDevTools}
<DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div>
<KeyboardManager />
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div>
<KeyboardManager />
{/if}
<style>
#spectrum-root {
height: 0;
visibility: hidden;
padding: 0;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
@ -270,11 +257,6 @@
background-color: transparent;
}
#spectrum-root.show {
height: 100%;
visibility: visible;
}
#app-root {
overflow: hidden;
height: 100%;

View File

@ -13,7 +13,6 @@
<style>
.free-footer {
min-height: 51px;
flex: 0 0 auto;
padding: 16px 20px;
border-top: 1px solid var(--spectrum-global-color-gray-300);

View File

@ -1,244 +0,0 @@
<script>
export let sideNav = false
export let hideDevTools = false
export let hideFooter = false
export let noAnimation = false
</script>
<div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
<div class="animation" class:noAnimation />
{#if !hideDevTools}
<div class="devTools" />
{/if}
<div class="main">
<div class="nav" />
<div class="body">
<div class="bodyVerticalPadding" />
<div class="bodyHorizontal">
<div class="bodyHorizontalPadding" />
<svg
class="svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="240"
height="256"
>
<mask id="mask">
<rect x="0" y="0" width="240" height="256" fill="white" />
<rect x="0" y="0" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="56" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="112" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="168" width="240" height="32" rx="6" fill="black" />
<rect x="71" y="224" width="98" height="32" rx="6" fill="black" />
</mask>
<rect
x="0"
y="0"
width="240"
height="256"
fill="black"
mask="url(#mask)"
/>
</svg>
<div class="bodyHorizontalPadding" />
</div>
<div class="bodyVerticalPadding" />
</div>
</div>
{#if !hideFooter}
<div class="footer" />
{/if}
</div>
<style>
.skeleton {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
background-color: var(--spectrum-global-color-gray-200);
}
.animation {
position: absolute;
height: 100%;
width: 100%;
background: linear-gradient(
to right,
transparent 0%,
var(--spectrum-global-color-gray-300) 20%,
transparent 40%,
transparent 100%
);
animation-duration: 1.3s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
animation-timing-function: linear;
}
.noAnimation {
animation-name: none;
background: transparent;
}
.devTools {
display: flex;
box-sizing: border-box;
background-color: black;
height: 60px;
padding: 1px 24px 1px 20px;
display: flex;
align-items: center;
z-index: 1;
flex-shrink: 0;
color: white;
mix-blend-mode: multiply;
background: rgb(0 0 0);
font-size: 30px;
font-family: Source Sans Pro;
-webkit-font-smoothing: antialiased;
}
.main {
height: 100%;
display: flex;
flex-direction: column;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
.sideNav .main {
flex-direction: row;
width: 100%;
}
.nav {
flex-shrink: 0;
width: 100%;
height: 141px;
background-color: transparent;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
.sideNav .nav {
height: 100%;
width: 251px;
}
.body {
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
.sideNav .body {
width: 100%;
height: initial;
}
.body :global(svg > rect) {
fill: var(--spectrum-alias-background-color-primary);
}
.body :global(svg) {
flex-shrink: 0;
}
.bodyHorizontal {
display: flex;
flex-shrink: 0;
}
.bodyHorizontalPadding {
height: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.bodyVerticalPadding {
width: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.footer {
flex-shrink: 0;
box-sizing: border-box;
z-index: 1;
height: 52px;
width: 100%;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
.sideNav .footer {
border-top: 3px solid var(--spectrum-alias-background-color-primary);
}
@keyframes shimmer {
0% {
left: -170%;
}
100% {
left: 170%;
}
}
</style>

View File

@ -5,4 +5,3 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"

View File

@ -17,8 +17,5 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}

View File

@ -50,7 +50,4 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}

View File

@ -52,7 +52,6 @@
"@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2",

View File

@ -20,6 +20,7 @@ import {
AutomationActionStepId,
AutomationResults,
UserCtx,
DeleteAutomationResponse,
} from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk"
@ -72,7 +73,9 @@ function cleanAutomationInputs(automation: Automation) {
return automation
}
export async function create(ctx: UserCtx) {
export async function create(
ctx: UserCtx<Automation, { message: string; automation: Automation }>
) {
const db = context.getAppDB()
let automation = ctx.request.body
automation.appId = ctx.appId
@ -207,7 +210,7 @@ export async function find(ctx: UserCtx) {
ctx.body = await db.get(ctx.params.id)
}
export async function destroy(ctx: UserCtx) {
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
const db = context.getAppDB()
const automationId = ctx.params.id
const oldAutomation = await db.get<Automation>(automationId)

View File

@ -1,9 +1,17 @@
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core"
import { BBContext, Layout } from "@budibase/types"
import {
BBContext,
Layout,
SaveLayoutRequest,
SaveLayoutResponse,
UserCtx,
} from "@budibase/types"
export async function save(ctx: BBContext) {
export async function save(
ctx: UserCtx<SaveLayoutRequest, SaveLayoutResponse>
) {
const db = context.getAppDB()
let layout = ctx.request.body

View File

@ -73,7 +73,7 @@ const _import = async (ctx: UserCtx) => {
}
export { _import as import }
export async function save(ctx: UserCtx) {
export async function save(ctx: UserCtx<Query, Query>) {
const db = context.getAppDB()
const query: Query = ctx.request.body

View File

@ -189,11 +189,12 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
const tableId = utils.getTableId(ctx)
const rowId = ctx.params.rowId as string
// need table to work out where links go in row, as well as the link docs
const [table, row, links] = await Promise.all([
const [table, links] = await Promise.all([
sdk.tables.getTable(tableId),
utils.findRow(ctx, tableId, rowId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
])
let row = await utils.findRow(ctx, tableId, rowId)
row = await outputProcessing(table, row)
const linkVals = links as LinkDocumentValue[]
// look up the actual rows based on the ids

View File

@ -7,7 +7,13 @@ import {
roles,
} from "@budibase/backend-core"
import { updateAppPackage } from "./application"
import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types"
import {
Plugin,
ScreenProps,
BBContext,
Screen,
UserCtx,
} from "@budibase/types"
import { builderSocket } from "../../websockets"
export async function fetch(ctx: BBContext) {
@ -31,7 +37,7 @@ export async function fetch(ctx: BBContext) {
)
}
export async function save(ctx: BBContext) {
export async function save(ctx: UserCtx<Screen, Screen>) {
const db = context.getAppDB()
let screen = ctx.request.body

View File

@ -1,5 +1,7 @@
import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants"
@ -22,13 +24,7 @@ import AWS from "aws-sdk"
import fs from "fs"
import sdk from "../../../sdk"
import * as pro from "@budibase/pro"
import {
UserCtx,
App,
Ctx,
ProcessAttachmentResponse,
Feature,
} from "@budibase/types"
import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types"
import {
getAppMigrationVersion,
getLatestMigrationId,
@ -36,61 +32,6 @@ import {
import send from "koa-send"
const getThemeVariables = (theme: string) => {
if (theme === "spectrum--lightest") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(244, 244, 244);
--spectrum-global-color-gray-300: rgb(234, 234, 234);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--light") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(234, 234, 234);
--spectrum-global-color-gray-300: rgb(225, 225, 225);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--dark") {
return `
--spectrum-global-color-gray-100: rgb(50, 50, 50);
--spectrum-global-color-gray-200: rgb(62, 62, 62);
--spectrum-global-color-gray-300: rgb(74, 74, 74);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--darkest") {
return `
--spectrum-global-color-gray-100: rgb(30, 30, 30);
--spectrum-global-color-gray-200: rgb(44, 44, 44);
--spectrum-global-color-gray-300: rgb(57, 57, 57);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--nord") {
return `
--spectrum-global-color-gray-100: #3b4252;
--spectrum-global-color-gray-200: #424a5c;
--spectrum-global-color-gray-300: #4c566a;
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--midnight") {
return `
--hue: 220;
--sat: 10%;
--spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
--spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
--spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
}
export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}`
@ -205,7 +146,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations
}
export const serveApp = async function (ctx: UserCtx) {
export const serveApp = async function (ctx: Ctx) {
const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed =
@ -226,19 +167,9 @@ export const serveApp = async function (ctx: UserCtx) {
const appInfo = await db.get<any>(DocumentType.APP_METADATA)
let appId = context.getAppId()
const hideDevTools = !!ctx.params.appUrl
const sideNav = appInfo.navigation.navigation === "Left"
const hideFooter =
ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
const themeVariables = getThemeVariables(appInfo?.theme)
if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({
hideDevTools,
sideNav,
hideFooter,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@ -263,7 +194,7 @@ export const serveApp = async function (ctx: UserCtx) {
ctx.body = await processString(appHbs, {
head,
body: html,
css: `:root{${themeVariables}} ${css.code}`,
style: css.code,
appId,
embedded: bbHeaderEmbed,
})

View File

@ -1,6 +1,4 @@
<script>
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
export let title = ""
export let favicon = ""
@ -11,10 +9,6 @@
export let clientLibPath
export let usedPlugins
export let appMigrating
export let hideDevTools
export let sideNav
export let hideFooter
</script>
<svelte:head>
@ -102,7 +96,6 @@
</svelte:head>
<body id="app">
<ClientAppSkeleton {hideDevTools} {sideNav} {hideFooter} />
<div id="error">
{#if clientLibPath}
<h1>There was an error loading your app</h1>

View File

@ -1,12 +1,8 @@
<html>
<script>
document.fonts.ready.then(() => {
window.parent.postMessage({ type: "docLoaded" });
})
</script>
<head>
{{{head}}}
<style>{{{css}}}</style>
<style>{{{style}}}</style>
</head>
<script>

View File

@ -51,8 +51,8 @@ router
controller.deleteObjects
)
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
.get("/app/:appUrl/:path*", controller.serveApp)
.get("/:appId/:path*", controller.serveApp)
.get("/app/:appUrl/:path*", controller.serveApp)
.post(
"/api/attachments/:datasourceId/url",
authorized(PermissionType.TABLE, PermissionLevel.READ),

View File

@ -394,7 +394,7 @@ describe("/automations", () => {
it("deletes a automation by its ID", async () => {
const automation = await config.createAutomation()
const res = await request
.delete(`/api/automations/${automation.id}/${automation.rev}`)
.delete(`/api/automations/${automation._id}/${automation._rev}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
@ -408,7 +408,7 @@ describe("/automations", () => {
await checkBuilderEndpoint({
config,
method: "DELETE",
url: `/api/automations/${automation.id}/${automation._rev}`,
url: `/api/automations/${automation._id}/${automation._rev}`,
})
})
})

View File

@ -44,7 +44,7 @@ describe("/backups", () => {
expect(headers["content-disposition"]).toEqual(
`attachment; filename="${
config.getApp()!.name
config.getApp().name
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
)
})

View File

@ -86,7 +86,7 @@ describe("/datasources", () => {
})
// check variables in cache
let contents = await checkCacheForDynamicVariable(
query._id,
query._id!,
"variable3"
)
expect(contents.rows.length).toEqual(1)
@ -102,7 +102,7 @@ describe("/datasources", () => {
expect(res.body.errors).toBeUndefined()
// check variables no longer in cache
contents = await checkCacheForDynamicVariable(query._id, "variable3")
contents = await checkCacheForDynamicVariable(query._id!, "variable3")
expect(contents).toBe(null)
})
})

View File

@ -467,7 +467,10 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}",
})
// check its in cache
const contents = await checkCacheForDynamicVariable(base._id, "variable3")
const contents = await checkCacheForDynamicVariable(
base._id!,
"variable3"
)
expect(contents.rows.length).toEqual(1)
const responseBody = await preview(datasource, {
path: "www.failonce.com",
@ -490,7 +493,7 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}",
})
// check its in cache
let contents = await checkCacheForDynamicVariable(base._id, "variable3")
let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents.rows.length).toEqual(1)
// delete the query
@ -500,7 +503,7 @@ describe("/queries", () => {
.expect(200)
// check variables no longer in cache
contents = await checkCacheForDynamicVariable(base._id, "variable3")
contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents).toBe(null)
})
})

View File

@ -110,7 +110,7 @@ describe.each([
config.api.row.get(tbl_Id, id, { expectStatus: status })
const getRowUsage = async () => {
const { total } = await config.doInContext(null, () =>
const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
)
return total

View File

@ -27,15 +27,17 @@ describe("/users", () => {
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" })
await config.createUser({ id: "uuidy" })
const id1 = `us_${utils.newid()}`
const id2 = `us_${utils.newid()}`
await config.createUser({ _id: id1 })
await config.createUser({ _id: id2 })
const res = await config.api.user.fetch()
expect(res.length).toBe(3)
const ids = res.map(u => u._id)
expect(ids).toContain(`ro_ta_users_us_uuidx`)
expect(ids).toContain(`ro_ta_users_us_uuidy`)
expect(ids).toContain(`ro_ta_users_${id1}`)
expect(ids).toContain(`ro_ta_users_${id2}`)
})
it("should apply authorization to endpoint", async () => {
@ -54,7 +56,7 @@ describe("/users", () => {
describe("update", () => {
it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({
id: `us_update${utils.newid()}`,
_id: `us_update${utils.newid()}`,
})
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev

View File

@ -4,6 +4,7 @@ import { AppStatus } from "../../../../db/utils"
import { roles, tenancy, context, db } from "@budibase/backend-core"
import env from "../../../../environment"
import Nano from "@budibase/nano"
import TestConfiguration from "src/tests/utilities/TestConfiguration"
class Request {
appId: any
@ -52,10 +53,10 @@ export const clearAllApps = async (
})
}
export const clearAllAutomations = async (config: any) => {
export const clearAllAutomations = async (config: TestConfiguration) => {
const automations = await config.getAllAutomations()
for (let auto of automations) {
await context.doInAppContext(config.appId, async () => {
await context.doInAppContext(config.getAppId(), async () => {
await config.deleteAutomation(auto)
})
}
@ -101,7 +102,12 @@ export const checkBuilderEndpoint = async ({
method,
url,
body,
}: any) => {
}: {
config: TestConfiguration
method: string
url: string
body?: any
}) => {
const headers = await config.login({
userId: "us_fail",
builder: false,

View File

@ -36,7 +36,7 @@ describe("/webhooks", () => {
const automation = await config.createAutomation()
const res = await request
.put(`/api/webhooks`)
.send(basicWebhook(automation._id))
.send(basicWebhook(automation._id!))
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
@ -145,7 +145,7 @@ describe("/webhooks", () => {
let automation = collectAutomation()
let newAutomation = await config.createAutomation(automation)
let syncWebhook = await config.createWebhook(
basicWebhook(newAutomation._id)
basicWebhook(newAutomation._id!)
)
// replicate changes before checking webhook

View File

@ -29,6 +29,6 @@ start().catch(err => {
throw err
})
export function getServer() {
export function getServer(): Server {
return server
}

View File

@ -1,9 +1,11 @@
import { Layout } from "@budibase/types"
export const BASE_LAYOUT_PROP_IDS = {
PRIVATE: "layout_private_master",
PUBLIC: "layout_public_master",
}
export const EMPTY_LAYOUT = {
export const EMPTY_LAYOUT: Layout = {
componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}",
favicon: "./_shared/favicon.png",

View File

@ -1,5 +1,6 @@
import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
import { Screen } from "@budibase/types"
export function createHomeScreen(
config: {
@ -9,10 +10,8 @@ export function createHomeScreen(
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/",
}
) {
): Screen {
return {
description: "",
url: "",
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",

View File

@ -400,7 +400,7 @@ class InternalBuilder {
return query.limit(BASE_LIMIT)
}
create(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
@ -422,7 +422,7 @@ class InternalBuilder {
}
}
bulkCreate(knex: Knex, json: QueryJson): KnexQuery {
bulkCreate(knex: Knex, json: QueryJson): Knex.QueryBuilder {
const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
@ -491,7 +491,7 @@ class InternalBuilder {
return this.addFilters(query, filters, { relationship: true })
}
update(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
@ -507,7 +507,7 @@ class InternalBuilder {
}
}
delete(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
@ -537,17 +537,17 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
* @return the query ready to be passed to the driver.
*/
_query(json: QueryJson, opts: QueryOptions = {}) {
_query(json: QueryJson, opts: QueryOptions = {}): Knex.SqlNative | Knex.Sql {
const sqlClient = this.getSqlClient()
const client = knex({ client: sqlClient })
let query
let query: Knex.QueryBuilder
const builder = new InternalBuilder(sqlClient)
switch (this._operation(json)) {
case Operation.CREATE:
query = builder.create(client, json, opts)
break
case Operation.READ:
query = builder.read(client, json, this.limit)
query = builder.read(client, json, this.limit) as Knex.QueryBuilder
break
case Operation.UPDATE:
query = builder.update(client, json, opts)
@ -565,8 +565,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
default:
throw `Operation type is not supported by SQL query builder`
}
// @ts-ignore
return query.toSQL().toNative()
}

View File

@ -9,7 +9,7 @@ import {
Table,
FieldType,
} from "@budibase/types"
import { breakExternalTableId } from "../utils"
import { breakExternalTableId, SqlClient } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder
import { utils } from "@budibase/shared-core"
@ -135,7 +135,8 @@ function generateSchema(
// need to check if any columns have been deleted
if (oldTable) {
const deletedColumns = Object.entries(oldTable.schema).filter(
([key, column]) => isIgnoredType(column.type) && table.schema[key] == null
([key, column]) =>
!isIgnoredType(column.type) && table.schema[key] == null
)
deletedColumns.forEach(([key, column]) => {
if (renamed?.old === key || isIgnoredType(column.type)) {
@ -197,13 +198,14 @@ class SqlTableQueryBuilder {
return json.endpoint.operation
}
_tableQuery(json: QueryJson): any {
_tableQuery(json: QueryJson): Knex.Sql | Knex.SqlNative {
let client = knex({ client: this.sqlClient }).schema
if (json?.endpoint?.schema) {
client = client.withSchema(json.endpoint.schema)
let schemaName = json?.endpoint?.schema
if (schemaName) {
client = client.withSchema(schemaName)
}
let query
let query: Knex.SchemaBuilder
if (!json.table || !json.meta || !json.meta.tables) {
throw "Cannot execute without table being specified"
}
@ -215,6 +217,18 @@ class SqlTableQueryBuilder {
if (!json.meta || !json.meta.table) {
throw "Must specify old table for update"
}
// renameColumn does not work for MySQL, so return a raw query
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
const updatedColumn = json.meta.renamed.updated
const tableName = schemaName
? `\`${schemaName}\`.\`${json.table.name}\``
: `\`${json.table.name}\``
const externalType = json.table.schema[updatedColumn].externalType!
return {
sql: `alter table ${tableName} change column \`${json.meta.renamed.old}\` \`${updatedColumn}\` ${externalType};`,
bindings: [],
}
}
query = buildUpdateTable(
client,
json.table,

View File

@ -12,7 +12,6 @@ import {
SourceName,
Schema,
TableSourceType,
FieldType,
} from "@budibase/types"
import {
getSqlQuery,

View File

@ -421,7 +421,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
async query(json: QueryJson) {
const operation = this._operation(json)
const input = this._query(json, { disableReturning: true })
const input = this._query(json, { disableReturning: true }) as SqlQuery
if (Array.isArray(input)) {
const responses = []
for (let query of input) {

View File

@ -419,7 +419,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
async query(json: QueryJson) {
const operation = this._operation(json).toLowerCase()
const input = this._query(json)
const input = this._query(json) as SqlQuery
if (Array.isArray(input)) {
const responses = []
for (let query of input) {

View File

@ -1,3 +1,11 @@
import {
Operation,
QueryJson,
TableSourceType,
Table,
FieldType,
} from "@budibase/types"
const Sql = require("../base/sql").default
const { SqlClient } = require("../utils")
@ -17,7 +25,7 @@ function generateReadJson({
filters,
sort,
paginate,
}: any = {}) {
}: any = {}): QueryJson {
return {
endpoint: endpoint(table || TABLE_NAME, "READ"),
resource: {
@ -28,6 +36,10 @@ function generateReadJson({
paginate: paginate || {},
meta: {
table: {
type: "table",
sourceType: TableSourceType.EXTERNAL,
sourceId: "SOURCE_ID",
schema: {},
name: table || TABLE_NAME,
primary: ["id"],
},
@ -35,34 +47,40 @@ function generateReadJson({
}
}
function generateCreateJson(table = TABLE_NAME, body = {}) {
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
return {
endpoint: endpoint(table, "CREATE"),
body,
}
}
function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) {
function generateUpdateJson({
table = TABLE_NAME,
body = {},
filters = {},
meta = {},
}): QueryJson {
return {
endpoint: endpoint(table, "UPDATE"),
filters,
body,
meta,
}
}
function generateDeleteJson(table = TABLE_NAME, filters = {}) {
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
return {
endpoint: endpoint(table, "DELETE"),
filters,
}
}
function generateRelationshipJson(config: { schema?: string } = {}) {
function generateRelationshipJson(config: { schema?: string } = {}): QueryJson {
return {
endpoint: {
datasourceId: "Postgres",
entityId: "brands",
operation: "READ",
operation: Operation.READ,
schema: config.schema,
},
resource: {
@ -76,7 +94,6 @@ function generateRelationshipJson(config: { schema?: string } = {}) {
},
filters: {},
sort: {},
paginate: {},
relationships: [
{
from: "brand_id",
@ -240,17 +257,17 @@ describe("SQL query builder", () => {
it("should test an update statement", () => {
const query = sql._query(
generateUpdateJson(
TABLE_NAME,
{
generateUpdateJson({
table: TABLE_NAME,
body: {
name: "John",
},
{
filters: {
equal: {
id: 1001,
},
}
)
},
})
)
expect(query).toEqual({
bindings: ["John", 1001],
@ -682,4 +699,99 @@ describe("SQL query builder", () => {
sql: `insert into \"test\" (\"name\") values ($1) returning *`,
})
})
it("should be able to rename column for MySQL", () => {
const table: Table = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
name: TABLE_NAME,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
},
sourceId: "SOURCE_ID",
}
const oldTable: Table = {
...table,
schema: {
name: {
type: FieldType.STRING,
name: "name",
externalType: "varchar(45)",
},
},
}
const query = new Sql(SqlClient.MY_SQL, limit)._query({
table,
endpoint: {
datasourceId: "MySQL",
operation: Operation.UPDATE_TABLE,
entityId: TABLE_NAME,
},
meta: {
table: oldTable,
tables: [oldTable],
renamed: {
old: "name",
updated: "first_name",
},
},
})
expect(query).toEqual({
bindings: [],
sql: `alter table \`${TABLE_NAME}\` change column \`name\` \`first_name\` varchar(45);`,
})
})
it("should be able to delete a column", () => {
const table: Table = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
name: TABLE_NAME,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
},
sourceId: "SOURCE_ID",
}
const oldTable: Table = {
...table,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
last_name: {
type: FieldType.STRING,
name: "last_name",
externalType: "varchar(45)",
},
},
}
const query = sql._query({
table,
endpoint: {
datasourceId: "Postgres",
operation: Operation.UPDATE_TABLE,
entityId: TABLE_NAME,
},
meta: {
table: oldTable,
tables: [oldTable],
},
})
expect(query).toEqual([
{
bindings: [],
sql: `alter table "${TABLE_NAME}" drop column "last_name"`,
},
])
})
})

View File

@ -13,7 +13,7 @@ describe("syncApps", () => {
afterAll(config.end)
it("runs successfully", async () => {
return config.doInContext(null, async () => {
return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)

View File

@ -12,8 +12,8 @@ describe("syncCreators", () => {
afterAll(config.end)
it("syncs creators", async () => {
return config.doInContext(null, async () => {
await config.createUser({ admin: true })
return config.doInContext(undefined, async () => {
await config.createUser({ admin: { global: true } })
await syncCreators.run()

View File

@ -14,7 +14,7 @@ describe("syncRows", () => {
afterAll(config.end)
it("runs successfully", async () => {
return config.doInContext(null, async () => {
return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)

View File

@ -12,7 +12,7 @@ describe("syncUsers", () => {
afterAll(config.end)
it("syncs users", async () => {
return config.doInContext(null, async () => {
return config.doInContext(undefined, async () => {
await config.createUser()
await syncUsers.run()

View File

@ -40,7 +40,7 @@ describe("migrations", () => {
describe("backfill", () => {
it("runs app db migration", async () => {
await config.doInContext(null, async () => {
await config.doInContext(undefined, async () => {
await clearMigrations()
await config.createAutomation()
await config.createAutomation(structures.newAutomation())
@ -93,18 +93,18 @@ describe("migrations", () => {
})
it("runs global db migration", async () => {
await config.doInContext(null, async () => {
await config.doInContext(undefined, async () => {
await clearMigrations()
const appId = config.prodAppId
const appId = config.getProdAppId()
const roles = { [appId]: "role_12345" }
await config.createUser({
builder: false,
admin: true,
builder: { global: false },
admin: { global: true },
roles,
}) // admin only
await config.createUser({
builder: false,
admin: false,
builder: { global: false },
admin: { global: false },
roles,
}) // non admin non builder
await config.createTable()

View File

@ -43,8 +43,8 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
const user = await config.createUser({
email,
roles,
builder: builder || false,
admin: false,
builder: { global: builder || false },
admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.created(user)
@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
async function removeUserRole(user: User) {
const final = await config.globalUser({
...user,
id: user._id,
_id: user._id,
roles: {},
builder: false,
admin: false,
builder: { global: false },
admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.updated(final)
@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
groupUser = await config.createUser({
email,
roles: {},
builder: false,
admin: false,
builder: { global: false },
admin: { global: false },
})
group = await config.createGroup()
await config.addUserToGroup(group._id!, groupUser._id!)

View File

@ -81,7 +81,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save(
table._id!,
row,
config.user._id
config.getUser()._id
)
expect(response).toEqual({
@ -129,7 +129,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save(
table._id!,
row,
config.user._id
config.getUser()._id
)
expect(response).toEqual({
@ -190,15 +190,15 @@ describe("sdk >> rows >> internal", () => {
await config.doInContext(config.appId, async () => {
for (const row of makeRows(5)) {
await internalSdk.save(table._id!, row, config.user._id)
await internalSdk.save(table._id!, row, config.getUser()._id)
}
await Promise.all(
makeRows(10).map(row =>
internalSdk.save(table._id!, row, config.user._id)
internalSdk.save(table._id!, row, config.getUser()._id)
)
)
for (const row of makeRows(5)) {
await internalSdk.save(table._id!, row, config.user._id)
await internalSdk.save(table._id!, row, config.getUser()._id)
}
})

View File

@ -22,15 +22,18 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(1)
expect(metadata).toEqual([
expect.objectContaining({
_id: db.generateUserMetadataID(config.user._id),
_id: db.generateUserMetadataID(config.getUser()._id!),
}),
])
})
})
it("admin and builders users are synced", async () => {
const user1 = await config.createUser({ admin: true })
const user2 = await config.createUser({ admin: false, builder: true })
const user1 = await config.createUser({ admin: { global: true } })
const user2 = await config.createUser({
admin: { global: false },
builder: { global: true },
})
await config.doInContext(config.appId, async () => {
expect(await rawUserMetadata()).toHaveLength(1)
await syncGlobalUsers()
@ -51,7 +54,10 @@ describe("syncGlobalUsers", () => {
})
it("app users are not synced if not specified", async () => {
const user = await config.createUser({ admin: false, builder: false })
const user = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await config.doInContext(config.appId, async () => {
await syncGlobalUsers()
@ -68,8 +74,14 @@ describe("syncGlobalUsers", () => {
it("app users are added when group is assigned to app", async () => {
await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false })
const user2 = await config.createUser({ admin: false, builder: false })
const user1 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
const user2 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => {
@ -103,8 +115,14 @@ describe("syncGlobalUsers", () => {
it("app users are removed when app is removed from user group", async () => {
await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false })
const user2 = await config.createUser({ admin: false, builder: false })
const user1 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
const user2 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await proSdk.groups.updateGroupApps(group.id, {
appsToAdd: [
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },

View File

@ -49,25 +49,31 @@ import {
AuthToken,
Automation,
CreateViewRequest,
Ctx,
Datasource,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
Layout,
Query,
RelationshipFieldMetadata,
RelationshipType,
Row,
Screen,
SearchParams,
SourceName,
Table,
TableSourceType,
User,
UserRoles,
UserCtx,
View,
Webhook,
WithRequired,
} from "@budibase/types"
import API from "./api"
import { cloneDeep } from "lodash"
import jwt, { Secret } from "jsonwebtoken"
import { Server } from "http"
mocks.licenses.init(pro)
@ -82,27 +88,23 @@ export interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
}
export default class TestConfiguration {
server: any
request: supertest.SuperTest<supertest.Test> | undefined
server?: Server
request?: supertest.SuperTest<supertest.Test>
started: boolean
appId: string | null
allApps: any[]
appId?: string
allApps: App[]
app?: App
prodApp: any
prodAppId: any
user: any
userMetadataId: any
prodApp?: App
prodAppId?: string
user?: User
userMetadataId?: string
table?: Table
automation: any
automation?: Automation
datasource?: Datasource
tenantId?: string
api: API
csrfToken?: string
private get globalUserId() {
return this.user._id
}
constructor(openServer = true) {
if (openServer) {
// use a random port because it doesn't matter
@ -114,7 +116,7 @@ export default class TestConfiguration {
} else {
this.started = false
}
this.appId = null
this.appId = undefined
this.allApps = []
this.api = new API(this)
@ -125,46 +127,86 @@ export default class TestConfiguration {
}
getApp() {
if (!this.app) {
throw new Error("app has not been initialised, call config.init() first")
}
return this.app
}
getProdApp() {
if (!this.prodApp) {
throw new Error(
"prodApp has not been initialised, call config.init() first"
)
}
return this.prodApp
}
getAppId() {
if (!this.appId) {
throw "appId has not been initialised properly"
throw new Error(
"appId has not been initialised, call config.init() first"
)
}
return this.appId
}
getProdAppId() {
if (!this.prodAppId) {
throw new Error(
"prodAppId has not been initialised, call config.init() first"
)
}
return this.prodAppId
}
getUser(): User {
if (!this.user) {
throw new Error("User has not been initialised, call config.init() first")
}
return this.user
}
getUserDetails() {
const user = this.getUser()
return {
globalId: this.globalUserId,
email: this.user.email,
firstName: this.user.firstName,
lastName: this.user.lastName,
globalId: user._id!,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
}
}
getAutomation() {
if (!this.automation) {
throw new Error(
"automation has not been initialised, call config.init() first"
)
}
return this.automation
}
getDatasource() {
if (!this.datasource) {
throw new Error(
"datasource has not been initialised, call config.init() first"
)
}
return this.datasource
}
async doInContext<T>(
appId: string | null,
appId: string | undefined,
task: () => Promise<T>
): Promise<T> {
if (!appId) {
appId = this.appId
}
const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => {
if (!appId) {
appId = this.appId
}
// check if already in a context
if (context.getAppId() == null && appId !== null) {
if (context.getAppId() == null && appId) {
return context.doInAppContext(appId, async () => {
return task()
})
@ -259,7 +301,11 @@ export default class TestConfiguration {
// UTILS
_req(body: any, params: any, controlFunc: any) {
_req<Req extends Record<string, any> | void, Res>(
handler: (ctx: UserCtx<Req, Res>) => Promise<void>,
body?: Req,
params?: Record<string, string | undefined>
): Promise<Res> {
// create a fake request ctx
const request: any = {}
const appId = this.appId
@ -278,63 +324,48 @@ export default class TestConfiguration {
throw new Error(`Error ${status} - ${message}`)
}
return this.doInContext(appId, async () => {
await controlFunc(request)
await handler(request)
return request.body
})
}
// USER / AUTH
async globalUser(
config: {
id?: string
firstName?: string
lastName?: string
builder?: boolean
admin?: boolean
email?: string
roles?: any
} = {}
): Promise<User> {
async globalUser(config: Partial<User> = {}): Promise<User> {
const {
id = `us_${newid()}`,
_id = `us_${newid()}`,
firstName = generator.first(),
lastName = generator.last(),
builder = true,
admin = false,
builder = { global: true },
admin = { global: false },
email = generator.email(),
roles,
tenantId = this.getTenantId(),
roles = {},
} = config
const db = tenancy.getTenantDB(this.getTenantId())
let existing
let existing: Partial<User> = {}
try {
existing = await db.get<any>(id)
existing = await db.get<User>(_id)
} catch (err) {
existing = { email }
// ignore
}
const user: User = {
_id: id,
_id,
...existing,
roles: roles || {},
tenantId: this.getTenantId(),
...config,
email,
roles,
tenantId,
firstName,
lastName,
builder,
admin,
}
await sessions.createASession(id, {
await sessions.createASession(_id, {
sessionId: "sessionid",
tenantId: this.getTenantId(),
csrfToken: this.csrfToken,
})
if (builder) {
user.builder = { global: true }
} else {
user.builder = { global: false }
}
if (admin) {
user.admin = { global: true }
} else {
user.admin = { global: false }
}
const resp = await db.put(user)
return {
_rev: resp.rev,
@ -342,38 +373,9 @@ export default class TestConfiguration {
}
}
async createUser(
user: {
id?: string
firstName?: string
lastName?: string
email?: string
builder?: boolean
admin?: boolean
roles?: UserRoles
} = {}
): Promise<User> {
const {
id,
firstName = generator.first(),
lastName = generator.last(),
email = generator.email(),
builder = true,
admin,
roles,
} = user
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
const resp = await this.globalUser({
id: globalId,
firstName,
lastName,
email,
builder,
admin,
roles: roles || {},
})
await cache.user.invalidateUser(globalId)
async createUser(user: Partial<User> = {}): Promise<User> {
const resp = await this.globalUser(user)
await cache.user.invalidateUser(resp._id!)
return resp
}
@ -381,7 +383,7 @@ export default class TestConfiguration {
return context.doInTenant(this.tenantId!, async () => {
const baseGroup = structures.userGroups.userGroup()
baseGroup.roles = {
[this.prodAppId]: roleId,
[this.getProdAppId()]: roleId,
}
const { id, rev } = await pro.sdk.groups.save(baseGroup)
return {
@ -404,8 +406,18 @@ export default class TestConfiguration {
})
}
async login({ roleId, userId, builder, prodApp = false }: any = {}) {
const appId = prodApp ? this.prodAppId : this.appId
async login({
roleId,
userId,
builder,
prodApp,
}: {
roleId?: string
userId: string
builder: boolean
prodApp: boolean
}) {
const appId = prodApp ? this.getProdAppId() : this.getAppId()
return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId
if (!this.request) {
@ -414,9 +426,9 @@ export default class TestConfiguration {
// make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({
id: userId,
builder,
roles: { [this.prodAppId]: roleId },
_id: userId,
builder: { global: builder },
roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
})
}
await sessions.createASession(userId, {
@ -445,8 +457,9 @@ export default class TestConfiguration {
defaultHeaders(extras = {}, prodApp = false) {
const tenantId = this.getTenantId()
const user = this.getUser()
const authObj: AuthToken = {
userId: this.globalUserId,
userId: user._id!,
sessionId: "sessionid",
tenantId,
}
@ -498,7 +511,7 @@ export default class TestConfiguration {
builder = false,
prodApp = true,
} = {}) {
return this.login({ email, roleId, builder, prodApp })
return this.login({ userId: email, roleId, builder, prodApp })
}
// TENANCY
@ -521,18 +534,22 @@ export default class TestConfiguration {
this.tenantId = structures.tenant.id()
this.user = await this.globalUser()
this.userMetadataId = generateUserMetadataID(this.user._id)
this.userMetadataId = generateUserMetadataID(this.user._id!)
return this.createApp(appName)
}
doInTenant(task: any) {
doInTenant<T>(task: () => T) {
return context.doInTenant(this.getTenantId(), task)
}
// API
async generateApiKey(userId = this.user._id) {
async generateApiKey(userId?: string) {
const user = this.getUser()
if (!userId) {
userId = user._id!
}
const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId)
let devInfo: any
@ -552,25 +569,28 @@ export default class TestConfiguration {
async createApp(appName: string): Promise<App> {
// create dev app
// clear any old app
this.appId = null
this.app = await context.doInTenant(this.tenantId!, async () => {
const app = await this._req({ name: appName }, null, appController.create)
this.appId = app.appId!
return app
})
return await context.doInAppContext(this.getAppId(), async () => {
this.appId = undefined
this.app = await context.doInTenant(
this.tenantId!,
async () =>
(await this._req(appController.create, {
name: appName,
})) as App
)
this.appId = this.app.appId
return await context.doInAppContext(this.app.appId!, async () => {
// create production app
this.prodApp = await this.publish()
this.allApps.push(this.prodApp)
this.allApps.push(this.app)
this.allApps.push(this.app!)
return this.app!
})
}
async publish() {
await this._req(null, null, deployController.publishApp)
await this._req(deployController.publishApp)
// @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId
@ -582,13 +602,11 @@ export default class TestConfiguration {
}
async unpublish() {
const response = await this._req(
null,
{ appId: this.appId },
appController.unpublish
)
this.prodAppId = null
this.prodApp = null
const response = await this._req(appController.unpublish, {
appId: this.appId,
})
this.prodAppId = undefined
this.prodApp = undefined
return response
}
@ -716,8 +734,7 @@ export default class TestConfiguration {
// ROLE
async createRole(config?: any) {
config = config || basicRole()
return this._req(config, null, roleController.save)
return this._req(roleController.save, config || basicRole())
}
// VIEW
@ -730,7 +747,7 @@ export default class TestConfiguration {
tableId: this.table!._id,
name: generator.guid(),
}
return this._req(view, null, viewController.v1.save)
return this._req(viewController.v1.save, view)
}
async createView(
@ -754,40 +771,38 @@ export default class TestConfiguration {
// AUTOMATION
async createAutomation(config?: any) {
async createAutomation(config?: Automation) {
config = config || basicAutomation()
if (config._rev) {
delete config._rev
}
this.automation = (
await this._req(config, null, automationController.create)
).automation
const res = await this._req(automationController.create, config)
this.automation = res.automation
return this.automation
}
async getAllAutomations() {
return this._req(null, null, automationController.fetch)
return this._req(automationController.fetch)
}
async deleteAutomation(automation?: any) {
async deleteAutomation(automation?: Automation) {
automation = automation || this.automation
if (!automation) {
return
}
return this._req(
null,
{ id: automation._id, rev: automation._rev },
automationController.destroy
)
return this._req(automationController.destroy, undefined, {
id: automation._id,
rev: automation._rev,
})
}
async createWebhook(config?: any) {
async createWebhook(config?: Webhook) {
if (!this.automation) {
throw "Must create an automation before creating webhook."
}
config = config || basicWebhook(this.automation._id)
config = config || basicWebhook(this.automation._id!)
return (await this._req(config, null, webhookController.save)).webhook
return (await this._req(webhookController.save, config)).webhook
}
// DATASOURCE
@ -809,7 +824,7 @@ export default class TestConfiguration {
return { ...this.datasource, _id: this.datasource!._id! }
}
async restDatasource(cfg?: any) {
async restDatasource(cfg?: Record<string, any>) {
return this.createDatasource({
datasource: {
...basicDatasource().datasource,
@ -866,26 +881,25 @@ export default class TestConfiguration {
// QUERY
async createQuery(config?: any) {
if (!this.datasource && !config) {
throw "No datasource created for query."
}
config = config || basicQuery(this.datasource!._id!)
return this._req(config, null, queryController.save)
async createQuery(config?: Query) {
return this._req(
queryController.save,
config || basicQuery(this.getDatasource()._id!)
)
}
// SCREEN
async createScreen(config?: any) {
async createScreen(config?: Screen) {
config = config || basicScreen()
return this._req(config, null, screenController.save)
return this._req(screenController.save, config)
}
// LAYOUT
async createLayout(config?: any) {
async createLayout(config?: Layout) {
config = config || basicLayout()
return await this._req(config, null, layoutController.save)
return await this._req(layoutController.save, config)
}
}

View File

@ -22,6 +22,8 @@ import {
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
Query,
Webhook,
WebhookActionType,
} from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations"
@ -407,12 +409,12 @@ export function basicLayout() {
return cloneDeep(EMPTY_LAYOUT)
}
export function basicWebhook(automationId: string) {
export function basicWebhook(automationId: string): Webhook {
return {
live: true,
name: "webhook",
action: {
type: "automation",
type: WebhookActionType.AUTOMATION,
target: automationId,
},
}

View File

@ -0,0 +1,3 @@
import { DocumentDestroyResponse } from "@budibase/nano"
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}

View File

@ -11,3 +11,5 @@ export * from "./global"
export * from "./pagination"
export * from "./searchFilter"
export * from "./cookies"
export * from "./automation"
export * from "./layout"

View File

@ -0,0 +1,5 @@
import { Layout } from "../../documents"
export interface SaveLayoutRequest extends Layout {}
export interface SaveLayoutResponse extends Layout {}

View File

@ -1,6 +1,11 @@
import { Document } from "../document"
export interface Layout extends Document {
componentLibraries: string[]
title: string
favicon: string
stylesheets: string[]
props: any
layoutId?: string
name?: string
}

View File

@ -22,4 +22,5 @@ export interface Screen extends Document {
routing: ScreenRouting
props: ScreenProps
name?: string
pluginAdded?: boolean
}

View File

@ -280,7 +280,7 @@ class TestConfiguration {
const db = context.getGlobalDB()
const id = dbCore.generateDevInfoID(this.user!._id)
const id = dbCore.generateDevInfoID(this.user!._id!)
// TODO: dry
this.apiKey = encryption.encrypt(
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`

View File

@ -17,6 +17,12 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
const svelteCompilePlugin = {
name: 'svelteCompile',
setup(build) {
// This resolve handler is necessary to bundle the Svelte runtime into the the final output,
// otherwise the bundled script will attempt to resolve it at runtime
build.onResolve({ filter: /svelte\/internal/ }, async () => {
return { path: `${process.cwd()}/../../node_modules/svelte/src/runtime/internal/ssr.js` }
})
// Compiles `.svelte` files into JS classes so that they can be directly imported into our
// Typescript packages
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
@ -31,7 +37,7 @@ const svelteCompilePlugin = {
contents: js.code,
// The loader this is passed to, basically how the above provided content is "treated",
// the contents provided above will be transpiled and bundled like any other JS file.
loader: 'js',
loader: 'js',
// Where to resolve any imports present in the loaded file
resolveDir: dir
}
@ -74,11 +80,11 @@ async function runBuild(entry, outfile) {
plugins: [
svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"]
}),
nodeExternalsPlugin(),
],
preserveSymlinks: true,
loader: {
},
metafile: true,
external: [
"deasync",