Merge branch 'develop' into test/qa-18-api-automation-testing-permissionsuser-settings
This commit is contained in:
commit
e6cc94be4f
|
@ -12,31 +12,22 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# - name: Fail if not a tag
|
- 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
|
|
||||||
run: |
|
run: |
|
||||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
-H 'Accept: application/vnd.github.v3.raw' \
|
echo "Workflow Dispatch can only be run on tags"
|
||||||
-o values.production.yaml \
|
exit 1
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
|
fi
|
||||||
wc -l values.production.yaml
|
- 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
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
|
@ -48,29 +39,10 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
env:
|
||||||
|
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||||
with:
|
with:
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
repository: budibase/budibase-deploys
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
event: budicloud-prod-deploy
|
||||||
aws-region: eu-west-1
|
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|
|
@ -24,51 +24,18 @@ jobs:
|
||||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
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
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
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"
|
|
||||||
]
|
|
||||||
env:
|
env:
|
||||||
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
|
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
- name: Discord Webhook Action
|
|
||||||
uses: tsickert/discord-webhook@v4.0.0
|
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
repository: budibase/budibase-deploys
|
||||||
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
|
event: budicloud-preprod-deploy
|
||||||
embed-title: ${{ env.RELEASE_VERSION }}
|
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.7.7-alpha.3",
|
"version": "2.7.20-alpha.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
@ -16,7 +16,6 @@
|
||||||
"packages/worker",
|
"packages/worker",
|
||||||
"packages/pro/packages/pro"
|
"packages/pro/packages/pro"
|
||||||
],
|
],
|
||||||
"useWorkspaces": true,
|
|
||||||
"command": {
|
"command": {
|
||||||
"publish": {
|
"publish": {
|
||||||
"ignoreChanges": [
|
"ignoreChanges": [
|
||||||
|
|
|
@ -15,10 +15,10 @@
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.0.0-alpha.0",
|
"lerna": "^7.0.1",
|
||||||
"madge": "^6.0.0",
|
"madge": "^6.0.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"nx": "^16.2.1",
|
"nx": "^16.3.2",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|
|
@ -343,6 +343,9 @@ export class QueryBuilder<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneOf = (key: string, value: any) => {
|
const oneOf = (key: string, value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return `*:*`
|
||||||
|
}
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
value = value.split(",")
|
value = value.split(",")
|
||||||
|
|
|
@ -114,6 +114,25 @@ describe("lucene", () => {
|
||||||
expect(resp.rows.length).toBe(2)
|
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 () => {
|
it("should be able to perform a contains search", async () => {
|
||||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||||
builder.addContains("property", ["word"])
|
builder.addContains("property", ["word"])
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
|
import fs from "fs"
|
||||||
|
import zlib from "zlib"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { join } from "path"
|
||||||
|
|
||||||
const ALGO = "aes-256-ctr"
|
const ALGO = "aes-256-ctr"
|
||||||
const SEPARATOR = "-"
|
const SEPARATOR = "-"
|
||||||
const ITERATIONS = 10000
|
const ITERATIONS = 10000
|
||||||
const RANDOM_BYTES = 16
|
|
||||||
const STRETCH_LENGTH = 32
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
|
const SALT_LENGTH = 16
|
||||||
|
const IV_LENGTH = 16
|
||||||
|
|
||||||
export enum SecretOption {
|
export enum SecretOption {
|
||||||
API = "api",
|
API = "api",
|
||||||
ENCRYPTION = "encryption",
|
ENCRYPTION = "encryption",
|
||||||
|
@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string {
|
||||||
return secret
|
return secret
|
||||||
}
|
}
|
||||||
|
|
||||||
function stretchString(string: string, salt: Buffer) {
|
function stretchString(secret: string, salt: Buffer) {
|
||||||
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encrypt(
|
export function encrypt(
|
||||||
input: string,
|
input: string,
|
||||||
secretOption: SecretOption = SecretOption.API
|
secretOption: SecretOption = SecretOption.API
|
||||||
) {
|
) {
|
||||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||||
const stretched = stretchString(getSecret(secretOption), salt)
|
const stretched = stretchString(getSecret(secretOption), salt)
|
||||||
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||||
const base = cipher.update(input)
|
const base = cipher.update(input)
|
||||||
|
@ -60,3 +65,115 @@ export function decrypt(
|
||||||
const final = decipher.final()
|
const final = decipher.final()
|
||||||
return Buffer.concat([base, final]).toString()
|
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
|
* 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.
|
* to check if the role inherits any others.
|
||||||
* @param {string|null} roleId The level ID to lookup.
|
* @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.
|
* @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) {
|
if (!roleId) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -161,6 +165,9 @@ export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
||||||
// finalise the ID
|
// finalise the ID
|
||||||
role._id = getExternalRoleID(role._id)
|
role._id = getExternalRoleID(role._id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!isBuiltin(roleId) && opts?.defaultPublic) {
|
||||||
|
return cloneDeep(BUILTIN_ROLES.PUBLIC)
|
||||||
|
}
|
||||||
// only throw an error if there is no role at all
|
// only throw an error if there is no role at all
|
||||||
if (Object.keys(role).length === 0) {
|
if (Object.keys(role).length === 0) {
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let validate = null
|
export let validate = null
|
||||||
|
export let indeterminate = false
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -21,11 +23,19 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
|
<FancyField
|
||||||
|
{error}
|
||||||
|
{value}
|
||||||
|
{validate}
|
||||||
|
{disabled}
|
||||||
|
{compact}
|
||||||
|
clickable
|
||||||
|
on:click={onChange}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<Checkbox {disabled} {value} />
|
<Checkbox {disabled} {value} {indeterminate} />
|
||||||
</span>
|
</span>
|
||||||
<div class="text">
|
<div class="text" class:compact>
|
||||||
{#if text}
|
{#if text}
|
||||||
{text}
|
{text}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -47,6 +57,10 @@
|
||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
.text.compact {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
.text > :global(*) {
|
.text > :global(*) {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import FancyCheckbox from "./FancyCheckbox.svelte"
|
||||||
|
import FancyForm from "./FancyForm.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let options = []
|
||||||
|
export let selected = []
|
||||||
|
export let showSelectAll = true
|
||||||
|
export let selectAllText = "Select all"
|
||||||
|
|
||||||
|
let selectedBooleans = reset()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
$: updateSelected(selectedBooleans)
|
||||||
|
$: dispatch("change", selected)
|
||||||
|
$: allSelected = selected?.length === options.length
|
||||||
|
$: noneSelected = !selected?.length
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
return Array(options.length).fill(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelected(selectedArr) {
|
||||||
|
const array = []
|
||||||
|
for (let [i, isSelected] of Object.entries(selectedArr)) {
|
||||||
|
if (isSelected) {
|
||||||
|
array.push(options[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected = array
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
if (allSelected === true) {
|
||||||
|
selectedBooleans = []
|
||||||
|
} else {
|
||||||
|
selectedBooleans = reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if options && Array.isArray(options)}
|
||||||
|
<div class="checkbox-group" class:has-select-all={showSelectAll}>
|
||||||
|
<FancyForm on:change>
|
||||||
|
{#if showSelectAll}
|
||||||
|
<FancyCheckbox
|
||||||
|
bind:value={allSelected}
|
||||||
|
on:change={toggleSelectAll}
|
||||||
|
text={selectAllText}
|
||||||
|
indeterminate={!allSelected && !noneSelected}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#each options as option, i}
|
||||||
|
<FancyCheckbox bind:value={selectedBooleans[i]} text={option} compact />
|
||||||
|
{/each}
|
||||||
|
</FancyForm>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checkbox-group.has-select-all :global(.fancy-field:first-of-type) {
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
.checkbox-group.has-select-all :global(.fancy-field:first-of-type:hover) {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,6 +11,7 @@
|
||||||
export let value
|
export let value
|
||||||
export let ref
|
export let ref
|
||||||
export let autoHeight
|
export let autoHeight
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const formContext = getContext("fancy-form")
|
const formContext = getContext("fancy-form")
|
||||||
const id = Math.random()
|
const id = Math.random()
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
class:disabled
|
class:disabled
|
||||||
class:focused
|
class:focused
|
||||||
class:clickable
|
class:clickable
|
||||||
|
class:compact
|
||||||
class:auto-height={autoHeight}
|
class:auto-height={autoHeight}
|
||||||
>
|
>
|
||||||
<div class="content" on:click>
|
<div class="content" on:click>
|
||||||
|
@ -61,7 +63,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fancy-field {
|
.fancy-field {
|
||||||
max-width: 400px;
|
|
||||||
background: var(--spectrum-global-color-gray-75);
|
background: var(--spectrum-global-color-gray-75);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -69,6 +70,12 @@
|
||||||
transition: border-color 130ms ease-out, background 130ms ease-out,
|
transition: border-color 130ms ease-out, background 130ms ease-out,
|
||||||
background 130ms ease-out;
|
background 130ms ease-out;
|
||||||
color: var(--spectrum-global-color-gray-800);
|
color: var(--spectrum-global-color-gray-800);
|
||||||
|
--padding: 16px;
|
||||||
|
--height: 64px;
|
||||||
|
}
|
||||||
|
.fancy-field.compact {
|
||||||
|
--padding: 8px;
|
||||||
|
--height: 36px;
|
||||||
}
|
}
|
||||||
.fancy-field:hover {
|
.fancy-field:hover {
|
||||||
border-color: var(--spectrum-global-color-gray-400);
|
border-color: var(--spectrum-global-color-gray-400);
|
||||||
|
@ -91,8 +98,8 @@
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 64px;
|
height: var(--height);
|
||||||
padding: 0 16px;
|
padding: 0 var(--padding);
|
||||||
}
|
}
|
||||||
.fancy-field.auto-height .content {
|
.fancy-field.auto-height .content {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -103,7 +110,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: var(--padding);
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -4,4 +4,5 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
||||||
export { default as FancyButton } from "./FancyButton.svelte"
|
export { default as FancyButton } from "./FancyButton.svelte"
|
||||||
export { default as FancyForm } from "./FancyForm.svelte"
|
export { default as FancyForm } from "./FancyForm.svelte"
|
||||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||||
|
export { default as FancyCheckboxGroup } from "./FancyCheckboxGroup.svelte"
|
||||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let text = null
|
export let text = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size
|
export let size
|
||||||
|
export let indeterminate = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = event => {
|
const onChange = event => {
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
class:checked={value}
|
class:checked={value}
|
||||||
|
class:is-indeterminate={indeterminate}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={value}
|
checked={value}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
export let fixed = false
|
export let fixed = false
|
||||||
export let inline = false
|
export let inline = false
|
||||||
|
export let disableCancel = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let visible = fixed || inline
|
let visible = fixed || inline
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancel() {
|
export function cancel() {
|
||||||
if (!visible) {
|
if (!visible || disableCancel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatch("cancel")
|
dispatch("cancel")
|
||||||
|
|
|
@ -204,6 +204,12 @@
|
||||||
})
|
})
|
||||||
return columns
|
return columns
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
if (a.divider) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
if (b.divider) {
|
||||||
|
return b
|
||||||
|
}
|
||||||
const orderA = a.order || Number.MAX_SAFE_INTEGER
|
const orderA = a.order || Number.MAX_SAFE_INTEGER
|
||||||
const orderB = b.order || Number.MAX_SAFE_INTEGER
|
const orderB = b.order || Number.MAX_SAFE_INTEGER
|
||||||
const nameA = getDisplayName(a)
|
const nameA = getDisplayName(a)
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
<meta charset='utf8'>
|
<meta charset='utf8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
<title>Budibase</title>
|
<title>Budibase</title>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||||
rel="stylesheet" />
|
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||||
|
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
"@codemirror/state": "^6.2.0",
|
"@codemirror/state": "^6.2.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.11.2",
|
"@codemirror/view": "^6.11.2",
|
||||||
|
"@fontsource/source-sans-pro": "^5.0.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
@ -122,6 +123,7 @@
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"typescript": "4.7.3",
|
"typescript": "4.7.3",
|
||||||
"vite": "^3.0.8",
|
"vite": "^3.0.8",
|
||||||
|
"vite-plugin-static-copy": "^0.16.0",
|
||||||
"vitest": "^0.29.2"
|
"vitest": "^0.29.2"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 787 B |
|
@ -23,10 +23,11 @@ function prepareData(config) {
|
||||||
return datasource
|
return datasource
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveDatasource(config, skipFetch = false) {
|
export async function saveDatasource(config, { skipFetch, tablesFilter } = {}) {
|
||||||
const datasource = prepareData(config)
|
const datasource = prepareData(config)
|
||||||
// Create datasource
|
// Create datasource
|
||||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
const fetchSchema = !skipFetch && datasource.plus
|
||||||
|
const resp = await datasources.save(datasource, { fetchSchema, tablesFilter })
|
||||||
|
|
||||||
// update the tables incase datasource plus
|
// update the tables incase datasource plus
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
|
@ -41,6 +42,13 @@ export async function createRestDatasource(integration) {
|
||||||
|
|
||||||
export async function validateDatasourceConfig(config) {
|
export async function validateDatasourceConfig(config) {
|
||||||
const datasource = prepareData(config)
|
const datasource = prepareData(config)
|
||||||
const resp = await API.validateDatasource(datasource)
|
return await API.validateDatasource(datasource)
|
||||||
return resp
|
}
|
||||||
|
|
||||||
|
export async function getDatasourceInfo(config) {
|
||||||
|
let datasource = config
|
||||||
|
if (!config._id) {
|
||||||
|
datasource = prepareData(config)
|
||||||
|
}
|
||||||
|
return await API.fetchInfoForDatasource(datasource)
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
propertyFocus: null,
|
propertyFocus: null,
|
||||||
builderSidePanel: false,
|
builderSidePanel: false,
|
||||||
hasLock: true,
|
hasLock: true,
|
||||||
|
showPreview: false,
|
||||||
|
|
||||||
// URL params
|
// URL params
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
import { admin, licensing } from "stores/portal"
|
import { admin, licensing } from "stores/portal"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
import { TriggerStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { checkForCollectStep } from "builderStore/utils"
|
import { checkForCollectStep } from "builderStore/utils"
|
||||||
|
|
||||||
export let blockIdx
|
export let blockIdx
|
||||||
|
@ -149,7 +149,7 @@
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
<Icon name={action.icon} />
|
<Icon name={action.icon} />
|
||||||
<Body size="XS">{action.name}</Body>
|
<Body size="XS">{action.name}</Body>
|
||||||
{#if isDisabled && !syncAutomationsEnabled}
|
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
|
||||||
<div class="tag-color">
|
<div class="tag-color">
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Business</Tag>
|
<Tag icon="LockClosed">Business</Tag>
|
||||||
|
|
|
@ -76,6 +76,10 @@ export function getBindings({
|
||||||
// will be replaced by the main array binding
|
// will be replaced by the main array binding
|
||||||
readableBinding: label,
|
readableBinding: label,
|
||||||
runtimeBinding: binding,
|
runtimeBinding: binding,
|
||||||
|
display: {
|
||||||
|
name: label,
|
||||||
|
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return bindings
|
return bindings
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
Table,
|
||||||
Toggle,
|
FancyCheckboxGroup,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, tables } from "stores/backend"
|
import { datasources, integrations, tables } from "stores/backend"
|
||||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ValuesList from "components/common/ValuesList.svelte"
|
import { getDatasourceInfo } from "builderStore/datasource"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let save
|
export let save
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
let selectedFromRelationship, selectedToRelationship
|
let selectedFromRelationship, selectedToRelationship
|
||||||
let confirmDialog
|
let confirmDialog
|
||||||
let specificTables = null
|
let specificTables = null
|
||||||
let requireSpecificTables = false
|
let tableList
|
||||||
|
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
$: plusTables = datasource?.plus
|
$: plusTables = datasource?.plus
|
||||||
|
@ -153,30 +153,28 @@
|
||||||
warning={false}
|
warning={false}
|
||||||
title="Confirm table fetch"
|
title="Confirm table fetch"
|
||||||
>
|
>
|
||||||
<Toggle
|
|
||||||
bind:value={requireSpecificTables}
|
|
||||||
on:change={e => {
|
|
||||||
requireSpecificTables = e.detail
|
|
||||||
specificTables = null
|
|
||||||
}}
|
|
||||||
thin
|
|
||||||
text="Fetch listed tables only (one per line)"
|
|
||||||
/>
|
|
||||||
{#if requireSpecificTables}
|
|
||||||
<ValuesList label="" bind:values={specificTables} />
|
|
||||||
{/if}
|
|
||||||
<br />
|
|
||||||
<Body>
|
<Body>
|
||||||
If you have fetched tables from this database before, this action may
|
If you have fetched tables from this database before, this action may
|
||||||
overwrite any changes you made after your initial fetch.
|
overwrite any changes you made after your initial fetch.
|
||||||
</Body>
|
</Body>
|
||||||
|
<br />
|
||||||
|
<div class="table-checkboxes">
|
||||||
|
<FancyCheckboxGroup options={tableList} bind:selected={specificTables} />
|
||||||
|
</div>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading size="S">Tables</Heading>
|
<Heading size="S">Tables</Heading>
|
||||||
<div class="table-buttons">
|
<div class="table-buttons">
|
||||||
<Button secondary on:click={() => confirmDialog.show()}>
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={async () => {
|
||||||
|
const info = await getDatasourceInfo(datasource)
|
||||||
|
tableList = info.tableNames
|
||||||
|
confirmDialog.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
Fetch tables
|
Fetch tables
|
||||||
</Button>
|
</Button>
|
||||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||||
|
@ -246,4 +244,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-checkboxes {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -44,6 +44,9 @@ export default ICONS
|
||||||
|
|
||||||
export function getIcon(integrationType, schema) {
|
export function getIcon(integrationType, schema) {
|
||||||
const integrationList = get(integrations)
|
const integrationList = get(integrations)
|
||||||
|
if (!integrationList) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (integrationList[integrationType]?.iconUrl) {
|
if (integrationList[integrationType]?.iconUrl) {
|
||||||
return { url: integrationList[integrationType].iconUrl }
|
return { url: integrationList[integrationType].iconUrl }
|
||||||
} else if (schema?.custom || !ICONS[integrationType]) {
|
} else if (schema?.custom || !ICONS[integrationType]) {
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
FancyCheckboxGroup,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import { IntegrationNames } from "constants/backend"
|
import { IntegrationNames } from "constants/backend"
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
import {
|
import {
|
||||||
saveDatasource as save,
|
saveDatasource as save,
|
||||||
validateDatasourceConfig,
|
validateDatasourceConfig,
|
||||||
|
getDatasourceInfo,
|
||||||
} from "builderStore/datasource"
|
} from "builderStore/datasource"
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
|
||||||
|
@ -15,11 +22,24 @@
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
let isValid = false
|
let isValid = false
|
||||||
|
let fetchTableStep = false
|
||||||
|
let selectedTables = []
|
||||||
|
let tableList = []
|
||||||
|
|
||||||
$: name =
|
$: name =
|
||||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
IntegrationNames[datasource?.type] || datasource?.name || datasource?.type
|
||||||
|
$: datasourcePlus = datasource?.plus
|
||||||
|
$: title = fetchTableStep ? "Fetch your tables" : `Connect to ${name}`
|
||||||
|
$: confirmText = fetchTableStep
|
||||||
|
? "Continue"
|
||||||
|
: datasourcePlus
|
||||||
|
? "Connect"
|
||||||
|
: "Save and continue to query"
|
||||||
|
|
||||||
async function validateConfig() {
|
async function validateConfig() {
|
||||||
|
if (!integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
const displayError = message =>
|
const displayError = message =>
|
||||||
notifications.error(message ?? "Error validating datasource")
|
notifications.error(message ?? "Error validating datasource")
|
||||||
|
|
||||||
|
@ -37,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
const valid = await validateConfig()
|
const valid = await validateConfig()
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return false
|
return false
|
||||||
|
@ -47,35 +67,75 @@
|
||||||
if (!datasource.name) {
|
if (!datasource.name) {
|
||||||
datasource.name = name
|
datasource.name = name
|
||||||
}
|
}
|
||||||
const resp = await save(datasource)
|
const opts = {}
|
||||||
|
if (datasourcePlus && selectedTables) {
|
||||||
|
opts.tablesFilter = selectedTables
|
||||||
|
}
|
||||||
|
const resp = await save(datasource, opts)
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource created successfully.`)
|
notifications.success("Datasource created successfully.")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(err?.message ?? "Error saving datasource")
|
notifications.error(err?.message ?? "Error saving datasource")
|
||||||
// prevent the modal from closing
|
// prevent the modal from closing
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function nextStep() {
|
||||||
|
let connected = true
|
||||||
|
if (datasourcePlus) {
|
||||||
|
connected = await validateConfig()
|
||||||
|
}
|
||||||
|
if (!connected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (datasourcePlus && !fetchTableStep) {
|
||||||
|
notifications.success("Connected to datasource successfully.")
|
||||||
|
const info = await getDatasourceInfo(datasource)
|
||||||
|
tableList = info.tableNames
|
||||||
|
fetchTableStep = true
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
await saveDatasource()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${name}`}
|
{title}
|
||||||
onConfirm={() => saveDatasource()}
|
onConfirm={() => nextStep()}
|
||||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
{confirmText}
|
||||||
cancelText="Back"
|
cancelText={fetchTableStep ? "Cancel" : "Back"}
|
||||||
showSecondaryButton={datasource.plus}
|
showSecondaryButton={datasourcePlus}
|
||||||
size="L"
|
size="L"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Body size="XS"
|
<Body size="XS">
|
||||||
>Connect your database to Budibase using the config below.
|
{#if !fetchTableStep}
|
||||||
|
Connect your database to Budibase using the config below
|
||||||
|
{:else}
|
||||||
|
Choose what tables you want to sync with Budibase
|
||||||
|
{/if}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<IntegrationConfigForm
|
{#if !fetchTableStep}
|
||||||
schema={datasource.schema}
|
<IntegrationConfigForm
|
||||||
bind:datasource
|
schema={datasource?.schema}
|
||||||
creating={true}
|
bind:datasource
|
||||||
on:valid={e => (isValid = e.detail)}
|
creating={true}
|
||||||
/>
|
on:valid={e => (isValid = e.detail)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="table-checkboxes">
|
||||||
|
<FancyCheckboxGroup options={tableList} bind:selected={selectedTables} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-checkboxes {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
ModalContent,
|
|
||||||
Body,
|
Body,
|
||||||
|
FancyCheckboxGroup,
|
||||||
|
InlineAlert,
|
||||||
Layout,
|
Layout,
|
||||||
Link,
|
Link,
|
||||||
|
ModalContent,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
import { organisation } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
import { validateDatasourceConfig } from "builderStore/datasource"
|
import {
|
||||||
|
getDatasourceInfo,
|
||||||
|
saveDatasource,
|
||||||
|
validateDatasourceConfig,
|
||||||
|
} from "builderStore/datasource"
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
import { saveDatasource } from "builderStore/datasource"
|
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let continueSetupId = false
|
export let continueSetupId = false
|
||||||
|
@ -24,16 +29,20 @@
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
datasource.config.continueSetupId = continueSetupId
|
datasource.config.continueSetupId = continueSetupId
|
||||||
|
|
||||||
|
let { schema } = datasource
|
||||||
|
|
||||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
||||||
|
|
||||||
export const GoogleDatasouceConfigStep = {
|
export const GoogleDatasouceConfigStep = {
|
||||||
AUTH: "Auth",
|
AUTH: "auth",
|
||||||
SET_URL: "Set_url",
|
SET_URL: "set_url",
|
||||||
|
SET_SHEETS: "set_sheets",
|
||||||
}
|
}
|
||||||
|
|
||||||
let step = continueSetupId
|
let step = continueSetupId
|
||||||
|
@ -42,12 +51,21 @@
|
||||||
|
|
||||||
let isValid = false
|
let isValid = false
|
||||||
|
|
||||||
const modalConfig = {
|
let allSheets
|
||||||
[GoogleDatasouceConfigStep.AUTH]: {},
|
let selectedSheets
|
||||||
|
let setSheetsErrorTitle, setSheetsErrorMessage
|
||||||
|
|
||||||
|
$: modalConfig = {
|
||||||
|
[GoogleDatasouceConfigStep.AUTH]: {
|
||||||
|
title: `Connect to ${integrationName}`,
|
||||||
|
},
|
||||||
[GoogleDatasouceConfigStep.SET_URL]: {
|
[GoogleDatasouceConfigStep.SET_URL]: {
|
||||||
|
title: `Connect your spreadsheet`,
|
||||||
confirmButtonText: "Connect",
|
confirmButtonText: "Connect",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
const checkConnection =
|
||||||
|
integration.features[DatasourceFeature.CONNECTION_CHECKING]
|
||||||
|
if (checkConnection) {
|
||||||
const resp = await validateDatasourceConfig(datasource)
|
const resp = await validateDatasourceConfig(datasource)
|
||||||
if (!resp.connected) {
|
if (!resp.connected) {
|
||||||
notifications.error(`Unable to connect - ${resp.error}`)
|
notifications.error(`Unable to connect - ${resp.error}`)
|
||||||
|
@ -56,21 +74,81 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await saveDatasource(datasource)
|
datasource = await saveDatasource(datasource, {
|
||||||
$goto(`./datasource/${resp._id}`)
|
tablesFilter: selectedSheets,
|
||||||
notifications.success(`Datasource created successfully.`)
|
skipFetch: true,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(err?.message ?? "Error saving datasource")
|
notifications.error(err?.message ?? "Error saving datasource")
|
||||||
// prevent the modal from closing
|
// prevent the modal from closing
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!integration.features[DatasourceFeature.FETCH_TABLE_NAMES]) {
|
||||||
|
notifications.success(`Datasource created successfully.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await getDatasourceInfo(datasource)
|
||||||
|
allSheets = info.tableNames
|
||||||
|
|
||||||
|
step = GoogleDatasouceConfigStep.SET_SHEETS
|
||||||
|
notifications.success(
|
||||||
|
checkConnection
|
||||||
|
? "Connection Successful"
|
||||||
|
: `Datasource created successfully.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// prevent the modal from closing
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[GoogleDatasouceConfigStep.SET_SHEETS]: {
|
||||||
|
title: `Choose your sheets`,
|
||||||
|
confirmButtonText: selectedSheets?.length
|
||||||
|
? "Fetch sheets"
|
||||||
|
: "Continue without fetching",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
if (selectedSheets.length) {
|
||||||
|
await API.buildDatasourceSchema({
|
||||||
|
datasourceId: datasource._id,
|
||||||
|
tablesFilter: selectedSheets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
const message = err?.message ?? "Error fetching the sheets"
|
||||||
|
// Handling message with format: Error title - error description
|
||||||
|
const indexSeparator = message.indexOf(" - ")
|
||||||
|
if (indexSeparator >= 0) {
|
||||||
|
setSheetsErrorTitle = message.substr(0, indexSeparator)
|
||||||
|
setSheetsErrorMessage =
|
||||||
|
message[indexSeparator + 3].toUpperCase() +
|
||||||
|
message.substr(indexSeparator + 4)
|
||||||
|
} else {
|
||||||
|
setSheetsErrorTitle = null
|
||||||
|
setSheetsErrorMessage = message
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent the modal from closing
|
||||||
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This will handle the user closing the modal pressing outside the modal
|
||||||
|
onDestroy(() => {
|
||||||
|
if (step === GoogleDatasouceConfigStep.SET_SHEETS) {
|
||||||
|
$goto(`./datasource/${datasource._id}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${integrationName}`}
|
title={modalConfig[step].title}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
size="L"
|
size="L"
|
||||||
confirmText={modalConfig[step].confirmButtonText}
|
confirmText={modalConfig[step].confirmButtonText}
|
||||||
|
@ -100,11 +178,30 @@
|
||||||
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
||||||
|
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
schema={datasource.schema}
|
{schema}
|
||||||
bind:datasource
|
bind:datasource
|
||||||
creating={true}
|
creating={true}
|
||||||
on:valid={e => (isValid = e.detail)}
|
on:valid={e => (isValid = e.detail)}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if step === GoogleDatasouceConfigStep.SET_SHEETS}
|
||||||
|
<Layout noPadding no>
|
||||||
|
<Body size="S">Select which spreadsheets you want to connect.</Body>
|
||||||
|
|
||||||
|
<FancyCheckboxGroup
|
||||||
|
options={allSheets}
|
||||||
|
bind:selected={selectedSheets}
|
||||||
|
selectAllText="Select all sheets"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if setSheetsErrorTitle || setSheetsErrorMessage}
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header={setSheetsErrorTitle}
|
||||||
|
message={setSheetsErrorMessage}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
name: "App",
|
name: "App",
|
||||||
description: "",
|
description: "",
|
||||||
icon: "Play",
|
icon: "Play",
|
||||||
action: () => window.open(`/${$store.appId}`),
|
action: () => store.update(state => ({ ...state, showPreview: true })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "Preview",
|
type: "Preview",
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { store } from "builderStore"
|
|
||||||
import { convertToJS } from "@budibase/string-templates"
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
|
||||||
|
@ -339,25 +339,28 @@
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="drawer-actions">
|
<div class="drawer-actions">
|
||||||
<Button
|
{#if drawerActions?.hide}
|
||||||
secondary
|
<Button
|
||||||
quiet
|
secondary
|
||||||
on:click={() => {
|
quiet
|
||||||
store.actions.settings.propertyFocus(null)
|
on:click={() => {
|
||||||
drawerActions.hide()
|
drawerActions.hide()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/if}
|
||||||
cta
|
{#if bindingDrawerActions?.save}
|
||||||
disabled={!valid}
|
<Button
|
||||||
on:click={() => {
|
cta
|
||||||
bindingDrawerActions.save()
|
disabled={!valid}
|
||||||
}}
|
on:click={() => {
|
||||||
>
|
bindingDrawerActions.save()
|
||||||
Save
|
}}
|
||||||
</Button>
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
.map(([name, categoryBindings]) => ({
|
.map(([name, categoryBindings]) => ({
|
||||||
name,
|
name,
|
||||||
bindings: categoryBindings?.filter(binding => {
|
bindings: categoryBindings?.filter(binding => {
|
||||||
return binding.readableBinding.match(searchRgx)
|
return !search || binding.readableBinding.match(searchRgx)
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.filter(category => {
|
.filter(category => {
|
||||||
|
@ -46,7 +46,11 @@
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
$: filteredHelpers = helpers?.filter(helper => {
|
$: 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) => {
|
const getHelperExample = (helper, js) => {
|
||||||
|
@ -124,9 +128,6 @@
|
||||||
<span
|
<span
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!search) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
search = null
|
search = null
|
||||||
}}
|
}}
|
||||||
class:searching={search}
|
class:searching={search}
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={bindingDrawer} {title}>
|
<Drawer bind:this={bindingDrawer} {title} headless>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Add the objects on the left to enrich your text.
|
Add the objects on the left to enrich your text.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
import { store } from "builderStore"
|
|
||||||
|
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { createEventDispatcher, setContext } from "svelte"
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
@ -36,7 +34,6 @@
|
||||||
|
|
||||||
const saveBinding = () => {
|
const saveBinding = () => {
|
||||||
onChange(tempValue)
|
onChange(tempValue)
|
||||||
store.actions.settings.propertyFocus(null)
|
|
||||||
onBlur()
|
onBlur()
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
@ -70,7 +67,6 @@
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
store.actions.settings.propertyFocus(key)
|
|
||||||
bindingDrawer.show()
|
bindingDrawer.show()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -62,7 +62,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewApp = () => {
|
const previewApp = () => {
|
||||||
window.open(`/${application}`)
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
showPreview: true,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewApp = () => {
|
const viewApp = () => {
|
||||||
|
|
|
@ -73,10 +73,6 @@
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
store.actions.settings.highlight(null)
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -186,7 +186,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.CodeMirror) {
|
div :global(.CodeMirror) {
|
||||||
width: var(--code-mirror-width) !important;
|
|
||||||
height: var(--code-mirror-height) !important;
|
height: var(--code-mirror-height) !important;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|
|
@ -1,27 +1,128 @@
|
||||||
<script>
|
<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 app
|
||||||
export let published
|
export let published
|
||||||
let excludeRows = false
|
let includeInternalTablesRows = true
|
||||||
|
let encypt = true
|
||||||
|
|
||||||
$: title = published ? "Export published app" : "Export latest app"
|
let password = null
|
||||||
$: confirmText = published ? "Export published" : "Export latest"
|
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 id = published ? app.prodId : app.devId
|
||||||
const appName = encodeURIComponent(app.name)
|
const url = `/api/backups/export?appId=${id}`
|
||||||
window.location = `/api/backups/export?appId=${id}&appname=${appName}&excludeRows=${excludeRows}`
|
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>
|
</script>
|
||||||
|
|
||||||
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
<ModalContent
|
||||||
<InlineAlert
|
title={stepConfig[currentStep].title}
|
||||||
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
confirmText={stepConfig[currentStep].confirmText}
|
||||||
/>
|
onConfirm={stepConfig[currentStep].onConfirm}
|
||||||
<Body
|
disabled={!stepConfig[currentStep].isValid}
|
||||||
>Apps can be exported with or without data that is within internal tables -
|
>
|
||||||
select this below.</Body
|
{#if currentStep === Step.CONFIG}
|
||||||
>
|
<Body>
|
||||||
<Toggle text="Exclude Rows" bind:value={excludeRows} />
|
<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>
|
</ModalContent>
|
||||||
|
|
|
@ -6,7 +6,6 @@ export function createValidationStore(initialValue, ...validators) {
|
||||||
let touched = false
|
let touched = false
|
||||||
|
|
||||||
const value = writable(initialValue || "")
|
const value = writable(initialValue || "")
|
||||||
const error = derived(value, $v => validate($v, validators))
|
|
||||||
const touchedStore = derived(value, () => {
|
const touchedStore = derived(value, () => {
|
||||||
if (!touched) {
|
if (!touched) {
|
||||||
touched = true
|
touched = true
|
||||||
|
@ -14,6 +13,10 @@ export function createValidationStore(initialValue, ...validators) {
|
||||||
}
|
}
|
||||||
return touched
|
return touched
|
||||||
})
|
})
|
||||||
|
const error = derived(
|
||||||
|
[value, touchedStore],
|
||||||
|
([$v, $t]) => $t && validate($v, validators)
|
||||||
|
)
|
||||||
|
|
||||||
return [value, error, touchedStore]
|
return [value, error, touchedStore]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export const createValidationStore = () => {
|
export const createValidationStore = () => {
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
|
values: {},
|
||||||
errors: {},
|
errors: {},
|
||||||
touched: {},
|
touched: {},
|
||||||
valid: false,
|
valid: false,
|
||||||
|
@ -33,6 +34,9 @@ export const createValidationStore = () => {
|
||||||
case "email":
|
case "email":
|
||||||
propertyValidator = string().email().nullable()
|
propertyValidator = string().email().nullable()
|
||||||
break
|
break
|
||||||
|
case "password":
|
||||||
|
propertyValidator = string().nullable()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
propertyValidator = string().nullable()
|
propertyValidator = string().nullable()
|
||||||
}
|
}
|
||||||
|
@ -41,9 +45,68 @@ export const createValidationStore = () => {
|
||||||
propertyValidator = propertyValidator.required()
|
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
|
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 check = async values => {
|
||||||
const obj = object().shape(validator)
|
const obj = object().shape(validator)
|
||||||
// clear the previous errors
|
// clear the previous errors
|
||||||
|
@ -87,5 +150,6 @@ export const createValidationStore = () => {
|
||||||
check,
|
check,
|
||||||
addValidator,
|
addValidator,
|
||||||
addValidatorType,
|
addValidatorType,
|
||||||
|
observe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
$: platformTitle =
|
$: platformTitle =
|
||||||
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
|
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
|
||||||
|
|
||||||
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
|
$: faviconUrl = $organisation.faviconUrl || "/builder/bblogo.png"
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
@ -27,6 +27,6 @@
|
||||||
<link rel="icon" href={faviconUrl} />
|
<link rel="icon" href={faviconUrl} />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- A default must be set or the browser defaults to favicon.ico behaviour -->
|
<!-- 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}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { fade, fly } from "svelte/transition"
|
||||||
|
import { store, selectedScreen } from "builderStore"
|
||||||
|
import { ProgressCircle } from "@budibase/bbui"
|
||||||
|
|
||||||
|
$: route = $selectedScreen?.routing.route || "/"
|
||||||
|
$: src = `/${$store.appId}#${route}`
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
showPreview: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.closePreview = () => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
showPreview: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="preview-overlay"
|
||||||
|
transition:fade={{ duration: 260 }}
|
||||||
|
on:click|self={close}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="container spectrum {$store.theme}"
|
||||||
|
transition:fly={{ duration: 260, y: 130 }}
|
||||||
|
>
|
||||||
|
<div class="header placeholder" />
|
||||||
|
<div class="loading placeholder">
|
||||||
|
<ProgressCircle />
|
||||||
|
</div>
|
||||||
|
<iframe title="Budibase App Preview" {src} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.preview-overlay {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
background: var(--spectrum-global-color-gray-75);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 80px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
height: 60px;
|
||||||
|
width: 100%;
|
||||||
|
background: black;
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateY(-50%) translateX(-50%);
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -24,6 +24,7 @@
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
import UserAvatars from "./_components/UserAvatars.svelte"
|
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||||
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@
|
||||||
<BuilderSidePanel />
|
<BuilderSidePanel />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="root">
|
<div class="root" class:blur={$store.showPreview}>
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
{#if $store.initialised}
|
{#if $store.initialised}
|
||||||
<div class="topleftnav">
|
<div class="topleftnav">
|
||||||
|
@ -230,6 +231,10 @@
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $store.showPreview}
|
||||||
|
<PreviewOverlay />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
<Modal bind:this={commandPaletteModal}>
|
<Modal bind:this={commandPaletteModal}>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
@ -248,6 +253,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
transition: filter 260ms ease-out;
|
||||||
|
}
|
||||||
|
.root.blur {
|
||||||
|
filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav {
|
.top-nav {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
|
||||||
const querySchema = {
|
const querySchema = {
|
||||||
name: {},
|
name: {},
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
let isValid = true
|
let isValid = true
|
||||||
let integration, baseDatasource, datasource
|
let integration, baseDatasource, datasource
|
||||||
let queryList
|
let queryList
|
||||||
|
let loading = false
|
||||||
|
|
||||||
$: baseDatasource = $datasources.selected
|
$: baseDatasource = $datasources.selected
|
||||||
$: queryList = $queries.list.filter(
|
$: queryList = $queries.list.filter(
|
||||||
|
@ -65,9 +67,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDatasource = async () => {
|
const saveDatasource = async () => {
|
||||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
loading = true
|
||||||
|
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
const valid = await validateConfig()
|
const valid = await validateConfig()
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
loading = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +86,8 @@
|
||||||
baseDatasource = cloneDeep(datasource)
|
baseDatasource = cloneDeep(datasource)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +125,17 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<Heading size="S">Configuration</Heading>
|
<Heading size="S">Configuration</Heading>
|
||||||
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}>
|
<Button
|
||||||
Save
|
disabled={!changed || !isValid || loading}
|
||||||
|
cta
|
||||||
|
on:click={saveDatasource}
|
||||||
|
>
|
||||||
|
<div class="save-button-content">
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="10">Save</Spinner>
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
|
@ -216,4 +231,10 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
<div tabindex="-1" class="exampleApp">
|
<div tabindex="-1" class="exampleApp">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="header">
|
<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>
|
<h1>{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav">Home</div>
|
<div class="nav">Home</div>
|
||||||
|
|
|
@ -373,7 +373,7 @@
|
||||||
<OnboardingTypeModal {chooseCreationType} />
|
<OnboardingTypeModal {chooseCreationType} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={passwordModal}>
|
<Modal bind:this={passwordModal} disableCancel={true}>
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
createUsersResponse={bulkSaveResponse}
|
createUsersResponse={bulkSaveResponse}
|
||||||
userData={userData.users}
|
userData={userData.users}
|
||||||
|
|
|
@ -57,7 +57,10 @@ export function createDatasourcesStore() {
|
||||||
return updateDatasource(response)
|
return updateDatasource(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (body, fetchSchema = false) => {
|
const save = async (body, { fetchSchema, tablesFilter } = {}) => {
|
||||||
|
if (fetchSchema == null) {
|
||||||
|
fetchSchema = false
|
||||||
|
}
|
||||||
let response
|
let response
|
||||||
if (body._id) {
|
if (body._id) {
|
||||||
response = await API.updateDatasource(body)
|
response = await API.updateDatasource(body)
|
||||||
|
@ -65,6 +68,7 @@ export function createDatasourcesStore() {
|
||||||
response = await API.createDatasource({
|
response = await API.createDatasource({
|
||||||
datasource: body,
|
datasource: body,
|
||||||
fetchSchema,
|
fetchSchema,
|
||||||
|
tablesFilter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return updateDatasource(response)
|
return updateDatasource(response)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||||
import replace from "@rollup/plugin-replace"
|
import replace from "@rollup/plugin-replace"
|
||||||
import { defineConfig, loadEnv } from "vite"
|
import { defineConfig, loadEnv } from "vite"
|
||||||
|
import { viteStaticCopy } from "vite-plugin-static-copy"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const ignoredWarnings = [
|
const ignoredWarnings = [
|
||||||
|
@ -59,6 +60,18 @@ export default defineConfig(({ mode }) => {
|
||||||
),
|
),
|
||||||
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
|
"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: {
|
optimizeDeps: {
|
||||||
exclude: ["@roxi/routify"],
|
exclude: ["@roxi/routify"],
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
"pouchdb-replication-stream": "1.2.9",
|
||||||
"randomstring": "1.1.5",
|
"randomstring": "1.1.5",
|
||||||
"tar": "6.1.11",
|
"tar": "6.1.15",
|
||||||
"yaml": "^2.1.1"
|
"yaml": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { createAPIClient } from "@budibase/frontend-core"
|
import { createAPIClient } from "@budibase/frontend-core"
|
||||||
import { notificationStore } from "../stores/notification.js"
|
|
||||||
import { authStore } from "../stores/auth.js"
|
import { authStore } from "../stores/auth.js"
|
||||||
import { devToolsStore } from "../stores/devTools.js"
|
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
||||||
|
@ -25,9 +24,10 @@ export const API = createAPIClient({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add role header
|
// Add role header
|
||||||
const devToolsState = get(devToolsStore)
|
const $devToolsStore = get(devToolsStore)
|
||||||
if (devToolsState.enabled && devToolsState.role) {
|
const $devToolsEnabled = get(devToolsEnabled)
|
||||||
headers["x-budibase-role"] = devToolsState.role
|
if ($devToolsEnabled && $devToolsStore.role) {
|
||||||
|
headers["x-budibase-role"] = $devToolsStore.role
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
appStore,
|
appStore,
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
devToolsEnabled,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
|
||||||
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
|
||||||
|
@ -47,10 +48,7 @@
|
||||||
let permissionError = false
|
let permissionError = false
|
||||||
|
|
||||||
// Determine if we should show devtools or not
|
// Determine if we should show devtools or not
|
||||||
$: showDevTools =
|
$: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek
|
||||||
!$builderStore.inBuilder &&
|
|
||||||
$devToolsStore.enabled &&
|
|
||||||
!$routeStore.queryParams?.peek
|
|
||||||
|
|
||||||
// Handle no matching route
|
// Handle no matching route
|
||||||
$: {
|
$: {
|
||||||
|
@ -107,6 +105,7 @@
|
||||||
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}
|
||||||
>
|
>
|
||||||
<DeviceBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<UserBindingsProvider>
|
<UserBindingsProvider>
|
||||||
|
@ -223,12 +222,14 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: transparent;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
#spectrum-root.builder {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
#clip-root {
|
#clip-root {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
|
@ -180,10 +180,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
{#if !hideLogo}
|
{#if !hideLogo}
|
||||||
<img
|
<img src={logoUrl || "/builder/bblogo.png"} alt={title} />
|
||||||
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"}
|
|
||||||
alt={title}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !hideTitle && title}
|
{#if !hideTitle && title}
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="S">{title}</Heading>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<img
|
<img
|
||||||
class="logo"
|
class="logo"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"}
|
src={logoUrl || "/builder/bblogo.png"}
|
||||||
height="48"
|
height="48"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Button, Select } from "@budibase/bbui"
|
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
||||||
import { devToolsStore } from "../../stores"
|
import { devToolsStore } from "../../stores"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dev-preview-header" class:mobile={$context.device.mobile}>
|
<div class="dev-preview-header" class:mobile={$context.device.mobile}>
|
||||||
<Heading size="XS">Budibase App Preview</Heading>
|
<Heading size="XS">Preview</Heading>
|
||||||
<Select
|
<Select
|
||||||
quiet
|
quiet
|
||||||
options={previewOptions}
|
options={previewOptions}
|
||||||
|
@ -40,36 +40,57 @@
|
||||||
on:change={e => devToolsStore.actions.changeRole(e.detail)}
|
on:change={e => devToolsStore.actions.changeRole(e.detail)}
|
||||||
/>
|
/>
|
||||||
{#if !$context.device.mobile}
|
{#if !$context.device.mobile}
|
||||||
<Button
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
overBackground
|
|
||||||
icon="Code"
|
icon="Code"
|
||||||
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
|
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
|
||||||
>
|
>
|
||||||
{$devToolsStore.visible ? "Close" : "Open"} DevTools
|
{$devToolsStore.visible ? "Close" : "Open"} DevTools
|
||||||
</Button>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Close"
|
||||||
|
on:click={() => window.parent.closePreview?.()}
|
||||||
|
>
|
||||||
|
Close preview
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dev-preview-header {
|
.dev-preview-header {
|
||||||
flex: 0 0 50px;
|
flex: 0 0 60px;
|
||||||
height: 50px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--spectrum-global-color-blue-400);
|
background-color: black;
|
||||||
padding: 0 var(--spacing-xl);
|
padding: 0 var(--spacing-xl);
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.dev-preview-header.mobile {
|
.dev-preview-header.mobile {
|
||||||
flex: 0 0 50px;
|
grid-template-columns: 1fr auto auto;
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
}
|
||||||
.dev-preview-header :global(.spectrum-Heading),
|
.dev-preview-header :global(.spectrum-Heading),
|
||||||
.dev-preview-header :global(.spectrum-Picker-menuIcon),
|
.dev-preview-header :global(.spectrum-Picker-menuIcon),
|
||||||
.dev-preview-header :global(.spectrum-Picker-label) {
|
.dev-preview-header :global(.spectrum-Icon),
|
||||||
color: white !important;
|
.dev-preview-header :global(.spectrum-Picker-label),
|
||||||
|
.dev-preview-header :global(.spectrum-ActionButton) {
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.dev-preview-header :global(.spectrum-Picker) {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.dev-preview-header :global(.spectrum-ActionButton:hover),
|
||||||
|
.dev-preview-header :global(.spectrum-Picker:hover),
|
||||||
|
.dev-preview-header :global(.spectrum-Picker.is-open) {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.dev-preview-header :global(.spectrum-ActionButton:active) {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
@media print {
|
@media print {
|
||||||
.dev-preview-header {
|
.dev-preview-header {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import ClientApp from "./components/ClientApp.svelte"
|
||||||
import {
|
import {
|
||||||
builderStore,
|
builderStore,
|
||||||
appStore,
|
appStore,
|
||||||
devToolsStore,
|
|
||||||
blockStore,
|
blockStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
@ -51,11 +50,6 @@ const loadBudibase = async () => {
|
||||||
await environmentStore.actions.fetchEnvironment()
|
await environmentStore.actions.fetchEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable dev tools or not. We need to be using a dev app and not inside
|
|
||||||
// the builder preview to enable them.
|
|
||||||
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
|
|
||||||
devToolsStore.actions.setEnabled(enableDevTools)
|
|
||||||
|
|
||||||
// Register handler for runtime events from the builder
|
// Register handler for runtime events from the builder
|
||||||
window.handleBuilderRuntimeEvent = (type, data) => {
|
window.handleBuilderRuntimeEvent = (type, data) => {
|
||||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { derived } from "svelte/store"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { devToolsStore } from "../devTools.js"
|
import { devToolsStore } from "../devTools.js"
|
||||||
import { authStore } from "../auth.js"
|
import { authStore } from "../auth.js"
|
||||||
|
import { devToolsEnabled } from "./devToolsEnabled.js"
|
||||||
|
|
||||||
// Derive the current role of the logged-in user
|
// Derive the current role of the logged-in user
|
||||||
export const currentRole = derived(
|
export const currentRole = derived(
|
||||||
[devToolsStore, authStore],
|
[devToolsEnabled, devToolsStore, authStore],
|
||||||
([$devToolsStore, $authStore]) => {
|
([$devToolsEnabled, $devToolsStore, $authStore]) => {
|
||||||
return (
|
return (
|
||||||
($devToolsStore.enabled && $devToolsStore.role) ||
|
($devToolsEnabled && $devToolsStore.role) ||
|
||||||
$authStore?.roleId ||
|
$authStore?.roleId ||
|
||||||
Constants.Roles.PUBLIC
|
Constants.Roles.PUBLIC
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
import { appStore } from "../app.js"
|
||||||
|
import { builderStore } from "../builder.js"
|
||||||
|
|
||||||
|
export const devToolsEnabled = derived(
|
||||||
|
[appStore, builderStore],
|
||||||
|
([$appStore, $builderStore]) => {
|
||||||
|
return !$builderStore.inBuilder && $appStore.isDevApp
|
||||||
|
}
|
||||||
|
)
|
|
@ -3,3 +3,4 @@
|
||||||
// separately we can keep our actual stores lean and performant.
|
// separately we can keep our actual stores lean and performant.
|
||||||
export { currentRole } from "./currentRole.js"
|
export { currentRole } from "./currentRole.js"
|
||||||
export { dndComponentPath } from "./dndComponentPath.js"
|
export { dndComponentPath } from "./dndComponentPath.js"
|
||||||
|
export { devToolsEnabled } from "./devToolsEnabled.js"
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { authStore } from "./auth"
|
||||||
import { API } from "../api"
|
import { API } from "../api"
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
|
||||||
visible: false,
|
visible: false,
|
||||||
allowSelection: false,
|
allowSelection: false,
|
||||||
role: null,
|
role: null,
|
||||||
|
@ -13,13 +12,6 @@ const initialState = {
|
||||||
const createDevToolStore = () => {
|
const createDevToolStore = () => {
|
||||||
const store = createLocalStorageStore("bb-devtools", initialState)
|
const store = createLocalStorageStore("bb-devtools", initialState)
|
||||||
|
|
||||||
const setEnabled = enabled => {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
enabled,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const setVisible = visible => {
|
const setVisible = visible => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -46,7 +38,7 @@ const createDevToolStore = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
actions: { setEnabled, setVisible, setAllowSelection, changeRole },
|
actions: { setVisible, setAllowSelection, changeRole },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,13 +26,16 @@ export const buildDatasourceEndpoints = API => ({
|
||||||
* Creates a datasource
|
* Creates a datasource
|
||||||
* @param datasource the datasource to create
|
* @param datasource the datasource to create
|
||||||
* @param fetchSchema whether to fetch the schema or not
|
* @param fetchSchema whether to fetch the schema or not
|
||||||
|
* @param tablesFilter a list of tables to actually fetch rather than simply
|
||||||
|
* all that are accessible.
|
||||||
*/
|
*/
|
||||||
createDatasource: async ({ datasource, fetchSchema }) => {
|
createDatasource: async ({ datasource, fetchSchema, tablesFilter }) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/datasources",
|
url: "/api/datasources",
|
||||||
body: {
|
body: {
|
||||||
datasource,
|
datasource,
|
||||||
fetchSchema,
|
fetchSchema,
|
||||||
|
tablesFilter,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -69,4 +72,15 @@ export const buildDatasourceEndpoints = API => ({
|
||||||
body: { datasource },
|
body: { datasource },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch table names available within the datasource, for filtering out undesired tables
|
||||||
|
* @param datasource the datasource configuration to use for fetching tables
|
||||||
|
*/
|
||||||
|
fetchInfoForDatasource: async datasource => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/datasources/info`,
|
||||||
|
body: { datasource },
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -148,9 +148,9 @@
|
||||||
class:floating={offset > 0}
|
class:floating={offset > 0}
|
||||||
style="--offset:{offset}px; --sticky-width:{width}px;"
|
style="--offset:{offset}px; --sticky-width:{width}px;"
|
||||||
>
|
>
|
||||||
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
|
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="underlay" transition:fade={{ duration: 130 }} />
|
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="sticky-column" transition:fade={{ duration: 130 }}>
|
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||||
<GutterCell on:expand={addViaModal} rowHovered>
|
<GutterCell on:expand={addViaModal} rowHovered>
|
||||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||||
{#if isAdding}
|
{#if isAdding}
|
||||||
|
@ -179,7 +179,7 @@
|
||||||
</DataCell>
|
</DataCell>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-columns" transition:fade={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $renderedColumns as column, columnIdx}
|
{#each $renderedColumns as column, columnIdx}
|
||||||
|
@ -209,7 +209,7 @@
|
||||||
</div>
|
</div>
|
||||||
</GridScrollWrapper>
|
</GridScrollWrapper>
|
||||||
</div>
|
</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}>
|
<Button size="M" cta on:click={addRow} disabled={isAdding}>
|
||||||
<div class="button-with-keys">
|
<div class="button-with-keys">
|
||||||
Save
|
Save
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 01fbc8670021c5a275c2a1a36ee18b984eeafad5
|
Subproject commit f4b8449aac9bd265214396afbdce7ff984a2ae34
|
|
@ -97,7 +97,7 @@
|
||||||
"koa2-ratelimit": "1.1.1",
|
"koa2-ratelimit": "1.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"memorystream": "0.3.1",
|
"memorystream": "0.3.1",
|
||||||
"mongodb": "4.9",
|
"mongodb": "5.6",
|
||||||
"mssql": "6.2.3",
|
"mssql": "6.2.3",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"socket.io": "4.6.1",
|
"socket.io": "4.6.1",
|
||||||
"svelte": "3.49.0",
|
"svelte": "3.49.0",
|
||||||
"swagger-parser": "10.0.3",
|
"swagger-parser": "10.0.3",
|
||||||
"tar": "6.1.11",
|
"tar": "6.1.15",
|
||||||
"to-json-schema": "0.2.5",
|
"to-json-schema": "0.2.5",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
|
@ -150,7 +150,7 @@
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
"@types/server-destroy": "1.0.1",
|
"@types/server-destroy": "1.0.1",
|
||||||
"@types/supertest": "2.0.12",
|
"@types/supertest": "2.0.12",
|
||||||
"@types/tar": "6.1.3",
|
"@types/tar": "6.1.5",
|
||||||
"@typescript-eslint/parser": "5.45.0",
|
"@typescript-eslint/parser": "5.45.0",
|
||||||
"apidoc": "0.50.4",
|
"apidoc": "0.50.4",
|
||||||
"babel-jest": "29.5.0",
|
"babel-jest": "29.5.0",
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
import sdk from "../../sdk"
|
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 { 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
|
// remove the 120 second limit for the request
|
||||||
ctx.req.setTimeout(0)
|
ctx.req.setTimeout(0)
|
||||||
const appName = decodeURI(ctx.query.appname)
|
|
||||||
excludeRows = isQsTrue(excludeRows)
|
const extension = encryptPassword ? "enc.tar.gz" : "tar.gz"
|
||||||
const backupIdentifier = `${appName}-export-${new Date().getTime()}.tar.gz`
|
const backupIdentifier = `${appName}-export-${new Date().getTime()}.${extension}`
|
||||||
ctx.attachment(backupIdentifier)
|
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 () => {
|
await context.doInAppContext(appId, async () => {
|
||||||
const appDb = context.getAppDB()
|
const appDb = context.getAppDB()
|
||||||
|
|
|
@ -103,6 +103,22 @@ async function buildSchemaHelper(datasource: Datasource) {
|
||||||
return { tables: connector.tables, error }
|
return { tables: connector.tables, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildFilteredSchema(datasource: Datasource, filter?: string[]) {
|
||||||
|
let { tables, error } = await buildSchemaHelper(datasource)
|
||||||
|
let finalTables = tables
|
||||||
|
if (filter) {
|
||||||
|
finalTables = {}
|
||||||
|
for (let key in tables) {
|
||||||
|
if (
|
||||||
|
filter.some((filter: any) => filter.toLowerCase() === key.toLowerCase())
|
||||||
|
) {
|
||||||
|
finalTables[key] = tables[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tables: finalTables, error }
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
// Get internal tables
|
// Get internal tables
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -174,43 +190,28 @@ export async function information(
|
||||||
}
|
}
|
||||||
const tableNames = await connector.getTableNames()
|
const tableNames = await connector.getTableNames()
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
tableNames,
|
tableNames: tableNames.sort(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildSchemaFromDb(ctx: UserCtx) {
|
export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
|
||||||
const tablesFilter = ctx.request.body.tablesFilter
|
const tablesFilter = ctx.request.body.tablesFilter
|
||||||
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
|
|
||||||
let { tables, error } = await buildSchemaHelper(datasource)
|
const { tables, error } = await buildFilteredSchema(datasource, tablesFilter)
|
||||||
if (tablesFilter) {
|
datasource.entities = tables
|
||||||
if (!datasource.entities) {
|
|
||||||
datasource.entities = {}
|
|
||||||
}
|
|
||||||
for (let key in tables) {
|
|
||||||
if (
|
|
||||||
tablesFilter.some(
|
|
||||||
(filter: any) => filter.toLowerCase() === key.toLowerCase()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
datasource.entities[key] = tables[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
datasource.entities = tables
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(datasource)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
||||||
|
|
||||||
const response: any = { datasource: cleanedDatasource }
|
const res: any = { datasource: cleanedDatasource }
|
||||||
if (error) {
|
if (error) {
|
||||||
response.error = error
|
res.error = error
|
||||||
}
|
}
|
||||||
ctx.body = response
|
ctx.body = res
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,6 +321,7 @@ export async function save(
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const plus = ctx.request.body.datasource.plus
|
const plus = ctx.request.body.datasource.plus
|
||||||
const fetchSchema = ctx.request.body.fetchSchema
|
const fetchSchema = ctx.request.body.fetchSchema
|
||||||
|
const tablesFilter = ctx.request.body.tablesFilter
|
||||||
|
|
||||||
const datasource = {
|
const datasource = {
|
||||||
_id: generateDatasourceID({ plus }),
|
_id: generateDatasourceID({ plus }),
|
||||||
|
@ -329,7 +331,10 @@ export async function save(
|
||||||
|
|
||||||
let schemaError = null
|
let schemaError = null
|
||||||
if (fetchSchema) {
|
if (fetchSchema) {
|
||||||
const { tables, error } = await buildSchemaHelper(datasource)
|
const { tables, error } = await buildFilteredSchema(
|
||||||
|
datasource,
|
||||||
|
tablesFilter
|
||||||
|
)
|
||||||
schemaError = error
|
schemaError = error
|
||||||
datasource.entities = tables
|
datasource.entities = tables
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
getUserMetadataParams,
|
getUserMetadataParams,
|
||||||
InternalTables,
|
InternalTables,
|
||||||
} from "../../db/utils"
|
} from "../../db/utils"
|
||||||
import { BBContext, Database } from "@budibase/types"
|
import { UserCtx, Database } from "@budibase/types"
|
||||||
|
|
||||||
const UpdateRolesOptions = {
|
const UpdateRolesOptions = {
|
||||||
CREATED: "created",
|
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()
|
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)
|
ctx.body = await roles.getRole(ctx.params.roleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: BBContext) {
|
export async function save(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
let { _id, name, inherits, permissionId } = ctx.request.body
|
let { _id, name, inherits, permissionId } = ctx.request.body
|
||||||
let isCreate = false
|
let isCreate = false
|
||||||
|
@ -72,7 +72,7 @@ export async function save(ctx: BBContext) {
|
||||||
ctx.message = `Role '${role.name}' created successfully.`
|
ctx.message = `Role '${role.name}' created successfully.`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const roleId = ctx.params.roleId
|
const roleId = ctx.params.roleId
|
||||||
const role = await db.get(roleId)
|
const role = await db.get(roleId)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getRoutingInfo } from "../../utilities/routing"
|
import { getRoutingInfo } from "../../utilities/routing"
|
||||||
import { roles } from "@budibase/backend-core"
|
import { roles } from "@budibase/backend-core"
|
||||||
import { BBContext } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
|
||||||
const URL_SEPARATOR = "/"
|
const URL_SEPARATOR = "/"
|
||||||
|
|
||||||
|
@ -56,11 +56,11 @@ async function getRoutingStructure() {
|
||||||
return { routes: routing.json }
|
return { routes: routing.json }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
ctx.body = await getRoutingStructure()
|
ctx.body = await getRoutingStructure()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clientFetch(ctx: BBContext) {
|
export async function clientFetch(ctx: UserCtx) {
|
||||||
const routing = await getRoutingStructure()
|
const routing = await getRoutingStructure()
|
||||||
let roleId = ctx.user?.role?._id
|
let roleId = ctx.user?.role?._id
|
||||||
const roleIds = (await roles.getUserRoleHierarchy(roleId, {
|
const roleIds = (await roles.getUserRoleHierarchy(roleId, {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
breakRowIdField,
|
breakRowIdField,
|
||||||
convertRowId,
|
convertRowId,
|
||||||
generateRowIdField,
|
generateRowIdField,
|
||||||
|
getPrimaryDisplay,
|
||||||
isRowId,
|
isRowId,
|
||||||
isSQL,
|
isSQL,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
|
@ -391,7 +392,10 @@ export class ExternalRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
relatedRow = processFormulas(linkedTable, relatedRow)
|
relatedRow = processFormulas(linkedTable, relatedRow)
|
||||||
const relatedDisplay = display ? relatedRow[display] : undefined
|
let relatedDisplay
|
||||||
|
if (display) {
|
||||||
|
relatedDisplay = getPrimaryDisplay(relatedRow[display])
|
||||||
|
}
|
||||||
row[relationship.column][key] = {
|
row[relationship.column][key] = {
|
||||||
primaryDisplay: relatedDisplay || "Invalid display column",
|
primaryDisplay: relatedDisplay || "Invalid display column",
|
||||||
_id: relatedRow._id,
|
_id: relatedRow._id,
|
||||||
|
|
|
@ -237,9 +237,15 @@ export async function exportRows(ctx: UserCtx) {
|
||||||
ctx.request.body = {
|
ctx.request.body = {
|
||||||
query: {
|
query: {
|
||||||
oneOf: {
|
oneOf: {
|
||||||
_id: ctx.request.body.rows.map(
|
_id: ctx.request.body.rows.map((row: string) => {
|
||||||
(row: string) => JSON.parse(decodeURI(row))[0]
|
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 !== ""}
|
{#if favicon !== ""}
|
||||||
<link rel="icon" type="image/png" href={favicon} />
|
<link rel="icon" type="image/png" href={favicon} />
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||||
<link
|
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Budibase Builder Preview</title>
|
<title>Budibase Builder Preview</title>
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||||
<link
|
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
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()
|
const router: Router = new Router()
|
||||||
|
|
||||||
router.get(
|
router.post(
|
||||||
"/api/backups/export",
|
"/api/backups/export",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.exportAppDump
|
controller.exportAppDump
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import tk from "timekeeper"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
describe("/backups", () => {
|
describe("/backups", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
|
@ -16,7 +18,7 @@ describe("/backups", () => {
|
||||||
describe("exportAppDump", () => {
|
describe("exportAppDump", () => {
|
||||||
it("should be able to export app", async () => {
|
it("should be able to export app", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/backups/export?appId=${config.getAppId()}&appname=test`)
|
.post(`/api/backups/export?appId=${config.getAppId()}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.headers["content-type"]).toEqual("application/gzip")
|
expect(res.headers["content-type"]).toEqual("application/gzip")
|
||||||
|
@ -26,10 +28,24 @@ describe("/backups", () => {
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await checkBuilderEndpoint({
|
await checkBuilderEndpoint({
|
||||||
config,
|
config,
|
||||||
method: "GET",
|
method: "POST",
|
||||||
url: `/api/backups/export?appId=${config.getAppId()}`,
|
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", () => {
|
describe("calculateBackupStats", () => {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
||||||
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import { cache, configs, context, HTTPError } from "@budibase/backend-core"
|
import { cache, configs, context, HTTPError } from "@budibase/backend-core"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters, utils } from "@budibase/shared-core"
|
||||||
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
|
|
||||||
|
@ -150,7 +150,6 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
|
|
||||||
async testConnection(): Promise<ConnectionInfo> {
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
try {
|
try {
|
||||||
await setupCreationAuth(this.config)
|
|
||||||
await this.connect()
|
await this.connect()
|
||||||
return { connected: true }
|
return { connected: true }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -211,6 +210,8 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
try {
|
try {
|
||||||
|
await setupCreationAuth(this.config)
|
||||||
|
|
||||||
// Initialise oAuth client
|
// Initialise oAuth client
|
||||||
let googleConfig = await configs.getGoogleDatasourceConfig()
|
let googleConfig = await configs.getGoogleDatasourceConfig()
|
||||||
if (!googleConfig) {
|
if (!googleConfig) {
|
||||||
|
@ -273,24 +274,24 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
||||||
// not fully configured yet
|
|
||||||
if (!this.config.auth) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await this.connect()
|
await this.connect()
|
||||||
const sheets = this.client.sheetsByIndex
|
const sheets = this.client.sheetsByIndex
|
||||||
const tables: Record<string, Table> = {}
|
const tables: Record<string, Table> = {}
|
||||||
for (let sheet of sheets) {
|
await utils.parallelForeach(
|
||||||
// must fetch rows to determine schema
|
sheets,
|
||||||
await sheet.getRows()
|
async sheet => {
|
||||||
|
// must fetch rows to determine schema
|
||||||
|
await sheet.getRows({ limit: 0, offset: 0 })
|
||||||
|
|
||||||
const id = buildExternalTableId(datasourceId, sheet.title)
|
const id = buildExternalTableId(datasourceId, sheet.title)
|
||||||
tables[sheet.title] = this.getTableSchema(
|
tables[sheet.title] = this.getTableSchema(
|
||||||
sheet.title,
|
sheet.title,
|
||||||
sheet.headerValues,
|
sheet.headerValues,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
10
|
||||||
|
)
|
||||||
const final = finaliseExternalTables(tables, entities)
|
const final = finaliseExternalTables(tables, entities)
|
||||||
this.tables = final.tables
|
this.tables = final.tables
|
||||||
this.schemaErrors = final.errors
|
this.schemaErrors = final.errors
|
||||||
|
|
|
@ -351,7 +351,7 @@ const SCHEMA: Integration = getSchema()
|
||||||
|
|
||||||
class MongoIntegration implements IntegrationBase {
|
class MongoIntegration implements IntegrationBase {
|
||||||
private config: MongoDBConfig
|
private config: MongoDBConfig
|
||||||
private client: any
|
private client: MongoClient
|
||||||
|
|
||||||
constructor(config: MongoDBConfig) {
|
constructor(config: MongoDBConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -372,6 +372,8 @@ class MongoIntegration implements IntegrationBase {
|
||||||
response.connected = true
|
response.connected = true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
response.error = e.message as string
|
response.error = e.message as string
|
||||||
|
} finally {
|
||||||
|
await this.client.close()
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -380,7 +382,7 @@ class MongoIntegration implements IntegrationBase {
|
||||||
return this.client.connect()
|
return this.client.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
createObjectIds(json: any): object {
|
createObjectIds(json: any) {
|
||||||
const self = this
|
const self = this
|
||||||
function interpolateObjectIds(json: any) {
|
function interpolateObjectIds(json: any) {
|
||||||
for (let field of Object.keys(json)) {
|
for (let field of Object.keys(json)) {
|
||||||
|
|
|
@ -91,7 +91,7 @@ const SCHEMA: Integration = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindingTypeCoerce(bindings: any[]) {
|
export function bindingTypeCoerce(bindings: any[]) {
|
||||||
for (let i = 0; i < bindings.length; i++) {
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
const binding = bindings[i]
|
const binding = bindings[i]
|
||||||
if (typeof binding !== "string") {
|
if (typeof binding !== "string") {
|
||||||
|
@ -109,7 +109,12 @@ function bindingTypeCoerce(bindings: any[]) {
|
||||||
dayjs(binding).isValid() &&
|
dayjs(binding).isValid() &&
|
||||||
!binding.includes(",")
|
!binding.includes(",")
|
||||||
) {
|
) {
|
||||||
bindings[i] = dayjs(binding).toDate()
|
let value: any
|
||||||
|
value = new Date(binding)
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = binding
|
||||||
|
}
|
||||||
|
bindings[i] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bindings
|
return bindings
|
||||||
|
|
|
@ -20,7 +20,7 @@ import Sql from "./base/sql"
|
||||||
import { PostgresColumn } from "./base/types"
|
import { PostgresColumn } from "./base/types"
|
||||||
import { escapeDangerousCharacters } from "../utilities"
|
import { escapeDangerousCharacters } from "../utilities"
|
||||||
|
|
||||||
import { Client, types } from "pg"
|
import { Client, ClientConfig, types } from "pg"
|
||||||
|
|
||||||
// Return "date" and "timestamp" types as plain strings.
|
// Return "date" and "timestamp" types as plain strings.
|
||||||
// This lets us reference the original stored timezone.
|
// This lets us reference the original stored timezone.
|
||||||
|
@ -42,6 +42,8 @@ interface PostgresConfig {
|
||||||
schema: string
|
schema: string
|
||||||
ssl?: boolean
|
ssl?: boolean
|
||||||
ca?: string
|
ca?: string
|
||||||
|
clientKey?: string
|
||||||
|
clientCert?: string
|
||||||
rejectUnauthorized?: boolean
|
rejectUnauthorized?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +100,19 @@ const SCHEMA: Integration = {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
ca: {
|
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,
|
type: DatasourceFieldType.LONGFORM,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -144,12 +159,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
super(SqlClient.POSTGRES)
|
super(SqlClient.POSTGRES)
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
let newConfig = {
|
let newConfig: ClientConfig = {
|
||||||
...this.config,
|
...this.config,
|
||||||
ssl: this.config.ssl
|
ssl: this.config.ssl
|
||||||
? {
|
? {
|
||||||
rejectUnauthorized: this.config.rejectUnauthorized,
|
rejectUnauthorized: this.config.rejectUnauthorized,
|
||||||
ca: this.config.ca,
|
ca: this.config.ca,
|
||||||
|
key: this.config.clientKey,
|
||||||
|
cert: this.config.clientCert,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
|
@ -322,7 +339,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
|
||||||
await this.openConnection()
|
await this.openConnection()
|
||||||
const columnsResponse: { rows: PostgresColumn[] } =
|
const columnsResponse: { rows: PostgresColumn[] } =
|
||||||
await this.client.query(this.COLUMNS_SQL)
|
await this.client.query(this.COLUMNS_SQL)
|
||||||
return columnsResponse.rows.map(row => row.table_name)
|
const names = columnsResponse.rows.map(row => row.table_name)
|
||||||
|
return [...new Set(names)]
|
||||||
} finally {
|
} finally {
|
||||||
await this.closeConnection()
|
await this.closeConnection()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { default as MySQLIntegration } from "../mysql"
|
import { default as MySQLIntegration, bindingTypeCoerce } from "../mysql"
|
||||||
jest.mock("mysql2")
|
jest.mock("mysql2")
|
||||||
|
|
||||||
class TestConfiguration {
|
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 }), {})
|
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {})
|
||||||
return { tables: finalTables, errors }
|
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,
|
userId,
|
||||||
globalId,
|
globalId,
|
||||||
roleId,
|
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 { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||||
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
||||||
import { ObjectStoreBuckets } from "../../../constants"
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
|
@ -18,7 +18,8 @@ import { join } from "path"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
|
||||||
const uuid = require("uuid/v4")
|
const uuid = require("uuid/v4")
|
||||||
const tar = require("tar")
|
import tar from "tar"
|
||||||
|
|
||||||
const MemoryStream = require("memorystream")
|
const MemoryStream = require("memorystream")
|
||||||
|
|
||||||
interface DBDumpOpts {
|
interface DBDumpOpts {
|
||||||
|
@ -30,16 +31,18 @@ interface ExportOpts extends DBDumpOpts {
|
||||||
tar?: boolean
|
tar?: boolean
|
||||||
excludeRows?: boolean
|
excludeRows?: boolean
|
||||||
excludeLogs?: boolean
|
excludeLogs?: boolean
|
||||||
|
encryptPassword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function tarFilesToTmp(tmpDir: string, files: 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(
|
tar.create(
|
||||||
{
|
{
|
||||||
sync: true,
|
sync: true,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
file: exportFile,
|
file: exportFile,
|
||||||
recursive: true,
|
noDirRecurse: false,
|
||||||
cwd: tmpDir,
|
cwd: tmpDir,
|
||||||
},
|
},
|
||||||
files
|
files
|
||||||
|
@ -124,6 +127,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadedPath = join(tmpPath, appPath)
|
const downloadedPath = join(tmpPath, appPath)
|
||||||
if (fs.existsSync(downloadedPath)) {
|
if (fs.existsSync(downloadedPath)) {
|
||||||
const allFiles = fs.readdirSync(downloadedPath)
|
const allFiles = fs.readdirSync(downloadedPath)
|
||||||
|
@ -141,12 +145,27 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
||||||
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
||||||
exportPath: dbPath,
|
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 tar requested, return where the tarball is
|
||||||
if (config?.tar) {
|
if (config?.tar) {
|
||||||
// now the tmpPath contains both the DB export and attachments, tar this
|
// now the tmpPath contains both the DB export and attachments, tar this
|
||||||
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath))
|
||||||
// cleanup the tmp export files as tarball returned
|
// cleanup the tmp export files as tarball returned
|
||||||
fs.rmSync(tmpPath, { recursive: true, force: true })
|
fs.rmSync(tmpPath, { recursive: true, force: true })
|
||||||
|
|
||||||
return tarPath
|
return tarPath
|
||||||
}
|
}
|
||||||
// tar not requested, turn the directory where export is
|
// 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.
|
* @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
|
* @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, {
|
const tmpPath = await exportApp(appId, {
|
||||||
excludeRows,
|
excludeRows,
|
||||||
excludeLogs: true,
|
excludeLogs: true,
|
||||||
tar: true,
|
tar: true,
|
||||||
|
encryptPassword,
|
||||||
})
|
})
|
||||||
return streamFile(tmpPath)
|
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 { Database, Row } from "@budibase/types"
|
||||||
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
|
||||||
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
||||||
|
@ -20,6 +20,7 @@ type TemplateType = {
|
||||||
file?: {
|
file?: {
|
||||||
type: string
|
type: string
|
||||||
path: string
|
path: string
|
||||||
|
password?: string
|
||||||
}
|
}
|
||||||
key?: string
|
key?: string
|
||||||
}
|
}
|
||||||
|
@ -123,6 +124,22 @@ export function untarFile(file: { path: string }) {
|
||||||
return tmpPath
|
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) {
|
export function getGlobalDBFile(tmpPath: string) {
|
||||||
return fs.readFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), "utf8")
|
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()
|
template.file && fs.lstatSync(template.file.path).isDirectory()
|
||||||
if (template.file && (isTar || isDirectory)) {
|
if (template.file && (isTar || isDirectory)) {
|
||||||
const tmpPath = isTar ? untarFile(template.file) : template.file.path
|
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)
|
const contents = fs.readdirSync(tmpPath)
|
||||||
// have to handle object import
|
// have to handle object import
|
||||||
if (contents.length) {
|
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
|
// specific to REST datasources, fix the auth configs again if required
|
||||||
if (hasAuthConfigs(update)) {
|
if (hasAuthConfigs(update)) {
|
||||||
const configs = update.config.authConfigs as RestAuthConfig[]
|
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) {
|
for (let config of configs) {
|
||||||
if (config.type !== RestAuthType.BASIC) {
|
if (config.type !== RestAuthType.BASIC) {
|
||||||
continue
|
continue
|
||||||
|
@ -164,5 +164,6 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
|
||||||
delete update.config[key]
|
delete update.config[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,42 @@ export function unreachable(
|
||||||
) {
|
) {
|
||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function parallelForeach<T>(
|
||||||
|
items: T[],
|
||||||
|
task: (item: T) => Promise<void>,
|
||||||
|
maxConcurrency: number
|
||||||
|
): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = []
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
const processItem = async (item: T) => {
|
||||||
|
try {
|
||||||
|
await task(item)
|
||||||
|
} finally {
|
||||||
|
processNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processNext = () => {
|
||||||
|
if (index >= items.length) {
|
||||||
|
// No more items to process
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = items[index]
|
||||||
|
index++
|
||||||
|
|
||||||
|
const promise = processItem(item)
|
||||||
|
promises.push(promise)
|
||||||
|
|
||||||
|
if (promises.length >= maxConcurrency) {
|
||||||
|
Promise.race(promises).then(processNext)
|
||||||
|
} else {
|
||||||
|
processNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processNext()
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export interface UpdateDatasourceResponse {
|
||||||
export interface CreateDatasourceRequest {
|
export interface CreateDatasourceRequest {
|
||||||
datasource: Datasource
|
datasource: Datasource
|
||||||
fetchSchema?: boolean
|
fetchSchema?: boolean
|
||||||
|
tablesFilter: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifyDatasourceRequest {
|
export interface VerifyDatasourceRequest {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "yarn && node scripts/createEnv.js",
|
"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:watch": "yarn run test --watch",
|
||||||
"test:debug": "DEBUG=1 yarn run test",
|
"test:debug": "DEBUG=1 yarn run test",
|
||||||
"test:notify": "node scripts/testResultsWebhook",
|
"test:notify": "node scripts/testResultsWebhook",
|
||||||
|
|
|
@ -60,9 +60,12 @@ export default class AccountAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(accountID: string) {
|
async delete(accountID: string) {
|
||||||
const [response, json] = await this.client.del(`/api/accounts/${accountID}`, {
|
const [response, json] = await this.client.del(
|
||||||
internal: true,
|
`/api/accounts/${accountID}`,
|
||||||
})
|
{
|
||||||
|
internal: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
// can't use expect here due to use in global teardown
|
// can't use expect here due to use in global teardown
|
||||||
if (response.status !== 204) {
|
if (response.status !== 204) {
|
||||||
throw new Error(`Could not delete accountId=${accountID}`)
|
throw new Error(`Could not delete accountId=${accountID}`)
|
||||||
|
|
|
@ -93,7 +93,7 @@ describe("datasource validators", () => {
|
||||||
const result = await integration.testConnection()
|
const result = await integration.testConnection()
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
connected: false,
|
connected: false,
|
||||||
error: "Error: getaddrinfo ENOTFOUND http",
|
error: "getaddrinfo ENOTFOUND http",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { GenericContainer } from "testcontainers"
|
import { GenericContainer } from "testcontainers"
|
||||||
|
import postgres from "../../../../packages/server/src/integrations/postgres"
|
||||||
|
|
||||||
jest.unmock("pg")
|
jest.unmock("pg")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue