Merge branch 'develop' of github.com:Budibase/budibase into grid-block
This commit is contained in:
commit
70eda7ff81
|
@ -12,31 +12,22 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# - name: Fail if not a tag
|
||||
# run: |
|
||||
# if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||
# echo "Workflow Dispatch can only be run on tags"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
|
||||
# - name: Fail if tag is not in master
|
||||
# run: |
|
||||
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
- name: Fail if not a tag
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
||||
-H 'Accept: application/vnd.github.v3.raw' \
|
||||
-o values.production.yaml \
|
||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
|
||||
wc -l values.production.yaml
|
||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||
echo "Workflow Dispatch can only be run on tags"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fail if tag is not in master
|
||||
run: |
|
||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
|
@ -48,29 +39,10 @@ jobs:
|
|||
fi
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Deploy to EKS
|
||||
uses: craftech-io/eks-helm-deploy-action@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
cluster-name: budibase-eks-production
|
||||
config-files: values.production.yaml
|
||||
chart-path: charts/budibase
|
||||
namespace: budibase
|
||||
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
|
||||
name: budibase-prod
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
repository: budibase/budibase-deploys
|
||||
event: budicloud-prod-deploy
|
||||
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
|
|
@ -24,51 +24,18 @@ jobs:
|
|||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
||||
-H 'Accept: application/vnd.github.v3.raw' \
|
||||
-o values.preprod.yaml \
|
||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
|
||||
wc -l values.preprod.yaml
|
||||
- name: Deploy to Preprod Environment
|
||||
uses: budibase/helm@v1.8.0
|
||||
with:
|
||||
release: budibase-preprod
|
||||
namespace: budibase
|
||||
chart: charts/budibase
|
||||
token: ${{ github.token }}
|
||||
helm: helm3
|
||||
values: |
|
||||
globals:
|
||||
appVersion: v${{ env.RELEASE_VERSION }}
|
||||
ingress:
|
||||
enabled: true
|
||||
nginx: true
|
||||
value-files: >-
|
||||
[
|
||||
"values.preprod.yaml"
|
||||
]
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
repository: budibase/budibase-deploys
|
||||
event: budicloud-preprod-deploy
|
||||
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.7.7-alpha.5",
|
||||
"version": "2.7.21-alpha.2",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
|
@ -16,7 +16,6 @@
|
|||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
],
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
"publish": {
|
||||
"ignoreChanges": [
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "7.0.0-alpha.0",
|
||||
"lerna": "^7.0.1",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "^16.2.1",
|
||||
"nx": "^16.3.2",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
|
@ -343,6 +343,9 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const oneOf = (key: string, value: any) => {
|
||||
if (!value) {
|
||||
return `*:*`
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
if (typeof value === "string") {
|
||||
value = value.split(",")
|
||||
|
|
|
@ -114,6 +114,25 @@ describe("lucene", () => {
|
|||
expect(resp.rows.length).toBe(2)
|
||||
})
|
||||
|
||||
it("should return all rows when doing a one of search against falsey value", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.addOneOf("property", null)
|
||||
let resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", undefined)
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", "")
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", [])
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should be able to perform a contains search", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.addContains("property", ["word"])
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import crypto from "crypto"
|
||||
import fs from "fs"
|
||||
import zlib from "zlib"
|
||||
import env from "../environment"
|
||||
import { join } from "path"
|
||||
|
||||
const ALGO = "aes-256-ctr"
|
||||
const SEPARATOR = "-"
|
||||
const ITERATIONS = 10000
|
||||
const RANDOM_BYTES = 16
|
||||
const STRETCH_LENGTH = 32
|
||||
|
||||
const SALT_LENGTH = 16
|
||||
const IV_LENGTH = 16
|
||||
|
||||
export enum SecretOption {
|
||||
API = "api",
|
||||
ENCRYPTION = "encryption",
|
||||
|
@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string {
|
|||
return secret
|
||||
}
|
||||
|
||||
function stretchString(string: string, salt: Buffer) {
|
||||
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||
function stretchString(secret: string, salt: Buffer) {
|
||||
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||
}
|
||||
|
||||
export function encrypt(
|
||||
input: string,
|
||||
secretOption: SecretOption = SecretOption.API
|
||||
) {
|
||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||
const stretched = stretchString(getSecret(secretOption), salt)
|
||||
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||
const base = cipher.update(input)
|
||||
|
@ -60,3 +65,115 @@ export function decrypt(
|
|||
const final = decipher.final()
|
||||
return Buffer.concat([base, final]).toString()
|
||||
}
|
||||
|
||||
export async function encryptFile(
|
||||
{ dir, filename }: { dir: string; filename: string },
|
||||
secret: string
|
||||
) {
|
||||
const outputFileName = `${filename}.enc`
|
||||
|
||||
const filePath = join(dir, filename)
|
||||
const inputFile = fs.createReadStream(filePath)
|
||||
const outputFile = fs.createWriteStream(join(dir, outputFileName))
|
||||
|
||||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||
const iv = crypto.randomBytes(IV_LENGTH)
|
||||
const stretched = stretchString(secret, salt)
|
||||
const cipher = crypto.createCipheriv(ALGO, stretched, iv)
|
||||
|
||||
outputFile.write(salt)
|
||||
outputFile.write(iv)
|
||||
|
||||
inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile)
|
||||
|
||||
return new Promise<{ filename: string; dir: string }>(r => {
|
||||
outputFile.on("finish", () => {
|
||||
r({
|
||||
filename: outputFileName,
|
||||
dir,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function getSaltAndIV(path: string) {
|
||||
const fileStream = fs.createReadStream(path)
|
||||
|
||||
const salt = await readBytes(fileStream, SALT_LENGTH)
|
||||
const iv = await readBytes(fileStream, IV_LENGTH)
|
||||
fileStream.close()
|
||||
return { salt, iv }
|
||||
}
|
||||
|
||||
export async function decryptFile(
|
||||
inputPath: string,
|
||||
outputPath: string,
|
||||
secret: string
|
||||
) {
|
||||
const { salt, iv } = await getSaltAndIV(inputPath)
|
||||
const inputFile = fs.createReadStream(inputPath, {
|
||||
start: SALT_LENGTH + IV_LENGTH,
|
||||
})
|
||||
|
||||
const outputFile = fs.createWriteStream(outputPath)
|
||||
|
||||
const stretched = stretchString(secret, salt)
|
||||
const decipher = crypto.createDecipheriv(ALGO, stretched, iv)
|
||||
|
||||
const unzip = zlib.createGunzip()
|
||||
|
||||
inputFile.pipe(decipher).pipe(unzip).pipe(outputFile)
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
outputFile.on("finish", () => {
|
||||
outputFile.close()
|
||||
res()
|
||||
})
|
||||
|
||||
inputFile.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
decipher.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
unzip.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
outputFile.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function readBytes(stream: fs.ReadStream, length: number) {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
let bytesRead = 0
|
||||
const data: Buffer[] = []
|
||||
|
||||
stream.on("readable", () => {
|
||||
let chunk
|
||||
|
||||
while ((chunk = stream.read(length - bytesRead)) !== null) {
|
||||
data.push(chunk)
|
||||
bytesRead += chunk.length
|
||||
}
|
||||
|
||||
resolve(Buffer.concat(data))
|
||||
})
|
||||
|
||||
stream.on("end", () => {
|
||||
reject(new Error("Insufficient data in the stream."))
|
||||
})
|
||||
|
||||
stream.on("error", error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -140,9 +140,13 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
|||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||
* to check if the role inherits any others.
|
||||
* @param {string|null} roleId The level ID to lookup.
|
||||
* @param {object|null} opts options for the function, like whether to halt errors, instead return public.
|
||||
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
||||
*/
|
||||
export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
||||
export async function getRole(
|
||||
roleId?: string,
|
||||
opts?: { defaultPublic?: boolean }
|
||||
): Promise<RoleDoc | undefined> {
|
||||
if (!roleId) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -161,6 +165,9 @@ export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
|||
// finalise the ID
|
||||
role._id = getExternalRoleID(role._id)
|
||||
} catch (err) {
|
||||
if (!isBuiltin(roleId) && opts?.defaultPublic) {
|
||||
return cloneDeep(BUILTIN_ROLES.PUBLIC)
|
||||
}
|
||||
// only throw an error if there is no role at all
|
||||
if (Object.keys(role).length === 0) {
|
||||
throw err
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export let fixed = false
|
||||
export let inline = false
|
||||
export let disableCancel = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
@ -38,7 +39,7 @@
|
|||
}
|
||||
|
||||
export function cancel() {
|
||||
if (!visible) {
|
||||
if (!visible || disableCancel) {
|
||||
return
|
||||
}
|
||||
dispatch("cancel")
|
||||
|
|
|
@ -204,6 +204,12 @@
|
|||
})
|
||||
return columns
|
||||
.sort((a, b) => {
|
||||
if (a.divider) {
|
||||
return a
|
||||
}
|
||||
if (b.divider) {
|
||||
return b
|
||||
}
|
||||
const orderA = a.order || Number.MAX_SAFE_INTEGER
|
||||
const orderB = b.order || Number.MAX_SAFE_INTEGER
|
||||
const nameA = getDisplayName(a)
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
<meta charset='utf8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<title>Budibase</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.11.2",
|
||||
"@fontsource/source-sans-pro": "^5.0.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
@ -122,6 +123,7 @@
|
|||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "4.7.3",
|
||||
"vite": "^3.0.8",
|
||||
"vite-plugin-static-copy": "^0.16.0",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"nx": {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 787 B |
|
@ -12,7 +12,7 @@
|
|||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
|
||||
export let blockIdx
|
||||
|
@ -149,7 +149,7 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled && !syncAutomationsEnabled}
|
||||
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
|
|
|
@ -76,6 +76,10 @@ export function getBindings({
|
|||
// will be replaced by the main array binding
|
||||
readableBinding: label,
|
||||
runtimeBinding: binding,
|
||||
display: {
|
||||
name: label,
|
||||
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
return bindings
|
||||
|
|
|
@ -57,6 +57,12 @@
|
|||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { store } from "builderStore"
|
||||
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
import { admin } from "stores/portal"
|
||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||
|
@ -339,25 +339,28 @@
|
|||
</Tab>
|
||||
{/if}
|
||||
<div class="drawer-actions">
|
||||
<Button
|
||||
secondary
|
||||
quiet
|
||||
on:click={() => {
|
||||
store.actions.settings.propertyFocus(null)
|
||||
drawerActions.hide()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
cta
|
||||
disabled={!valid}
|
||||
on:click={() => {
|
||||
bindingDrawerActions.save()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{#if drawerActions?.hide}
|
||||
<Button
|
||||
secondary
|
||||
quiet
|
||||
on:click={() => {
|
||||
drawerActions.hide()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
{#if bindingDrawerActions?.save}
|
||||
<Button
|
||||
cta
|
||||
disabled={!valid}
|
||||
on:click={() => {
|
||||
bindingDrawerActions.save()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
.map(([name, categoryBindings]) => ({
|
||||
name,
|
||||
bindings: categoryBindings?.filter(binding => {
|
||||
return binding.readableBinding.match(searchRgx)
|
||||
return !search || binding.readableBinding.match(searchRgx)
|
||||
}),
|
||||
}))
|
||||
.filter(category => {
|
||||
|
@ -46,7 +46,11 @@
|
|||
)
|
||||
})
|
||||
$: filteredHelpers = helpers?.filter(helper => {
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||
return (
|
||||
!search ||
|
||||
helper.label.match(searchRgx) ||
|
||||
helper.description.match(searchRgx)
|
||||
)
|
||||
})
|
||||
|
||||
const getHelperExample = (helper, js) => {
|
||||
|
@ -124,9 +128,6 @@
|
|||
<span
|
||||
class="search-input-icon"
|
||||
on:click={() => {
|
||||
if (!search) {
|
||||
return
|
||||
}
|
||||
search = null
|
||||
}}
|
||||
class:searching={search}
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={bindingDrawer} {title}>
|
||||
<Drawer bind:this={bindingDrawer} {title} headless>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
import { store } from "builderStore"
|
||||
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { createEventDispatcher, setContext } from "svelte"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
@ -36,7 +34,6 @@
|
|||
|
||||
const saveBinding = () => {
|
||||
onChange(tempValue)
|
||||
store.actions.settings.propertyFocus(null)
|
||||
onBlur()
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
@ -70,7 +67,6 @@
|
|||
<div
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
store.actions.settings.propertyFocus(key)
|
||||
bindingDrawer.show()
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -73,10 +73,6 @@
|
|||
if (highlighted) {
|
||||
store.actions.settings.highlight(null)
|
||||
}
|
||||
// To fix focus 'affect' when property is target of a drawer other actions in the builder.
|
||||
if (propertyFocus) {
|
||||
store.actions.settings.propertyFocus(null)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -186,7 +186,6 @@
|
|||
}
|
||||
|
||||
div :global(.CodeMirror) {
|
||||
width: var(--code-mirror-width) !important;
|
||||
height: var(--code-mirror-height) !important;
|
||||
border-radius: var(--border-radius-s);
|
||||
font-family: var(--font-mono);
|
||||
|
|
|
@ -1,27 +1,128 @@
|
|||
<script>
|
||||
import { ModalContent, Toggle, Body, InlineAlert } from "@budibase/bbui"
|
||||
import {
|
||||
ModalContent,
|
||||
Toggle,
|
||||
Body,
|
||||
InlineAlert,
|
||||
Input,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
|
||||
export let app
|
||||
export let published
|
||||
let excludeRows = false
|
||||
let includeInternalTablesRows = true
|
||||
let encypt = true
|
||||
|
||||
$: title = published ? "Export published app" : "Export latest app"
|
||||
$: confirmText = published ? "Export published" : "Export latest"
|
||||
let password = null
|
||||
const validation = createValidationStore()
|
||||
validation.addValidatorType("password", "password", true)
|
||||
$: validation.observe("password", password)
|
||||
|
||||
const exportApp = () => {
|
||||
const Step = { CONFIG: "config", SET_PASSWORD: "set_password" }
|
||||
let currentStep = Step.CONFIG
|
||||
|
||||
$: exportButtonText = published ? "Export published" : "Export latest"
|
||||
$: stepConfig = {
|
||||
[Step.CONFIG]: {
|
||||
title: published ? "Export published app" : "Export latest app",
|
||||
confirmText: encypt ? "Continue" : exportButtonText,
|
||||
onConfirm: () => {
|
||||
if (!encypt) {
|
||||
exportApp()
|
||||
} else {
|
||||
currentStep = Step.SET_PASSWORD
|
||||
return false
|
||||
}
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
[Step.SET_PASSWORD]: {
|
||||
title: "Add password to encrypt your export",
|
||||
confirmText: exportButtonText,
|
||||
onConfirm: async () => {
|
||||
await validation.check({ password })
|
||||
if (!$validation.valid) {
|
||||
return false
|
||||
}
|
||||
exportApp(password)
|
||||
},
|
||||
isValid: $validation.valid,
|
||||
},
|
||||
}
|
||||
|
||||
const exportApp = async () => {
|
||||
const id = published ? app.prodId : app.devId
|
||||
const appName = encodeURIComponent(app.name)
|
||||
window.location = `/api/backups/export?appId=${id}&appname=${appName}&excludeRows=${excludeRows}`
|
||||
const url = `/api/backups/export?appId=${id}`
|
||||
await downloadFile(url, {
|
||||
excludeRows: !includeInternalTablesRows,
|
||||
encryptPassword: password,
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadFile(url, body) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const contentDisposition = response.headers.get("Content-Disposition")
|
||||
|
||||
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(
|
||||
contentDisposition
|
||||
)
|
||||
|
||||
const filename = matches[1].replace(/['"]/g, "")
|
||||
|
||||
const url = URL.createObjectURL(await response.blob())
|
||||
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
} else {
|
||||
notifications.error("Error exporting the app.")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(error.message || "Error downloading the exported app")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
||||
<InlineAlert
|
||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
||||
/>
|
||||
<Body
|
||||
>Apps can be exported with or without data that is within internal tables -
|
||||
select this below.</Body
|
||||
>
|
||||
<Toggle text="Exclude Rows" bind:value={excludeRows} />
|
||||
<ModalContent
|
||||
title={stepConfig[currentStep].title}
|
||||
confirmText={stepConfig[currentStep].confirmText}
|
||||
onConfirm={stepConfig[currentStep].onConfirm}
|
||||
disabled={!stepConfig[currentStep].isValid}
|
||||
>
|
||||
{#if currentStep === Step.CONFIG}
|
||||
<Body>
|
||||
<Toggle
|
||||
text="Export rows from internal tables"
|
||||
bind:value={includeInternalTablesRows}
|
||||
/>
|
||||
<Toggle text="Encrypt my export" bind:value={encypt} />
|
||||
</Body>
|
||||
{#if !encypt}
|
||||
<InlineAlert
|
||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if currentStep === Step.SET_PASSWORD}
|
||||
<Input
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder="Type here..."
|
||||
bind:value={password}
|
||||
error={$validation.errors.password}
|
||||
/>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
|
|
@ -6,7 +6,6 @@ export function createValidationStore(initialValue, ...validators) {
|
|||
let touched = false
|
||||
|
||||
const value = writable(initialValue || "")
|
||||
const error = derived(value, $v => validate($v, validators))
|
||||
const touchedStore = derived(value, () => {
|
||||
if (!touched) {
|
||||
touched = true
|
||||
|
@ -14,6 +13,10 @@ export function createValidationStore(initialValue, ...validators) {
|
|||
}
|
||||
return touched
|
||||
})
|
||||
const error = derived(
|
||||
[value, touchedStore],
|
||||
([$v, $t]) => $t && validate($v, validators)
|
||||
)
|
||||
|
||||
return [value, error, touchedStore]
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { notifications } from "@budibase/bbui"
|
|||
|
||||
export const createValidationStore = () => {
|
||||
const DEFAULT = {
|
||||
values: {},
|
||||
errors: {},
|
||||
touched: {},
|
||||
valid: false,
|
||||
|
@ -33,6 +34,9 @@ export const createValidationStore = () => {
|
|||
case "email":
|
||||
propertyValidator = string().email().nullable()
|
||||
break
|
||||
case "password":
|
||||
propertyValidator = string().nullable()
|
||||
break
|
||||
default:
|
||||
propertyValidator = string().nullable()
|
||||
}
|
||||
|
@ -41,9 +45,68 @@ export const createValidationStore = () => {
|
|||
propertyValidator = propertyValidator.required()
|
||||
}
|
||||
|
||||
// We want to do this after the possible required validation, to prioritise the required error
|
||||
switch (type) {
|
||||
case "password":
|
||||
propertyValidator = propertyValidator.min(8)
|
||||
break
|
||||
}
|
||||
|
||||
validator[propertyName] = propertyValidator
|
||||
}
|
||||
|
||||
const observe = async (propertyName, value) => {
|
||||
const values = get(validation).values
|
||||
let fieldIsValid
|
||||
if (!values.hasOwnProperty(propertyName)) {
|
||||
// Initial setup
|
||||
values[propertyName] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (value === values[propertyName]) {
|
||||
return
|
||||
}
|
||||
|
||||
const obj = object().shape(validator)
|
||||
try {
|
||||
validation.update(store => {
|
||||
store.errors[propertyName] = null
|
||||
return store
|
||||
})
|
||||
await obj.validateAt(propertyName, { [propertyName]: value })
|
||||
fieldIsValid = true
|
||||
} catch (error) {
|
||||
const [fieldError] = error.errors
|
||||
if (fieldError) {
|
||||
validation.update(store => {
|
||||
store.errors[propertyName] = capitalise(fieldError)
|
||||
store.valid = false
|
||||
return store
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldIsValid) {
|
||||
// Validate the rest of the fields
|
||||
try {
|
||||
await obj.validate(
|
||||
{ ...values, [propertyName]: value },
|
||||
{ abortEarly: false }
|
||||
)
|
||||
validation.update(store => {
|
||||
store.valid = true
|
||||
return store
|
||||
})
|
||||
} catch {
|
||||
validation.update(store => {
|
||||
store.valid = false
|
||||
return store
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const check = async values => {
|
||||
const obj = object().shape(validator)
|
||||
// clear the previous errors
|
||||
|
@ -87,5 +150,6 @@ export const createValidationStore = () => {
|
|||
check,
|
||||
addValidator,
|
||||
addValidatorType,
|
||||
observe,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
$: platformTitle =
|
||||
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
|
||||
|
||||
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
|
||||
$: faviconUrl = $organisation.faviconUrl || "/builder/bblogo.png"
|
||||
|
||||
onMount(async () => {
|
||||
await organisation.init()
|
||||
|
@ -27,6 +27,6 @@
|
|||
<link rel="icon" href={faviconUrl} />
|
||||
{:else}
|
||||
<!-- A default must be set or the browser defaults to favicon.ico behaviour -->
|
||||
<link rel="icon" href={"https://i.imgur.com/Xhdt1YP.png"} />
|
||||
<link rel="icon" href={"/builder/bblogo.png"} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
import { API } from "api"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
|
||||
const querySchema = {
|
||||
name: {},
|
||||
|
@ -33,6 +34,7 @@
|
|||
let isValid = true
|
||||
let integration, baseDatasource, datasource
|
||||
let queryList
|
||||
let loading = false
|
||||
|
||||
$: baseDatasource = $datasources.selected
|
||||
$: queryList = $queries.list.filter(
|
||||
|
@ -65,9 +67,11 @@
|
|||
}
|
||||
|
||||
const saveDatasource = async () => {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
loading = true
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
loading = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +86,8 @@
|
|||
baseDatasource = cloneDeep(datasource)
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving datasource: ${err}`)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,8 +125,17 @@
|
|||
<Divider />
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}>
|
||||
Save
|
||||
<Button
|
||||
disabled={!changed || !isValid || loading}
|
||||
cta
|
||||
on:click={saveDatasource}
|
||||
>
|
||||
<div class="save-button-content">
|
||||
{#if loading}
|
||||
<Spinner size="10">Save</Spinner>
|
||||
{/if}
|
||||
Save
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<IntegrationConfigForm
|
||||
|
@ -216,4 +231,10 @@
|
|||
flex-direction: column;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.save-button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<div tabindex="-1" class="exampleApp">
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<img alt="Budibase Logo" src={"https://i.imgur.com/Xhdt1YP.png"} />
|
||||
<img alt="Budibase Logo" src={"/builder/bblogo.png"} />
|
||||
<h1>{name}</h1>
|
||||
</div>
|
||||
<div class="nav">Home</div>
|
||||
|
|
|
@ -373,7 +373,7 @@
|
|||
<OnboardingTypeModal {chooseCreationType} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={passwordModal}>
|
||||
<Modal bind:this={passwordModal} disableCancel={true}>
|
||||
<PasswordModal
|
||||
createUsersResponse={bulkSaveResponse}
|
||||
userData={userData.users}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import replace from "@rollup/plugin-replace"
|
||||
import { defineConfig, loadEnv } from "vite"
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy"
|
||||
import path from "path"
|
||||
|
||||
const ignoredWarnings = [
|
||||
|
@ -59,6 +60,18 @@ export default defineConfig(({ mode }) => {
|
|||
),
|
||||
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
|
||||
}),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: "../../node_modules/@fontsource/source-sans-pro",
|
||||
dest: "fonts",
|
||||
},
|
||||
{
|
||||
src: "../../node_modules/remixicon/fonts/*",
|
||||
dest: "fonts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: ["@roxi/routify"],
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"pouchdb": "7.3.0",
|
||||
"pouchdb-replication-stream": "1.2.9",
|
||||
"randomstring": "1.1.5",
|
||||
"tar": "6.1.11",
|
||||
"tar": "6.1.15",
|
||||
"yaml": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -180,10 +180,7 @@
|
|||
{/if}
|
||||
<div class="logo">
|
||||
{#if !hideLogo}
|
||||
<img
|
||||
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"}
|
||||
alt={title}
|
||||
/>
|
||||
<img src={logoUrl || "/builder/bblogo.png"} alt={title} />
|
||||
{/if}
|
||||
{#if !hideTitle && title}
|
||||
<Heading size="S">{title}</Heading>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<img
|
||||
class="logo"
|
||||
alt="logo"
|
||||
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"}
|
||||
src={logoUrl || "/builder/bblogo.png"}
|
||||
height="48"
|
||||
/>
|
||||
</a>
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
import { authStore } from "../stores/auth.js"
|
||||
import { appStore } from "../stores/app.js"
|
||||
import { get } from "svelte/store"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
const getLicense = () => {
|
||||
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 = () => {
|
||||
const license = getLicense()
|
||||
if (license) {
|
||||
return license.plan.type === Constants.PlanType.FREE
|
||||
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
|
||||
|
|
|
@ -162,9 +162,9 @@
|
|||
class:floating={offset > 0}
|
||||
style="--offset:{offset}px; --sticky-width:{width}px;"
|
||||
>
|
||||
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
|
||||
<div class="underlay" transition:fade={{ duration: 130 }} />
|
||||
<div class="sticky-column" transition:fade={{ duration: 130 }}>
|
||||
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||
<GutterCell on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
{#if isAdding}
|
||||
|
@ -193,7 +193,7 @@
|
|||
</DataCell>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="normal-columns" transition:fade={{ duration: 130 }}>
|
||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
||||
<div class="row">
|
||||
{#each $renderedColumns as column, columnIdx}
|
||||
|
@ -223,7 +223,7 @@
|
|||
</div>
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
<div class="buttons" transition:fade={{ duration: 130 }}>
|
||||
<div class="buttons" transition:fade|local={{ duration: 130 }}>
|
||||
<Button size="M" cta on:click={addRow} disabled={isAdding}>
|
||||
<div class="button-with-keys">
|
||||
Save
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 01fbc8670021c5a275c2a1a36ee18b984eeafad5
|
||||
Subproject commit f4b8449aac9bd265214396afbdce7ff984a2ae34
|
|
@ -117,7 +117,7 @@
|
|||
"socket.io": "4.6.1",
|
||||
"svelte": "3.49.0",
|
||||
"swagger-parser": "10.0.3",
|
||||
"tar": "6.1.11",
|
||||
"tar": "6.1.15",
|
||||
"to-json-schema": "0.2.5",
|
||||
"uuid": "3.3.2",
|
||||
"validate.js": "0.13.1",
|
||||
|
@ -150,7 +150,7 @@
|
|||
"@types/redis": "4.0.11",
|
||||
"@types/server-destroy": "1.0.1",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tar": "6.1.5",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"apidoc": "0.50.4",
|
||||
"babel-jest": "29.5.0",
|
||||
|
|
|
@ -1,53 +1,54 @@
|
|||
import env from "../../environment"
|
||||
import {
|
||||
createAllSearchIndex,
|
||||
createLinkView,
|
||||
createRoutingView,
|
||||
createAllSearchIndex,
|
||||
} from "../../db/views/staticViews"
|
||||
import { createApp, deleteApp } from "../../utilities/fileSystem"
|
||||
import {
|
||||
backupClientLibrary,
|
||||
createApp,
|
||||
deleteApp,
|
||||
revertClientLibrary,
|
||||
updateClientLibrary,
|
||||
} from "../../utilities/fileSystem"
|
||||
import {
|
||||
AppStatus,
|
||||
DocumentType,
|
||||
generateAppID,
|
||||
generateDevAppID,
|
||||
getLayoutParams,
|
||||
getScreenParams,
|
||||
generateDevAppID,
|
||||
DocumentType,
|
||||
AppStatus,
|
||||
} from "../../db/utils"
|
||||
import {
|
||||
db as dbCore,
|
||||
roles,
|
||||
cache,
|
||||
tenancy,
|
||||
context,
|
||||
db as dbCore,
|
||||
env as envCore,
|
||||
ErrorCode,
|
||||
events,
|
||||
migrations,
|
||||
objectStore,
|
||||
ErrorCode,
|
||||
env as envCore,
|
||||
roles,
|
||||
tenancy,
|
||||
} from "@budibase/backend-core"
|
||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||
import {
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
buildDefaultDocs,
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
} from "../../db/defaultData/datasource_bb_default"
|
||||
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
||||
import { stringToReadStream, isQsTrue } from "../../utilities"
|
||||
import { getLocksById, doesUserHaveLock } from "../../utilities/redis"
|
||||
import {
|
||||
updateClientLibrary,
|
||||
backupClientLibrary,
|
||||
revertClientLibrary,
|
||||
} from "../../utilities/fileSystem"
|
||||
import { stringToReadStream } from "../../utilities"
|
||||
import { doesUserHaveLock, getLocksById } from "../../utilities/redis"
|
||||
import { cleanupAutomations } from "../../automations/utils"
|
||||
import { checkAppMetadata } from "../../automations/logging"
|
||||
import { getUniqueRows } from "../../utilities/usageQuota/rows"
|
||||
import { quotas, groups } from "@budibase/pro"
|
||||
import { groups, licensing, quotas } from "@budibase/pro"
|
||||
import {
|
||||
App,
|
||||
Layout,
|
||||
Screen,
|
||||
MigrationType,
|
||||
Database,
|
||||
PlanType,
|
||||
Screen,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
|
@ -207,6 +208,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
|||
let application = await db.get(DocumentType.APP_METADATA)
|
||||
const layouts = await getLayouts()
|
||||
let screens = await getScreens()
|
||||
const license = await licensing.cache.getCachedLicense()
|
||||
|
||||
// Enrich plugin URLs
|
||||
application.usedPlugins = objectStore.enrichPluginURLs(
|
||||
|
@ -227,6 +229,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
|||
|
||||
ctx.body = {
|
||||
application: { ...application, upgradableVersion: envCore.VERSION },
|
||||
licenseType: license?.plan.type || PlanType.FREE,
|
||||
screens,
|
||||
layouts,
|
||||
clientLibPath,
|
||||
|
|
|
@ -1,17 +1,31 @@
|
|||
import sdk from "../../sdk"
|
||||
import { events, context } from "@budibase/backend-core"
|
||||
import { events, context, db } from "@budibase/backend-core"
|
||||
import { DocumentType } from "../../db/utils"
|
||||
import { isQsTrue } from "../../utilities"
|
||||
import { Ctx } from "@budibase/types"
|
||||
|
||||
interface ExportAppDumpRequest {
|
||||
excludeRows: boolean
|
||||
encryptPassword?: string
|
||||
}
|
||||
|
||||
export async function exportAppDump(ctx: Ctx<ExportAppDumpRequest>) {
|
||||
const { appId } = ctx.query as any
|
||||
const { excludeRows, encryptPassword } = ctx.request.body
|
||||
|
||||
const [app] = await db.getAppsByIDs([appId])
|
||||
const appName = app.name
|
||||
|
||||
export async function exportAppDump(ctx: any) {
|
||||
let { appId, excludeRows } = ctx.query
|
||||
// remove the 120 second limit for the request
|
||||
ctx.req.setTimeout(0)
|
||||
const appName = decodeURI(ctx.query.appname)
|
||||
excludeRows = isQsTrue(excludeRows)
|
||||
const backupIdentifier = `${appName}-export-${new Date().getTime()}.tar.gz`
|
||||
|
||||
const extension = encryptPassword ? "enc.tar.gz" : "tar.gz"
|
||||
const backupIdentifier = `${appName}-export-${new Date().getTime()}.${extension}`
|
||||
ctx.attachment(backupIdentifier)
|
||||
ctx.body = await sdk.backups.streamExportApp(appId, excludeRows)
|
||||
ctx.body = await sdk.backups.streamExportApp({
|
||||
appId,
|
||||
excludeRows,
|
||||
encryptPassword,
|
||||
})
|
||||
|
||||
await context.doInAppContext(appId, async () => {
|
||||
const appDb = context.getAppDB()
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
getUserMetadataParams,
|
||||
InternalTables,
|
||||
} from "../../db/utils"
|
||||
import { BBContext, Database } from "@budibase/types"
|
||||
import { UserCtx, Database } from "@budibase/types"
|
||||
|
||||
const UpdateRolesOptions = {
|
||||
CREATED: "created",
|
||||
|
@ -38,15 +38,15 @@ async function updateRolesOnUserTable(
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetch(ctx: BBContext) {
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
ctx.body = await roles.getAllRoles()
|
||||
}
|
||||
|
||||
export async function find(ctx: BBContext) {
|
||||
export async function find(ctx: UserCtx) {
|
||||
ctx.body = await roles.getRole(ctx.params.roleId)
|
||||
}
|
||||
|
||||
export async function save(ctx: BBContext) {
|
||||
export async function save(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
let { _id, name, inherits, permissionId } = ctx.request.body
|
||||
let isCreate = false
|
||||
|
@ -72,7 +72,7 @@ export async function save(ctx: BBContext) {
|
|||
ctx.message = `Role '${role.name}' created successfully.`
|
||||
}
|
||||
|
||||
export async function destroy(ctx: BBContext) {
|
||||
export async function destroy(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const roleId = ctx.params.roleId
|
||||
const role = await db.get(roleId)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getRoutingInfo } from "../../utilities/routing"
|
||||
import { roles } from "@budibase/backend-core"
|
||||
import { BBContext } from "@budibase/types"
|
||||
import { UserCtx } from "@budibase/types"
|
||||
|
||||
const URL_SEPARATOR = "/"
|
||||
|
||||
|
@ -56,11 +56,11 @@ async function getRoutingStructure() {
|
|||
return { routes: routing.json }
|
||||
}
|
||||
|
||||
export async function fetch(ctx: BBContext) {
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
ctx.body = await getRoutingStructure()
|
||||
}
|
||||
|
||||
export async function clientFetch(ctx: BBContext) {
|
||||
export async function clientFetch(ctx: UserCtx) {
|
||||
const routing = await getRoutingStructure()
|
||||
let roleId = ctx.user?.role?._id
|
||||
const roleIds = (await roles.getUserRoleHierarchy(roleId, {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
breakRowIdField,
|
||||
convertRowId,
|
||||
generateRowIdField,
|
||||
getPrimaryDisplay,
|
||||
isRowId,
|
||||
isSQL,
|
||||
} from "../../../integrations/utils"
|
||||
|
@ -391,7 +392,10 @@ export class ExternalRequest {
|
|||
}
|
||||
}
|
||||
relatedRow = processFormulas(linkedTable, relatedRow)
|
||||
const relatedDisplay = display ? relatedRow[display] : undefined
|
||||
let relatedDisplay
|
||||
if (display) {
|
||||
relatedDisplay = getPrimaryDisplay(relatedRow[display])
|
||||
}
|
||||
row[relationship.column][key] = {
|
||||
primaryDisplay: relatedDisplay || "Invalid display column",
|
||||
_id: relatedRow._id,
|
||||
|
|
|
@ -237,9 +237,15 @@ export async function exportRows(ctx: UserCtx) {
|
|||
ctx.request.body = {
|
||||
query: {
|
||||
oneOf: {
|
||||
_id: ctx.request.body.rows.map(
|
||||
(row: string) => JSON.parse(decodeURI(row))[0]
|
||||
),
|
||||
_id: ctx.request.body.rows.map((row: string) => {
|
||||
const ids = JSON.parse(
|
||||
decodeURI(row).replace(/'/g, `"`).replace(/%2C/g, ",")
|
||||
)
|
||||
if (ids.length > 1) {
|
||||
ctx.throw(400, "Export data does not support composite keys.")
|
||||
}
|
||||
return ids[0]
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -40,19 +40,14 @@
|
|||
{#if favicon !== ""}
|
||||
<link rel="icon" type="image/png" href={favicon} />
|
||||
{:else}
|
||||
<link rel="icon" type="image/png" href="https://i.imgur.com/Xhdt1YP.png" />
|
||||
<link rel="icon" type="image/png" href="/builder/bblogo.png" />
|
||||
{/if}
|
||||
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>Budibase Builder Preview</title>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { exportRows } from "../row/external"
|
||||
import sdk from "../../../sdk"
|
||||
import { ExternalRequest } from "../row/ExternalRequest"
|
||||
|
||||
// @ts-ignore
|
||||
sdk.datasources = {
|
||||
get: jest.fn(),
|
||||
}
|
||||
|
||||
jest.mock("../row/ExternalRequest")
|
||||
jest.mock("../view/exporters", () => ({
|
||||
csv: jest.fn(),
|
||||
Format: {
|
||||
CSV: "csv",
|
||||
},
|
||||
}))
|
||||
jest.mock("../../../utilities/fileSystem")
|
||||
|
||||
function getUserCtx() {
|
||||
return {
|
||||
params: {
|
||||
tableId: "datasource__tablename",
|
||||
},
|
||||
query: {
|
||||
format: "csv",
|
||||
},
|
||||
request: {
|
||||
body: {},
|
||||
},
|
||||
throw: jest.fn(() => {
|
||||
throw "Err"
|
||||
}),
|
||||
attachment: jest.fn(),
|
||||
}
|
||||
}
|
||||
|
||||
describe("external row controller", () => {
|
||||
describe("exportRows", () => {
|
||||
beforeAll(() => {
|
||||
//@ts-ignore
|
||||
jest.spyOn(ExternalRequest.prototype, "run").mockImplementation(() => [])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should throw a 400 if no datasource entities are present", async () => {
|
||||
let userCtx = getUserCtx()
|
||||
try {
|
||||
//@ts-ignore
|
||||
await exportRows(userCtx)
|
||||
} catch (e) {
|
||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
||||
400,
|
||||
"Datasource has not been configured for plus API."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle single quotes from a row ID", async () => {
|
||||
//@ts-ignore
|
||||
sdk.datasources.get.mockImplementation(() => ({
|
||||
entities: {
|
||||
tablename: {
|
||||
schema: {},
|
||||
},
|
||||
},
|
||||
}))
|
||||
let userCtx = getUserCtx()
|
||||
userCtx.request.body = {
|
||||
rows: ["['d001']"],
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
await exportRows(userCtx)
|
||||
|
||||
expect(userCtx.request.body).toEqual({
|
||||
query: {
|
||||
oneOf: {
|
||||
_id: ["d001"],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw a 400 if any composite keys are present", async () => {
|
||||
let userCtx = getUserCtx()
|
||||
userCtx.request.body = {
|
||||
rows: ["[123]", "['d001'%2C'10111']"],
|
||||
}
|
||||
try {
|
||||
//@ts-ignore
|
||||
await exportRows(userCtx)
|
||||
} catch (e) {
|
||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
||||
400,
|
||||
"Export data does not support composite keys."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw a 400 if no table name was found", async () => {
|
||||
let userCtx = getUserCtx()
|
||||
userCtx.params.tableId = "datasource__"
|
||||
userCtx.request.body = {
|
||||
rows: ["[123]"],
|
||||
}
|
||||
try {
|
||||
//@ts-ignore
|
||||
await exportRows(userCtx)
|
||||
} catch (e) {
|
||||
expect(userCtx.throw).toHaveBeenCalledWith(
|
||||
400,
|
||||
"Could not find table name."
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -5,7 +5,7 @@ import { permissions } from "@budibase/backend-core"
|
|||
|
||||
const router: Router = new Router()
|
||||
|
||||
router.get(
|
||||
router.post(
|
||||
"/api/backups/export",
|
||||
authorized(permissions.BUILDER),
|
||||
controller.exportAppDump
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import tk from "timekeeper"
|
||||
import * as setup from "./utilities"
|
||||
import { events } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
|
||||
describe("/backups", () => {
|
||||
let request = setup.getRequest()
|
||||
|
@ -16,7 +18,7 @@ describe("/backups", () => {
|
|||
describe("exportAppDump", () => {
|
||||
it("should be able to export app", async () => {
|
||||
const res = await request
|
||||
.get(`/api/backups/export?appId=${config.getAppId()}&appname=test`)
|
||||
.post(`/api/backups/export?appId=${config.getAppId()}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(200)
|
||||
expect(res.headers["content-type"]).toEqual("application/gzip")
|
||||
|
@ -26,10 +28,24 @@ describe("/backups", () => {
|
|||
it("should apply authorization to endpoint", async () => {
|
||||
await checkBuilderEndpoint({
|
||||
config,
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
url: `/api/backups/export?appId=${config.getAppId()}`,
|
||||
})
|
||||
})
|
||||
|
||||
it("should infer the app name from the app", async () => {
|
||||
tk.freeze(mocks.date.MOCK_DATE)
|
||||
|
||||
const res = await request
|
||||
.post(`/api/backups/export?appId=${config.getAppId()}`)
|
||||
.set(config.defaultHeaders())
|
||||
|
||||
expect(res.headers["content-disposition"]).toEqual(
|
||||
`attachment; filename="${
|
||||
config.getApp()!.name
|
||||
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateBackupStats", () => {
|
||||
|
|
|
@ -91,7 +91,7 @@ const SCHEMA: Integration = {
|
|||
},
|
||||
}
|
||||
|
||||
function bindingTypeCoerce(bindings: any[]) {
|
||||
export function bindingTypeCoerce(bindings: any[]) {
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i]
|
||||
if (typeof binding !== "string") {
|
||||
|
@ -109,7 +109,12 @@ function bindingTypeCoerce(bindings: any[]) {
|
|||
dayjs(binding).isValid() &&
|
||||
!binding.includes(",")
|
||||
) {
|
||||
bindings[i] = dayjs(binding).toDate()
|
||||
let value: any
|
||||
value = new Date(binding)
|
||||
if (isNaN(value)) {
|
||||
value = binding
|
||||
}
|
||||
bindings[i] = value
|
||||
}
|
||||
}
|
||||
return bindings
|
||||
|
|
|
@ -20,7 +20,7 @@ import Sql from "./base/sql"
|
|||
import { PostgresColumn } from "./base/types"
|
||||
import { escapeDangerousCharacters } from "../utilities"
|
||||
|
||||
import { Client, types } from "pg"
|
||||
import { Client, ClientConfig, types } from "pg"
|
||||
|
||||
// Return "date" and "timestamp" types as plain strings.
|
||||
// This lets us reference the original stored timezone.
|
||||
|
@ -42,6 +42,8 @@ interface PostgresConfig {
|
|||
schema: string
|
||||
ssl?: boolean
|
||||
ca?: string
|
||||
clientKey?: string
|
||||
clientCert?: string
|
||||
rejectUnauthorized?: boolean
|
||||
}
|
||||
|
||||
|
@ -98,6 +100,19 @@ const SCHEMA: Integration = {
|
|||
required: false,
|
||||
},
|
||||
ca: {
|
||||
display: "Server CA",
|
||||
type: DatasourceFieldType.LONGFORM,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
clientKey: {
|
||||
display: "Client key",
|
||||
type: DatasourceFieldType.LONGFORM,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
clientCert: {
|
||||
display: "Client cert",
|
||||
type: DatasourceFieldType.LONGFORM,
|
||||
default: false,
|
||||
required: false,
|
||||
|
@ -144,12 +159,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
|||
super(SqlClient.POSTGRES)
|
||||
this.config = config
|
||||
|
||||
let newConfig = {
|
||||
let newConfig: ClientConfig = {
|
||||
...this.config,
|
||||
ssl: this.config.ssl
|
||||
? {
|
||||
rejectUnauthorized: this.config.rejectUnauthorized,
|
||||
ca: this.config.ca,
|
||||
key: this.config.clientKey,
|
||||
cert: this.config.clientCert,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { default as MySQLIntegration } from "../mysql"
|
||||
import { default as MySQLIntegration, bindingTypeCoerce } from "../mysql"
|
||||
jest.mock("mysql2")
|
||||
|
||||
class TestConfiguration {
|
||||
|
@ -131,3 +131,21 @@ describe("MySQL Integration", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("bindingTypeCoercion", () => {
|
||||
it("shouldn't coerce something that looks like a date", () => {
|
||||
const response = bindingTypeCoerce(["202205-1500"])
|
||||
expect(response[0]).toBe("202205-1500")
|
||||
})
|
||||
|
||||
it("should coerce an actual date", () => {
|
||||
const date = new Date("2023-06-13T14:24:22.620Z")
|
||||
const response = bindingTypeCoerce(["2023-06-13T14:24:22.620Z"])
|
||||
expect(response[0]).toEqual(date)
|
||||
})
|
||||
|
||||
it("should coerce numbers", () => {
|
||||
const response = bindingTypeCoerce(["0"])
|
||||
expect(response[0]).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -328,3 +328,27 @@ export function finaliseExternalTables(
|
|||
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {})
|
||||
return { tables: finalTables, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided input is an object, but specifically not a date type object.
|
||||
* Used during coercion of types and relationship handling, dates are considered valid
|
||||
* and can be used as a display field, but objects and arrays cannot.
|
||||
* @param testValue an unknown type which this function will attempt to extract
|
||||
* a valid primary display string from.
|
||||
*/
|
||||
export function getPrimaryDisplay(testValue: unknown): string | undefined {
|
||||
if (testValue instanceof Date) {
|
||||
return testValue.toISOString()
|
||||
}
|
||||
if (
|
||||
Array.isArray(testValue) &&
|
||||
testValue[0] &&
|
||||
typeof testValue[0] !== "object"
|
||||
) {
|
||||
return testValue.join(", ")
|
||||
}
|
||||
if (typeof testValue === "object") {
|
||||
return undefined
|
||||
}
|
||||
return testValue as string
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export default async (ctx: UserCtx, next: any) => {
|
|||
userId,
|
||||
globalId,
|
||||
roleId,
|
||||
role: await roles.getRole(roleId),
|
||||
role: await roles.getRole(roleId, { defaultPublic: true }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { db as dbCore, objectStore } from "@budibase/backend-core"
|
||||
import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
|
||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
||||
import { ObjectStoreBuckets } from "../../../constants"
|
||||
|
@ -18,7 +18,8 @@ import { join } from "path"
|
|||
import env from "../../../environment"
|
||||
|
||||
const uuid = require("uuid/v4")
|
||||
const tar = require("tar")
|
||||
import tar from "tar"
|
||||
|
||||
const MemoryStream = require("memorystream")
|
||||
|
||||
interface DBDumpOpts {
|
||||
|
@ -30,16 +31,18 @@ interface ExportOpts extends DBDumpOpts {
|
|||
tar?: boolean
|
||||
excludeRows?: boolean
|
||||
excludeLogs?: boolean
|
||||
encryptPassword?: string
|
||||
}
|
||||
|
||||
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
||||
const exportFile = join(budibaseTempDir(), `${uuid()}.tar.gz`)
|
||||
const fileName = `${uuid()}.tar.gz`
|
||||
const exportFile = join(budibaseTempDir(), fileName)
|
||||
tar.create(
|
||||
{
|
||||
sync: true,
|
||||
gzip: true,
|
||||
file: exportFile,
|
||||
recursive: true,
|
||||
noDirRecurse: false,
|
||||
cwd: tmpDir,
|
||||
},
|
||||
files
|
||||
|
@ -124,6 +127,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadedPath = join(tmpPath, appPath)
|
||||
if (fs.existsSync(downloadedPath)) {
|
||||
const allFiles = fs.readdirSync(downloadedPath)
|
||||
|
@ -141,12 +145,27 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
||||
exportPath: dbPath,
|
||||
})
|
||||
|
||||
if (config?.encryptPassword) {
|
||||
for (let file of fs.readdirSync(tmpPath)) {
|
||||
const path = join(tmpPath, file)
|
||||
|
||||
await encryption.encryptFile(
|
||||
{ dir: tmpPath, filename: file },
|
||||
config.encryptPassword
|
||||
)
|
||||
|
||||
fs.rmSync(path)
|
||||
}
|
||||
}
|
||||
|
||||
// if tar requested, return where the tarball is
|
||||
if (config?.tar) {
|
||||
// now the tmpPath contains both the DB export and attachments, tar this
|
||||
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
||||
// cleanup the tmp export files as tarball returned
|
||||
fs.rmSync(tmpPath, { recursive: true, force: true })
|
||||
|
||||
return tarPath
|
||||
}
|
||||
// tar not requested, turn the directory where export is
|
||||
|
@ -161,11 +180,20 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
* @param {boolean} excludeRows Flag to state whether the export should include data.
|
||||
* @returns {*} a readable stream of the backup which is written in real time
|
||||
*/
|
||||
export async function streamExportApp(appId: string, excludeRows: boolean) {
|
||||
export async function streamExportApp({
|
||||
appId,
|
||||
excludeRows,
|
||||
encryptPassword,
|
||||
}: {
|
||||
appId: string
|
||||
excludeRows: boolean
|
||||
encryptPassword?: string
|
||||
}) {
|
||||
const tmpPath = await exportApp(appId, {
|
||||
excludeRows,
|
||||
excludeLogs: true,
|
||||
tar: true,
|
||||
encryptPassword,
|
||||
})
|
||||
return streamFile(tmpPath)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { db as dbCore, objectStore } from "@budibase/backend-core"
|
||||
import { db as dbCore, encryption, objectStore } from "@budibase/backend-core"
|
||||
import { Database, Row } from "@budibase/types"
|
||||
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||
|
@ -20,6 +20,7 @@ type TemplateType = {
|
|||
file?: {
|
||||
type: string
|
||||
path: string
|
||||
password?: string
|
||||
}
|
||||
key?: string
|
||||
}
|
||||
|
@ -123,6 +124,22 @@ export function untarFile(file: { path: string }) {
|
|||
return tmpPath
|
||||
}
|
||||
|
||||
async function decryptFiles(path: string, password: string) {
|
||||
try {
|
||||
for (let file of fs.readdirSync(path)) {
|
||||
const inputPath = join(path, file)
|
||||
const outputPath = inputPath.replace(/\.enc$/, "")
|
||||
await encryption.decryptFile(inputPath, outputPath, password)
|
||||
fs.rmSync(inputPath)
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === "incorrect header check") {
|
||||
throw new Error("File cannot be imported")
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function getGlobalDBFile(tmpPath: string) {
|
||||
return fs.readFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), "utf8")
|
||||
}
|
||||
|
@ -143,6 +160,9 @@ export async function importApp(
|
|||
template.file && fs.lstatSync(template.file.path).isDirectory()
|
||||
if (template.file && (isTar || isDirectory)) {
|
||||
const tmpPath = isTar ? untarFile(template.file) : template.file.path
|
||||
if (isTar && template.file.password) {
|
||||
await decryptFiles(tmpPath, template.file.password)
|
||||
}
|
||||
const contents = fs.readdirSync(tmpPath)
|
||||
// have to handle object import
|
||||
if (contents.length) {
|
||||
|
|
|
@ -135,7 +135,7 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
|
|||
// specific to REST datasources, fix the auth configs again if required
|
||||
if (hasAuthConfigs(update)) {
|
||||
const configs = update.config.authConfigs as RestAuthConfig[]
|
||||
const oldConfigs = old.config?.authConfigs as RestAuthConfig[]
|
||||
const oldConfigs = (old.config?.authConfigs as RestAuthConfig[]) || []
|
||||
for (let config of configs) {
|
||||
if (config.type !== RestAuthType.BASIC) {
|
||||
continue
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"setup": "yarn && node scripts/createEnv.js",
|
||||
"test": "jest --runInBand --json --outputFile=testResults.json",
|
||||
"test": "jest --runInBand --json --outputFile=testResults.json --forceExit",
|
||||
"test:watch": "yarn run test --watch",
|
||||
"test:debug": "DEBUG=1 yarn run test",
|
||||
"test:notify": "node scripts/testResultsWebhook",
|
||||
|
|
Loading…
Reference in New Issue