Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes

This commit is contained in:
Andrew Kingston 2023-06-27 16:09:59 +01:00
commit 8c9d4a9126
52 changed files with 1054 additions and 753 deletions

View File

@ -10,4 +10,4 @@ packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
packages/builder/cypress/reports
packages/sdk/sdk
packages/sdk/sdk

View File

@ -16,8 +16,7 @@
"dist",
"public",
"*.spec.js",
"bundle.js",
"packages/pro"
"bundle.js"
],
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],
@ -30,9 +29,7 @@
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": [],
"extends": [
"eslint:recommended"
],
"extends": ["eslint:recommended"],
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",

View File

@ -32,10 +32,11 @@ jobs:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- run: cd scripts && yarn
- name: Tag prerelease
run: |
cd scripts
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./scripts/versionCommit.sh prerelease
./versionCommit.sh prerelease

View File

@ -42,12 +42,13 @@ jobs:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- run: cd scripts && yarn
- name: Tag release
run: |
cd scripts
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./scripts/versionCommit.sh $BUMP_TYPE
./versionCommit.sh $BUMP_TYPE

View File

@ -1,9 +1,7 @@
{
"version": "2.7.34-alpha.9",
"version": "2.7.35",
"npmClient": "yarn",
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"useNx": true,
"command": {
"publish": {
@ -19,4 +17,4 @@
"loadEnvFiles": false
}
}
}
}

View File

@ -22,7 +22,6 @@
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"semver": "^7.5.0",
"svelte": "^3.38.2",
"typescript": "4.7.3"
},

View File

