1
0
Fork 0
mirror of synced 2024-07-04 22:11:23 +12:00

Merge branch 'master' into fix/automation-step-js-bindings

This commit is contained in:
deanhannigan 2024-04-02 09:15:31 +01:00 committed by GitHub
commit 28968237c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 1242 additions and 1786 deletions

View file

@ -13,3 +13,4 @@ packages/account-portal/packages/server/build
packages/account-portal/packages/ui/.routify packages/account-portal/packages/ui/.routify
packages/account-portal/packages/ui/build packages/account-portal/packages/ui/build
**/*.ivm.bundle.js **/*.ivm.bundle.js
packages/server/build/oldClientVersions/**/**

View file

@ -66,7 +66,8 @@ jobs:
# Run build all the projects # Run build all the projects
- name: Build - name: Build
run: | run: |
yarn build yarn build:oss
yarn build:account-portal
# Check the types of the projects built via esbuild # Check the types of the projects built via esbuild
- name: Check types - name: Check types
run: | run: |
@ -161,7 +162,7 @@ jobs:
docker pull postgres:16.1-bullseye docker pull postgres:16.1-bullseye
docker pull mongo:7.0-jammy docker pull mongo:7.0-jammy
docker pull mariadb:lts docker pull mariadb:lts
docker pull testcontainers/ryuk:0.3.0 docker pull testcontainers/ryuk:0.5.1
docker pull budibase/couchdb docker pull budibase/couchdb
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
@ -231,27 +232,34 @@ jobs:
echo "pro_commit=$pro_commit" echo "pro_commit=$pro_commit"
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT" echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
echo "base_commit=$base_commit" echo "base_commit=$base_commit"
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit)
echo "base_commit_excluding_merges=$base_commit_excluding_merges"
echo "base_commit_excluding_merges=$base_commit_excluding_merges" >> "$GITHUB_OUTPUT"
else else
echo "Nothing to do - branch to branch merge." echo "Nothing to do - branch to branch merge."
fi fi
- name: Check submodule merged to base branch - name: Check submodule merged and latest on base branch
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }} if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }}
uses: actions/github-script@v7 run: |
with: cd packages/pro
github-token: ${{ secrets.GITHUB_TOKEN }} base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}'
script: | pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}'
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
if (submoduleCommit !== baseCommit) { any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.');
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md') if [ -n "$any_commit" ]; then
process.exit(1); echo $any_commit
} else {
console.log('All good, the submodule had been merged and setup correctly!') echo "An error occurred: <error_message>"
} echo 'Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.'
echo 'Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md'
exit 1
else
echo 'All good, the submodule had been merged and setup correctly!'
fi
check-accountportal-submodule: check-accountportal-submodule:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -264,7 +272,15 @@ jobs:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Check account portal commit - uses: dorny/paths-filter@v3
id: changes
with:
filters: |
src:
- packages/account-portal/**
- if: steps.changes.outputs.src == 'true'
name: Check account portal commit
id: get_accountportal_commits id: get_accountportal_commits
run: | run: |
cd packages/account-portal cd packages/account-portal

3
.gitignore vendored
View file

@ -5,6 +5,9 @@ packages/server/runtime_apps/
bb-airgapped.tar.gz bb-airgapped.tar.gz
*.iml *.iml
packages/server/build/oldClientVersions/**/*
packages/builder/src/components/deploy/clientVersions.json
# Logs # Logs
logs logs
*.log *.log

8
.vscode/launch.json vendored
View file

@ -1,4 +1,3 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
@ -20,6 +19,13 @@
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["${workspaceFolder}/packages/worker/src/index.ts"], "args": ["${workspaceFolder}/packages/worker/src/index.ts"],
"cwd": "${workspaceFolder}/packages/worker" "cwd": "${workspaceFolder}/packages/worker"
},
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:10000",
"webRoot": "${workspaceFolder}"
} }
], ],
"compounds": [ "compounds": [

25
globalSetup.ts Normal file
View file

@ -0,0 +1,25 @@
import { GenericContainer, Wait } from "testcontainers"
export default async function setup() {
await new GenericContainer("budibase/couchdb")
.withExposedPorts(5984)
.withEnvironment({
COUCHDB_PASSWORD: "budibase",
COUCHDB_USER: "budibase",
})
.withCopyContentToContainer([
{
content: `
[log]
level = warn
`,
target: "/opt/couchdb/etc/local.d/test-couchdb.ini",
},
])
.withWaitStrategy(
Wait.forSuccessfulCommand(
"curl http://budibase:budibase@localhost:5984/_up"
).withStartupTimeout(20000)
)
.start()
}

View file

@ -1,16 +0,0 @@
module.exports = () => {
return {
couchdb: {
image: "budibase/couchdb",
ports: [5984],
env: {
COUCHDB_PASSWORD: "budibase",
COUCHDB_USER: "budibase",
},
wait: {
type: "ports",
timeout: 20000,
}
}
}
}

View file

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

View file

@ -31,8 +31,11 @@
}, },
"scripts": { "scripts": {
"preinstall": "node scripts/syncProPackage.js", "preinstall": "node scripts/syncProPackage.js",
"get-past-client-version": "node scripts/getPastClientVersion.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream", "build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types", "check:types": "lerna run check:types",
"build:sdk": "lerna run --stream build:sdk", "build:sdk": "lerna run --stream build:sdk",

@ -1 +1 @@
Subproject commit f5b467b6b1c55c48847545db41be7b1c035e167a Subproject commit 63ce32bca871f0a752323f5f7ebb5ec16bbbacc3

View file

@ -1,8 +0,0 @@
const { join } = require("path")
require("dotenv").config({
path: join(__dirname, "..", "..", "hosting", ".env"),
})
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
module.exports = jestTestcontainersConfigGenerator()

View file

@ -1,8 +1,8 @@
import { Config } from "@jest/types" import { Config } from "@jest/types"
const baseConfig: Config.InitialProjectOptions = { const baseConfig: Config.InitialProjectOptions = {
preset: "@trendyol/jest-testcontainers",
setupFiles: ["./tests/jestEnv.ts"], setupFiles: ["./tests/jestEnv.ts"],
globalSetup: "./../../globalSetup.ts",
setupFilesAfterEnv: ["./tests/jestSetup.ts"], setupFilesAfterEnv: ["./tests/jestSetup.ts"],
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",

View file

@ -60,7 +60,6 @@
"@shopify/jest-koa-mocks": "5.1.1", "@shopify/jest-koa-mocks": "5.1.1",
"@swc/core": "1.3.71", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3", "@types/chance": "1.1.3",
"@types/cookies": "0.7.8", "@types/cookies": "0.7.8",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",

View file

@ -27,7 +27,7 @@ class Replication {
return resolve(info) return resolve(info)
}) })
.on("error", function (err) { .on("error", function (err) {
throw new Error(`Replication Error: ${err}`) throw err
}) })
}) })
} }

View file

@ -1,4 +1,4 @@
import { Event } from "@budibase/types" import { Event, Identity } from "@budibase/types"
import { processors } from "./processors" import { processors } from "./processors"
import identification from "./identification" import identification from "./identification"
import * as backfill from "./backfill" import * as backfill from "./backfill"
@ -7,12 +7,19 @@ import { publishAsyncEvent } from "./asyncEvents"
export const publishEvent = async ( export const publishEvent = async (
event: Event, event: Event,
properties: any, properties: any,
timestamp?: string | number timestamp?: string | number,
identityOverride?: Identity
) => { ) => {
// in future this should use async events via a distributed queue. // in future this should use async events via a distributed queue.
const identity = await identification.getCurrentIdentity() const identity =
identityOverride || (await identification.getCurrentIdentity())
// Backfilling is get from the user cache, but when we override the identity cache is not available. Overrides are
// normally performed in automatic actions or operations in async flows (BPM) where the user session is not available.
const backfilling = identityOverride
? false
: await backfill.isBackfillingEvent(event)
const backfilling = await backfill.isBackfillingEvent(event)
// no backfill - send the event and exit // no backfill - send the event and exit
if (!backfilling) { if (!backfilling) {
// send off async events if required // send off async events if required

View file

@ -5,13 +5,19 @@ import {
AccountCreatedEvent, AccountCreatedEvent,
AccountDeletedEvent, AccountDeletedEvent,
AccountVerifiedEvent, AccountVerifiedEvent,
Identity,
} from "@budibase/types" } from "@budibase/types"
async function created(account: Account) { async function created(account: Account, identityOverride?: Identity) {
const properties: AccountCreatedEvent = { const properties: AccountCreatedEvent = {
tenantId: account.tenantId, tenantId: account.tenantId,
} }
await publishEvent(Event.ACCOUNT_CREATED, properties) await publishEvent(
Event.ACCOUNT_CREATED,
properties,
undefined,
identityOverride
)
} }
async function deleted(account: Account) { async function deleted(account: Account) {

View file

@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) {
return user.tenantId return user.tenantId
} }
async function getUserDoc(emailOrId: string): Promise<PlatformUser> { export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
const db = getPlatformDB() const db = getPlatformDB()
return db.get(emailOrId) return db.get(emailOrId)
} }
@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) {
} }
} }
export async function addSsoUser(
ssoId: string,
email: string,
userId: string,
tenantId: string
) {
return addUserDoc(ssoId, () =>
newUserSsoIdDoc(ssoId, email, userId, tenantId)
)
}
export async function addUser( export async function addUser(
tenantId: string, tenantId: string,
userId: string, userId: string,
@ -91,9 +102,7 @@ export async function addUser(
] ]
if (ssoId) { if (ssoId) {
promises.push( promises.push(addSsoUser(ssoId, email, userId, tenantId))
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
)
} }
await Promise.all(promises) await Promise.all(promises)

View file

@ -500,13 +500,13 @@ export class UserDB {
static async createAdminUser( static async createAdminUser(
email: string, email: string,
password: string,
tenantId: string, tenantId: string,
password?: string,
opts?: CreateAdminUserOpts opts?: CreateAdminUserOpts
) { ) {
const user: User = { const user: User = {
email: email, email: email,
password: password, password,
createdAt: Date.now(), createdAt: Date.now(),
roles: {}, roles: {},
builder: { builder: {

View file

@ -1,80 +1,58 @@
import { DatabaseImpl } from "../../../src/db"
import { execSync } from "child_process" import { execSync } from "child_process"
let dockerPsResult: string | undefined interface ContainerInfo {
Command: string
function formatDockerPsResult(serverName: string, port: number) { CreatedAt: string
const lines = dockerPsResult?.split("\n") ID: string
let first = true Image: string
if (!lines) { Labels: string
return null LocalVolumes: string
} Mounts: string
for (let line of lines) { Names: string
if (first) { Networks: string
first = false Ports: string
continue RunningFor: string
} Size: string
let toLookFor = serverName.split("-service")[0] State: string
if (!line.includes(toLookFor)) { Status: string
continue
}
const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g")
const found = line.match(regex)
if (found) {
return found[0].split(":")[1].split("->")[0]
}
}
return null
} }
function getTestContainerSettings( function getTestcontainers(): ContainerInfo[] {
serverName: string, return execSync("docker ps --format json")
key: string .toString()
): string | null { .split("\n")
const entry = Object.entries(global).find( .filter(x => x.length > 0)
([k]) => .map(x => JSON.parse(x) as ContainerInfo)
k.includes(`${serverName.toUpperCase()}`) && .filter(x => x.Labels.includes("org.testcontainers=true"))
k.includes(`${key.toUpperCase()}`)
)
if (!entry) {
return null
}
return entry[1]
} }
function getContainerInfo(containerName: string, port: number) { function getContainerByImage(image: string) {
let assignedPort = getTestContainerSettings( return getTestcontainers().find(x => x.Image.startsWith(image))
containerName.toUpperCase(),
`PORT_${port}`
)
if (!dockerPsResult) {
try {
const outputBuffer = execSync("docker ps")
dockerPsResult = outputBuffer.toString("utf8")
} catch (err) {
//no-op
}
}
const possiblePort = formatDockerPsResult(containerName, port)
if (possiblePort) {
assignedPort = possiblePort
}
const host = getTestContainerSettings(containerName.toUpperCase(), "IP")
return {
port: assignedPort,
host,
url: host && assignedPort && `http://${host}:${assignedPort}`,
}
} }
function getCouchConfig() { function getExposedPort(container: ContainerInfo, port: number) {
return getContainerInfo("couchdb", 5984) const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`))
if (!match) {
return undefined
}
return parseInt(match[1])
} }
export function setupEnv(...envs: any[]) { export function setupEnv(...envs: any[]) {
const couch = getCouchConfig() const couch = getContainerByImage("budibase/couchdb")
if (!couch) {
throw new Error("CouchDB container not found")
}
const couchPort = getExposedPort(couch, 5984)
if (!couchPort) {
throw new Error("CouchDB port not found")
}
const configs = [ const configs = [
{ key: "COUCH_DB_PORT", value: couch.port }, { key: "COUCH_DB_PORT", value: `${couchPort}` },
{ key: "COUCH_DB_URL", value: couch.url }, { key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` },
] ]
for (const config of configs.filter(x => !!x.value)) { for (const config of configs.filter(x => !!x.value)) {
@ -82,4 +60,7 @@ export function setupEnv(...envs: any[]) {
env._set(config.key, config.value) env._set(config.key, config.value)
} }
} }
// @ts-expect-error
DatabaseImpl.nano = undefined
} }

View file

@ -4,3 +4,7 @@ process.env.NODE_ENV = "jest"
process.env.MOCK_REDIS = "1" process.env.MOCK_REDIS = "1"
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
process.env.REDIS_PASSWORD = "budibase" process.env.REDIS_PASSWORD = "budibase"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.API_ENCRYPTION_KEY = "testsecret"
process.env.JWT_SECRET = "testsecret"

View file

@ -12,6 +12,13 @@ export default {
format: "esm", format: "esm",
file: "dist/bbui.es.js", file: "dist/bbui.es.js",
}, },
onwarn(warning, warn) {
// suppress eval warnings
if (warning.code === "EVAL") {
return
}
warn(warning)
},
plugins: [ plugins: [
resolve(), resolve(),
commonjs(), commonjs(),

View file

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -1,8 +0,0 @@
{
"javascript.format.enable": false,
"svelte.plugin.svelte.format.enable": false,
"html.format.enable": false,
"json.format.enable": false,
"editor.trimAutoWhitespace": false,
"sass.format.deleteWhitespace": false
}

View file

@ -371,6 +371,7 @@
<style> <style>
.binding-panel { .binding-panel {
height: 100%; height: 100%;
overflow: hidden;
} }
.binding-panel, .binding-panel,
.tabs { .tabs {

View file

@ -0,0 +1,33 @@
<script>
import { API } from "api"
import clientVersions from "./clientVersions.json"
import { appStore } from "stores/builder"
import { Select } from "@budibase/bbui"
export let revertableVersion
$: appId = $appStore.appId
const handleChange = e => {
const value = e.detail
if (value == null) return
API.setRevertableVersion(appId, value)
}
</script>
<div class="select">
<Select
autoWidth
value={revertableVersion}
options={clientVersions}
on:change={handleChange}
footer={"Older versions of the Budibase client can be acquired using `yarn get-past-client-version x.x.x`. This toggle is only available in dev mode."}
/>
</div>
<style>
.select {
width: 120px;
display: inline-block;
}
</style>

View file

@ -1,4 +1,5 @@
<script> <script>
import { admin } from "stores/portal"
import { import {
Modal, Modal,
notifications, notifications,
@ -9,6 +10,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { appStore, initialise } from "stores/builder" import { appStore, initialise } from "stores/builder"
import { API } from "api" import { API } from "api"
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
export function show() { export function show() {
updateModal.show() updateModal.show()
@ -28,7 +30,9 @@
$appStore.upgradableVersion && $appStore.upgradableVersion &&
$appStore.version && $appStore.version &&
$appStore.upgradableVersion !== $appStore.version $appStore.upgradableVersion !== $appStore.version
$: revertAvailable = $appStore.revertableVersion != null $: revertAvailable =
$appStore.revertableVersion != null ||
($admin.isDev && $appStore.version === "0.0.0")
const refreshAppPackage = async () => { const refreshAppPackage = async () => {
try { try {
@ -62,7 +66,9 @@
// Don't wait for the async refresh, since this causes modal flashing // Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage() refreshAppPackage()
notifications.success( notifications.success(
`App reverted successfully to version ${$appStore.revertableVersion}` $appStore.revertableVersion
? `App reverted successfully to version ${$appStore.revertableVersion}`
: "App reverted successfully"
) )
} catch (err) { } catch (err) {
notifications.error(`Error reverting app: ${err}`) notifications.error(`Error reverting app: ${err}`)
@ -103,7 +109,13 @@
{#if revertAvailable} {#if revertAvailable}
<Body size="S"> <Body size="S">
You can revert this app to version You can revert this app to version
<b>{$appStore.revertableVersion}</b> {#if $admin.isDev}
<RevertModalVersionSelect
revertableVersion={$appStore.revertableVersion}
/>
{:else}
<b>{$appStore.revertableVersion}</b>
{/if}
if you're experiencing issues with the current version. if you're experiencing issues with the current version.
</Body> </Body>
{/if} {/if}

View file

@ -0,0 +1 @@
[]

View file

@ -14,17 +14,11 @@
snippets, snippets,
} from "stores/builder" } from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw" import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components" import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify" import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe let iframe
let layout let layout
@ -254,8 +248,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container"> <div class="component-container">
{#if loading} {#if loading}
<div class="center"> <div
<ProgressCircle /> class={`loading ${$themeStore.baseTheme} ${$themeStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$navigationStore?.navigation === "Left"}
hideFooter
hideDevTools
/>
</div> </div>
{:else if error} {:else if error}
<div class="center error"> <div class="center error">
@ -272,8 +274,6 @@
bind:this={iframe} bind:this={iframe}
src="/app/preview" src="/app/preview"
class:hidden={loading || error} class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/> />
<div <div
class="add-component" class="add-component"
@ -293,6 +293,25 @@
/> />
<style> <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 { .component-container {
grid-row-start: middle; grid-row-start: middle;
grid-column-start: middle; grid-column-start: middle;

View file

@ -1,6 +1,12 @@
<script> <script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import { auth, sideBarCollapsed, enrichedApps } from "stores/portal" import {
licensing,
auth,
sideBarCollapsed,
enrichedApps,
} from "stores/portal"
import AppRowContext from "components/start/AppRowContext.svelte" import AppRowContext from "components/start/AppRowContext.svelte"
import FavouriteAppButton from "../FavouriteAppButton.svelte" import FavouriteAppButton from "../FavouriteAppButton.svelte"
import { import {
@ -14,12 +20,17 @@
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { API } from "api" import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte" import ErrorSVG from "./ErrorSVG.svelte"
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $enrichedApps.find(app => app.appId === $params.appId) $: app = $enrichedApps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app) $: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId) $: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => { const getIframeURL = app => {
loading = true
if (app.status === "published") { if (app.status === "published") {
return `/app${app.url}` return `/app${app.url}`
} }
@ -37,6 +48,20 @@
} }
$: fetchScreens(app?.devId) $: 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> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
@ -108,7 +133,26 @@
</Body> </Body>
</div> </div>
{:else} {:else}
<iframe src={iframeUrl} title={app.name} /> <div
class:hide={!loading || !app?.features?.skeletonLoader}
class="loading"
>
<div
class={`loadingThemeWrapper ${getBaseTheme(app.theme)} ${app.theme}`}
>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe
class:hide={loading && app?.features?.skeletonLoader}
src={iframeUrl}
title={app.name}
/>
{/if} {/if}
</div> </div>
@ -139,6 +183,23 @@
flex: 0 0 50px; 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 { iframe {
flex: 1 1 auto; flex: 1 1 auto;
border-radius: var(--spacing-s); border-radius: var(--spacing-s);

View file

@ -1,5 +1,6 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { getBaseTheme } from "@budibase/frontend-core"
const INITIAL_THEMES_STATE = { const INITIAL_THEMES_STATE = {
theme: "", theme: "",
@ -12,11 +13,15 @@ export const themes = () => {
}) })
const syncAppTheme = app => { const syncAppTheme = app => {
store.update(state => ({ store.update(state => {
...state, const theme = app.theme || "spectrum--light"
theme: app.theme || "spectrum--light", return {
customTheme: app.customTheme, ...state,
})) theme,
baseTheme: getBaseTheme(theme),
customTheme: app.customTheme,
}
})
} }
const save = async (theme, appId) => { const save = async (theme, appId) => {

View file

@ -10,7 +10,8 @@
"rowSelection": true, "rowSelection": true,
"continueIfAction": true, "continueIfAction": true,
"showNotificationAction": true, "showNotificationAction": true,
"sidePanel": true "sidePanel": true,
"skeletonLoader": true
}, },
"layout": { "layout": {
"name": "Layout", "name": "Layout",

View file

@ -45,7 +45,8 @@ export default {
onwarn(warning, warn) { onwarn(warning, warn) {
if ( if (
warning.code === "THIS_IS_UNDEFINED" || warning.code === "THIS_IS_UNDEFINED" ||
warning.code === "CIRCULAR_DEPENDENCY" warning.code === "CIRCULAR_DEPENDENCY" ||
warning.code === "EVAL"
) { ) {
return return
} }

View file

@ -7,6 +7,7 @@
import Component from "./Component.svelte" import Component from "./Component.svelte"
import SDK from "sdk" import SDK from "sdk"
import { import {
featuresStore,
createContextStore, createContextStore,
initialise, initialise,
screenStore, screenStore,
@ -38,7 +39,6 @@
import DevTools from "components/devtools/DevTools.svelte" import DevTools from "components/devtools/DevTools.svelte"
import FreeFooter from "components/FreeFooter.svelte" import FreeFooter from "components/FreeFooter.svelte"
import MaintenanceScreen from "components/MaintenanceScreen.svelte" import MaintenanceScreen from "components/MaintenanceScreen.svelte"
import licensing from "../licensing"
import SnippetsProvider from "./context/SnippetsProvider.svelte" import SnippetsProvider from "./context/SnippetsProvider.svelte"
// Provide contexts // Provide contexts
@ -83,11 +83,18 @@
} }
} }
let fontsLoaded = false
// Load app config // Load app config
onMount(async () => { onMount(async () => {
document.fonts.ready.then(() => {
fontsLoaded = true
})
await initialise() await initialise()
await authStore.actions.fetchUser() await authStore.actions.fetchUser()
dataLoaded = true dataLoaded = true
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded() builderStore.actions.notifyLoaded()
} else { } else {
@ -96,6 +103,12 @@
}) })
} }
}) })
$: {
if (dataLoaded && fontsLoaded) {
document.getElementById("clientAppSkeletonLoader")?.remove()
}
}
</script> </script>
<svelte:head> <svelte:head>
@ -106,148 +119,148 @@
{/if} {/if}
</svelte:head> </svelte:head>
{#if dataLoaded} <div
<div id="spectrum-root"
id="spectrum-root" lang="en"
lang="en" dir="ltr"
dir="ltr" class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}" class:builder={$builderStore.inBuilder}
class:builder={$builderStore.inBuilder} class:show={fontsLoaded && dataLoaded}
> >
{#if $environmentStore.maintenance.length > 0} {#if $environmentStore.maintenance.length > 0}
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} /> <MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
{:else} {:else}
<DeviceBindingsProvider> <DeviceBindingsProvider>
<UserBindingsProvider> <UserBindingsProvider>
<StateBindingsProvider> <StateBindingsProvider>
<RowSelectionProvider> <RowSelectionProvider>
<QueryParamsProvider> <QueryParamsProvider>
<SnippetsProvider> <SnippetsProvider>
<!-- Settings bar can be rendered outside of device preview --> <!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks --> <!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId} {#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<SettingsBar /> <SettingsBar />
{/if} {/if}
{/key} {/key}
<!-- Clip boundary for selection indicators --> <!-- Clip boundary for selection indicators -->
<div <div
id="clip-root" id="clip-root"
class:preview={$builderStore.inBuilder} class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === class:tablet-preview={$builderStore.previewDevice ===
"tablet"} "tablet"}
class:mobile-preview={$builderStore.previewDevice === class:mobile-preview={$builderStore.previewDevice ===
"mobile"} "mobile"}
> >
<!-- Actual app --> <!-- Actual app -->
<div id="app-root"> <div id="app-root">
{#if showDevTools} {#if showDevTools}
<DevToolsHeader /> <DevToolsHeader />
{/if}
<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}
<div id="app-body"> {#if showDevTools}
{#if permissionError} <DevTools />
<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} {/if}
</div> </div>
<!-- Preview and dev tools utilities --> {#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
{#if $appStore.isDevApp} <FreeFooter />
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if} {/if}
</div> </div>
</SnippetsProvider>
</QueryParamsProvider> <!-- Preview and dev tools utilities -->
</RowSelectionProvider> {#if $appStore.isDevApp}
</StateBindingsProvider> <SelectionIndicator />
</UserBindingsProvider> {/if}
</DeviceBindingsProvider> {#if $builderStore.inBuilder || $devToolsStore.allowSelection}
{/if} <HoverIndicator />
</div> {/if}
<KeyboardManager /> {#if $builderStore.inBuilder}
{/if} <DNDHandler />
<GridDNDHandler />
{/if}
</div>
</SnippetsProvider>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
{/if}
</div>
<KeyboardManager />
<style> <style>
#spectrum-root { #spectrum-root {
height: 0;
visibility: hidden;
padding: 0; padding: 0;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -268,6 +281,11 @@
background-color: transparent; background-color: transparent;
} }
#spectrum-root.show {
height: 100%;
visibility: visible;
}
#app-root { #app-root {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;

View file

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

View file

@ -1,5 +0,0 @@
import { isFreePlan } from "./utils.js"
export const logoEnabled = () => {
return isFreePlan()
}

View file

@ -1,7 +0,0 @@
import * as features from "./features"
const licensing = {
...features,
}
export default licensing

View file

@ -1,32 +0,0 @@
import { authStore } from "../stores/auth.js"
import { appStore } from "../stores/app.js"
import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core"
const getUserLicense = () => {
const user = get(authStore)
if (user) {
return user.license
}
}
const getAppLicenseType = () => {
const appDef = get(appStore)
if (appDef?.licenseType) {
return appDef.licenseType
}
}
export const isFreePlan = () => {
let licenseType = getAppLicenseType()
if (!licenseType) {
const license = getUserLicense()
licenseType = license?.plan?.type
}
if (licenseType) {
return licenseType === Constants.PlanType.FREE
} else {
// safety net - no license means free plan
return true
}
}

View file

@ -0,0 +1,42 @@
import { derived } from "svelte/store"
import { appStore } from "./app"
import { authStore } from "./auth"
import { Constants } from "@budibase/frontend-core"
const createFeaturesStore = () => {
return derived([authStore, appStore], ([$authStore, $appStore]) => {
const getUserLicense = () => {
const user = $authStore
if (user) {
return user.license
}
}
const getAppLicenseType = () => {
const appDef = $appStore
if (appDef?.licenseType) {
return appDef.licenseType
}
}
const isFreePlan = () => {
let licenseType = getAppLicenseType()
if (!licenseType) {
const license = getUserLicense()
licenseType = license?.plan?.type
}
if (licenseType) {
return licenseType === Constants.PlanType.FREE
} else {
// safety net - no license means free plan
return true
}
}
return {
logoEnabled: isFreePlan(),
}
})
}
export const featuresStore = createFeaturesStore()

View file

@ -31,6 +31,7 @@ export { hoverStore } from "./hover"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"
export { featuresStore } from "./features"
// Initialises an app by loading screens and routes // Initialises an app by loading screens and routes
export { initialise } from "./initialise" export { initialise } from "./initialise"

View file

@ -1,7 +1,7 @@
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { appStore } from "./app" import { appStore } from "./app"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { Constants } from "@budibase/frontend-core" import { getBaseTheme } from "@budibase/frontend-core"
// This is the good old acorn bug where having the word "g l o b a l" makes it // This is the good old acorn bug where having the word "g l o b a l" makes it
// think that this is not ES6 compatible and starts throwing errors when using // think that this is not ES6 compatible and starts throwing errors when using
@ -29,13 +29,6 @@ const createThemeStore = () => {
// Ensure theme is set // Ensure theme is set
theme = theme || defaultTheme theme = theme || defaultTheme
// Get base theme
let base =
Constants.Themes.find(x => `spectrum--${x.class}` === theme)?.base || ""
if (base) {
base = `spectrum--${base}`
}
// Delete and nullish keys from the custom theme // Delete and nullish keys from the custom theme
if (customTheme) { if (customTheme) {
Object.entries(customTheme).forEach(([key, value]) => { Object.entries(customTheme).forEach(([key, value]) => {
@ -59,7 +52,7 @@ const createThemeStore = () => {
return { return {
theme, theme,
baseTheme: base, baseTheme: getBaseTheme(theme),
customTheme, customTheme,
customThemeCss, customThemeCss,
} }

View file

@ -197,4 +197,13 @@ export const buildAppEndpoints = API => ({
url: `/api/applications/${appId}/sample`, url: `/api/applications/${appId}/sample`,
}) })
}, },
setRevertableVersion: async (appId, revertableVersion) => {
return await API.post({
url: `/api/applications/${appId}/setRevertableVersion`,
body: {
revertableVersion,
},
})
},
}) })

View file

@ -0,0 +1,244 @@
<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,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte" export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte" export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid" export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"

View file

@ -3,6 +3,7 @@
*/ */
export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core" export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core"
export { Feature as Features } from "@budibase/types" export { Feature as Features } from "@budibase/types"
import { BpmCorrelationKey } from "@budibase/shared-core"
// Cookie names // Cookie names
export const Cookies = { export const Cookies = {
@ -10,6 +11,7 @@ export const Cookies = {
CurrentApp: "budibase:currentapp", CurrentApp: "budibase:currentapp",
ReturnUrl: "budibase:returnurl", ReturnUrl: "budibase:returnurl",
AccountReturnUrl: "budibase:account:returnurl", AccountReturnUrl: "budibase:account:returnurl",
OnboardingProcessCorrelationKey: BpmCorrelationKey.ONBOARDING,
} }
// Table names // Table names

View file

@ -18,4 +18,3 @@
--drop-shadow: rgba(0, 0, 0, 0.25) !important; --drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important; --spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
} }

View file

@ -7,3 +7,4 @@ export * as RowUtils from "./rows"
export { memo, derivedMemo } from "./memo" export { memo, derivedMemo } from "./memo"
export { createWebsocket } from "./websocket" export { createWebsocket } from "./websocket"
export * from "./download" export * from "./download"
export * from "./theme"

View file

@ -0,0 +1,12 @@
import { Themes } from "../constants.js"
export const getBaseTheme = theme => {
if (!theme) {
return ""
}
let base = Themes.find(x => `spectrum--${x.class}` === theme)?.base || ""
if (base) {
base = `spectrum--${base}`
}
return base
}

@ -1 +1 @@
Subproject commit dd748e045ffdbc6662c5d2b76075f01d65a96a2f Subproject commit 6b62505be0c0b50a57b4f4980d86541ebdc86428

View file

@ -1,142 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Start Server",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["src/index.ts"],
"cwd": "${workspaceRoot}",
},
{
"type": "node",
"request": "launch",
"name": "Jest - All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Users",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["user.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Instances",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["instance.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Roles",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["role.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Records",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["record.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Models",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["table.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Views",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["view.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Applications",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["application.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Builder",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["builder", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Initialise Budibase",
"program": "yarn",
"args": ["run", "initialise"],
"console": "externalTerminal"
}
]
}

View file

@ -1,8 +0,0 @@
const { join } = require("path")
require("dotenv").config({
path: join(__dirname, "..", "..", "hosting", ".env"),
})
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
module.exports = jestTestcontainersConfigGenerator()

View file

@ -4,7 +4,6 @@ import * as fs from "fs"
import { join } from "path" import { join } from "path"
const baseConfig: Config.InitialProjectOptions = { const baseConfig: Config.InitialProjectOptions = {
preset: "@trendyol/jest-testcontainers",
setupFiles: ["./src/tests/jestEnv.ts"], setupFiles: ["./src/tests/jestEnv.ts"],
moduleFileExtensions: [ moduleFileExtensions: [
"js", "js",
@ -18,6 +17,7 @@ const baseConfig: Config.InitialProjectOptions = {
"svelte", "svelte",
], ],
setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],
globalSetup: "./../../globalSetup.ts",
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",
"^.+\\.js?$": "@swc/jest", "^.+\\.js?$": "@swc/jest",

View file

@ -53,6 +53,7 @@
"@budibase/pro": "0.0.0", "@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0", "@budibase/string-templates": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0", "@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2", "@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2", "@bull-board/koa": "5.10.2",
@ -121,7 +122,6 @@
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@swc/core": "1.3.71", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "2.1.1",
"@types/global-agent": "2.1.1", "@types/global-agent": "2.1.1",
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",

View file

@ -306,6 +306,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
features: { features: {
componentValidation: true, componentValidation: true,
disableUserMetadata: true, disableUserMetadata: true,
skeletonLoader: true,
}, },
} }
@ -486,10 +487,11 @@ export async function updateClient(ctx: UserCtx) {
const application = await db.get<App>(DocumentType.APP_METADATA) const application = await db.get<App>(DocumentType.APP_METADATA)
const currentVersion = application.version const currentVersion = application.version
let manifest
// Update client library and manifest // Update client library and manifest
if (!env.isTest()) { if (!env.isTest()) {
await backupClientLibrary(ctx.params.appId) await backupClientLibrary(ctx.params.appId)
await updateClientLibrary(ctx.params.appId) manifest = await updateClientLibrary(ctx.params.appId)
} }
// Update versions in app package // Update versions in app package
@ -497,6 +499,10 @@ export async function updateClient(ctx: UserCtx) {
const appPackageUpdates = { const appPackageUpdates = {
version: updatedToVersion, version: updatedToVersion,
revertableVersion: currentVersion, revertableVersion: currentVersion,
features: {
...(application.features ?? {}),
skeletonLoader: manifest?.features?.skeletonLoader ?? false,
},
} }
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId) const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionUpdated(app, currentVersion, updatedToVersion) await events.app.versionUpdated(app, currentVersion, updatedToVersion)
@ -512,9 +518,10 @@ export async function revertClient(ctx: UserCtx) {
ctx.throw(400, "There is no version to revert to") ctx.throw(400, "There is no version to revert to")
} }
let manifest
// Update client library and manifest // Update client library and manifest
if (!env.isTest()) { if (!env.isTest()) {
await revertClientLibrary(ctx.params.appId) manifest = await revertClientLibrary(ctx.params.appId)
} }
// Update versions in app package // Update versions in app package
@ -523,6 +530,10 @@ export async function revertClient(ctx: UserCtx) {
const appPackageUpdates = { const appPackageUpdates = {
version: revertedToVersion, version: revertedToVersion,
revertableVersion: undefined, revertableVersion: undefined,
features: {
...(application.features ?? {}),
skeletonLoader: manifest?.features?.skeletonLoader ?? false,
},
} }
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId) const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionReverted(app, currentVersion, revertedToVersion) await events.app.versionReverted(app, currentVersion, revertedToVersion)
@ -729,6 +740,21 @@ export async function updateAppPackage(
}) })
} }
export async function setRevertableVersion(
ctx: UserCtx<{ revertableVersion: string }, App>
) {
if (!env.isDev()) {
ctx.status = 403
return
}
const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA)
app.revertableVersion = ctx.request.body.revertableVersion
await db.put(app)
ctx.status = 200
}
async function migrateAppNavigation() { async function migrateAppNavigation() {
const db = context.getAppDB() const db = context.getAppDB()
const existing: App = await db.get(DocumentType.APP_METADATA) const existing: App = await db.get(DocumentType.APP_METADATA)

View file

@ -1,10 +1,8 @@
import { InvalidFileExtensions } from "@budibase/shared-core" import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte" import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath" import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid" import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants" import { ObjectStoreBuckets, devClientVersion } from "../../../constants"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { import {
loadHandlebarsFile, loadHandlebarsFile,
@ -24,13 +22,20 @@ 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, ProcessAttachmentResponse } from "@budibase/types" import {
UserCtx,
App,
Ctx,
ProcessAttachmentResponse,
Feature,
} from "@budibase/types"
import { import {
getAppMigrationVersion, getAppMigrationVersion,
getLatestMigrationId, getLatestMigrationId,
} from "../../../appMigrations" } from "../../../appMigrations"
import send from "koa-send" import send from "koa-send"
import { getThemeVariables } from "../../../constants/themes"
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}`
@ -146,7 +151,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations return requiresMigrations
} }
export const serveApp = async function (ctx: Ctx) { export const serveApp = async function (ctx: UserCtx) {
const needMigrations = await requiresMigration(ctx) const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed = const bbHeaderEmbed =
@ -165,12 +170,23 @@ export const serveApp = async function (ctx: Ctx) {
try { try {
db = context.getAppDB({ skip_setup: true }) db = context.getAppDB({ skip_setup: true })
const appInfo = await db.get<any>(DocumentType.APP_METADATA) const appInfo = await db.get<any>(DocumentType.APP_METADATA)
let appId = context.getAppId() 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()) { if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins) const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({ const { head, html, css } = AppComponent.render({
title: branding?.platformTitle || `${appInfo.name}`, title: branding?.platformTitle || `${appInfo.name}`,
showSkeletonLoader: appInfo.features?.skeletonLoader ?? false,
hideDevTools,
sideNav,
hideFooter,
metaImage: metaImage:
branding?.metaImageUrl || branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png", "https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@ -195,7 +211,7 @@ export const serveApp = async function (ctx: Ctx) {
ctx.body = await processString(appHbs, { ctx.body = await processString(appHbs, {
head, head,
body: html, body: html,
style: css.code, css: `:root{${themeVariables}} ${css.code}`,
appId, appId,
embedded: bbHeaderEmbed, embedded: bbHeaderEmbed,
}) })
@ -247,18 +263,20 @@ export const serveBuilderPreview = async function (ctx: Ctx) {
} }
export const serveClientLibrary = async function (ctx: Ctx) { export const serveClientLibrary = async function (ctx: Ctx) {
const version = ctx.request.query.version
const appId = context.getAppId() || (ctx.request.query.appId as string) const appId = context.getAppId() || (ctx.request.query.appId as string)
let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist") let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist")
if (!appId) { if (!appId) {
ctx.throw(400, "No app ID provided - cannot fetch client library.") ctx.throw(400, "No app ID provided - cannot fetch client library.")
} }
if (env.isProd()) { if (env.isProd() || (env.isDev() && version !== devClientVersion)) {
ctx.body = await objectStore.getReadStream( ctx.body = await objectStore.getReadStream(
ObjectStoreBuckets.APPS, ObjectStoreBuckets.APPS,
objectStore.clientLibraryPath(appId!) objectStore.clientLibraryPath(appId!)
) )
ctx.set("Content-Type", "application/javascript") ctx.set("Content-Type", "application/javascript")
} else if (env.isDev()) { } else if (env.isDev() && version === devClientVersion) {
// incase running from TS directly // incase running from TS directly
const tsPath = join(require.resolve("@budibase/client"), "..") const tsPath = join(require.resolve("@budibase/client"), "..")
return send(ctx, "budibase-client.js", { return send(ctx, "budibase-client.js", {

View file

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

View file

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

View file

@ -68,5 +68,10 @@ router
authorized(permissions.BUILDER), authorized(permissions.BUILDER),
controller.importToApp controller.importToApp
) )
.post(
"/api/applications/:appId/setRevertableVersion",
authorized(permissions.BUILDER),
controller.setRevertableVersion
)
export default router export default router

View file

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

View file

@ -165,6 +165,8 @@ export enum AutomationErrors {
FAILURE_CONDITION = "FAILURE_CONDITION_MET", FAILURE_CONDITION = "FAILURE_CONDITION_MET",
} }
export const devClientVersion = "0.0.0"
// pass through the list from the auth/core lib // pass through the list from the auth/core lib
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
export const MAX_AUTOMATION_RECURRING_ERRORS = 5 export const MAX_AUTOMATION_RECURRING_ERRORS = 5

View file

@ -0,0 +1,54 @@
export 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);
`
}
}

View file

@ -9,6 +9,7 @@ import {
QueryType, QueryType,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { HOST_ADDRESS } from "./utils"
interface CouchDBConfig { interface CouchDBConfig {
url: string url: string
@ -28,7 +29,7 @@ const SCHEMA: Integration = {
url: { url: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
required: true, required: true,
default: "http://localhost:5984", default: `http://${HOST_ADDRESS}:5984`,
}, },
database: { database: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,

View file

@ -8,6 +8,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { Client, ClientOptions } from "@elastic/elasticsearch" import { Client, ClientOptions } from "@elastic/elasticsearch"
import { HOST_ADDRESS } from "./utils"
interface ElasticsearchConfig { interface ElasticsearchConfig {
url: string url: string
@ -29,7 +30,7 @@ const SCHEMA: Integration = {
url: { url: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
required: true, required: true,
default: "http://localhost:9200", default: `http://${HOST_ADDRESS}:9200`,
}, },
ssl: { ssl: {
type: DatasourceFieldType.BOOLEAN, type: DatasourceFieldType.BOOLEAN,

View file

@ -22,6 +22,7 @@ import {
finaliseExternalTables, finaliseExternalTables,
SqlClient, SqlClient,
checkExternalTables, checkExternalTables,
HOST_ADDRESS,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types" import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
@ -88,7 +89,6 @@ const SCHEMA: Integration = {
user: { user: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
required: true, required: true,
default: "localhost",
}, },
password: { password: {
type: DatasourceFieldType.PASSWORD, type: DatasourceFieldType.PASSWORD,
@ -96,7 +96,7 @@ const SCHEMA: Integration = {
}, },
server: { server: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
default: "localhost", default: HOST_ADDRESS,
}, },
port: { port: {
type: DatasourceFieldType.NUMBER, type: DatasourceFieldType.NUMBER,

View file

@ -22,6 +22,7 @@ import {
InsertManyResult, InsertManyResult,
} from "mongodb" } from "mongodb"
import environment from "../environment" import environment from "../environment"
import { HOST_ADDRESS } from "./utils"
export interface MongoDBConfig { export interface MongoDBConfig {
connectionString: string connectionString: string
@ -51,7 +52,7 @@ const getSchema = () => {
connectionString: { connectionString: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
required: true, required: true,
default: "mongodb://localhost:27017", default: `mongodb://${HOST_ADDRESS}:27017`,
display: "Connection string", display: "Connection string",
}, },
db: { db: {

View file

@ -21,6 +21,7 @@ import {
generateColumnDefinition, generateColumnDefinition,
finaliseExternalTables, finaliseExternalTables,
checkExternalTables, checkExternalTables,
HOST_ADDRESS,
} from "./utils" } from "./utils"
import dayjs from "dayjs" import dayjs from "dayjs"
import { NUMBER_REGEX } from "../utilities" import { NUMBER_REGEX } from "../utilities"
@ -49,7 +50,7 @@ const SCHEMA: Integration = {
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
default: "localhost", default: HOST_ADDRESS,
required: true, required: true,
}, },
port: { port: {

View file

@ -22,6 +22,7 @@ import {
finaliseExternalTables, finaliseExternalTables,
getSqlQuery, getSqlQuery,
SqlClient, SqlClient,
HOST_ADDRESS,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { import {
@ -63,7 +64,7 @@ const SCHEMA: Integration = {
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
default: "localhost", default: HOST_ADDRESS,
required: true, required: true,
}, },
port: { port: {

View file

@ -21,6 +21,7 @@ import {
finaliseExternalTables, finaliseExternalTables,
SqlClient, SqlClient,
checkExternalTables, checkExternalTables,
HOST_ADDRESS,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { PostgresColumn } from "./base/types" import { PostgresColumn } from "./base/types"
@ -72,7 +73,7 @@ const SCHEMA: Integration = {
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
default: "localhost", default: HOST_ADDRESS,
required: true, required: true,
}, },
port: { port: {

View file

@ -6,6 +6,7 @@ import {
QueryType, QueryType,
} from "@budibase/types" } from "@budibase/types"
import Redis from "ioredis" import Redis from "ioredis"
import { HOST_ADDRESS } from "./utils"
interface RedisConfig { interface RedisConfig {
host: string host: string
@ -28,7 +29,7 @@ const SCHEMA: Integration = {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
required: true, required: true,
default: "localhost", default: HOST_ADDRESS,
}, },
port: { port: {
type: DatasourceFieldType.NUMBER, type: DatasourceFieldType.NUMBER,

View file

@ -13,6 +13,7 @@ import {
DEFAULT_BB_DATASOURCE_ID, DEFAULT_BB_DATASOURCE_ID,
} from "../constants" } from "../constants"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import env from "../environment"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g const ROW_ID_REGEX = /^\[.*]$/g
@ -92,6 +93,14 @@ export enum SqlClient {
ORACLE = "oracledb", ORACLE = "oracledb",
} }
const isCloud = env.isProd() && !env.SELF_HOSTED
const isSelfHost = env.isProd() && env.SELF_HOSTED
export const HOST_ADDRESS = isSelfHost
? "host.docker.internal"
: isCloud
? ""
: "localhost"
export function isExternalTableID(tableId: string) { export function isExternalTableID(tableId: string) {
return tableId.includes(DocumentType.DATASOURCE) return tableId.includes(DocumentType.DATASOURCE)
} }

View file

@ -11,3 +11,6 @@ process.env.PLATFORM_URL = "http://localhost:10000"
process.env.REDIS_PASSWORD = "budibase" process.env.REDIS_PASSWORD = "budibase"
process.env.BUDIBASE_VERSION = "0.0.0+jest" process.env.BUDIBASE_VERSION = "0.0.0+jest"
process.env.WORKER_URL = "http://localhost:10000" process.env.WORKER_URL = "http://localhost:10000"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.JWT_SECRET = "jwtsecret"

View file

@ -1,11 +1,13 @@
import { budibaseTempDir } from "../budibaseDir" import { budibaseTempDir } from "../budibaseDir"
import fs from "fs" import fs from "fs"
import { join } from "path" import { join } from "path"
import { ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets, devClientVersion } from "../../constants"
import { updateClientLibrary } from "./clientLibrary" import { updateClientLibrary } from "./clientLibrary"
import env from "../../environment" import env from "../../environment"
import { objectStore, context } from "@budibase/backend-core" import { objectStore, context } from "@budibase/backend-core"
import { TOP_LEVEL_PATH } from "./filesystem" import { TOP_LEVEL_PATH } from "./filesystem"
import { DocumentType } from "../../db/utils"
import { App } from "@budibase/types"
export const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules") export const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
@ -35,20 +37,25 @@ export const getComponentLibraryManifest = async (library: string) => {
const filename = "manifest.json" const filename = "manifest.json"
if (env.isDev() || env.isTest()) { if (env.isDev() || env.isTest()) {
const paths = [ const db = context.getAppDB()
join(TOP_LEVEL_PATH, "packages/client", filename), const app = await db.get<App>(DocumentType.APP_METADATA)
join(process.cwd(), "client", filename),
] if (app.version === devClientVersion || env.isTest()) {
for (let path of paths) { const paths = [
if (fs.existsSync(path)) { join(TOP_LEVEL_PATH, "packages/client", filename),
// always load from new so that updates are refreshed join(process.cwd(), "client", filename),
delete require.cache[require.resolve(path)] ]
return require(path) for (let path of paths) {
if (fs.existsSync(path)) {
// always load from new so that updates are refreshed
delete require.cache[require.resolve(path)]
return require(path)
}
} }
throw new Error(
`Unable to find ${filename} in development environment (may need to build).`
)
} }
throw new Error(
`Unable to find ${filename} in development environment (may need to build).`
)
} }
if (!appId) { if (!appId) {

View file

@ -1,10 +1,12 @@
import path, { join } from "path" import path, { join } from "path"
import { ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import fs from "fs" import fs from "fs"
import { objectStore } from "@budibase/backend-core" import { context, objectStore } from "@budibase/backend-core"
import { resolve } from "../centralPath" import { resolve } from "../centralPath"
import env from "../../environment" import env from "../../environment"
import { TOP_LEVEL_PATH } from "./filesystem" import { TOP_LEVEL_PATH } from "./filesystem"
import { DocumentType } from "../../db/utils"
import { App } from "@budibase/types"
export function devClientLibPath() { export function devClientLibPath() {
return require.resolve("@budibase/client") return require.resolve("@budibase/client")
@ -120,7 +122,12 @@ export async function updateClientLibrary(appId: string) {
ContentType: "application/javascript", ContentType: "application/javascript",
} }
) )
await Promise.all([manifestUpload, clientUpload])
const manifestSrc = fs.promises.readFile(manifest, "utf8")
await Promise.all([manifestUpload, clientUpload, manifestSrc])
return JSON.parse(await manifestSrc)
} }
/** /**
@ -130,30 +137,49 @@ export async function updateClientLibrary(appId: string) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function revertClientLibrary(appId: string) { export async function revertClientLibrary(appId: string) {
// Copy backups manifest to tmp directory let manifestPath, clientPath
const tmpManifestPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "manifest.json.bak")
)
// Copy backup client lib to tmp if (env.isDev()) {
const tmpClientPath = await objectStore.retrieveToTmp( const db = context.getAppDB()
ObjectStoreBuckets.APPS, const app = await db.get<App>(DocumentType.APP_METADATA)
join(appId, "budibase-client.js.bak") clientPath = join(
) __dirname,
`/oldClientVersions/${app.revertableVersion}/app.js`
)
manifestPath = join(
__dirname,
`/oldClientVersions/${app.revertableVersion}/manifest.json`
)
} else {
// Copy backups manifest to tmp directory
manifestPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "manifest.json.bak")
)
// Copy backup client lib to tmp
clientPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "budibase-client.js.bak")
)
}
const manifestSrc = fs.promises.readFile(manifestPath, "utf8")
// Upload backups as new versions // Upload backups as new versions
const manifestUpload = objectStore.upload({ const manifestUpload = objectStore.upload({
bucket: ObjectStoreBuckets.APPS, bucket: ObjectStoreBuckets.APPS,
filename: join(appId, "manifest.json"), filename: join(appId, "manifest.json"),
path: tmpManifestPath, path: manifestPath,
type: "application/json", type: "application/json",
}) })
const clientUpload = objectStore.upload({ const clientUpload = objectStore.upload({
bucket: ObjectStoreBuckets.APPS, bucket: ObjectStoreBuckets.APPS,
filename: join(appId, "budibase-client.js"), filename: join(appId, "budibase-client.js"),
path: tmpClientPath, path: clientPath,
type: "application/javascript", type: "application/javascript",
}) })
await Promise.all([manifestUpload, clientUpload]) await Promise.all([manifestSrc, manifestUpload, clientUpload])
return JSON.parse(await manifestSrc)
} }

View file

@ -159,3 +159,22 @@ export const InvalidFileExtensions = [
"wsh", "wsh",
"zip", "zip",
] ]
export enum BpmCorrelationKey {
ONBOARDING = "budibase:onboarding:correlationkey",
}
export enum BpmInstanceKey {
ONBOARDING = "budibase:onboarding:instancekey",
}
export enum BpmStatusKey {
ONBOARDING = "budibase:onboarding:status",
}
export enum BpmStatusValue {
STARTED = "started",
COMPLETING_ACCOUNT_INFO = "completing_account_info",
VERIFYING_EMAIL = "verifying_email",
COMPLETED = "completed",
}

View file

@ -17,6 +17,12 @@ const config = (format, outputFile) => ({
format, format,
file: outputFile, file: outputFile,
}, },
onwarn(warning, warn) {
if (warning.code === "EVAL") {
return
}
warn(warning)
},
plugins: [ plugins: [
typescript(), typescript(),
resolve({ resolve({

View file

@ -63,11 +63,16 @@ export interface SearchUsersRequest {
export interface CreateAdminUserRequest { export interface CreateAdminUserRequest {
email: string email: string
password: string password?: string
tenantId: string tenantId: string
ssoId?: string ssoId?: string
} }
export interface AddSSoUserRequest {
ssoId: string
email: string
}
export interface CreateAdminUserResponse { export interface CreateAdminUserResponse {
_id: string _id: string
_rev: string _rev: string

View file

@ -71,6 +71,7 @@ export interface AppIcon {
export interface AppFeatures { export interface AppFeatures {
componentValidation?: boolean componentValidation?: boolean
disableUserMetadata?: boolean disableUserMetadata?: boolean
skeletonLoader?: boolean
} }
export interface AutomationSettings { export interface AutomationSettings {

View file

@ -1,142 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Start Server",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["src/index.ts"],
"cwd": "${workspaceRoot}",
},
{
"type": "node",
"request": "launch",
"name": "Jest - All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Users",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["user.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Instances",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["instance.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Roles",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["role.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Records",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["record.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Models",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["table.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Views",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["view.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Applications",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["application.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Builder",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["builder", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Initialise Budibase",
"program": "yarn",
"args": ["run", "initialise"],
"console": "externalTerminal"
}
]
}

View file

@ -1,8 +0,0 @@
const { join } = require("path")
require("dotenv").config({
path: join(__dirname, "..", "..", "hosting", ".env"),
})
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
module.exports = jestTestcontainersConfigGenerator()

View file

@ -2,7 +2,7 @@ import { Config } from "@jest/types"
import * as fs from "fs" import * as fs from "fs"
const config: Config.InitialOptions = { const config: Config.InitialOptions = {
preset: "@trendyol/jest-testcontainers", globalSetup: "./../../globalSetup.ts",
setupFiles: ["./src/tests/jestEnv.ts"], setupFiles: ["./src/tests/jestEnv.ts"],
setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"],
collectCoverageFrom: ["src/**/*.{js,ts}", "../backend-core/src/**/*.{js,ts}"], collectCoverageFrom: ["src/**/*.{js,ts}", "../backend-core/src/**/*.{js,ts}"],

View file

@ -75,7 +75,6 @@
"devDependencies": { "devDependencies": {
"@swc/core": "1.3.71", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27", "@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "2.1.1",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/jsonwebtoken": "9.0.3", "@types/jsonwebtoken": "9.0.3",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",

View file

@ -3,6 +3,7 @@ import env from "../../../environment"
import { import {
AcceptUserInviteRequest, AcceptUserInviteRequest,
AcceptUserInviteResponse, AcceptUserInviteResponse,
AddSSoUserRequest,
BulkUserRequest, BulkUserRequest,
BulkUserResponse, BulkUserResponse,
CloudAccount, CloudAccount,
@ -15,6 +16,7 @@ import {
LockName, LockName,
LockType, LockType,
MigrationType, MigrationType,
PlatformUserByEmail,
SaveUserResponse, SaveUserResponse,
SearchUsersRequest, SearchUsersRequest,
User, User,
@ -53,6 +55,25 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
} }
} }
export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
const { email, ssoId } = ctx.request.body
try {
// Status is changed to 404 from getUserDoc if user is not found
let userByEmail = (await platform.users.getUserDoc(
email
)) as PlatformUserByEmail
await platform.users.addSsoUser(
ssoId,
email,
userByEmail.userId,
userByEmail.tenantId
)
ctx.status = 200
} catch (err: any) {
ctx.throw(err.status || 400, err)
}
}
const bulkDelete = async (userIds: string[], currentUserId: string) => { const bulkDelete = async (userIds: string[], currentUserId: string) => {
if (userIds?.indexOf(currentUserId) !== -1) { if (userIds?.indexOf(currentUserId) !== -1) {
throw new Error("Unable to delete self.") throw new Error("Unable to delete self.")
@ -127,8 +148,8 @@ export const adminUser = async (
try { try {
const finalUser = await userSdk.db.createAdminUser( const finalUser = await userSdk.db.createAdminUser(
email, email,
password,
tenantId, tenantId,
password,
{ {
ssoId, ssoId,
hashPassword, hashPassword,

View file

@ -41,6 +41,10 @@ const PUBLIC_ENDPOINTS = [
route: "/api/global/users/init", route: "/api/global/users/init",
method: "POST", method: "POST",
}, },
{
route: "/api/global/users/sso",
method: "POST",
},
{ {
route: "/api/global/users/invite/accept", route: "/api/global/users/invite/accept",
method: "POST", method: "POST",
@ -81,6 +85,11 @@ const NO_TENANCY_ENDPOINTS = [
route: "/api/global/users/init", route: "/api/global/users/init",
method: "POST", method: "POST",
}, },
// tenant is retrieved from the user found by the requested email
{
route: "/api/global/users/sso",
method: "POST",
},
// deprecated single tenant sso callback // deprecated single tenant sso callback
{ {
route: "/api/admin/auth/google/callback", route: "/api/admin/auth/google/callback",

View file

@ -520,10 +520,51 @@ describe("/api/global/users", () => {
}) })
} }
function createPasswordUser() {
return config.doInTenant(() => {
const user = structures.users.user()
return userSdk.db.save(user)
})
}
it("should be able to update an sso user that has no password", async () => { it("should be able to update an sso user that has no password", async () => {
const user = await createSSOUser() const user = await createSSOUser()
await config.api.users.saveUser(user) await config.api.users.saveUser(user)
}) })
it("sso support couldn't be used by admin. It is cloud restricted and needs internal key", async () => {
const user = await config.createUser()
const ssoId = "fake-ssoId"
await config.api.users
.addSsoSupportDefaultAuth(ssoId, user.email)
.expect("Content-Type", /json/)
.expect(403)
})
it("if user email doesn't exist, SSO support couldn't be added. Not found error returned", async () => {
const ssoId = "fake-ssoId"
const email = "fake-email@budibase.com"
await config.api.users
.addSsoSupportInternalAPIAuth(ssoId, email)
.expect("Content-Type", /json/)
.expect(404)
})
it("if user email exist, SSO support is added", async () => {
const user = await createPasswordUser()
const ssoId = "fakessoId"
await config.api.users
.addSsoSupportInternalAPIAuth(ssoId, user.email)
.expect(200)
})
it("if user ssoId is already assigned, no change will be applied", async () => {
const user = await createSSOUser()
user.ssoId = "testssoId"
await config.api.users
.addSsoSupportInternalAPIAuth(user.ssoId, user.email)
.expect(200)
})
}) })
}) })

View file

@ -7,12 +7,13 @@ import { users } from "../validation"
import * as selfController from "../../controllers/global/self" import * as selfController from "../../controllers/global/self"
const router: Router = new Router() const router: Router = new Router()
const OPTIONAL_STRING = Joi.string().optional().allow(null).allow("")
function buildAdminInitValidation() { function buildAdminInitValidation() {
return auth.joiValidator.body( return auth.joiValidator.body(
Joi.object({ Joi.object({
email: Joi.string().required(), email: Joi.string().required(),
password: Joi.string(), password: OPTIONAL_STRING,
tenantId: Joi.string().required(), tenantId: Joi.string().required(),
ssoId: Joi.string(), ssoId: Joi.string(),
}) })
@ -64,6 +65,12 @@ router
users.buildUserSaveValidation(), users.buildUserSaveValidation(),
controller.save controller.save
) )
.post(
"/api/global/users/sso",
cloudRestricted,
users.buildAddSsoSupport(),
controller.addSsoSupport
)
.post( .post(
"/api/global/users/bulk", "/api/global/users/bulk",
auth.adminOnly, auth.adminOnly,

View file

@ -41,6 +41,15 @@ export const buildUserSaveValidation = () => {
return auth.joiValidator.body(Joi.object(schema).required().unknown(true)) return auth.joiValidator.body(Joi.object(schema).required().unknown(true))
} }
export const buildAddSsoSupport = () => {
return auth.joiValidator.body(
Joi.object({
ssoId: Joi.string().required(),
email: Joi.string().required(),
}).required()
)
}
export const buildUserBulkUserValidation = (isSelf = false) => { export const buildUserBulkUserValidation = (isSelf = false) => {
if (!isSelf) { if (!isSelf) {
schema = { schema = {

View file

@ -127,6 +127,20 @@ export class UserAPI extends TestAPI {
.expect(status ? status : 200) .expect(status ? status : 200)
} }
addSsoSupportInternalAPIAuth = (ssoId: string, email: string) => {
return this.request
.post(`/api/global/users/sso`)
.send({ ssoId, email })
.set(this.config.internalAPIHeaders())
}
addSsoSupportDefaultAuth = (ssoId: string, email: string) => {
return this.request
.post(`/api/global/users/sso`)
.send({ ssoId, email })
.set(this.config.defaultHeaders())
}
deleteUser = (userId: string, status?: number) => { deleteUser = (userId: string, status?: number) => {
return this.request return this.request
.delete(`/api/global/users/${userId}`) .delete(`/api/global/users/${userId}`)

View file

@ -11,3 +11,5 @@ process.env.INTERNAL_API_KEY = "tet"
process.env.DISABLE_ACCOUNT_PORTAL = "0" process.env.DISABLE_ACCOUNT_PORTAL = "0"
process.env.MOCK_REDIS = "1" process.env.MOCK_REDIS = "1"
process.env.BUDIBASE_VERSION = "0.0.0+jest" process.env.BUDIBASE_VERSION = "0.0.0+jest"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.COUCH_DB_USER = "budibase"

View file

@ -1,42 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
describe("Internal API - Application creation", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Get applications without applications", async () => {
await config.api.apps.fetchEmptyAppList()
})
it("Get all Applications after creating an application", async () => {
await config.api.apps.create({
...fixtures.apps.generateApp(),
useTemplate: "false",
})
await config.api.apps.fetchAllApplications()
})
it("Get application details", async () => {
const app = await config.createApp({
...fixtures.apps.generateApp(),
useTemplate: "false",
})
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.application.name).toEqual(app.name)
expect(appPackageJson.application.version).toEqual(app.version)
expect(appPackageJson.application.url).toEqual(app.url)
expect(appPackageJson.application.tenantId).toEqual(app.tenantId)
expect(appPackageJson.application.status).toEqual(app.status)
})
})

View file

@ -1,19 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
describe("Internal API - Application creation, update, publish and delete", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("DELETE - Delete an application", async () => {
const app = await config.createApp()
await config.api.apps.delete(app.appId!)
})
})

View file

@ -1,54 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { db } from "@budibase/backend-core"
import * as fixtures from "../../fixtures"
describe("Internal API - Application creation, update, publish and delete", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Publish app", async () => {
// create the app
const app = await config.createApp(fixtures.apps.appFromTemplate())
// check preview renders
await config.api.apps.canRender()
// publish app
await config.api.apps.publish(app.appId)
// check published app renders
config.state.appId = db.getProdAppID(app.appId!)
await config.api.apps.canRender()
// unpublish app
await config.api.apps.unpublish(app.appId!)
})
it("Sync application before deployment", async () => {
const app = await config.createApp()
const [syncResponse, sync] = await config.api.apps.sync(app.appId!)
expect(sync).toEqual({
message: "App sync completed successfully.",
})
})
it("Sync application after deployment", async () => {
const app = await config.createApp()
// publish app
await config.api.apps.publish(app._id)
const [syncResponse, sync] = await config.api.apps.sync(app.appId!)
expect(sync).toEqual({
message: "App sync completed successfully.",
})
})
})

View file

@ -1,45 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { generator } from "../../../shared"
import * as fixtures from "../../fixtures"
describe("Internal API - Application creation, update, publish and delete", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Update an application", async () => {
const app = await config.createApp()
await config.api.apps.rename(app.appId!, app.name!, {
name: generator.word(),
})
})
it("Revert Changes without changes", async () => {
const app = await config.createApp()
await config.api.apps.revertUnpublished(app.appId!)
})
it("Revert Changes", async () => {
const app = await config.createApp()
// publish app
await config.api.apps.publish(app._id)
// Change/add component to the app
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
// // Revert the app to published state
await config.api.apps.revertPublished(app.appId!)
// Check screen is removed
await config.api.apps.getRoutes()
})
})

View file

@ -1,51 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
describe("Internal API - /screens endpoints", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create a screen with each role type", async () => {
// Create app
await config.createApp()
// Create Screen
const roleArray = ["BASIC", "POWER", "ADMIN", "PUBLIC"]
for (let role in roleArray) {
const [response, screen] = await config.api.screens.create(
fixtures.screens.generateScreen(roleArray[role])
)
}
})
it("Get screens", async () => {
// Create app
await config.createApp()
// Create Screen
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
// Check screen exists
await config.api.apps.getRoutes(true)
})
it("Delete a screen", async () => {
// Create app
await config.createApp()
// Create Screen
const [screenResponse, screen] = await config.api.screens.create(
fixtures.screens.generateScreen("BASIC")
)
// Delete Screen
await config.api.screens.delete(screen._id!, screen._rev!)
})
})

View file

@ -1,133 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { generator } from "../../../shared"
import * as fixtures from "../../fixtures"
describe("Internal API - Table Operations", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create and delete table, columns and rows", async () => {
// create the app
await config.createApp(fixtures.apps.appFromTemplate())
// Get current tables: expect 2 in this template
await config.api.tables.getAll(2)
// Add new table
const [createdTableResponse, createdTableData] =
await config.api.tables.save(fixtures.tables.generateTable())
//Table was added
await config.api.tables.getAll(3)
//Get information about the table
await config.api.tables.getTableById(createdTableData._id!)
//Add Column to table
const newColumn =
fixtures.tables.generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.api.tables.save(
newColumn,
true
)
//Add Row to table
const newRow = fixtures.rows.generateNewRowForTable(addColumnData._id!)
await config.api.rows.add(addColumnData._id!, newRow)
//Get Row from table
const [getRowResponse, getRowData] = await config.api.rows.getAll(
addColumnData._id!
)
//Delete Row from table
const rowToDelete = {
rows: [getRowData[0]],
}
const [deleteRowResponse, deleteRowData] = await config.api.rows.delete(
addColumnData._id!,
rowToDelete
)
expect(deleteRowData[0]._id).toEqual(getRowData[0]._id)
//Delete the table
const [deleteTableResponse, deleteTable] = await config.api.tables.delete(
addColumnData._id!,
addColumnData._rev!
)
//Table was deleted
await config.api.tables.getAll(2)
})
it("Search and pagination", async () => {
// create the app
await config.createApp(fixtures.apps.appFromTemplate())
// Get current tables: expect 2 in this template
await config.api.tables.getAll(2)
// Add new table
const [createdTableResponse, createdTableData] =
await config.api.tables.save(fixtures.tables.generateTable())
//Table was added
await config.api.tables.getAll(3)
//Get information about the table
await config.api.tables.getTableById(createdTableData._id!)
//Add Column to table
const newColumn =
fixtures.tables.generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.api.tables.save(
newColumn,
true
)
//Add Row to table
let newRow = fixtures.rows.generateNewRowForTable(addColumnData._id!)
await config.api.rows.add(addColumnData._id!, newRow)
//Search single row
await config.api.rows.searchNoPagination(
createdTableData._id!,
fixtures.rows.searchBody(createdTableData.primaryDisplay!)
)
//Add 10 more rows
for (let i = 0; i < 10; i++) {
let newRow = fixtures.rows.generateNewRowForTable(addColumnData._id!)
await config.api.rows.add(addColumnData._id!, newRow)
}
//Search rows with pagination
const [allRowsResponse, allRowsJson] =
await config.api.rows.searchWithPagination(
createdTableData._id!,
fixtures.rows.searchBody(createdTableData.primaryDisplay!)
)
//Delete Rows from table
const rowToDelete = {
rows: [allRowsJson],
}
const [deleteRowResponse, deleteRowData] = await config.api.rows.delete(
createdTableData._id!,
rowToDelete
)
//Search single row
await config.api.rows.searchWithPagination(
createdTableData._id!,
fixtures.rows.searchBody(createdTableData.primaryDisplay!)
)
})
})

View file

@ -1,104 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { User } from "@budibase/types"
import { db } from "@budibase/backend-core"
import * as fixtures from "../../fixtures"
describe("Internal API - App Specific Roles & Permissions", () => {
const config = new TestConfiguration()
// Before each test, login as admin. Some tests will require login as a different user
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Add BASIC user to app", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
const app = await config.createApp(fixtures.apps.appFromTemplate())
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "BASIC",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("BASIC")
})
it("Add ADMIN user to app", async () => {
// Create a user with ADMIN role and check if it was created successfully
const adminUser = fixtures.users.generateUser(1, "admin")
expect(adminUser[0].builder?.global).toEqual(true)
expect(adminUser[0].admin?.global).toEqual(true)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(adminUser)
// const app = await config.createApp(fixtures.apps.appFromTemplate())
const app = await config.createApp(fixtures.apps.appFromTemplate())
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "ADMIN",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("ADMIN")
// publish app
await config.api.apps.publish(app.appId)
// check published app renders
config.state.appId = db.getProdAppID(app.appId!)
await config.api.apps.canRender()
})
it("Add POWER user to app", async () => {
const powerUser = fixtures.users.generateUser(1, "developer")
expect(powerUser[0].builder?.global).toEqual(true)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(powerUser)
const app = await config.createApp()
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "POWER",
},
}
await config.api.users.updateInfo(body)
// Get the user information again and check if the role was added
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("POWER")
})
})

View file

@ -1,90 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { User } from "@budibase/types"
import * as fixtures from "./../../fixtures"
describe("Internal API - User Management & Permissions", () => {
const config = new TestConfiguration()
// Before each test, login as admin. Some tests will require login as a different user
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Add Users with different roles", async () => {
// Get all users
await config.api.users.search()
// Get all roles
await config.api.users.getRoles()
// Add users with each role
const admin = fixtures.users.generateUser(1, "admin")
expect(admin[0].builder?.global).toEqual(true)
expect(admin[0].admin?.global).toEqual(true)
const developer = fixtures.users.generateUser(1, "developer")
expect(developer[0].builder?.global).toEqual(true)
const appUser = fixtures.users.generateUser(1, "appUser")
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const userList = [...admin, ...developer, ...appUser]
await config.api.users.addMultiple(userList)
// Check users are added
const [allUsersResponse, allUsersJson] = await config.api.users.getAll()
expect(allUsersJson.length).toBeGreaterThan(0)
})
it("Delete User", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [userResponse, userJson] = await config.api.users.addMultiple(appUser)
const userId = userJson.created.successful[0]._id
await config.api.users.delete(userId)
})
it("Reset Password", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [userResponse, userJson] = await config.api.users.addMultiple(appUser)
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
userJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
password: "newPassword",
}
await config.api.users.forcePasswordReset(body)
})
it("Change User information", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [userResponse, userJson] = await config.api.users.addMultiple(appUser)
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
userJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
firstName: "newFirstName",
lastName: "newLastName",
builder: {
global: true,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(userJson.created.successful[0]._id)
expect(changedUserInfoJson.builder?.global).toBeDefined()
expect(changedUserInfoJson.builder?.global).toEqual(true)
})
})

View file

@ -1,97 +0,0 @@
import { db as dbCore } from "@budibase/backend-core"
import { TestConfiguration } from "../../config"
import { Application } from "../../../types"
import * as fixtures from "../../fixtures"
describe("Public API - /applications endpoints", () => {
const config = new TestConfiguration<Application>()
beforeAll(async () => {
await config.beforeAll()
await config.createApp()
config.context = (await config.api.apps.read(config.state.appId!))[1]
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create an application", async () => {
const [response, app] = await config.api.apps.create(
fixtures.apps.generateApp()
)
expect(response).toHaveStatusCode(200)
expect(app._id).toBeDefined()
})
it("POST - Search applications", async () => {
const [response, apps] = await config.api.apps.search({
name: config.context.name,
})
expect(response).toHaveStatusCode(200)
expect(apps[0]).toEqual(config.context)
})
it("GET - Retrieve an application", async () => {
const [response, app] = await config.api.apps.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(app).toEqual(config.context)
})
it("PUT - update an application", async () => {
config.context.name = "UpdatedName"
const [response, app] = await config.api.apps.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(app.updatedAt).not.toEqual(config.context.updatedAt)
expect(app.name).toEqual(config.context.name)
})
it("POST - publish an application", async () => {
config.context.name = "UpdatedName"
const [response, deployment] = await config.api.apps.publish(
config.context._id
)
expect(response).toHaveStatusCode(200)
expect(deployment).toEqual({
status: "SUCCESS",
})
// Verify publish
const prodAppId = dbCore.getProdAppID(config.context._id)
const [_, publishedApp] = await config.api.apps.read(prodAppId)
expect(response).toHaveStatusCode(200)
expect(publishedApp._id).toEqual(prodAppId)
})
it("POST - unpublish a published application", async () => {
await config.api.apps.publish(config.context._id)
const [response] = await config.api.apps.unpublish(config.context._id)
expect(response).toHaveStatusCode(204)
})
it("POST - unpublish an unpublished application", async () => {
const [response] = await config.api.apps.unpublish(config.context._id)
expect(response).toHaveStatusCode(400)
})
it("DELETE - delete a published application and the dev application", async () => {
await config.api.apps.publish(config.context._id)
const [response, deletion] = await config.api.apps.delete(
config.context._id
)
expect(response).toHaveStatusCode(200)
expect(deletion._id).toEqual(config.context._id)
// verify dev app deleted
const [devAppResponse] = await config.api.apps.read(config.context._id)
expect(devAppResponse).toHaveStatusCode(404)
// verify prod app deleted
const prodAppId = dbCore.getProdAppID(config.context._id)
const [publishedAppResponse] = await config.api.apps.read(prodAppId)
expect(publishedAppResponse).toHaveStatusCode(404)
})
})

View file

@ -1,62 +0,0 @@
import { TestConfiguration } from "../../config"
import * as fixtures from "../../fixtures"
import { Row } from "../../../types"
describe("Public API - /rows endpoints", () => {
const config = new TestConfiguration<Row>()
beforeAll(async () => {
await config.beforeAll()
await config.createApp()
const [tResp, table] = await config.api.tables.seed()
config.state.tableId = table._id
const [rResp, row] = await config.api.rows.seed(table._id)
config.context = row
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a row", async () => {
const [response, row] = await config.api.rows.create(
fixtures.rows.generateRow()
)
expect(response).toHaveStatusCode(200)
expect(row._id).toBeDefined()
})
it("POST - Search rows", async () => {
const [response, rows] = await config.api.rows.search({
query: {
string: {
testColumn: config.context.testColumn as string,
},
},
})
expect(response).toHaveStatusCode(200)
expect(rows.length).toEqual(1)
expect(rows[0]._id).toEqual(config.context._id)
expect(rows[0].tableId).toEqual(config.context.tableId)
expect(rows[0].testColumn).toEqual(config.context.testColumn)
})
it("GET - Retrieve a row", async () => {
const [response, row] = await config.api.rows.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(row._id).toEqual(config.context._id)
expect(row.tableId).toEqual(config.context.tableId)
})
it("PUT - update a row", async () => {
config.context.testColumn = "UpdatedName"
const [response, row] = await config.api.rows.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(row.testColumn).toEqual(config.context.testColumn)
})
})

View file

@ -1,51 +0,0 @@
import { TestConfiguration } from "../../config"
import * as fixtures from "../../fixtures"
import { Table } from "../../../types"
describe("Public API - /tables endpoints", () => {
const config = new TestConfiguration<Table>()
beforeAll(async () => {
await config.beforeAll()
await config.createApp()
const [tableResp, table] = await config.api.tables.seed()
config.context = table
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a table", async () => {
const [response, table] = await config.api.tables.create(
fixtures.tables.generateTable()
)
expect(response).toHaveStatusCode(200)
expect(table._id).toBeDefined()
})
it("POST - Search tables", async () => {
const [response, tables] = await config.api.tables.search({
name: config.context.name,
})
expect(response).toHaveStatusCode(200)
expect(tables[0]).toEqual(config.context)
})
it("GET - Retrieve a table", async () => {
const [response, table] = await config.api.tables.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(table).toEqual(config.context)
})
it("PUT - update a table", async () => {
config.context.name = "updatedName"
const [response, table] = await config.api.tables.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(table).toEqual(config.context)
})
})

View file

@ -1,49 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { User } from "../../../types"
describe("Public API - /users endpoints", () => {
const config = new TestConfiguration<User>()
beforeAll(async () => {
await config.beforeAll()
const [_, user] = await config.api.users.seed()
config.context = user
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a user", async () => {
const [response, user] = await config.api.users.create(
fixtures.users.generateUser()
)
expect(response).toHaveStatusCode(200)
expect(user._id).toBeDefined()
})
it("POST - Search users", async () => {
const [response, users] = await config.api.users.search({
name: config.context.email,
})
expect(response).toHaveStatusCode(200)
expect(users[0]).toEqual(config.context)
})
it("GET - Retrieve a user", async () => {
const [response, user] = await config.api.users.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(user).toEqual(config.context)
})
it("PUT - update a user", async () => {
config.context.firstName = "Updated First Name"
const [response, user] = await config.api.users.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(user).toEqual(config.context)
})
})

View file

@ -3,7 +3,7 @@
const start = Date.now() const start = Date.now()
const fs = require("fs") const fs = require("fs")
const { readdir, copyFile, mkdir } = require('node:fs/promises'); const { cp, readdir, copyFile, mkdir } = require('node:fs/promises');
const path = require("path") const path = require("path")
const { build } = require("esbuild") const { build } = require("esbuild")
@ -17,12 +17,6 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
const svelteCompilePlugin = { const svelteCompilePlugin = {
name: 'svelteCompile', name: 'svelteCompile',
setup(build) { 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 // Compiles `.svelte` files into JS classes so that they can be directly imported into our
// Typescript packages // Typescript packages
build.onLoad({ filter: /\.svelte$/ }, async (args) => { build.onLoad({ filter: /\.svelte$/ }, async (args) => {
@ -80,11 +74,11 @@ async function runBuild(entry, outfile) {
plugins: [ plugins: [
svelteCompilePlugin, svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }), TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin(), nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"]
}),
], ],
preserveSymlinks: true, preserveSymlinks: true,
loader: {
},
metafile: true, metafile: true,
external: [ external: [
"deasync", "deasync",
@ -109,13 +103,23 @@ async function runBuild(entry, outfile) {
await Promise.all(fileCopyPromises) await Promise.all(fileCopyPromises)
})() })()
const oldClientVersions = (async () => {
try {
await cp('./build/oldClientVersions', './dist/oldClientVersions', { recursive: true });
} catch (e) {
if (e.code !== "EEXIST" && e.code !== "ENOENT") {
throw e;
}
}
})()
const mainBuild = build({ const mainBuild = build({
...sharedConfig, ...sharedConfig,
platform: "node", platform: "node",
outfile, outfile,
}) })
await Promise.all([hbsFiles, mainBuild]) await Promise.all([hbsFiles, mainBuild, oldClientVersions])
fs.writeFileSync( fs.writeFileSync(
`dist/${path.basename(outfile)}.meta.json`, `dist/${path.basename(outfile)}.meta.json`,

View file

@ -0,0 +1,45 @@
const fs = require('node:fs/promises');
const util = require('node:util');
const { argv } = require('node:process');
const exec = util.promisify(require('node:child_process').exec);
const version = argv[2]
const getPastClientVersion = async () => {
const manifestRaw = await fetch(`https://api.github.com/repos/budibase/budibase/contents/packages/client/manifest.json?ref=v${version}`).then(r => r.json());
// GitHub response will contain a message field containing the following string if the version can't be found.
if (manifestRaw?.message?.includes("No commit found")) {
throw `Can't find a GitHub tag with version "${version}"`
}
const manifest = Buffer.from(manifestRaw.content, 'base64').toString('utf8')
await fs.mkdir(`packages/server/build/oldClientVersions/${version}`, { recursive: true });
await fs.writeFile(`packages/server/build/oldClientVersions/${version}/manifest.json`, manifest)
const npmRegistry = await fetch(`https://registry.npmjs.org/@budibase/client/${version}`).then(r => r.json());
// The json response from npm is just a string starting with the following if the version can't be found
if (typeof npmRegistry === "string" && npmRegistry.startsWith("version not found")) {
throw `Can't find @budibase/client with version "${version}" in npm registry`
}
// Create a temp directory to store the @budibase/client library in
await fs.mkdir("clientVersionTmp", { recursive: true });
// Get the tarball of the @budibase/client library and pipe it into tar to extract it
await exec(`curl -L ${npmRegistry.dist.tarball} --output - | tar -xvzf - -C clientVersionTmp`);
// Copy the compiled client version we want to the oldClientVersions dir and delete the temp directory
await fs.copyFile('./clientVersionTmp/package/dist/budibase-client.js', `./packages/server/build/oldClientVersions/${version}/app.js`);
await fs.rm("clientVersionTmp", { recursive: true });
// Check what client versions the user has locally and update the `clientVersions.json` file in the builder so that they can be selected
const rootDir = await fs.readdir('packages/server/build/oldClientVersions/', { withFileTypes: true });
const dirs = rootDir.filter(entry => entry.isDirectory()).map(dir => dir.name);
await fs.writeFile("packages/builder/src/components/deploy/clientVersions.json", JSON.stringify(dirs))
}
getPastClientVersion().catch(e => console.error(e));

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