@ -5,6 +5,7 @@ import {
GoogleInnerConfig,
OIDCConfig,
OIDCInnerConfig,
OIDCLogosConfig,
SCIMConfig,
SCIMInnerConfig,
SettingsConfig,
@ -191,6 +192,10 @@ export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
// OIDC
export async function getOIDCLogosDoc(): Promise<OIDCLogosConfig | undefined> {
return getConfig<OIDCLogosConfig>(ConfigType.OIDC_LOGOS)
}
async function getOIDCConfigDoc(): Promise<OIDCConfig | undefined> {
return getConfig<OIDCConfig>(ConfigType.OIDC)
}

View File

@ -57,6 +57,9 @@ class Replication {
appReplicateOpts() {
return {
filter: (doc: any) => {
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
return false
}
return doc._id !== DocumentType.APP_METADATA
},
}

View File

@ -99,9 +99,15 @@
bind:this={button}
>
{#if fieldIcon}
<span class="option-extra icon">
<Icon size="S" name={fieldIcon} />
</span>
{#if !useOptionIconImage}
<span class="option-extra icon">
<Icon size="S" name={fieldIcon} />
</span>
{:else}
<span class="option-extra icon field-icon">
<img src={fieldIcon} alt="icon" width="15" height="15" />
</span>
{/if}
{/if}
{#if fieldColour}
<span class="option-extra">
@ -311,4 +317,8 @@
max-width: 170px;
font-size: 12px;
}
.option-extra.icon.field-icon {
display: flex;
}
</style>

View File

@ -1,18 +1,14 @@
<script>
import { get } from "svelte/store"
import { ActionButton, Modal, notifications } from "@budibase/bbui"
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
import { datasources, integrations } from "../../../../stores/backend"
import { ActionButton, notifications } from "@budibase/bbui"
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
import { datasources } from "../../../../stores/backend"
import { createEventDispatcher } from "svelte"
import { integrationForDatasource } from "stores/selectors"
export let table
const dispatch = createEventDispatcher()
$: datasource = findDatasource(table?._id)
$: plusTables = datasource?.plus
? Object.values(datasource?.entities || {})
: []
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
let modal
@ -26,34 +22,32 @@
})
}
async function saveRelationship() {
try {
// Create datasource
await datasources.update({
datasource,
integration: integrationForDatasource(get(integrations), datasource),
})
notifications.success(`Relationship information saved.`)
dispatch("updatecolumns")
} catch (err) {
notifications.error(`Error saving relationship info: ${err}`)
}
const afterSave = ({ action }) => {
notifications.success(`Relationship ${action} successfully`)
dispatch("updatecolumns")
}
const onError = err => {
notifications.error(`Error saving relationship info: ${err}`)
}
</script>
{#if datasource}
<div>
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
<ActionButton
icon="DataCorrelated"
primary
quiet
on:click={() => modal.show({ fromTable: table })}
>
Define relationship
</ActionButton>
</div>
<Modal bind:this={modal}>
<CreateEditRelationship
{datasource}
save={saveRelationship}
close={modal.hide}
{plusTables}
selectedFromTable={table}
/>
</Modal>
<CreateEditRelationshipModal
bind:this={modal}
{datasource}
{tables}
{afterSave}
{onError}
/>
{/if}

View File

@ -1,249 +0,0 @@
<script>
import {
Heading,
Body,
Divider,
InlineAlert,
Button,
notifications,
Modal,
Table,
FancyCheckboxGroup,
} from "@budibase/bbui"
import { datasources, integrations, tables } from "stores/backend"
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { goto } from "@roxi/routify"
export let datasource
export let save
let tableSchema = {
name: {},
primary: { displayName: "Primary Key" },
}
let relationshipSchema = {
tables: {},
columns: {},
}
let relationshipModal
let createExternalTableModal
let selectedFromRelationship, selectedToRelationship
let confirmDialog
let specificTables = null
let tableList
$: integration = datasource && $integrations[datasource.source]
$: plusTables = datasource?.plus
? Object.values(datasource?.entities || {})
: []
$: relationships = getRelationships(plusTables)
$: schemaError = $datasources.schemaError
$: relationshipInfo = relationshipTableData(relationships)
function getRelationships(tables) {
if (!tables || !Array.isArray(tables)) {
return {}
}
let pairs = {}
for (let table of tables) {
for (let column of Object.values(table.schema)) {
if (column.type !== "link") {
continue
}
// these relationships have an id to pair them to each other
// one has a main for the from side
const key = column.main ? "from" : "to"
pairs[column._id] = {
...pairs[column._id],
[key]: column,
}
}
}
return pairs
}
function buildRelationshipDisplayString(fromCol, toCol) {
function getTableName(tableId) {
if (!tableId || typeof tableId !== "string") {
return null
}
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
}
if (!toCol || !fromCol) {
return "Cannot build name"
}
const fromTableName = getTableName(toCol.tableId)
const toTableName = getTableName(fromCol.tableId)
const throughTableName = getTableName(fromCol.through)
let displayString
if (throughTableName) {
displayString = `${fromTableName} ↔ ${toTableName}`
} else {
displayString = `${fromTableName} → ${toTableName}`
}
return displayString
}
async function updateDatasourceSchema() {
try {
await datasources.updateSchema(datasource, specificTables)
notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch()
} catch (error) {
notifications.error(
`Error updating datasource schema ${
error?.message ? `: ${error.message}` : ""
}`
)
}
}
function onClickTable(table) {
$goto(`../../table/${table._id}`)
}
function openRelationshipModal(fromRelationship, toRelationship) {
selectedFromRelationship = fromRelationship || {}
selectedToRelationship = toRelationship || {}
relationshipModal.show()
}
function createNewTable() {
createExternalTableModal.show()
}
function relationshipTableData(relations) {
return Object.values(relations).map(relationship => ({
tables: buildRelationshipDisplayString(
relationship.from,
relationship.to
),
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
from: relationship.from,
to: relationship.to,
}))
}
</script>
<Modal bind:this={relationshipModal}>
<CreateEditRelationship
{datasource}
{save}
close={relationshipModal.hide}
{plusTables}
fromRelationship={selectedFromRelationship}
toRelationship={selectedToRelationship}
/>
</Modal>
<Modal bind:this={createExternalTableModal}>
<CreateExternalTableModal {datasource} />
</Modal>
<ConfirmDialog
bind:this={confirmDialog}
okText="Fetch tables"
onOk={updateDatasourceSchema}
onCancel={() => confirmDialog.hide()}
warning={false}
title="Confirm table fetch"
>
<Body>
If you have fetched tables from this database before, this action may
overwrite any changes you made after your initial fetch.
</Body>
<br />
<div class="table-checkboxes">
<FancyCheckboxGroup options={tableList} bind:selected={specificTables} />
</div>
</ConfirmDialog>
<Divider />
<div class="query-header">
<Heading size="S">Tables</Heading>
<div class="table-buttons">
<Button
secondary
on:click={async () => {
tableList = await datasources.getTableNames(datasource)
confirmDialog.show()
}}
>
Fetch tables
</Button>
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
</div>
</div>
<Body>
This datasource can determine tables automatically. Budibase can fetch your
tables directly from the database and you can use them without having to write
any queries at all.
</Body>
{#if schemaError}
<InlineAlert
type="error"
header="Error fetching tables"
message={schemaError}
onConfirm={datasources.removeSchemaError}
/>
{/if}
{#if plusTables && Object.values(plusTables).length > 0}
<Table
on:click={({ detail }) => onClickTable(detail)}
schema={tableSchema}
data={Object.values(plusTables)}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
/>
{:else}
<Body size="S"><i>No tables found.</i></Body>
{/if}
{#if integration.relationships !== false}
<Divider />
<div class="query-header">
<Heading size="S">Relationships</Heading>
<Button primary on:click={() => openRelationshipModal()}>
Define relationship
</Button>
</div>
<Body>
Tell budibase how your tables are related to get even more smart features.
</Body>
{#if relationshipInfo && relationshipInfo.length > 0}
<Table
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
schema={relationshipSchema}
data={relationshipInfo}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
{:else}
<Body size="S"><i>No relationships configured.</i></Body>
{/if}
{/if}
<style>
.query-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 0 0 var(--spacing-s) 0;
}
.table-buttons {
display: flex;
gap: var(--spacing-m);
}
.table-checkboxes {
width: 100%;
}
</style>

View File

@ -1,86 +0,0 @@
<script>
import { Body, notifications } from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import ICONS from "../icons"
export let integration = {}
let integrations = []
const INTERNAL = "BUDIBASE"
async function fetchIntegrations() {
let otherIntegrations
try {
otherIntegrations = await API.getIntegrations()
} catch (error) {
otherIntegrations = {}
notifications.error("Error getting integrations")
}
integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...otherIntegrations,
}
}
function selectIntegration(integrationType) {
const selected = integrations[integrationType]
// build the schema
const schema = {}
for (let key of Object.keys(selected.datasource)) {
schema[key] = selected.datasource[key].default
}
integration = {
type: integrationType,
plus: selected.plus,
...schema,
}
}
onMount(() => {
fetchIntegrations()
})
</script>
<section>
<div class="integration-list">
{#each Object.entries(integrations) as [integrationType, schema]}
<div
class="integration hoverable"
class:selected={integration.type === integrationType}
on:click={() => selectIntegration(integrationType)}
>
<svelte:component
this={ICONS[integrationType]}
height="50"
width="50"
/>
<Body size="XS">{schema.name || integrationType}</Body>
</div>
{/each}
</div>
</section>
<style>
.integration-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-gap: var(--spectrum-alias-grid-baseline);
}
.integration {
display: grid;
background: var(--background-alt);
place-items: center;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
transition: 0.3s all;
border-radius: var(--spectrum-alias-item-rounded-border-radius-s);
}
.integration:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
</style>

View File

@ -1,137 +0,0 @@
<script>
import {
Divider,
Heading,
ActionButton,
Badge,
Body,
Layout,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
import {
getRestBindings,
getEnvironmentBindings,
readableToRuntimeBinding,
runtimeToReadableMap,
} from "builderStore/dataBinding"
import { cloneDeep } from "lodash/fp"
import { licensing } from "stores/portal"
export let datasource
export let queries
let addHeader
let parsedHeaders = runtimeToReadableMap(
getRestBindings(),
cloneDeep(datasource?.config?.defaultHeaders)
)
const onDefaultHeaderUpdate = headers => {
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
return acc
}, {})
datasource.config.defaultHeaders = flatHeaders
}
</script>
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Headers</Heading>
<Badge quiet grey>Optional</Badge>
</div>
<div class="headerRight">
<slot name="headerRight" />
</div>
</div>
<Body size="S">
Headers enable you to provide additional information about the request, such
as format.
</Body>
<KeyValueBuilder
bind:this={addHeader}
bind:object={parsedHeaders}
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
noAddButton
bindings={getRestBindings()}
/>
<div>
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
Add header
</ActionButton>
</div>
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Authentication</Heading>
<Badge quiet grey>Optional</Badge>
</div>
<div class="headerRight">
<slot name="headerRight" />
</div>
</div>
<Body size="S">
Create an authentication config that can be shared with queries.
</Body>
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
<Divider />
<div class="section-header">
<div class="badge">
<Heading size="S">Variables</Heading>
<Badge quiet grey>Optional</Badge>
</div>
<div class="headerRight">
<slot name="headerRight" />
</div>
</div>
<Body size="S"
>Variables enable you to store and re-use values in queries, with the choice
of a static value such as a token using static variables, or a value from a
query response using dynamic variables.</Body
>
<Heading size="XS">Static</Heading>
<Layout noPadding gap="XS">
<KeyValueBuilder
name="Variable"
keyPlaceholder="Name"
headings
bind:object={datasource.config.staticVariables}
on:change
bindings={$licensing.environmentVariablesEnabled
? getEnvironmentBindings()
: []}
/>
</Layout>
<div />
<Heading size="XS">Dynamic</Heading>
<Body size="S">
Dynamic variables are evaluated when a dependant query is executed. The value
is cached for a period of time and will be refreshed if a query fails.
</Body>
<ViewDynamicVariables {queries} {datasource} />
<style>
.section-header {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.badge {
display: flex;
gap: var(--spacing-m);
}
.headerRight {
margin-left: auto;
}
</style>

View File

@ -0,0 +1,66 @@
<script>
import { Modal } from "@budibase/bbui"
import { get } from "svelte/store"
import CreateEditRelationship from "./CreateEditRelationship.svelte"
import { integrations, datasources } from "stores/backend"
import { integrationForDatasource } from "stores/selectors"
export let datasource
export let tables
export let beforeSave = async () => {}
export let afterSave = async () => {}
export let onError = async () => {}
let relationshipModal
let fromRelationship = {}
let toRelationship = {}
let fromTable = null
export function show({
fromRelationship: selectedFromRelationship = {},
toRelationship: selectedToRelationship = {},
fromTable: selectedFromTable = null,
}) {
fromRelationship = selectedFromRelationship
toRelationship = selectedToRelationship
fromTable = selectedFromTable
relationshipModal.show()
}
export function hide() {
relationshipModal.hide()
}
// action is one of 'created', 'updated' or 'deleted'
async function saveRelationship(action) {
try {
await beforeSave({ action, datasource })
const integration = integrationForDatasource(
get(integrations),
datasource
)
await datasources.update({ datasource, integration })
await afterSave({ datasource, action })
} catch (err) {
await onError({ err, datasource, action })
}
}
</script>
<Modal bind:this={relationshipModal}>
<CreateEditRelationship
save={saveRelationship}
close={relationshipModal.hide}
selectedFromTable={fromTable}
{datasource}
plusTables={tables}
{fromRelationship}
{toRelationship}
/>
</Modal>
<style>
</style>

View File

@ -40,6 +40,8 @@
let userOnboardResponse = null
let userLimitReachedModal
let inviteFailureResponse = ""
$: queryIsEmail = emailValidator(query) === true
$: prodAppId = apps.getProdAppID($store.appId)
$: promptInvite = showInvite(
@ -308,19 +310,6 @@
let userInviteResponse
try {
userInviteResponse = await users.onboard(payload)
const newUser = userInviteResponse?.successful.find(
user => user.email === newUserEmail
)
if (newUser) {
notifications.success(
userInviteResponse.created
? "User created successfully"
: "User invite successful"
)
} else {
throw new Error("User invite failed")
}
} catch (error) {
console.error(error.message)
notifications.error("Error inviting user")
@ -331,12 +320,31 @@
const onInviteUser = async () => {
userOnboardResponse = await inviteUser()
const originalQuery = query + ""
query = null
const userInviteSuccess = userOnboardResponse?.successful
if (userInviteSuccess && userInviteSuccess[0].email === query) {
query = null
query = userInviteSuccess[0].email
const newUser = userOnboardResponse?.successful.find(
user => user.email === originalQuery
)
if (newUser) {
query = originalQuery
notifications.success(
userOnboardResponse.created
? "User created successfully"
: "User invite successful"
)
} else {
const failedUser = userOnboardResponse?.unsuccessful.find(
user => user.email === originalQuery
)
inviteFailureResponse =
failedUser?.reason === "Unavailable"
? "Email already in use. Please use a different email."
: failedUser?.reason
notifications.error(inviteFailureResponse)
}
userOnboardResponse = null
}
const onUpdateUserInvite = async (invite, role) => {

View File

@ -33,7 +33,7 @@
// A REST integration is created immediately, we don't need to display a config modal.
loading = true
datasources
.create({ integration, fields: configFromIntegration(integration) })
.create({ integration, config: configFromIntegration(integration) })
.then(datasource => {
store.setIntegration(integration)
store.setDatasource(datasource)

View File

@ -3,9 +3,11 @@
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
import { Helpers } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
export let configs = []
export let authConfigs = []
const dispatch = createEventDispatcher()
let currentConfig = null
let modal
@ -20,8 +22,10 @@
}
const onConfirm = config => {
let newAuthConfigs
if (currentConfig) {
configs = configs.map(c => {
newAuthConfigs = authConfigs.map(c => {
// replace the current config with the new one
if (c._id === currentConfig._id) {
return config
@ -30,27 +34,36 @@
})
} else {
config._id = Helpers.uuid()
configs = [...configs, config]
newAuthConfigs = [...authConfigs, config]
}
dispatch("change", newAuthConfigs)
}
const onRemove = () => {
configs = configs.filter(c => {
const newAuthConfigs = authConfigs.filter(c => {
return c._id !== currentConfig._id
})
dispatch("change", newAuthConfigs)
}
</script>
<Modal bind:this={modal}>
<RestAuthenticationModal {configs} {currentConfig} {onConfirm} {onRemove} />
<RestAuthenticationModal
configs={authConfigs}
{currentConfig}
{onConfirm}
{onRemove}
/>
</Modal>
<Layout gap="S" noPadding>
{#if configs && configs.length > 0}
{#if authConfigs && authConfigs.length > 0}
<Table
on:click={({ detail }) => openConfigModal(detail)}
{schema}
data={configs}
data={authConfigs}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}

View File

@ -0,0 +1,27 @@
<script>
import RestAuthenticationBuilder from "./RestAuthenticationBuilder.svelte"
import { cloneDeep } from "lodash/fp"
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
import Panel from "../Panel.svelte"
import Tooltip from "../Tooltip.svelte"
export let datasource
$: updatedDatasource = cloneDeep(datasource)
const updateAuthConfigs = newAuthConfigs => {
updatedDatasource.config.authConfigs = newAuthConfigs
}
</script>
<Panel>
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
<Tooltip
slot="tooltip"
title="REST Authentication"
href="https://docs.budibase.com/docs/rest-authentication"
/>
<RestAuthenticationBuilder
on:change={({ detail }) => updateAuthConfigs(detail)}
authConfigs={updatedDatasource.config.authConfigs}
/>
</Panel>

View File

@ -0,0 +1,53 @@
<script>
import { ActionButton } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import {
getRestBindings,
readableToRuntimeBinding,
runtimeToReadableMap,
} from "builderStore/dataBinding"
import { cloneDeep } from "lodash/fp"
import SaveDatasourceButton from "./SaveDatasourceButton.svelte"
import Panel from "./Panel.svelte"
import Tooltip from "./Tooltip.svelte"
export let datasource
let restBindings = getRestBindings()
let addHeader
$: updatedDatasource = cloneDeep(datasource)
$: parsedHeaders = runtimeToReadableMap(
restBindings,
updatedDatasource?.config?.defaultHeaders
)
const onDefaultHeaderUpdate = headers => {
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
acc[entry.name] = readableToRuntimeBinding(restBindings, entry.value)
return acc
}, {})
updatedDatasource.config.defaultHeaders = flatHeaders
}
</script>
<Panel>
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
<Tooltip
slot="tooltip"
title="REST Headers"
href="https://docs.budibase.com/docs/rest-queries#headers"
/>
<KeyValueBuilder
bind:this={addHeader}
object={parsedHeaders}
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
noAddButton
bindings={restBindings}
/>
<div>
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
Add header
</ActionButton>
</div>
</Panel>

View File

@ -0,0 +1,27 @@
<script>
</script>
<section>
<div class="header">
<div class="controls">
<slot name="controls" />
</div>
<div class="tooltip">
<slot name="tooltip" />
</div>
</div>
<div>
<slot />
</div>
</section>
<style>
.header {
display: flex;
margin-bottom: 15px;
}
.tooltip {
margin-left: auto;
}
</style>

View File

@ -0,0 +1,21 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
let importQueriesModal
</script>
<Modal bind:this={importQueriesModal}>
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} />
</Modal>
<div class="button">
<Button secondary on:click={() => importQueriesModal.show()}>Import</Button>
</div>
<style>
.button {
display: inline-block;
margin-left: 8px;
}
</style>

View File

@ -0,0 +1,132 @@
<script>
import { goto } from "@roxi/routify"
import {
ModalContent,
notifications,
Body,
Layout,
Tabs,
Tab,
Heading,
TextArea,
Dropzone,
} from "@budibase/bbui"
import { datasources, queries } from "stores/backend"
import { writable } from "svelte/store"
export let navigateDatasource = false
export let datasourceId
export let createDatasource = false
export let onCancel
const data = writable({
url: "",
raw: "",
file: undefined,
})
let lastTouched = "url"
const getData = async () => {
let dataString
// parse the file into memory and send as string
if (lastTouched === "file") {
dataString = await $data.file.text()
} else if (lastTouched === "url") {
const response = await fetch($data.url)
dataString = await response.text()
} else if (lastTouched === "raw") {
dataString = $data.raw
}
return dataString
}
async function importQueries() {
try {
const dataString = await getData()
if (!datasourceId && !createDatasource) {
throw new Error("No datasource id")
}
const body = {
data: dataString,
datasourceId,
}
const importResult = await queries.import(body)
if (!datasourceId) {
datasourceId = importResult.datasourceId
}
// reload
await datasources.fetch()
await queries.fetch()
if (navigateDatasource) {
$goto(`./datasource/${datasourceId}`)
}
notifications.success("Imported successfully")
return true
} catch (error) {
notifications.error("Error importing queries")
return false
}
}
</script>
<ModalContent
onConfirm={() => importQueries()}
{onCancel}
confirmText={"Import"}
cancelText="Back"
size="L"
>
<Layout noPadding>
<Heading size="S">Import</Heading>
<Body size="XS"
>Import your rest collection using one of the options below</Body
>
<Tabs selected="File">
<!-- Commenting until nginx csp issue resolved -->
<!-- <Tab title="Link">
<Input
bind:value={$data.url}
on:change={() => (lastTouched = "url")}
label="Enter a URL"
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
/>
</Tab> -->
<Tab title="File">
<Dropzone
gallery={false}
value={$data.file ? [$data.file] : []}
on:change={e => {
$data.file = e.detail?.[0]
lastTouched = "file"
}}
fileTags={[
"OpenAPI 3.0",
"OpenAPI 2.0",
"Swagger 2.0",
"cURL",
"YAML",
"JSON",
]}
maximum={1}
/>
</Tab>
<Tab title="Raw Text">
<TextArea
bind:value={$data.raw}
on:change={() => (lastTouched = "raw")}
label={"Paste raw text"}
placeholder={'e.g. curl --location --request GET "https://example.com"'}
/>
</Tab>
</Tabs>
</Layout>
</ModalContent>

View File

@ -0,0 +1,43 @@
<script>
import { goto } from "@roxi/routify"
import { Button, Table } from "@budibase/bbui"
import { queries } from "stores/backend"
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
import RestImportButton from "./RestImportButton.svelte"
import Panel from "../Panel.svelte"
import Tooltip from "../Tooltip.svelte"
export let datasource
$: queryList = $queries.list.filter(
query => query.datasourceId === datasource._id
)
</script>
<Panel>
<div slot="controls">
<Button cta on:click={() => $goto(`../../query/new/${datasource._id}`)}>
Create new query
</Button>
{#if datasource.source === "REST"}
<RestImportButton datasourceId={datasource._id} />
{/if}
</div>
<Tooltip
slot="tooltip"
title="Custom queries"
href="https://docs.budibase.com/docs/data-sources#custom-queries "
/>
<Table
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
schema={{
name: {},
queryVerb: { displayName: "Method" },
}}
data={queryList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "queryVerb", component: CapitaliseRenderer }]}
/>
</Panel>

View File

@ -0,0 +1,108 @@
<script>
import { Button, Table, notifications } from "@budibase/bbui"
import CreateEditRelationshipModal from "components/backend/Datasources/CreateEditRelationshipModal.svelte"
import {
tables as tablesStore,
integrations as integrationsStore,
datasources as datasourcesStore,
} from "stores/backend"
import { DatasourceFeature } from "@budibase/types"
import { API } from "api"
import Panel from "./Panel.svelte"
import Tooltip from "./Tooltip.svelte"
export let datasource
let modal
$: tables = Object.values(datasource.entities)
$: relationships = getRelationships(tables)
function getRelationships(tables) {
const relatedColumns = {}
tables.forEach(({ name: tableName, schema }) => {
Object.values(schema).forEach(column => {
if (column.type !== "link") return
relatedColumns[column._id] ??= {}
relatedColumns[column._id].through =
relatedColumns[column._id].through || column.through
relatedColumns[column._id][column.main ? "from" : "to"] = {
...column,
tableName,
}
})
})
return Object.values(relatedColumns).map(({ from, to, through }) => {
return {
tables: `${from.tableName} ${through ? "↔" : "→"} ${to.tableName}`,
columns: `${from.name} to ${to.name}`,
from,
to,
}
})
}
const handleRowClick = ({ detail }) => {
modal.show({ fromRelationship: detail.from, toRelationship: detail.to })
}
const beforeSave = async ({ datasource }) => {
const integration = $integrationsStore[datasource.source]
if (!integration.features[DatasourceFeature.CONNECTION_CHECKING]) return
const { connected } = await API.validateDatasource(datasource)
if (!connected) {
throw "Invalid connection"
}
}
const afterSave = async ({ action }) => {
await tablesStore.fetch()
await datasourcesStore.fetch()
notifications.success(`Relationship ${action} successfully`)
}
const onError = async ({ action, err }) => {
let notificationVerb = "creating"
if (action === "updated") {
notificationVerb = "updating"
} else if (action === "deleted") {
notificationVerb = "deleting"
}
notifications.error(`Error ${notificationVerb} datasource: ${err}`)
}
</script>
<CreateEditRelationshipModal
bind:this={modal}
{datasource}
{beforeSave}
{afterSave}
{onError}
{tables}
/>
<Panel>
<Button slot="controls" cta on:click={modal.show}>
Define relationships
</Button>
<Tooltip
slot="tooltip"
title="Relationships"
href="https://docs.budibase.com/docs/relationships"
/>
<Table
on:click={handleRowClick}
schema={{ tables: {}, columns: {} }}
data={relationships}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
</Panel>

View File

@ -0,0 +1,29 @@
<script>
import { get } from "svelte/store"
import { isEqual } from "lodash"
import { integrationForDatasource } from "stores/selectors"
import { integrations, datasources } from "stores/backend"
import { notifications, Button } from "@budibase/bbui"
export let datasource
export let updatedDatasource
$: hasChanged = !isEqual(datasource, updatedDatasource)
const save = async () => {
try {
const integration = integrationForDatasource(
get(integrations),
updatedDatasource
)
await datasources.update({ datasource: updatedDatasource, integration })
notifications.success(
`Datasource ${updatedDatasource.name} updated successfully`
)
} catch (error) {
notifications.error(`Error saving datasource: ${error.message}`)
}
}
</script>
<Button disabled={!hasChanged} cta on:click={save}>Save</Button>

View File

@ -0,0 +1,71 @@
<script>
import { Body, Button, notifications, Modal, Toggle } from "@budibase/bbui"
import { datasources, tables } from "stores/backend"
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import ValuesList from "components/common/ValuesList.svelte"
export let datasource
let createExternalTableModal
let confirmDialog
let specificTables = null
let requireSpecificTables = false
async function updateDatasourceSchema() {
try {
await datasources.updateSchema(datasource, specificTables)
notifications.success(`Datasource ${name} tables updated successfully`)
await tables.fetch()
} catch (error) {
notifications.error(
`Error updating datasource schema ${
error?.message ? `: ${error.message}` : ""
}`
)
}
}
</script>
<Modal bind:this={createExternalTableModal}>
<CreateExternalTableModal {datasource} />
</Modal>
<ConfirmDialog
bind:this={confirmDialog}
okText="Fetch tables"
onOk={updateDatasourceSchema}
onCancel={() => confirmDialog.hide()}
warning={false}
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>
If you have fetched tables from this database before, this action may
overwrite any changes you made after your initial fetch.
</Body>
</ConfirmDialog>
<div class="buttons">
<Button cta on:click={createExternalTableModal.show}>Create new table</Button>
<Button secondary on:click={confirmDialog.show}>Fetch tables</Button>
</div>
<style>
.buttons {
display: flex;
gap: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,33 @@
<script>
import { goto } from "@roxi/routify"
import { Table } from "@budibase/bbui"
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
import Controls from "./Controls.svelte"
import Panel from "../Panel.svelte"
import Tooltip from "../Tooltip.svelte"
export let datasource
let tableSchema = {
name: {},
primary: { displayName: "Primary Key" },
}
</script>
<Panel>
<Controls slot="controls" {datasource} />
<Tooltip
slot="tooltip"
title="Using data in your app"
href="https://docs.budibase.com/docs/data"
/>
<Table
on:click={({ detail: table }) => $goto(`../../table/${table._id}`)}
schema={tableSchema}
data={Object.values(datasource?.entities || {})}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
/>
</Panel>

View File

@ -0,0 +1,10 @@
<script>
import { ActionButton } from "@budibase/bbui"
export let title = ""
export let href = null
</script>
<ActionButton quiet icon="Help" on:click={() => window.open(href, "_blank")}>
{title}
</ActionButton>

View File

@ -1,14 +1,13 @@
<script>
import { Body, Table, BoldRenderer, CodeRenderer } from "@budibase/bbui"
import { queries as queriesStore } from "stores/backend"
import { queries } from "stores/backend"
import { goto } from "@roxi/routify"
export let datasource
export let queries
let dynamicVariables = []
$: enrichDynamicVariables(datasource, queries)
$: enrichDynamicVariables(datasource, $queries.list)
const dynamicVariableSchema = {
name: "",
@ -18,7 +17,7 @@
const onClick = dynamicVariable => {
const queryId = dynamicVariable.queryId
queriesStore.select({ _id: queryId })
queries.select({ _id: queryId })
$goto(`./${queryId}`)
}

View File

@ -0,0 +1,59 @@
<script>
import { Heading, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import ViewDynamicVariables from "./ViewDynamicVariables.svelte"
import { getEnvironmentBindings } from "builderStore/dataBinding"
import { licensing } from "stores/portal"
import { queries } from "stores/backend"
import { cloneDeep } from "lodash/fp"
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
import Panel from "../Panel.svelte"
import Tooltip from "../Tooltip.svelte"
export let datasource
$: updatedDatasource = cloneDeep(datasource)
$: queriesForDatasource = $queries.list.filter(
query => query.datasourceId === datasource?._id
)
const handleChange = newUnparsedStaticVariables => {
const newStaticVariables = {}
newUnparsedStaticVariables.forEach(({ name, value }) => {
newStaticVariables[name] = value
})
updatedDatasource.config.staticVariables = newStaticVariables
}
</script>
<Panel>
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
<Tooltip
slot="tooltip"
title="REST variables"
href="https://docs.budibase.com/docs/rest-variables"
/>
<Layout>
<Layout noPadding gap="XS">
<Heading size="S">Static</Heading>
<KeyValueBuilder
name="Variable"
keyPlaceholder="Name"
headings
object={updatedDatasource.config.staticVariables}
on:change={({ detail }) => handleChange(detail)}
bindings={$licensing.environmentVariablesEnabled
? getEnvironmentBindings()
: []}
/>
</Layout>
<Layout noPadding gap="XS">
<Heading size="S">Dynamic</Heading>
<ViewDynamicVariables queries={queriesForDatasource} {datasource} />
</Layout>
</Layout>
</Panel>

View File

@ -1,163 +1,89 @@
<script>
import { goto } from "@roxi/routify"
import {
Button,
Heading,
Body,
Divider,
Layout,
notifications,
Table,
Modal,
} from "@budibase/bbui"
import { datasources, integrations, queries, tables } from "stores/backend"
import EditDatasourceConfig from "./_components/EditDatasourceConfig.svelte"
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte"
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
import { datasources, integrations } from "stores/backend"
import ICONS from "components/backend/DatasourceNavigator/icons"
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
import { IntegrationTypes } from "constants/backend"
import { isEqual } from "lodash"
import { cloneDeep } from "lodash/fp"
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
import Spinner from "components/common/Spinner.svelte"
import EditDatasourceConfig from "./_components/EditDatasourceConfig.svelte"
import TablesPanel from "./_components/panels/Tables/index.svelte"
import RelationshipsPanel from "./_components/panels/Relationships.svelte"
import QueriesPanel from "./_components/panels/Queries/index.svelte"
import RestHeadersPanel from "./_components/panels/Headers.svelte"
import RestAuthenticationPanel from "./_components/panels/Authentication/index.svelte"
import RestVariablesPanel from "./_components/panels/Variables/index.svelte"
const querySchema = {
name: {},
queryVerb: { displayName: "Method" },
}
let selectedPanel = null
let panelOptions = []
let importQueriesModal
let changed = false
let isValid = true
let integration, baseDatasource, datasource
let queryList
let loading = false
// datasources.selected can return null temporarily on datasource deletion
$: datasource = $datasources.selected || {}
$: baseDatasource = $datasources.selected
$: queryList = $queries.list.filter(
query => query.datasourceId === datasource?._id
)
$: hasChanged(baseDatasource, datasource)
$: updateDatasource(baseDatasource)
$: getOptions(datasource)
const hasChanged = (base, ds) => {
if (base && ds) {
changed = !isEqual(base, ds)
}
}
const saveDatasource = async () => {
try {
// Create datasource
await datasources.update({ datasource, integration })
if (datasource?.plus) {
await tables.fetch()
}
await datasources.fetch()
notifications.success(`Datasource ${name} updated successfully.`)
baseDatasource = cloneDeep(datasource)
} catch (err) {
notifications.error(`Error saving datasource: ${err}`)
}
}
const updateDatasource = base => {
if (base) {
datasource = cloneDeep(base)
integration = $integrations[datasource.source]
const getOptions = datasource => {
if (datasource.plus) {
// Google Sheets' integration definition specifies `relationships: false` as it doesn't support relationships like other plus datasources
panelOptions =
$integrations[datasource.source].relationships === false
? ["Tables", "Queries"]
: ["Tables", "Relationships", "Queries"]
// TODO what is this for?
selectedPanel = panelOptions.includes(selectedPanel)
? selectedPanel
: "Tables"
} else if (datasource.source === "REST") {
panelOptions = ["Queries", "Headers", "Authentication", "Variables"]
selectedPanel = panelOptions.includes(selectedPanel)
? selectedPanel
: "Queries"
} else {
panelOptions = ["Queries"]
selectedPanel = "Queries"
}
}
</script>
<Modal bind:this={importQueriesModal}>
{#if datasource.source === "REST"}
<ImportRestQueriesModal
createDatasource={false}
datasourceId={datasource._id}
/>
{/if}
</Modal>
{#if datasource && integration}
<section>
<Layout noPadding>
<Layout gap="XS" noPadding>
<header>
<svelte:component
this={ICONS[datasource.source]}
height="26"
width="26"
/>
<Heading size="M">{$datasources.selected?.name}</Heading>
</header>
</Layout>
<EditDatasourceConfig {datasource} />
{#if datasource.plus}
<PlusConfigForm bind:datasource save={saveDatasource} />
{/if}
<Divider />
<div class="query-header">
<Heading size="S">Queries</Heading>
<div class="query-buttons">
{#if datasource?.source === IntegrationTypes.REST}
<Button secondary on:click={() => importQueriesModal.show()}>
Import
</Button>
{/if}
<Button
cta
icon="Add"
on:click={() => $goto(`../../query/new/${datasource._id}`)}
>
Add query
</Button>
</div>
</div>
<Body size="S">
To build an app using a datasource, you must first query the data. A
query is a request for data or information from a datasource, for
example a database table.
</Body>
{#if queryList && queryList.length > 0}
<div class="query-list">
<Table
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
schema={querySchema}
data={queryList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[
{ column: "queryVerb", component: CapitaliseRenderer },
]}
/>
</div>
{/if}
{#if datasource?.source === IntegrationTypes.REST}
<RestExtraConfigForm
queries={queryList}
bind:datasource
on:change={hasChanged}
>
<Button
slot="headerRight"
disabled={!changed || !isValid || loading}
cta
on:click={saveDatasource}
>
<div class="save-button-content">
{#if loading}
<Spinner size="10">Save</Spinner>
{/if}
Save
</div>
</Button>
</RestExtraConfigForm>
{/if}
<section>
<Layout noPadding>
<Layout gap="XS" noPadding>
<header>
<svelte:component
this={ICONS[datasource.source]}
height="26"
width="26"
/>
<Heading size="M">{$datasources.selected?.name}</Heading>
</header>
</Layout>
</section>
{/if}
<EditDatasourceConfig {datasource} />
<div class="tabs">
<Tabs size="L" noPadding noHorizPadding selected={selectedPanel}>
{#each panelOptions as panelOption}
<Tab
title={panelOption}
on:click={() => (selectedPanel = panelOption)}
/>
{/each}
</Tabs>
</div>
{#if selectedPanel === null}
<Body>loading...</Body>
{:else if selectedPanel === "Tables"}
<TablesPanel {datasource} />
{:else if selectedPanel === "Relationships"}
<RelationshipsPanel {datasource} />
{:else if selectedPanel === "Queries"}
<QueriesPanel {datasource} />
{:else if selectedPanel === "Headers"}
<RestHeadersPanel {datasource} />
{:else if selectedPanel === "Authentication"}
<RestAuthenticationPanel {datasource} />
{:else if selectedPanel === "Variables"}
<RestVariablesPanel {datasource} />
{:else}
<Body>Something went wrong</Body>
{/if}
</Layout>
</section>
<style>
section {
@ -166,32 +92,13 @@
}
header {
margin-top: 35px;
display: flex;
gap: var(--spacing-l);
align-items: center;
margin-bottom: 12px;
}
.query-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.query-buttons {
display: flex;
gap: var(--spacing-l);
}
.query-list {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.save-button-content {
display: flex;
align-items: center;
gap: var(--spacing-s);
.tabs {
}
</style>

View File

@ -29,14 +29,13 @@
}
})
$: src = !$oidc.logo
? OidcLogo
: preDefinedIcons[$oidc.logo] || `/global/logos_oidc/${$oidc.logo}`
$: oidcLogoImageURL = preDefinedIcons[$oidc.logo] ?? $oidc.logo
$: logoSrc = oidcLogoImageURL ?? OidcLogo
</script>
{#if show}
<FancyButton
icon={src}
icon={logoSrc}
on:click={() => {
const url = `/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`
if (samePage) {

View File

@ -125,11 +125,14 @@ export function createDatasourcesStore() {
}
const create = async ({ integration, config }) => {
const count = sourceCount(integration.name)
const nameModifier = count === 0 ? "" : ` ${count + 1}`
const datasource = {
type: "datasource",
source: integration.name,
config,
name: `${integration.friendlyName}-${sourceCount(integration.name) + 1}`,
name: `${integration.friendlyName}${nameModifier}`,
plus: integration.plus && integration.name !== IntegrationTypes.REST,
}

View File

@ -2,6 +2,7 @@
import { getContext } from "svelte"
import InnerForm from "./InnerForm.svelte"
import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
export let dataSource
export let theme
@ -23,6 +24,7 @@
let loaded = false
let schema
let table
let currentStep = writable(1)
$: fetchSchema(dataSource)
$: schemaKey = generateSchemaKey(schema)
@ -92,6 +94,7 @@
{initialValues}
{disableValidation}
{editAutoColumns}
{currentStep}
>
<slot />
</InnerForm>

View File

@ -13,16 +13,19 @@
export let disableValidation = false
export let editAutoColumns = false
// We export this store so that when we remount the inner form we can still
// persist what step we're on
export let currentStep
const component = getContext("component")
const { styleable, Provider, ActionTypes } = getContext("sdk")
let fields = []
const currentStep = writable(1)
const formState = writable({
values: {},
errors: {},
valid: true,
currentStep: 1,
currentStep: get(currentStep),
})
// Reactive derived stores to derive form state from field array

@ -1 +1 @@
Subproject commit d99e1baee70edf726bcc1a83684bf9bd641d04de
Subproject commit 544c7e067de69832469cde673e59501480d6d98a

View File

@ -14,7 +14,7 @@ jest.mock("../../../utilities/redis", () => ({
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils"
import { events, utils } from "@budibase/backend-core"
import { events, utils, context } from "@budibase/backend-core"
import env from "../../../environment"
jest.setTimeout(15000)
@ -324,4 +324,35 @@ describe("/applications", () => {
expect(events.app.unpublished).toBeCalledTimes(1)
})
})
describe("POST /api/applications/:appId/sync", () => {
it("should not sync automation logs", async () => {
// setup the apps
await config.createApp("testing-auto-logs")
const automation = await config.createAutomation()
await config.publish()
await context.doInAppContext(config.getProdAppId(), () => {
return config.createAutomationLog(automation)
})
// do the sync
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// does exist in prod
const prodLogs = await config.getAutomationLogs()
expect(prodLogs.data.length).toBe(1)
// delete prod app so we revert to dev log search
await config.unpublish()
// doesn't exist in dev
const devLogs = await config.getAutomationLogs()
expect(devLogs.data.length).toBe(0)
})
})
})

View File

@ -382,18 +382,26 @@ class MongoIntegration implements IntegrationBase {
return this.client.connect()
}
createObjectIds(json: any) {
matchId(value?: string) {
return value?.match(/(?<=objectid\(['"]).*(?=['"]\))/gi)?.[0]
}
hasObjectId(value?: any): boolean {
return (
typeof value === "string" && value.toLowerCase().startsWith("objectid")
)
}
createObjectIds(json: any): any {
const self = this
function interpolateObjectIds(json: any) {
for (let field of Object.keys(json || {})) {
if (json[field] instanceof Object) {
json[field] = self.createObjectIds(json[field])
}
if (
typeof json[field] === "string" &&
json[field].toLowerCase().startsWith("objectid")
) {
const id = json[field].match(/(?<=objectid\(['"]).*(?=['"]\))/gi)?.[0]
if (self.hasObjectId(json[field])) {
const id = self.matchId(json[field])
if (id) {
json[field] = ObjectId.createFromHexString(id)
}
@ -404,7 +412,14 @@ class MongoIntegration implements IntegrationBase {
if (Array.isArray(json)) {
for (let i = 0; i < json.length; i++) {
json[i] = interpolateObjectIds(json[i])
if (self.hasObjectId(json[i])) {
const id = self.matchId(json[i])
if (id) {
json[i] = ObjectId.createFromHexString(id)
}
} else {
json[i] = interpolateObjectIds(json[i])
}
}
return json
}

View File

@ -21,6 +21,7 @@ import {
basicScreen,
basicLayout,
basicWebhook,
basicAutomationResults,
} from "./structures"
import {
constants,
@ -48,6 +49,7 @@ import {
Table,
SearchFilters,
UserRoles,
Automation,
} from "@budibase/types"
import { BUILTIN_ROLE_IDS } from "@budibase/backend-core/src/security/roles"
@ -720,6 +722,26 @@ class TestConfiguration {
return { datasource, query: basedOnQuery }
}
// AUTOMATION LOG
async createAutomationLog(automation: Automation) {
return await context.doInAppContext(this.getProdAppId(), async () => {
return await pro.sdk.automations.logs.storeLog(
automation,
basicAutomationResults(automation._id!)
)
})
}
async getAutomationLogs() {
return context.doInAppContext(this.appId, async () => {
const now = new Date()
return await pro.sdk.automations.logs.logSearch({
startDate: new Date(now.getTime() - 100000).toISOString(),
})
})
}
// QUERY
async previewQuery(

View File

@ -9,6 +9,8 @@ import {
import {
Automation,
AutomationActionStepId,
AutomationResults,
AutomationStatus,
AutomationStep,
AutomationStepType,
AutomationTrigger,
@ -241,6 +243,23 @@ export function collectAutomation(tableId?: string): Automation {
return automation as Automation
}
export function basicAutomationResults(
automationId: string
): AutomationResults {
return {
automationId,
status: AutomationStatus.SUCCESS,
trigger: "trigger",
steps: [
{
stepId: AutomationActionStepId.SERVER_LOG,
inputs: {},
outputs: {},
},
],
}
}
export function basicRow(tableId: string) {
return {
name: "Test Contact",

View File

@ -79,6 +79,12 @@ export interface OIDCConfigs {
configs: OIDCInnerConfig[]
}
export interface OIDCLogosInnerConfig {
[key: string]: string
}
export interface OIDCLogosConfig extends Config<OIDCLogosInnerConfig> {}
export interface OIDCInnerConfig {
configUrl: string
clientID: string

View File

@ -28,6 +28,7 @@ import {
SSOConfig,
SSOConfigType,
UserCtx,
OIDCLogosConfig,
} from "@budibase/types"
import * as pro from "@budibase/pro"
@ -280,13 +281,39 @@ export async function save(ctx: UserCtx<Config>) {
}
}
function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
if (!oidcLogos) {
return
}
oidcLogos.config = Object.keys(oidcLogos.config || {}).reduce(
(acc: any, key: string) => {
if (!key.endsWith("Etag")) {
const etag = oidcLogos.config[`${key}Etag`]
const objectStoreUrl = objectStore.getGlobalFileUrl(
oidcLogos.type,
key,
etag
)
acc[key] = objectStoreUrl
} else {
acc[key] = oidcLogos.config[key]
}
return acc
},
{}
)
}
export async function find(ctx: UserCtx) {
try {
// Find the config with the most granular scope based on context
const type = ctx.params.type
const scopedConfig = await configs.getConfig(type)
let scopedConfig = await configs.getConfig(type)
if (scopedConfig) {
if (type === ConfigType.OIDC_LOGOS) {
enrichOIDCLogos(scopedConfig)
}
ctx.body = scopedConfig
} else {
// don't throw an error, there simply is nothing to return
@ -300,16 +327,21 @@ export async function find(ctx: UserCtx) {
export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
try {
// Find the config with the most granular scope based on context
const config = await configs.getOIDCConfig()
const oidcConfig = await configs.getOIDCConfig()
const oidcCustomLogos = await configs.getOIDCLogosDoc()
if (!config) {
if (oidcCustomLogos) {
enrichOIDCLogos(oidcCustomLogos)
}
if (!oidcConfig) {
ctx.body = []
} else {
ctx.body = [
{
logo: config.logo,
name: config.name,
uuid: config.uuid,
logo: oidcCustomLogos?.config[oidcConfig.logo] ?? oidcConfig.logo,
name: oidcConfig.name,
uuid: oidcConfig.uuid,
},
]
}

1
scripts/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -1,7 +1,7 @@
const fs = require("fs")
const semver = require("semver")
const filePath = "lerna.json"
const filePath = "../lerna.json"
const versionBump = process.argv[2] || "patch"
// Read and parse lerna.json file

10
scripts/package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "scripts",
"private": true,
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"semver": "^7.5.3"
}
}

View File

@ -7,10 +7,10 @@ then
fi
# Bump the version in lerna.json
node scripts/bumpVersion.js $1
node ./bumpVersion.js $1
NEW_VERSION=$(node -p "require('./lerna.json').version")
NEW_VERSION=$(node -p "require('../lerna.json').version")
git add lerna.json
git commit -m "Bump version to $NEW_VERSION"
git tag v$NEW_VERSION

22
scripts/yarn.lock Normal file
View File

@ -0,0 +1,22 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
semver@^7.5.3:
version "7.5.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
dependencies:
lru-cache "^6.0.0"
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==

View File

@ -22426,13 +22426,6 @@ semver@^7.2.1, semver@^7.3.5:
dependencies:
lru-cache "^6.0.0"
semver@^7.5.0:
version "7.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
dependencies:
lru-cache "^6.0.0"
semver@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"