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

Merge branch 'develop' of github.com:Budibase/budibase into feature/audit-logs

This commit is contained in:
mike12345567 2023-02-24 12:46:58 +00:00
commit 42fc004212
84 changed files with 1912 additions and 1230 deletions

View file

@ -68,83 +68,6 @@ jobs:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
deploy-to-release-env:
needs: [release-images]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the current budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Pull values.yaml from budibase-infra
run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.release.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml
wc -l values.release.yaml
- name: Deploy to Release Environment
uses: glopezep/helm@v1.7.1
with:
release: budibase-release
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
helm: helm3
values: |
globals:
appVersion: develop
ingress:
enabled: true
nginx: true
value-files: >-
[
"values.release.yaml"
]
env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Re roll app-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment app-service -n budibase
- name: Re roll proxy-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase
- name: Re roll worker-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment worker-service -n budibase
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
embed-title: ${{ env.RELEASE_VERSION }}
release-helm-chart:
needs: [release-images]
runs-on: ubuntu-latest

View file

@ -1,2 +1,2 @@
nodejs 14.19.3
python 3.11.1
python 3.10.0

View file

@ -1,5 +1,5 @@
{
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -18,13 +18,13 @@
"build:pro": "../../scripts/pro/build.sh",
"postbuild": "yarn run build:pro",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"test": "jest --coverage --runInBand",
"test": "bash scripts/test.sh",
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/nano": "10.1.1",
"@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "2.3.18-alpha.0",
"@budibase/types": "2.3.18-alpha.10",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",

View file

@ -0,0 +1,12 @@
#!/bin/bash
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
echo "jest --coverage --runInBand"
jest --coverage --runInBand
else
# --maxWorkers performs better in development
echo "jest --coverage"
jest --coverage
fi

View file

@ -85,17 +85,20 @@ export const doWithLock = async (opts: LockOptions, task: any) => {
const result = await task()
return result
} catch (e: any) {
console.log("lock error")
console.warn("lock error")
// lock limit exceeded
if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) {
// don't throw for try-once locks, they will always error
// due to retry count (0) exceeded
console.warn(e)
return
} else {
console.error(e)
throw e
}
} else {
console.error(e)
throw e
}
} finally {

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.10",
"@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1",

View file

@ -67,6 +67,9 @@
color: var(--spectrum-alias-icon-color-selected-hover) !important;
cursor: pointer;
}
svg.hoverable:active {
color: var(--spectrum-global-color-blue-400) !important;
}
svg.disabled {
color: var(--spectrum-global-color-gray-500) !important;

View file

@ -57,5 +57,7 @@
--spectrum-semantic-negative-icon-color: #e34850;
min-width: 100px;
margin: 0;
border-color: var(--spectrum-global-color-gray-400);
border-width: 1px;
}
</style>

View file

@ -21,7 +21,7 @@
label {
padding: 0;
white-space: nowrap;
color: var(--spectrum-global-color-gray-600);
color: var(--spectrum-global-color-gray-700);
}
.muted {

View file

@ -1,7 +1,7 @@
<script>
import "@spectrum-css/modal/dist/index-vars.css"
import "@spectrum-css/underlay/dist/index-vars.css"
import { createEventDispatcher, setContext, tick } from "svelte"
import { createEventDispatcher, setContext, tick, onMount } from "svelte"
import { fade, fly } from "svelte/transition"
import Portal from "svelte-portal"
import Context from "../context"
@ -62,9 +62,14 @@
}
setContext(Context.Modal, { show, hide, cancel })
</script>
<svelte:window on:keydown={handleKey} />
onMount(() => {
document.addEventListener("keydown", handleKey)
return () => {
document.removeEventListener("keydown", handleKey)
}
})
</script>
{#if inline}
{#if visible}

View file

@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => {
* @param obj the object to clone
*/
export const cloneDeep = obj => {
if (!obj) {
return obj
}
return JSON.parse(JSON.stringify(obj))
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -58,10 +58,10 @@
}
},
"dependencies": {
"@budibase/bbui": "2.3.18-alpha.0",
"@budibase/client": "2.3.18-alpha.0",
"@budibase/frontend-core": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/bbui": "2.3.18-alpha.10",
"@budibase/client": "2.3.18-alpha.10",
"@budibase/frontend-core": "2.3.18-alpha.10",
"@budibase/string-templates": "2.3.18-alpha.10",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@ -72,6 +72,7 @@
"codemirror": "^5.59.0",
"dayjs": "^1.11.2",
"downloadjs": "1.4.7",
"fast-json-patch": "^3.1.1",
"lodash": "4.17.21",
"posthog-js": "^1.36.0",
"remixicon": "2.5.0",

View file

@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history"
import { get } from "svelte/store"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
// Setup history for screens
export const screenHistoryStore = createHistoryStore({
getDoc: id => get(store).screens?.find(screen => screen._id === id),
selectDoc: store.actions.screens.select,
afterAction: () => {
// Ensure a valid component is selected
if (!get(selectedComponent)) {
store.update(state => ({
...state,
selectedComponentId: get(selectedScreen)?.props._id,
}))
}
},
})
store.actions.screens.save = screenHistoryStore.wrapSaveDoc(
store.actions.screens.save
)
store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc(
store.actions.screens.delete
)
// Setup history for automations
export const automationHistoryStore = createHistoryStore({
getDoc: automationStore.actions.getDefinition,
selectDoc: automationStore.actions.select,
})
automationStore.actions.save = automationHistoryStore.wrapSaveDoc(
automationStore.actions.save
)
automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc(
automationStore.actions.delete
)
export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
})
@ -71,3 +106,13 @@ export const selectedComponentPath = derived(
).map(component => component._id)
}
)
// Derived automation state
export const selectedAutomation = derived(automationStore, $automationStore => {
if (!$automationStore.selectedAutomationId) {
return null
}
return $automationStore.automations?.find(
x => x._id === $automationStore.selectedAutomationId
)
})

View file

@ -1,69 +0,0 @@
import { generate } from "shortid"
/**
* Class responsible for the traversing of the automation definition.
* Automation definitions are stored in linked lists.
*/
export default class Automation {
constructor(automation) {
this.automation = automation
}
hasTrigger() {
return this.automation.definition.trigger
}
addTestData(data) {
this.automation.testData = { ...this.automation.testData, ...data }
}
addBlock(block, idx) {
// Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") {
const trigger = { id: generate(), ...block }
this.automation.definition.trigger = trigger
return trigger
}
const newBlock = { id: generate(), ...block }
this.automation.definition.steps.splice(idx, 0, newBlock)
return newBlock
}
updateBlock(updatedBlock, id) {
const { steps, trigger } = this.automation.definition
if (trigger && trigger.id === id) {
this.automation.definition.trigger = updatedBlock
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1, updatedBlock)
this.automation.definition.steps = steps
}
deleteBlock(id) {
const { steps, trigger } = this.automation.definition
if (trigger && trigger.id === id) {
this.automation.definition.trigger = null
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1)
this.automation.definition.steps = steps
}
constructBlock(type, stepId, blockDefinition) {
return {
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type,
}
}
}

View file

@ -1,16 +1,18 @@
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
import { API } from "api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
import { generate } from "shortid"
import { selectedAutomation } from "builderStore"
const initialAutomationState = {
automations: [],
testResults: null,
showTestPanel: false,
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
selectedAutomationId: null,
}
export const getAutomationStore = () => {
@ -37,49 +39,41 @@ const automationActions = store => ({
API.getAutomationDefinitions(),
])
store.update(state => {
let selected = state.selectedAutomation?.automation
state.automations = responses[0]
state.automations.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
state.blockDefinitions = {
TRIGGER: responses[1].trigger,
ACTION: responses[1].action,
}
// If previously selected find the new obj and select it
if (selected) {
selected = responses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
}
return state
})
},
create: async ({ name }) => {
create: async (name, trigger) => {
const automation = {
name,
type: "automation",
definition: {
steps: [],
trigger,
},
}
const response = await API.createAutomation(automation)
store.update(state => {
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
const response = await store.actions.save(automation)
await store.actions.fetch()
store.actions.select(response._id)
return response
},
duplicate: async automation => {
const response = await API.createAutomation({
const response = await store.actions.save({
...automation,
name: `${automation.name} - copy`,
_id: undefined,
_ref: undefined,
})
store.update(state => {
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
await store.actions.fetch()
store.actions.select(response._id)
return response
},
save: async automation => {
const response = await API.updateAutomation(automation)
@ -90,11 +84,13 @@ const automationActions = store => ({
)
if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations]
store.actions.select(updatedAutomation)
return state
} else {
state.automations = [...state.automations, updatedAutomation]
}
return state
})
return response.automation
},
delete: async automation => {
await API.deleteAutomation({
@ -102,34 +98,83 @@ const automationActions = store => ({
automationRev: automation?._rev,
})
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === automation?._id
// Remove the automation
state.automations = state.automations.filter(
x => x._id !== automation._id
)
state.automations.splice(existingIdx, 1)
state.automations = [...state.automations]
state.selectedAutomation = null
state.selectedBlock = null
// Select a new automation if required
if (automation._id === state.selectedAutomationId) {
store.actions.select(state.automations[0]?._id)
}
return state
})
await store.actions.fetch()
},
updateBlockInputs: async (block, data) => {
// Create new modified block
let newBlock = {
...block,
inputs: {
...block.inputs,
...data,
},
}
// Remove any nullish or empty string values
Object.keys(newBlock.inputs).forEach(key => {
const val = newBlock.inputs[key]
if (val == null || val === "") {
delete newBlock.inputs[key]
}
})
// Create new modified automation
const automation = get(selectedAutomation)
const newAutomation = store.actions.getUpdatedDefinition(
automation,
newBlock
)
// Don't save if no changes were made
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
return
}
await store.actions.save(newAutomation)
},
test: async (automation, testData) => {
store.update(state => {
state.selectedAutomation.testResults = null
return state
})
const result = await API.testAutomation({
automationId: automation?._id,
testData,
})
if (!result?.trigger && !result?.steps?.length) {
throw "Something went wrong testing your automation"
}
store.update(state => {
state.selectedAutomation.testResults = result
state.testResults = result
return state
})
},
select: automation => {
getDefinition: id => {
return get(store).automations?.find(x => x._id === id)
},
getUpdatedDefinition: (automation, block) => {
let newAutomation = cloneDeep(automation)
if (automation.definition.trigger?.id === block.id) {
newAutomation.definition.trigger = block
} else {
const idx = automation.definition.steps.findIndex(x => x.id === block.id)
newAutomation.definition.steps.splice(idx, 1, block)
}
return newAutomation
},
select: id => {
if (!id || id === get(store).selectedAutomationId) {
return
}
store.update(state => {
state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedBlock = null
state.selectedAutomationId = id
state.testResults = null
state.showTestPanel = false
return state
})
},
@ -147,48 +192,57 @@ const automationActions = store => ({
appId,
})
},
addTestDataToAutomation: data => {
store.update(state => {
state.selectedAutomation.addTestData(data)
return state
})
addTestDataToAutomation: async data => {
let newAutomation = cloneDeep(get(selectedAutomation))
newAutomation.testData = {
...newAutomation.testData,
...data,
}
await store.actions.save(newAutomation)
},
addBlockToAutomation: (block, blockIdx) => {
store.update(state => {
state.selectedBlock = state.selectedAutomation.addBlock(
cloneDeep(block),
blockIdx
)
return state
})
constructBlock(type, stepId, blockDefinition) {
return {
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type,
id: generate(),
}
},
toggleFieldControl: value => {
store.update(state => {
state.selectedBlock.rowControl = value
return state
})
addBlockToAutomation: async (block, blockIdx) => {
const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation)
if (!automation) {
return
}
newAutomation.definition.steps.splice(blockIdx, 0, block)
await store.actions.save(newAutomation)
},
deleteAutomationBlock: block => {
store.update(state => {
const idx =
state.selectedAutomation.automation.definition.steps.findIndex(
x => x.id === block.id
)
state.selectedAutomation.deleteBlock(block.id)
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
toggleRowControl: async (block, rowControl) => {
const newBlock = { ...block, rowControl }
const newAutomation = store.actions.getUpdatedDefinition(
get(selectedAutomation),
newBlock
)
await store.actions.save(newAutomation)
},
deleteAutomationBlock: async block => {
const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation)
// Select next closest step
const steps = state.selectedAutomation.automation.definition.steps
let nextSelectedBlock
if (steps[idx] != null) {
nextSelectedBlock = steps[idx]
} else if (steps[idx - 1] != null) {
nextSelectedBlock = steps[idx - 1]
} else {
nextSelectedBlock =
state.selectedAutomation.automation.definition.trigger || null
}
state.selectedBlock = nextSelectedBlock
return state
})
// Delete trigger if required
if (newAutomation.definition.trigger?.id === block.id) {
delete newAutomation.definition.trigger
} else {
// Otherwise remove step
newAutomation.definition.steps = newAutomation.definition.steps.filter(
step => step.id !== block.id
)
}
await store.actions.save(newAutomation)
},
})

View file

@ -1,48 +0,0 @@
import Automation from "../Automation"
import TEST_AUTOMATION from "./testAutomation"
const TEST_BLOCK = {
id: "AUXJQGZY7",
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the automation until an amount of time has passed.",
params: { time: "number" },
type: "LOGIC",
args: { time: "5000" },
stepId: "DELAY",
}
describe("Automation Data Object", () => {
let automation
beforeEach(() => {
automation = new Automation({ ...TEST_AUTOMATION })
})
it("adds a automation block to the automation", () => {
automation.addBlock(TEST_BLOCK)
expect(automation.automation.definition)
})
it("updates a automation block with new attributes", () => {
const firstBlock = automation.automation.definition.steps[0]
const updatedBlock = {
...firstBlock,
name: "UPDATED",
}
automation.updateBlock(updatedBlock, firstBlock.id)
expect(automation.automation.definition.steps[0]).toEqual(updatedBlock)
})
it("deletes a automation block successfully", () => {
const { steps } = automation.automation.definition
const originalLength = steps.length
const lastBlock = steps[steps.length - 1]
automation.deleteBlock(lastBlock.id)
expect(automation.automation.definition.steps.length).toBeLessThan(
originalLength
)
})
})

View file

@ -1,78 +0,0 @@
export default {
name: "Test automation",
definition: {
steps: [
{
id: "ANBDINAPS",
description: "Send an email.",
tagline: "Send email to <b>{{to}}</b>",
icon: "ri-mail-open-fill",
name: "Send Email",
params: {
to: "string",
from: "string",
subject: "longText",
text: "longText",
},
type: "ACTION",
args: {
text: "A user was created!",
subject: "New Budibase User",
from: "budimaster@budibase.com",
to: "test@test.com",
},
stepId: "SEND_EMAIL",
},
],
trigger: {
id: "iRzYMOqND",
name: "Row Saved",
event: "row:save",
icon: "ri-save-line",
tagline: "Row is added to <b>{{table.name}}</b>",
description: "Fired when a row is saved to your database.",
params: { table: "table" },
type: "TRIGGER",
args: {
table: {
type: "table",
views: {},
name: "users",
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: 123 },
presence: { allowEmpty: false },
},
name: "name",
},
age: {
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: false },
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
name: "age",
},
},
_id: "c6b4e610cd984b588837bca27188a451",
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
},
},
stepId: "ROW_SAVED",
},
},
type: "automation",
ok: true,
id: "b384f861f4754e1693835324a7fcca62",
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
live: false,
_id: "b384f861f4754e1693835324a7fcca62",
_rev: "108-4116829ec375e0481d0ecab9e83a2caf",
}

View file

@ -1,6 +1,11 @@
import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { selectedScreen, selectedComponent } from "builderStore"
import {
selectedScreen,
selectedComponent,
screenHistoryStore,
automationHistoryStore,
} from "builderStore"
import {
datasources,
integrations,
@ -122,6 +127,8 @@ export const getFrontendStore = () => {
navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [],
}))
screenHistoryStore.reset()
automationHistoryStore.reset()
// Initialise backend stores
database.set(application.instance)
@ -179,10 +186,7 @@ export const getFrontendStore = () => {
}
// Check screen isn't already selected
if (
state.selectedScreenId === screen._id &&
state.selectedComponentId === screen.props?._id
) {
if (state.selectedScreenId === screen._id) {
return
}
@ -256,7 +260,7 @@ export const getFrontendStore = () => {
}
},
save: async screen => {
/*
/*
Temporarily disabled to accomodate migration issues.
store.actions.screens.validate(screen)
*/
@ -347,6 +351,7 @@ export const getFrontendStore = () => {
return state
})
return null
},
updateSetting: async (screen, name, value) => {
if (!screen || !name) {

View file

@ -0,0 +1,319 @@
import * as jsonpatch from "fast-json-patch/index.mjs"
import { writable, derived, get } from "svelte/store"
const Operations = {
Add: "Add",
Delete: "Delete",
Change: "Change",
}
const initialState = {
history: [],
position: 0,
loading: false,
}
export const createHistoryStore = ({
getDoc,
selectDoc,
beforeAction,
afterAction,
}) => {
// Use a derived store to check if we are able to undo or redo any operations
const store = writable(initialState)
const derivedStore = derived(store, $store => {
return {
...$store,
canUndo: $store.position > 0,
canRedo: $store.position < $store.history.length,
}
})
// Wrapped versions of essential functions which we call ourselves when using
// undo and redo
let saveFn
let deleteFn
/**
* Internal util to set the loading flag
*/
const startLoading = () => {
store.update(state => {
state.loading = true
return state
})
}
/**
* Internal util to unset the loading flag
*/
const stopLoading = () => {
store.update(state => {
state.loading = false
return state
})
}
/**
* Resets history state
*/
const reset = () => {
store.set(initialState)
}
/**
* Adds or updates an operation in history.
* For internal use only.
* @param operation the operation to save
*/
const saveOperation = operation => {
store.update(state => {
// Update history
let history = state.history
let position = state.position
if (!operation.id) {
// Every time a new operation occurs we discard any redo potential
operation.id = Math.random()
history = [...history.slice(0, state.position), operation]
position += 1
} else {
// If this is a redo/undo of an existing operation, just update history
// to replace the doc object as revisions may have changed
const idx = history.findIndex(op => op.id === operation.id)
history[idx].doc = operation.doc
}
return { history, position }
})
}
/**
* Wraps the save function, which asynchronously updates a doc.
* The returned function is an enriched version of the real save function so
* that we can control history.
* @param fn the save function
* @returns {function} a wrapped version of the save function
*/
const wrapSaveDoc = fn => {
saveFn = async (doc, operationId) => {
// Only works on a single doc at a time
if (!doc || Array.isArray(doc)) {
return
}
startLoading()
try {
const oldDoc = getDoc(doc._id)
const newDoc = jsonpatch.deepClone(await fn(doc))
// Store the change
if (!oldDoc) {
// If no old doc, this is an add operation
saveOperation({
type: Operations.Add,
doc: newDoc,
id: operationId,
})
} else {
// Otherwise this is a change operation
saveOperation({
type: Operations.Change,
forwardPatch: jsonpatch.compare(oldDoc, doc),
backwardsPatch: jsonpatch.compare(doc, oldDoc),
doc: newDoc,
id: operationId,
})
}
stopLoading()
return newDoc
} catch (error) {
// We want to allow errors to propagate up to normal handlers, but we
// want to stop loading first
stopLoading()
throw error
}
}
return saveFn
}
/**
* Wraps the delete function, which asynchronously deletes a doc.
* The returned function is an enriched version of the real delete function so
* that we can control history.
* @param fn the delete function
* @returns {function} a wrapped version of the delete function
*/
const wrapDeleteDoc = fn => {
deleteFn = async (doc, operationId) => {
// Only works on a single doc at a time
if (!doc || Array.isArray(doc)) {
return
}
startLoading()
try {
const oldDoc = jsonpatch.deepClone(doc)
await fn(doc)
saveOperation({
type: Operations.Delete,
doc: oldDoc,
id: operationId,
})
stopLoading()
} catch (error) {
// We want to allow errors to propagate up to normal handlers, but we
// want to stop loading first
stopLoading()
throw error
}
}
return deleteFn
}
/**
* Asynchronously undoes the previous operation.
* Optionally selects the changed document so that changes are visible.
* @returns {Promise<void>}
*/
const undo = async () => {
// Sanity checks
const { canUndo, history, position, loading } = get(derivedStore)
if (!canUndo || loading) {
return
}
const operation = history[position - 1]
if (!operation) {
return
}
startLoading()
// Before hook
await beforeAction?.(operation)
// Update state immediately to prevent further clicks and to prevent bad
// history in the event of an update failing
store.update(state => {
return {
...state,
position: state.position - 1,
}
})
// Undo the operation
try {
// Undo ADD
if (operation.type === Operations.Add) {
// Try to get the latest doc version to delete
const latestDoc = getDoc(operation.doc._id)
const doc = latestDoc || operation.doc
await deleteFn(doc, operation.id)
}
// Undo DELETE
else if (operation.type === Operations.Delete) {
// Delete the _rev from the deleted doc so that we can save it as a new
// doc again without conflicts
let doc = jsonpatch.deepClone(operation.doc)
delete doc._rev
const created = await saveFn(doc, operation.id)
selectDoc?.(created?._id || doc._id)
}
// Undo CHANGE
else {
// Get the current doc and apply the backwards patch on top of it
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
if (doc) {
jsonpatch.applyPatch(
doc,
jsonpatch.deepClone(operation.backwardsPatch)
)
await saveFn(doc, operation.id)
selectDoc?.(doc._id)
}
}
stopLoading()
} catch (error) {
stopLoading()
throw error
}
// After hook
await afterAction?.(operation)
}
/**
* Asynchronously redoes the previous undo.
* Optionally selects the changed document so that changes are visible.
* @returns {Promise<void>}
*/
const redo = async () => {
// Sanity checks
const { canRedo, history, position, loading } = get(derivedStore)
if (!canRedo || loading) {
return
}
const operation = history[position]
if (!operation) {
return
}
startLoading()
// Before hook
await beforeAction?.(operation)
// Update state immediately to prevent further clicks and to prevent bad
// history in the event of an update failing
store.update(state => {
return {
...state,
position: state.position + 1,
}
})
// Redo the operation
try {
// Redo ADD
if (operation.type === Operations.Add) {
// Delete the _rev from the deleted doc so that we can save it as a new
// doc again without conflicts
let doc = jsonpatch.deepClone(operation.doc)
delete doc._rev
const created = await saveFn(doc, operation.id)
selectDoc?.(created?._id || doc._id)
}
// Redo DELETE
else if (operation.type === Operations.Delete) {
// Try to get the latest doc version to delete
const latestDoc = getDoc(operation.doc._id)
const doc = latestDoc || operation.doc
await deleteFn(doc, operation.id)
}
// Redo CHANGE
else {
// Get the current doc and apply the forwards patch on top of it
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
if (doc) {
jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch))
await saveFn(doc, operation.id)
selectDoc?.(doc._id)
}
}
stopLoading()
} catch (error) {
stopLoading()
throw error
}
// After hook
await afterAction?.(operation)
}
return {
subscribe: derivedStore.subscribe,
wrapSaveDoc,
wrapDeleteDoc,
reset,
undo,
redo,
}
}

View file

@ -1,10 +1,10 @@
<script>
import { automationStore } from "builderStore"
import { selectedAutomation } from "builderStore"
import Flowchart from "./FlowChart/FlowChart.svelte"
$: automation = $automationStore.selectedAutomation?.automation
</script>
{#if automation}
<Flowchart {automation} />
{#if $selectedAutomation}
{#key $selectedAutomation._id}
<Flowchart automation={$selectedAutomation} />
{/key}
{/if}

View file

@ -5,7 +5,6 @@
Detail,
Body,
Icon,
Tooltip,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
@ -13,7 +12,6 @@
import { externalActions } from "./ExternalActions"
export let blockIdx
export let blockComplete
const disabled = {
SEND_EMAIL_SMTP: {
@ -50,15 +48,12 @@
async function addBlockToAutomation() {
try {
const newBlock = $automationStore.selectedAutomation.constructBlock(
const newBlock = automationStore.actions.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
} catch (error) {
notifications.error("Error saving automation")
}
@ -66,20 +61,14 @@
</script>
<ModalContent
title="Create Automation"
title="Add automation step"
confirmText="Save"
size="M"
disabled={!selectedAction}
onConfirm={() => {
blockComplete = true
addBlockToAutomation()
}}
onConfirm={addBlockToAutomation}
>
<Body size="XS">Select an app or event.</Body>
<Layout noPadding>
<Body size="S">Apps</Body>
<Layout noPadding gap="XS">
<Detail size="S">Apps</Detail>
<div class="item-list">
{#each Object.entries(external) as [idx, action]}
<div
@ -95,64 +84,45 @@
alt="zapier"
/>
<span class="icon-spacing">
<Body size="XS">{idx.charAt(0).toUpperCase() + idx.slice(1)}</Body
></span
>
<Body size="XS">
{idx.charAt(0).toUpperCase() + idx.slice(1)}
</Body>
</span>
</div>
</div>
{/each}
</div>
</Layout>
<Layout noPadding gap="XS">
<Detail size="S">Actions</Detail>
<div class="item-list">
{#each Object.entries(internal) as [idx, action]}
{#if disabled[idx] && disabled[idx].disabled}
<Tooltip text={disabled[idx].message} direction="bottom">
<div
class="item"
class:selected={selectedAction === action.name}
class:disabled={true}
on:click={() => selectAction(action)}
>
<div class="item-body">
<Icon name={action.icon} />
<span class="icon-spacing">
<Body size="XS">{action.name}</Body></span
>
</div>
</div>
</Tooltip>
{:else}
<div
class="item"
class:selected={selectedAction === action.name}
on:click={() => selectAction(action)}
>
<div class="item-body">
<Icon name={action.icon} />
<span class="icon-spacing">
<Body size="XS">{action.name}</Body></span
>
</div>
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
<div
class="item"
class:disabled={isDisabled}
class:selected={selectedAction === action.name}
on:click={isDisabled ? null : () => selectAction(action)}
>
<div class="item-body">
<Icon name={action.icon} />
<Body size="XS">{action.name}</Body>
{#if isDisabled}
<Icon name="Help" tooltip={disabled[idx].message} />
{/if}
</div>
{/if}
</div>
{/each}
</div>
</Layout>
</ModalContent>
<style>
.disabled {
opacity: 0.3;
pointer-events: none;
}
.icon-spacing {
margin-left: var(--spacing-m);
}
.item-body {
display: flex;
margin-left: var(--spacing-m);
gap: var(--spacing-m);
}
.item-list {
display: grid;
@ -171,8 +141,15 @@
box-sizing: border-box;
border-width: 2px;
}
.item:hover,
.item:not(.disabled):hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.disabled {
background: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-500);
}
.disabled :global(.spectrum-Body) {
color: var(--spectrum-global-color-gray-600);
}
</style>

View file

@ -1,5 +1,5 @@
<script>
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte"
@ -13,27 +13,28 @@
Modal,
} from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { automationHistoryStore } from "builderStore"
export let automation
let testDataModal
let blocks
let confirmDeleteDialog
$: {
blocks = []
if (automation) {
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks.concat(automation.definition.steps || [])
$: blocks = getBlocks(automation)
const getBlocks = automation => {
let blocks = []
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks.concat(automation.definition.steps || [])
return blocks
}
async function deleteAutomation() {
try {
await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation
)
await automationStore.actions.delete($selectedAutomation)
} catch (error) {
notifications.error("Error deleting automation")
}
@ -41,20 +42,17 @@
</script>
<div class="canvas">
<div style="float: left; padding-left: var(--spacing-xl);">
<div class="header">
<Heading size="S">{automation.name}</Heading>
</div>
<div style="float: right; padding-right: var(--spacing-xl);" class="title">
<div class="subtitle">
<div style="display:flex; align-items: center;">
<div class="icon">
<Icon
on:click={confirmDeleteDialog.show}
hoverable
size="M"
name="DeleteOutline"
/>
</div>
<div class="controls">
<UndoRedoControl store={automationHistoryStore} />
<Icon
on:click={confirmDeleteDialog.show}
hoverable
size="M"
name="DeleteOutline"
/>
<div class="buttons">
<ActionButton
on:click={() => {
testDataModal.show()
@ -62,15 +60,13 @@
icon="MultipleCheck"
size="M">Run test</ActionButton
>
<div style="padding-left: var(--spacing-m);">
<ActionButton
disabled={!$automationStore.selectedAutomation?.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
size="M">Test Details</ActionButton
>
</div>
<ActionButton
disabled={!$automationStore.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
size="M">Test Details</ActionButton
>
</div>
</div>
</div>
@ -80,7 +76,7 @@
<div
class="block"
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 500 }}
in:fly={{ x: 500, duration: 500 }}
out:fly|local={{ x: 500, duration: 500 }}
>
{#if block.stepId !== ActionStepID.LOOP}
@ -105,6 +101,9 @@
</Modal>
<style>
.canvas {
padding: var(--spacing-l) var(--spacing-xl);
}
/* Fix for firefox not respecting bottom padding in scrolling containers */
.canvas > *:last-child {
padding-bottom: 40px;
@ -122,18 +121,19 @@
text-align: left;
}
.title {
padding-bottom: var(--spacing-xl);
}
.subtitle {
padding-bottom: var(--spacing-xl);
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.icon {
cursor: pointer;
padding-right: var(--spacing-m);
.controls,
.buttons {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
}
.buttons {
gap: var(--spacing-s);
}
</style>

View file

@ -1,5 +1,5 @@
<script>
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import {
Icon,
Divider,
@ -23,36 +23,26 @@
export let block
export let testDataModal
export let idx
let selected
let webhookModal
let actionModal
let blockComplete
let open = true
let showLooping = false
let role
$: automationId = $automationStore.selectedAutomation?.automation._id
$: automationId = $selectedAutomation?._id
$: showBindingPicker =
block.stepId === ActionStepID.CREATE_ROW ||
block.stepId === ActionStepID.UPDATE_ROW
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id
$: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: steps = $selectedAutomation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
$: totalBlocks =
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
1
$: loopingSelected =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
$: totalBlocks = $selectedAutomation?.definition?.steps.length + 1
$: loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
$: isAppAction = block?.stepId === TriggerStepID.APP
$: isAppAction && setPermissions(role)
$: isAppAction && getPermissions(automationId)
@ -81,76 +71,54 @@
}
async function removeLooping() {
loopingSelected = false
let loopBlock =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
automationStore.actions.deleteAutomationBlock(loopBlock)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
let loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
try {
await automationStore.actions.deleteAutomationBlock(loopBlock)
} catch (error) {
notifications.error("Error saving automation")
}
}
async function deleteStep() {
let loopBlock =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
let loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
try {
if (loopBlock) {
automationStore.actions.deleteAutomationBlock(loopBlock)
await automationStore.actions.deleteAutomationBlock(loopBlock)
}
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
await automationStore.actions.deleteAutomationBlock(block)
} catch (error) {
notifications.error("Error saving notification")
notifications.error("Error saving automation")
}
}
function toggleFieldControl(evt) {
onSelect(block)
let rowControl
if (evt.detail === "Use values") {
rowControl = false
} else {
rowControl = true
}
automationStore.actions.toggleFieldControl(rowControl)
automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
function toggleRowControl(evt) {
const rowControl = evt.detail !== "Use values"
automationStore.actions.toggleRowControl(block, rowControl)
}
async function addLooping() {
loopingSelected = true
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = $automationStore.selectedAutomation.constructBlock(
const loopBlock = automationStore.actions.constructBlock(
"ACTION",
"LOOP",
loopDefinition
)
loopBlock.blockToLoop = block.id
block.loopBlock = loopBlock.id
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
async function onSelect(block) {
await automationStore.update(state => {
state.selectedBlock = block
return state
})
await automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
}
</script>
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
{#if loopingSelected}
{#if loopBlock}
<div class="blockSection">
<div
on:click={() => {
@ -174,13 +142,8 @@
</div>
<div class="blockTitle">
<div
style="margin-left: 10px;"
on:click={() => {
onSelect(block)
}}
>
<Icon name={showLooping ? "ChevronUp" : "ChevronDown"} />
<div style="margin-left: 10px;" on:click={() => {}}>
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div>
@ -198,9 +161,7 @@
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
)}
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)}
block={loopBlock}
{webhookModal}
/>
</Layout>
@ -209,22 +170,28 @@
{/if}
{/if}
<FlowItemHeader bind:blockComplete {block} {testDataModal} {idx} />
{#if !blockComplete}
<FlowItemHeader
{open}
{block}
{testDataModal}
{idx}
on:toggle={() => (open = !open)}
/>
{#if open}
<Divider noMargin />
<div class="blockSection">
<Layout noPadding gap="S">
{#if !isTrigger}
<div>
<div class="block-options">
{#if !loopingSelected}
<ActionButton on:click={() => addLooping()} icon="Reuse"
>Add Looping</ActionButton
>
{#if !loopBlock}
<ActionButton on:click={() => addLooping()} icon="Reuse">
Add Looping
</ActionButton>
{/if}
{#if showBindingPicker}
<Select
on:change={toggleFieldControl}
on:change={toggleRowControl}
defaultValue="Use values"
autoWidth
value={block.rowControl ? "Use bindings" : "Use values"}
@ -250,16 +217,16 @@
{webhookModal}
/>
{#if lastStep}
<Button on:click={() => testDataModal.show()} cta
>Finish and test automation</Button
>
<Button on:click={() => testDataModal.show()} cta>
Finish and test automation
</Button>
{/if}
</Layout>
</div>
{/if}
<Modal bind:this={actionModal} width="30%">
<ActionModal {blockIdx} bind:blockComplete />
<ActionModal {blockIdx} />
</Modal>
<Modal bind:this={webhookModal} width="30%">

View file

@ -2,21 +2,22 @@
import { automationStore } from "builderStore"
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
import { externalActions } from "./ExternalActions"
import { createEventDispatcher } from "svelte"
export let block
export let blockComplete
export let open
export let showTestStatus = false
export let showParameters = {}
export let testResult
export let isTrigger
export let idx
const dispatch = createEventDispatcher()
$: {
if (!testResult) {
testResult =
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
block.id ? step.id === block.id : step.stepId === block.stepId
)[0]
testResult = $automationStore.testResults?.steps?.filter(step =>
block.id ? step.id === block.id : step.stepId === block.stepId
)?.[0]
}
}
$: isTrigger = isTrigger || block.type === "TRIGGER"
@ -45,13 +46,7 @@
</script>
<div class="blockSection">
<div
on:click={() => {
blockComplete = !blockComplete
showParameters[block.id] = blockComplete
}}
class="splitHeader"
>
<div on:click={() => dispatch("toggle")} class="splitHeader">
<div class="center-items">
{#if externalActions[block.stepId]}
<img
@ -99,7 +94,7 @@
onSelect(block)
}}
>
<Icon hoverable name={blockComplete ? "ChevronUp" : "ChevronDown"} />
<Icon hoverable name={open ? "ChevronUp" : "ChevronDown"} />
</div>
</div>
</div>

View file

@ -7,7 +7,7 @@
Label,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp"
@ -17,9 +17,7 @@
$: {
// clone the trigger so we're not mutating the reference
trigger = cloneDeep(
$automationStore.selectedAutomation.automation.definition.trigger
)
trigger = cloneDeep($selectedAutomation.definition.trigger)
// get the outputs so we can define the fields
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
@ -32,7 +30,7 @@
}
// check to see if there is existing test data in the store
$: testData = $automationStore.selectedAutomation.automation.testData || {}
$: testData = $selectedAutomation.testData || {}
// Check the schema to see if required fields have been entered
$: isError = !trigger.schema.outputs.required.every(
@ -51,10 +49,7 @@
const testAutomation = async () => {
try {
await automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
await automationStore.actions.test($selectedAutomation, testData)
$automationStore.showTestPanel = true
} catch (error) {
notifications.error("Error testing automation")
@ -70,8 +65,8 @@
onConfirm={testAutomation}
cancelText="Cancel"
>
<Tabs selected="Form" quiet
><Tab icon="Form" title="Form">
<Tabs selected="Form" quiet>
<Tab icon="Form" title="Form">
<div class="tab-content-padding">
<AutomationBlockSetup
{testData}
@ -86,11 +81,7 @@
<Label>JSON</Label>
<div class="text-area-container">
<TextArea
value={JSON.stringify(
$automationStore.selectedAutomation.automation.testData,
null,
2
)}
value={JSON.stringify($selectedAutomation.testData, null, 2)}
error={failedParse}
on:change={e => parseTestJSON(e)}
/>

View file

@ -7,7 +7,7 @@
export let testResults
export let width = "400px"
let showParameters
let openBlocks = {}
let blocks
function prepTestResults(results) {
@ -48,14 +48,15 @@
<div class="block" style={width ? `width: ${width}` : ""}>
{#if block.stepId !== ActionStepID.LOOP}
<FlowItemHeader
showTestStatus={true}
bind:showParameters
{block}
open={!!openBlocks[block.id]}
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
isTrigger={idx === 0}
{idx}
testResult={filteredResults?.[idx]}
showTestStatus
{block}
{idx}
/>
{#if showParameters && showParameters[block.id]}
{#if openBlocks[block.id]}
<Divider noMargin />
{#if filteredResults?.[idx]?.outputs.iterations}
<div style="display: flex; padding: 10px 10px 0px 12px;">

View file

@ -2,26 +2,8 @@
import { Icon, Divider } from "@budibase/bbui"
import TestDisplay from "./TestDisplay.svelte"
import { automationStore } from "builderStore"
import { ActionStepID } from "constants/backend/automations"
export let automation
let blocks, testResults
$: {
blocks = []
if (automation) {
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks
.concat(automation.definition.steps || [])
.filter(x => x.stepId !== ActionStepID.LOOP)
} else if ($automationStore.selectedAutomation) {
automation = $automationStore.selectedAutomation
}
}
$: testResults = $automationStore.selectedAutomation?.testResults
</script>
<div class="title">
@ -42,7 +24,7 @@
<Divider />
<TestDisplay {automation} {testResults} />
<TestDisplay {automation} testResults={$automationStore.testResults} />
<style>
.title {

View file

@ -1,12 +1,11 @@
<script>
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
$: selectedAutomationId = $selectedAutomation?._id
onMount(async () => {
try {
@ -16,9 +15,8 @@
}
})
function selectAutomation(automation) {
automationStore.actions.select(automation)
$goto(`./${automation._id}`)
function selectAutomation(id) {
automationStore.actions.select(id)
}
</script>
@ -29,7 +27,7 @@
icon="ShareAndroid"
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation)}
on:click={() => selectAutomation(automation._id)}
>
<EditAutomationPopover {automation} />
</NavItem>
@ -42,5 +40,6 @@
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
margin: 0 calc(-1 * var(--spacing-xl));
}
</style>

View file

@ -1,36 +1,20 @@
<script>
import AutomationList from "./AutomationList.svelte"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Modal, Tabs, Tab, Button, Layout } from "@budibase/bbui"
import { Modal, Button, Layout } from "@budibase/bbui"
import Panel from "components/design/Panel.svelte"
export let modal
export let webhookModal
</script>
<div class="nav">
<Tabs selected="Automations">
<Tab title="Automations">
<Layout paddingX="L" paddingY="L" gap="S">
<Button cta wide on:click={modal.show}>Add automation</Button>
</Layout>
<AutomationList />
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
</Tab>
</Tabs>
</div>
<Panel title="Automations" borderRight>
<Layout paddingX="L" paddingY="XL" gap="S">
<Button cta on:click={modal.show}>Add automation</Button>
<AutomationList />
</Layout>
</Panel>
<style>
.nav {
overflow-y: auto;
background: var(--background);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
border-right: var(--border-light);
padding-bottom: 60px;
}
</style>
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>

View file

@ -1,6 +1,4 @@
<script>
import { goto } from "@roxi/routify"
import { database } from "stores/backend"
import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import {
@ -10,48 +8,37 @@
Layout,
Body,
Icon,
Label,
} from "@budibase/bbui"
import { TriggerStepID } from "constants/backend/automations"
export let webhookModal
let name
let selectedTrigger
let nameTouched = false
let triggerVal
export let webhookModal
$: instanceId = $database._id
$: nameError =
nameTouched && !name ? "Please specify a name for the automation." : null
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
async function createAutomation() {
try {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
const trigger = automationStore.actions.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.create(name, trigger)
if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
webhookModal.show
webhookModal.show()
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
} catch (error) {
notifications.error("Error creating automation")
}
}
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
const selectTrigger = trigger => {
triggerVal = trigger
@ -70,9 +57,9 @@
header="You must publish your app to activate your automations."
message="To test your automation before publishing, you can use the 'Run Test' functionality on the next screen."
/>
<Body size="XS"
>Please name your automation, then select a trigger. Every automation must
start with a trigger.
<Body size="S">
Please name your automation, then select a trigger.<br />
Every automation must start with a trigger.
</Body>
<Input
bind:value={name}
@ -81,9 +68,8 @@
label="Name"
/>
<Layout noPadding>
<Body size="S">Triggers</Body>
<Layout noPadding gap="XS">
<Label size="S">Trigger</Label>
<div class="item-list">
{#each triggers as [idx, trigger]}
<div

View file

@ -1,5 +1,4 @@
<script>
import { goto } from "@roxi/routify"
import { automationStore } from "builderStore"
import { ActionMenu, MenuItem, notifications, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -14,7 +13,6 @@
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
$goto("../automate")
} catch (error) {
notifications.error("Error deleting automation")
}
@ -24,7 +22,6 @@
try {
await automationStore.actions.duplicate(automation)
notifications.success("Automation has been duplicated successfully")
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
} catch (error) {
notifications.error("Error duplicating automation")
}

View file

@ -3,13 +3,13 @@
import { notifications } from "@budibase/bbui"
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui"
export let automation
export let onCancel = undefined
let name
let error = ""
let modal
export let automation
export let onCancel = undefined
export const show = () => {
name = automation?.name
modal.show()

View file

@ -15,8 +15,7 @@
notifications,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import { tables } from "stores/backend"
import { environment, licensing } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
@ -50,22 +49,8 @@
$: filters = lookForFilters(schemaProperties) || []
$: tempFilters = filters
$: stepId = block.stepId
$: bindings = getAvailableBindings(
block || $automationStore.selectedBlock,
$automationStore.selectedAutomation?.automation?.definition
)
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
$: getInputData(testData, block.inputs)
const getInputData = (testData, blockInputs) => {
let newInputData = testData || blockInputs
if (block.event === "app:trigger" && !newInputData?.fields) {
newInputData = cloneDeep(blockInputs)
}
inputData = newInputData
}
$: tableId = inputData ? inputData.tableId : null
$: table = tableId
? $tables.list.find(table => table._id === inputData.tableId)
@ -76,39 +61,48 @@
$: isTrigger = block?.type === "TRIGGER"
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
const getInputData = (testData, blockInputs) => {
let newInputData = testData || blockInputs
if (block.event === "app:trigger" && !newInputData?.fields) {
newInputData = cloneDeep(blockInputs)
}
inputData = newInputData
}
const onChange = Utils.sequential(async (e, key) => {
// We need to cache the schema as part of the definition because it is
// used in the server to detect relationships. It would be far better to
// instead fetch the schema in the backend at runtime.
let schema
if (e.detail?.tableId) {
const tableSchema = getSchemaForTable(e.detail.tableId, {
schema = getSchemaForTable(e.detail.tableId, {
searchableSchema: true,
}).schema
if (isTestModal) {
testData.schema = tableSchema
} else {
block.inputs.schema = tableSchema
}
}
try {
if (isTestModal) {
let newTestData = { schema }
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === TriggerStepID.WEBHOOK) {
automationStore.actions.addTestDataToAutomation({
newTestData = {
...newTestData,
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData?.body,
...$selectedAutomation.testData?.body,
},
})
}
}
automationStore.actions.addTestDataToAutomation({
newTestData = {
...newTestData,
[key]: e.detail,
})
testData[key] = e.detail
}
await automationStore.actions.addTestDataToAutomation(newTestData)
} else {
block.inputs[key] = e.detail
const data = { schema, [key]: e.detail }
await automationStore.actions.updateBlockInputs(block, data)
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}

View file

@ -5,7 +5,11 @@
const dispatch = createEventDispatcher()
export let value
const onChange = e => {
if (e.detail === value) {
return
}
value = e.detail
dispatch("change", e.detail)
}
@ -43,7 +47,12 @@
</script>
<div class="block-field">
<Input on:change={onChange} {value} on:blur={() => (touched = true)} />
<Input
on:change={onChange}
{value}
on:blur={() => (touched = true)}
updateOnChange={false}
/>
{#if touched && !value}
<Label><div class="error">Please specify a CRON expression</div></Label>
{/if}

View file

@ -1,17 +1,18 @@
<script>
import { Icon, notifications } from "@budibase/bbui"
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte"
const POLL_RATE_MS = 2500
let interval
let finished = false
let schemaURL
let propCount = 0
$: automation = $automationStore.selectedAutomation?.automation
$: automation = $selectedAutomation
onMount(async () => {
if (!automation?.definition?.trigger?.inputs.schemaUrl) {

View file

@ -0,0 +1,57 @@
<script>
import { Icon } from "@budibase/bbui"
import { onMount } from "svelte"
export let store
const handleKeyPress = e => {
if (!(e.ctrlKey || e.metaKey)) {
return
}
if (e.shiftKey && e.key === "Z") {
store.redo()
} else if (e.key === "z") {
store.undo()
}
}
onMount(() => {
document.addEventListener("keydown", handleKeyPress)
return () => {
document.removeEventListener("keydown", handleKeyPress)
}
})
</script>
<div class="undo-redo">
<Icon
name="Undo"
hoverable
on:click={store.undo}
disabled={!$store.canUndo}
tooltip="Undo latest change"
/>
<Icon
name="Redo"
hoverable
on:click={store.redo}
disabled={!$store.canRedo}
tooltip="Redo latest undo"
/>
</div>
<style>
.undo-redo {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
padding-right: var(--spacing-xl);
border-right: var(--border-light);
}
.undo-redo :global(svg) {
padding: 6px;
}
</style>

View file

@ -42,29 +42,22 @@
return
}
try {
await automationStore.actions.create({
name: parameters.newAutomationName,
})
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
let trigger = automationStore.actions.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
$automationStore.blockDefinitions.TRIGGER.APP
)
newBlock.inputs = {
trigger.inputs = {
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
fields[key] = "string"
return fields
}, {}),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
const automation = await automationStore.actions.create(
parameters.newAutomationName,
trigger
)
parameters.automationId =
$automationStore.selectedAutomation.automation._id
parameters.automationId = automation._id
delete parameters.newAutomationName
} catch (error) {
notifications.error("Error creating automation")

View file

@ -0,0 +1,13 @@
const getUserInitials = user => {
if (user.firstName && user.lastName) {
return user.firstName[0] + user.lastName[0]
} else if (user.firstName) {
return user.firstName[0]
} else if (user.email) {
return user.email[0]
}
return "U"
}
export default getUserInitials

View file

@ -149,6 +149,7 @@
<Layout gap="XS" noPadding justifyItems="center">
<Button
cta
size="L"
disabled={Object.keys(errors).length > 0 || submitted}
on:click={save}
>

View file

@ -1,30 +1,40 @@
<script>
import { Heading, Body, Layout, Button, Modal } from "@budibase/bbui"
import { automationStore } from "builderStore"
import { automationStore, selectedAutomation } from "builderStore"
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
import { onMount } from "svelte"
import { onDestroy, onMount } from "svelte"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
$: automation =
$automationStore.selectedAutomation?.automation ||
$automationStore.automations[0]
// Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({
urlParam: "automationId",
stateKey: "selectedAutomationId",
validate: id => $automationStore.automations.some(x => x._id === id),
fallbackUrl: "./index",
store: automationStore,
up: automationStore.actions.select,
routify,
})
let modal
let webhookModal
onMount(() => {
$automationStore.showTestPanel = false
})
onDestroy(stopSyncing)
</script>
<!-- routify:options index=3 -->
<div class="root">
<div class="nav">
<AutomationPanel {modal} {webhookModal} />
</div>
<AutomationPanel {modal} {webhookModal} />
<div class="content">
{#if automation}
{#if $automationStore.automations?.length}
<slot />
{:else}
<div class="centered">
@ -40,9 +50,9 @@
</svg>
<Heading size="M">You have no automations</Heading>
<Body size="M">Let's fix that. Call the bots!</Body>
<Button on:click={() => modal.show()} size="M" cta
>Create automation</Button
>
<Button on:click={() => modal.show()} size="M" cta>
Create automation
</Button>
</Layout>
</div>
</div>
@ -51,7 +61,7 @@
{#if $automationStore.showTestPanel}
<div class="setup">
<TestPanel {automation} />
<TestPanel automation={$selectedAutomation} />
</div>
{/if}
<Modal bind:this={modal}>
@ -71,22 +81,8 @@
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
overflow: hidden;
}
.nav {
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
border-right: var(--border-light);
background-color: var(--background);
padding-bottom: 60px;
overflow: hidden;
}
.content {
position: relative;
padding-top: var(--spacing-l);
display: flex;
flex-direction: column;
justify-content: flex-start;

View file

@ -1,17 +1,10 @@
<script>
import { redirect, leftover } from "@roxi/routify"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify"
import { automationStore } from "builderStore"
onMount(async () => {
// navigate to first automation in list, if not already selected
if (
!$leftover &&
$automationStore.automations.length > 0 &&
(!$automationStore.selectedAutomation ||
!$automationStore.selectedAutomation?.automation?._id)
) {
$: {
if ($automationStore.automations?.length) {
$redirect(`./${$automationStore.automations[0]._id}`)
}
})
}
</script>

View file

@ -1,9 +1,11 @@
<script>
import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
import AppPreview from "./AppPreview.svelte"
import { store, sortedScreens } from "builderStore"
import { store, sortedScreens, screenHistoryStore } from "builderStore"
import { Select } from "@budibase/bbui"
import { RoleUtils } from "@budibase/frontend-core"
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { isActive } from "@roxi/routify"
</script>
<div class="app-panel">
@ -22,6 +24,9 @@
/>
</div>
<div class="header-right">
{#if $isActive("./screens") || $isActive("./components")}
<UndoRedoControl store={screenHistoryStore} />
{/if}
{#if $store.clientFeatures.devicePreview}
<DevicePreviewSelect />
{/if}
@ -52,6 +57,7 @@
align-items: flex-start;
gap: var(--spacing-l);
margin: 0 2px;
z-index: 1;
}
.header-left,
.header-right {
@ -59,7 +65,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-l);
gap: var(--spacing-xl);
}
.header-left {
flex: 1 1 auto;

View file

@ -12,30 +12,30 @@
let componentToEject
const keyHandlers = {
["^ArrowUp"]: async component => {
["Ctrl+ArrowUp"]: async component => {
await store.actions.components.moveUp(component)
},
["^ArrowDown"]: async component => {
["Ctrl+ArrowDown"]: async component => {
await store.actions.components.moveDown(component)
},
["^c"]: component => {
["Ctrl+c"]: component => {
store.actions.components.copy(component, false)
},
["^x"]: component => {
["Ctrl+x"]: component => {
store.actions.components.copy(component, true)
},
["^v"]: async component => {
["Ctrl+v"]: async component => {
await store.actions.components.paste(component, "inside")
},
["^d"]: async component => {
["Ctrl+d"]: async component => {
store.actions.components.copy(component)
await store.actions.components.paste(component, "below")
},
["^e"]: component => {
["Ctrl+e"]: component => {
componentToEject = component
confirmEjectDialog.show()
},
["^Enter"]: () => {
["Ctrl+Enter"]: () => {
$goto("./new")
},
["Delete"]: component => {
@ -53,14 +53,19 @@
store.actions.components.selectNext()
},
["Escape"]: () => {
if (!$isActive("/new")) {
return false
if ($isActive("./new")) {
$goto("./")
}
$goto("./")
},
}
const handleKeyAction = async (event, component, key, ctrlKey = false) => {
const handleKeyAction = async ({
event,
component,
key,
ctrlKey = false,
shiftKey = false,
}) => {
if (!component || !key) {
return false
}
@ -69,9 +74,12 @@
if (key === "Backspace") {
key = "Delete"
}
// Prefix key with a caret for ctrl modifier
// Prefix keys for modifiers
if (shiftKey) {
key = "Shift+" + key
}
if (ctrlKey) {
key = "^" + key
key = "Ctrl+" + key
}
const handler = keyHandlers[key]
if (!handler) {
@ -97,19 +105,26 @@
return
}
// Key events are always for the selected component
return await handleKeyAction(
e,
$selectedComponent,
e.key,
e.ctrlKey || e.metaKey
)
return await handleKeyAction({
event: e,
component: $selectedComponent,
key: e.key,
ctrlKey: e.ctrlKey || e.metaKey,
shiftKey: e.shiftKey,
})
}
const handleComponentMenu = async e => {
// Menu events can be for any component
const { id, key, ctrlKey } = e.detail
const { id, key, ctrlKey, shiftKey } = e.detail
const component = findComponent($selectedScreen.props, id)
return await handleKeyAction(null, component, key, ctrlKey)
return await handleKeyAction({
event: null,
component,
key,
ctrlKey,
shiftKey,
})
}
onMount(() => {

View file

@ -1,11 +1,13 @@
<script>
import { tables } from "stores/backend"
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
import blankScreenPreview from "./blankScreenPreview.png"
import listScreenPreview from "./listScreenPreview.png"
export let onConfirm
export let onCancel
let autoCreateModeKey = "autoCreate"
let listScreenModeKey = "autoCreate"
let blankScreenModeKey = "blankScreen"
let selectedScreenMode
@ -23,61 +25,77 @@
onConfirm={confirmScreenSelection}
{onCancel}
disabled={!selectedScreenMode}
size="L"
size="M"
>
<Layout noPadding gap="S">
<div
class="screen-type item"
class="screen-type item blankView"
class:selected={selectedScreenMode == blankScreenModeKey}
on:click={() => {
selectedScreenMode = blankScreenModeKey
}}
>
<div class="content screen-type-wrap">
<Icon name="WebPage" />
<img
alt="blank screen preview"
class="preview"
src={blankScreenPreview}
/>
<div class="screen-type-text">
<Heading size="XS">Blank screen</Heading>
<Body size="S">Add a blank screen</Body>
<Body size="S">Add an empty blank screen</Body>
</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
{#if selectedScreenMode == blankScreenModeKey}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircle" />
</div>
{/if}
<div
class={`checkmark-spacing ${
selectedScreenMode == blankScreenModeKey ? "visible" : ""
}`}
>
<Icon size="S" name="CheckmarkCircle" />
</div>
</div>
</div>
<div class="listViewTitle">
<Heading size="XS">Quickly create a screen from your data</Heading>
</div>
<div
class="screen-type item"
class:selected={selectedScreenMode == autoCreateModeKey}
class:selected={selectedScreenMode == listScreenModeKey}
on:click={() => {
selectedScreenMode = autoCreateModeKey
selectedScreenMode = listScreenModeKey
}}
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
.length}
>
<div class="content screen-type-wrap">
<Icon name="WebPages" />
<img
alt="list screen preview"
class="preview"
src={listScreenPreview}
/>
<div class="screen-type-text">
<Heading size="XS">Autogenerated screens</Heading>
<Heading size="XS">List view</Heading>
<Body size="S">
Add autogenerated screens with CRUD functionality to get a working
app quickly! (Requires a datasource)
Create, edit and view your data in a list view screen with side
panel
</Body>
</div>
</div>
<div
style="color: var(--spectrum-global-color-green-600); float: right"
>
{#if selectedScreenMode == autoCreateModeKey}
<div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircle" />
</div>
{/if}
<div
class={`checkmark-spacing ${
selectedScreenMode == listScreenModeKey ? "visible" : ""
}`}
>
<Icon size="S" name="CheckmarkCircle" />
</div>
</div>
</div>
</Layout>
@ -85,9 +103,6 @@
</div>
<style>
.screen-type.item {
padding: var(--spectrum-alias-item-padding-xl);
}
.screen-type-wrap {
display: flex;
flex-direction: row;
@ -99,6 +114,7 @@
}
.checkmark-spacing {
margin-right: var(--spacing-m);
opacity: 0;
}
.content {
letter-spacing: 0px;
@ -106,7 +122,6 @@
.item {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
@ -132,4 +147,19 @@
.screen-type-wrap :global(.spectrum-Heading) {
padding-bottom: var(--spectrum-alias-item-padding-s);
}
.preview {
width: 140px;
}
.listViewTitle {
margin-top: 35px;
}
.blankView {
margin-top: 10px;
}
.visible {
opacity: 1;
}
</style>

View file

@ -25,7 +25,7 @@
let errors = {}
const routeTaken = url => {
const roleId = get(selectedScreen)?.routing.roleId || "BASIC"
const roleId = get(selectedScreen).routing.roleId || "BASIC"
return get(store).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
@ -34,7 +34,7 @@
}
const roleTaken = roleId => {
const url = get(selectedScreen)?.routing.route
const url = get(selectedScreen).routing.route
return get(store).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
@ -95,7 +95,7 @@
return sanitizeUrl(val)
},
validate: route => {
const existingRoute = get(selectedScreen)?.routing.route
const existingRoute = get(selectedScreen).routing.route
if (route !== existingRoute && routeTaken(route)) {
return "That URL is already in use for this role"
}
@ -107,7 +107,7 @@
label: "Access",
control: RoleSelect,
validate: role => {
const existingRole = get(selectedScreen)?.routing.roleId
const existingRole = get(selectedScreen).routing.roleId
if (role !== existingRole && roleTaken(role)) {
return "That role is already in use for this URL"
}
@ -146,7 +146,7 @@
</script>
<Panel
title={$selectedScreen?.routing.route}
title={$selectedScreen.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
borderLeft
>

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View file

@ -5,6 +5,8 @@
</script>
<ScreenListPanel />
{#key $selectedScreen?._id}
<ScreenSettingsPanel />
{/key}
{#if $selectedScreen}
{#key $selectedScreen._id}
<ScreenSettingsPanel />
{/key}
{/if}

View file

@ -3,31 +3,26 @@
ActionMenu,
MenuItem,
Icon,
Input,
Heading,
Body,
Modal,
} from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CreateRestoreModal from "./CreateRestoreModal.svelte"
import { createEventDispatcher, onMount } from "svelte"
import { createEventDispatcher } from "svelte"
export let row
let deleteDialog
let restoreDialog
let updateDialog
let name
let restoreBackupModal
const dispatch = createEventDispatcher()
const onClickRestore = name => {
const onClickRestore = () => {
dispatch("buttonclick", {
type: "backupRestore",
name,
backupId: row._id,
restoreBackupName: name,
})
}
@ -38,21 +33,9 @@
})
}
const onClickUpdate = () => {
dispatch("buttonclick", {
type: "backupUpdate",
backupId: row._id,
name,
})
}
async function downloadExport() {
window.open(`/api/apps/${row.appId}/backups/${row._id}/file`, "_blank")
}
onMount(() => {
name = row.name
})
</script>
<div class="cell">
@ -66,12 +49,11 @@
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
{/if}
<MenuItem on:click={updateDialog.show} icon="Edit">Rename</MenuItem>
</ActionMenu>
</div>
<Modal bind:this={restoreBackupModal}>
<CreateRestoreModal confirm={name => onClickRestore(name)} />
<CreateRestoreModal confirm={onClickRestore} />
</Modal>
<ConfirmDialog
@ -80,9 +62,7 @@
onOk={onClickDelete}
title="Confirm Deletion"
>
Are you sure you wish to delete the backup
<i>{row.name}?</i>
This action cannot be undone.
Are you sure you wish to delete this backup? This action cannot be undone.
</ConfirmDialog>
<ConfirmDialog
@ -92,21 +72,10 @@
title="Confirm restore"
warning={false}
>
<Heading size="S">{row.name || "Backup"}</Heading>
<Heading size="S">Backup</Heading>
<Body size="S">{new Date(row.timestamp).toLocaleString()}</Body>
</ConfirmDialog>
<ConfirmDialog
bind:this={updateDialog}
disabled={!name}
okText="Confirm"
onOk={onClickUpdate}
title="Update Backup"
warning={false}
>
<Input onlabel="Backup name" bind:value={name} />
</ConfirmDialog>
<style>
.cell {
display: flex;

View file

@ -1,22 +0,0 @@
<script>
import { ModalContent, Input } from "@budibase/bbui"
import { auth } from "stores/portal"
export let createManualBackup
let templateName = $auth.user.firstName
? `${$auth.user.firstName}'s Backup`
: "New Backup"
let name = templateName
</script>
<ModalContent
onConfirm={() => createManualBackup(name)}
title="Create new backup"
diabled={!name}
confirmText="Create"
><Input label="Backup name" bind:value={name} /></ModalContent
>
<style>
</style>

View file

@ -1,8 +0,0 @@
<script>
import { truncate } from "lodash"
export let value
$: truncatedValue = truncate(value, { length: 12 })
</script>
{truncatedValue}

View file

@ -0,0 +1,10 @@
<script>
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
dayjs.extend(relativeTime)
export let value
</script>
<span title={value}>{dayjs(value).fromNow()}</span>

View file

@ -1,17 +1,14 @@
<script>
import getUserInitials from "helpers/userInitials.js"
import { Avatar } from "@budibase/bbui"
export let value
let firstName = value?.firstName
let lastName = value?.lastName || ""
$: username =
firstName && lastName ? `${firstName} ${lastName}` : value?.email
$: initials = getUserInitials(value)
</script>
<div class="cell">
{#if value != null}
<div>{username}</div>
{/if}
<div title={value.email} class="cell">
<Avatar size="M" {initials} />
</div>
<style>

View file

@ -4,7 +4,6 @@
DatePicker,
Divider,
Layout,
Modal,
notifications,
Pagination,
Select,
@ -16,25 +15,22 @@
} from "@budibase/bbui"
import { backups, licensing, auth, admin, overview } from "stores/portal"
import { createPaginationStore } from "helpers/pagination"
import DateRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import TimeAgoRenderer from "./_components/TimeAgoRenderer.svelte"
import AppSizeRenderer from "./_components/AppSizeRenderer.svelte"
import CreateBackupModal from "./_components/CreateBackupModal.svelte"
import ActionsRenderer from "./_components/ActionsRenderer.svelte"
import UserRenderer from "./_components/UserRenderer.svelte"
import StatusRenderer from "./_components/StatusRenderer.svelte"
import TypeRenderer from "./_components/TypeRenderer.svelte"
import NameRenderer from "./_components/NameRenderer.svelte"
import BackupsDefault from "assets/backups-default.png"
import { BackupTrigger, BackupType } from "constants/backend/backups"
import { onMount } from "svelte"
let loading = true
let backupData = null
let modal
let pageInfo = createPaginationStore()
let filterOpt = null
let startDate = null
let endDate = null
let loaded = false
let filters = [
{
label: "Manual backup",
@ -44,10 +40,6 @@
label: "Published backup",
value: { type: BackupType.BACKUP, trigger: BackupTrigger.PUBLISH },
},
{
label: "Scheduled backup",
value: { type: BackupType.BACKUP, trigger: BackupTrigger.SCHEDULED },
},
{
label: "Pre-restore backup",
value: { type: BackupType.BACKUP, trigger: BackupTrigger.RESTORING },
@ -71,10 +63,6 @@
displayName: "Date",
width: "auto",
},
name: {
displayName: "Name",
width: "auto",
},
appSize: {
displayName: "App size",
width: "auto",
@ -96,11 +84,10 @@
const customRenderers = [
{ column: "appSize", component: AppSizeRenderer },
{ column: "actions", component: ActionsRenderer },
{ column: "createdAt", component: DateRenderer },
{ column: "createdAt", component: TimeAgoRenderer },
{ column: "createdBy", component: UserRenderer },
{ column: "status", component: StatusRenderer },
{ column: "type", component: TypeRenderer },
{ column: "name", component: NameRenderer },
]
function flattenBackups(backups) {
@ -126,11 +113,11 @@
backupData = flattenBackups(response.data)
}
async function createManualBackup(name) {
async function createManualBackup() {
try {
loading = true
let response = await backups.createManualBackup({
appId: app.instance._id,
name,
})
await fetchBackups(filterOpt, page)
notifications.success(response.message)
@ -139,6 +126,20 @@
}
}
const poll = backupData => {
if (backupData === null) {
return
}
if (backupData.some(datum => datum.status === "started")) {
setTimeout(() => fetchBackups(filterOpt, page), 2000)
} else {
loading = false
}
}
$: poll(backupData)
async function handleButtonClick({ detail }) {
if (detail.type === "backupDelete") {
await backups.deleteBackup({
@ -165,7 +166,7 @@
onMount(async () => {
await fetchBackups(filterOpt, page, startDate, endDate)
loaded = true
loading = false
})
</script>
@ -206,7 +207,7 @@
View plans
</Button>
</div>
{:else if !backupData?.length && loaded && !filterOpt && !startDate}
{:else if !backupData?.length && !loading && !filterOpt && !startDate}
<div class="center">
<Layout noPadding gap="S" justifyItems="center">
<img height="130px" src={BackupsDefault} alt="BackupsDefault" />
@ -215,11 +216,13 @@
<Body>You can manually back up your app any time</Body>
</Layout>
<div>
<Button on:click={modal.show} cta>Create backup</Button>
<Button cta disabled={loading} on:click={createManualBackup}>
Create backup
</Button>
</div>
</Layout>
</div>
{:else if loaded}
{:else}
<Layout noPadding gap="M" alignContent="start">
<div class="controls">
<div class="search">
@ -245,7 +248,9 @@
/>
</div>
<div>
<Button cta on:click={modal.show}>Create new backup</Button>
<Button cta disabled={loading} on:click={createManualBackup}
>Create new backup</Button
>
</div>
</div>
<div class="table">
@ -275,10 +280,6 @@
{/if}
</Layout>
<Modal bind:this={modal}>
<CreateBackupModal {createManualBackup} />
</Modal>
<style>
.title {
display: flex;

View file

@ -2,6 +2,7 @@ import { derived, writable, get } from "svelte/store"
import { API } from "api"
import { admin } from "stores/portal"
import analytics from "analytics"
import getUserInitials from "helpers/userInitials.js"
export function createAuthStore() {
const auth = writable({
@ -18,16 +19,7 @@ export function createAuthStore() {
let isBuilder = false
if ($store.user) {
const user = $store.user
if (user.firstName) {
initials = user.firstName[0]
if (user.lastName) {
initials += user.lastName[0]
}
} else if (user.email) {
initials = user.email[0]
} else {
initials = "Unknown"
}
initials = getUserInitials(user)
isAdmin = !!user.admin?.global
isBuilder = !!user.builder?.global
}

View file

@ -30,8 +30,8 @@ export function createBackupsStore() {
return API.deleteBackup({ appId, backupId })
}
async function createManualBackup(appId, name) {
return API.createManualBackup(appId, name)
async function createManualBackup(appId) {
return API.createManualBackup(appId)
}
async function updateBackup({ appId, backupId, name }) {

View file

@ -3178,6 +3178,11 @@ fast-glob@^3.0.3:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-patch@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"
integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/types": "2.3.18-alpha.0",
"@budibase/backend-core": "2.3.18-alpha.10",
"@budibase/string-templates": "2.3.18-alpha.10",
"@budibase/types": "2.3.18-alpha.10",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "2.3.18-alpha.0",
"@budibase/frontend-core": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/bbui": "2.3.18-alpha.10",
"@budibase/frontend-core": "2.3.18-alpha.10",
"@budibase/string-templates": "2.3.18-alpha.10",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View file

@ -72,6 +72,9 @@
// These are a combination of the enriched, nested and conditional settings.
let cachedSettings
// Conditional UI expressions, enriched and ready to evaluate
let conditions
// Latest timestamp that we started a props update.
// Due to enrichment now being async, we need to avoid overwriting newer
// settings with old ones, depending on how long enrichment takes.
@ -150,9 +153,7 @@
$: enrichComponentSettings($context, settingsDefinitionMap)
// Evaluate conditional UI settings and store any component setting changes
// which need to be made. This is broken into 2 lines to avoid svelte
// reactivity re-evaluating conditions more often than necessary.
$: conditions = enrichedSettings?._conditions
// which need to be made
$: evaluateConditions(conditions)
// Determine and apply settings to the component
@ -297,7 +298,7 @@
let newStaticSettings = { ...settings }
let newDynamicSettings = { ...settings }
// Attach some internal properties
// Attach some internal properties which we assume always need enriched
newDynamicSettings["_conditions"] = instance._conditions
newDynamicSettings["_css"] = instance._styles?.custom
@ -336,6 +337,24 @@
}
}
// Generates the array of conditional UI expressions, accounting for both
// nested and non-nested settings, extracting a mixture of values from both
// the un-enriched and enriched settings
const generateConditions = () => {
if (!enrichedSettings?._conditions) {
conditions = []
return
}
conditions = enrichedSettings._conditions.map(condition => {
const raw = instance._conditions?.find(x => x.id === condition.id)
if (settingsDefinitionMap[condition.setting]?.nested && raw) {
return { ...condition, settingValue: raw.settingValue }
} else {
return condition
}
})
}
// Enriches any string component props using handlebars
const enrichComponentSettings = (
context,
@ -364,7 +383,11 @@
return
}
// Store new enriched settings
enrichedSettings = newEnrichedSettings
// Once settings have been enriched, re-evaluate conditions
generateConditions()
}
// Evaluates the list of conditional UI conditions and determines any setting

View file

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.3.18-alpha.0",
"@budibase/bbui": "2.3.18-alpha.10",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View file

@ -21,10 +21,9 @@ export const buildBackupsEndpoints = API => ({
})
},
createManualBackup: async ({ appId, name }) => {
createManualBackup: async ({ appId }) => {
return await API.post({
url: `/api/apps/${appId}/backups`,
body: { name },
})
},

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -14,7 +14,7 @@
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --runInBand",
"test": "bash scripts/test.sh",
"test:watch": "jest --watch",
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
"build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
@ -43,11 +43,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.3.18-alpha.0",
"@budibase/client": "2.3.18-alpha.0",
"@budibase/pro": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/types": "2.3.18-alpha.0",
"@budibase/backend-core": "2.3.18-alpha.10",
"@budibase/client": "2.3.18-alpha.10",
"@budibase/pro": "2.3.18-alpha.10",
"@budibase/string-templates": "2.3.18-alpha.10",
"@budibase/types": "2.3.18-alpha.10",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View file

@ -0,0 +1,12 @@
#!/bin/bash
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
echo "jest --coverage --runInBand"
jest --coverage --runInBand
else
# --maxWorkers performs better in development
echo "jest --coverage --maxWorkers=2"
jest --coverage --maxWorkers=2
fi

View file

@ -65,10 +65,14 @@ export async function create(ctx: BBContext) {
// call through to update if already exists
if (automation._id && automation._rev) {
return update(ctx)
await update(ctx)
return
}
automation._id = generateAutomationID()
// Respect existing IDs if recreating a deleted automation
if (!automation._id) {
automation._id = generateAutomationID()
}
automation.type = "automation"
automation = cleanAutomationInputs(automation)
@ -126,6 +130,13 @@ export async function update(ctx: BBContext) {
const db = context.getAppDB()
let automation = ctx.request.body
automation.appId = ctx.appId
// Call through to create if it doesn't exist
if (!automation._id || !automation._rev) {
await create(ctx)
return
}
const oldAutomation = await db.get(automation._id)
automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({

View file

@ -141,7 +141,11 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig {
return config
}
function generateIdForRow(row: Row | undefined, table: Table): string {
function generateIdForRow(
row: Row | undefined,
table: Table,
isLinked: boolean = false
): string {
const primary = table.primary
if (!row || !primary) {
return ""
@ -149,8 +153,12 @@ function generateIdForRow(row: Row | undefined, table: Table): string {
// build id array
let idParts = []
for (let field of primary) {
// need to handle table name + field or just field, depending on if relationships used
const fieldValue = row[`${table.name}.${field}`] || row[field]
let fieldValue = extractFieldValue({
row,
tableName: table.name,
fieldName: field,
isLinked,
})
if (fieldValue) {
idParts.push(fieldValue)
}
@ -173,18 +181,52 @@ function getEndpoint(tableId: string | undefined, operation: string) {
}
}
function basicProcessing(row: Row, table: Table): Row {
// need to handle table name + field or just field, depending on if relationships used
function extractFieldValue({
row,
tableName,
fieldName,
isLinked,
}: {
row: Row
tableName: string
fieldName: string
isLinked: boolean
}) {
let value = row[`${tableName}.${fieldName}`]
if (value == null && !isLinked) {
value = row[fieldName]
}
return value
}
function basicProcessing({
row,
table,
isLinked,
}: {
row: Row
table: Table
isLinked: boolean
}): Row {
const thisRow: Row = {}
// filter the row down to what is actually the row (not joined)
for (let fieldName of Object.keys(table.schema)) {
const pathValue = row[`${table.name}.${fieldName}`]
const value = pathValue != null ? pathValue : row[fieldName]
for (let field of Object.values(table.schema)) {
const fieldName = field.name
const value = extractFieldValue({
row,
tableName: table.name,
fieldName,
isLinked,
})
// all responses include "select col as table.col" so that overlaps are handled
if (value != null) {
thisRow[fieldName] = value
}
}
thisRow._id = generateIdForRow(row, table)
thisRow._id = generateIdForRow(row, table, isLinked)
thisRow.tableId = table._id
thisRow._rev = "rev"
return processFormulas(table, thisRow)
@ -292,7 +334,7 @@ export class ExternalRequest {
// we're not inserting a doc, will be a bunch of update calls
const otherKey: string = field.throughFrom || linkTablePrimary
const thisKey: string = field.throughTo || tablePrimary
row[key].map((relationship: any) => {
row[key].forEach((relationship: any) => {
manyRelationships.push({
tableId: field.through || field.tableId,
isUpdate: false,
@ -308,7 +350,7 @@ export class ExternalRequest {
const thisKey: string = "id"
// @ts-ignore
const otherKey: string = field.fieldName
row[key].map((relationship: any) => {
row[key].forEach((relationship: any) => {
manyRelationships.push({
tableId: field.tableId,
isUpdate: true,
@ -378,7 +420,8 @@ export class ExternalRequest {
) {
continue
}
let linked = basicProcessing(row, linkedTable)
let linked = basicProcessing({ row, table: linkedTable, isLinked: true })
if (!linked._id) {
continue
}
@ -426,7 +469,10 @@ export class ExternalRequest {
)
continue
}
const thisRow = fixArrayTypes(basicProcessing(row, table), table)
const thisRow = fixArrayTypes(
basicProcessing({ row, table, isLinked: false }),
table
)
if (thisRow._id == null) {
throw "Unable to generate row ID for SQL rows"
}
@ -566,19 +612,41 @@ export class ExternalRequest {
const { key, tableId, isUpdate, id, ...rest } = relationship
const body: { [key: string]: any } = processObjectSync(rest, row, {})
const linkTable = this.getTable(tableId)
// @ts-ignore
const linkPrimary = linkTable?.primary[0]
const relationshipPrimary = linkTable?.primary || []
const linkPrimary = relationshipPrimary[0]
if (!linkTable || !linkPrimary) {
return
}
const linkSecondary = relationshipPrimary[1]
const rows = related[key]?.rows || []
const found = rows.find(
(row: { [key: string]: any }) =>
function relationshipMatchPredicate({
row,
linkPrimary,
linkSecondary,
}: {
row: { [key: string]: any }
linkPrimary: string
linkSecondary?: string
}) {
const matchesPrimaryLink =
row[linkPrimary] === relationship.id ||
row[linkPrimary] === body?.[linkPrimary]
if (!matchesPrimaryLink || !linkSecondary) {
return matchesPrimaryLink
}
const matchesSecondayLink = row[linkSecondary] === body?.[linkSecondary]
return matchesPrimaryLink && matchesSecondayLink
}
const existingRelationship = rows.find((row: { [key: string]: any }) =>
relationshipMatchPredicate({ row, linkPrimary, linkSecondary })
)
const operation = isUpdate ? Operation.UPDATE : Operation.CREATE
if (!found) {
if (!existingRelationship) {
promises.push(
getDatasourceAndQuery({
endpoint: getEndpoint(tableId, operation),
@ -589,7 +657,7 @@ export class ExternalRequest {
)
} else {
// remove the relationship from cache so it isn't adjusted again
rows.splice(rows.indexOf(found), 1)
rows.splice(rows.indexOf(existingRelationship), 1)
}
}
// finally cleanup anything that needs to be removed
@ -628,10 +696,7 @@ export class ExternalRequest {
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
* is more performant and has the added benefit of protecting against this scenario.
*/
buildFields(
table: Table,
includeRelations: IncludeRelationship = IncludeRelationship.INCLUDE
) {
buildFields(table: Table, includeRelations: boolean) {
function extractRealFields(table: Table, existing: string[] = []) {
return Object.entries(table.schema)
.filter(
@ -690,6 +755,10 @@ export class ExternalRequest {
}
filters = buildFilters(id, filters || {}, table)
const relationships = this.buildRelationships(table)
const includeSqlRelationships =
config.includeSqlRelationships === IncludeRelationship.INCLUDE
// clean up row on ingress using schema
const processed = this.inputProcessing(row, table)
row = processed.row
@ -707,9 +776,7 @@ export class ExternalRequest {
},
resource: {
// have to specify the fields to avoid column overlap (for SQL)
fields: isSql
? this.buildFields(table, config.includeSqlRelationships)
: [],
fields: isSql ? this.buildFields(table, includeSqlRelationships) : [],
},
filters,
sort,
@ -724,6 +791,7 @@ export class ExternalRequest {
table,
},
}
// can't really use response right now
const response = await getDatasourceAndQuery(json)
// handle many to many relationships now if we know the ID (could be auto increment)

View file

@ -58,7 +58,7 @@ export async function patch(ctx: BBContext) {
return handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(id),
row: inputs,
includeSqlRelationships: IncludeRelationship.EXCLUDE,
includeSqlRelationships: IncludeRelationship.INCLUDE,
})
}

View file

@ -38,7 +38,7 @@ router
"/api/automations",
bodyResource("_id"),
authorized(permissions.BUILDER),
automationValidator(true),
automationValidator(false),
controller.update
)
.post(

View file

@ -27,7 +27,9 @@ describe("row api - postgres", () => {
let makeRequest: MakeRequestResponse,
postgresDatasource: Datasource,
primaryPostgresTable: Table,
auxPostgresTable: Table
oneToManyRelationshipInfo: ForeignTableInfo,
manyToOneRelationshipInfo: ForeignTableInfo,
manyToManyRelationshipInfo: ForeignTableInfo
let host: string
let port: number
@ -67,37 +69,58 @@ describe("row api - postgres", () => {
},
})
auxPostgresTable = await config.createTable({
name: generator.word({ length: 10 }),
type: "external",
primary: ["id"],
schema: {
id: {
name: "id",
type: FieldType.AUTO,
constraints: {
presence: true,
async function createAuxTable(prefix: string) {
return await config.createTable({
name: `${prefix}_${generator.word({ length: 6 })}`,
type: "external",
primary: ["id"],
primaryDisplay: "title",
schema: {
id: {
name: "id",
type: FieldType.AUTO,
autocolumn: true,
constraints: {
presence: true,
},
},
title: {
name: "title",
type: FieldType.STRING,
constraints: {
presence: true,
},
},
},
title: {
name: "title",
type: FieldType.STRING,
constraints: {
presence: true,
},
},
},
sourceId: postgresDatasource._id,
})
sourceId: postgresDatasource._id,
})
}
oneToManyRelationshipInfo = {
table: await createAuxTable("o2m"),
fieldName: "oneToManyRelation",
relationshipType: RelationshipTypes.ONE_TO_MANY,
}
manyToOneRelationshipInfo = {
table: await createAuxTable("m2o"),
fieldName: "manyToOneRelation",
relationshipType: RelationshipTypes.MANY_TO_ONE,
}
manyToManyRelationshipInfo = {
table: await createAuxTable("m2m"),
fieldName: "manyToManyRelation",
relationshipType: RelationshipTypes.MANY_TO_MANY,
}
primaryPostgresTable = await config.createTable({
name: generator.word({ length: 10 }),
name: `p_${generator.word({ length: 6 })}`,
type: "external",
primary: ["id"],
schema: {
id: {
name: "id",
type: FieldType.AUTO,
autocolumn: true,
constraints: {
presence: true,
},
@ -117,25 +140,48 @@ describe("row api - postgres", () => {
name: "value",
type: FieldType.NUMBER,
},
linkedField: {
oneToManyRelation: {
type: FieldType.LINK,
constraints: {
type: "array",
presence: false,
},
fieldName: "foreignField",
name: "linkedField",
fieldName: oneToManyRelationshipInfo.fieldName,
name: "oneToManyRelation",
relationshipType: RelationshipTypes.ONE_TO_MANY,
tableId: auxPostgresTable._id,
tableId: oneToManyRelationshipInfo.table._id,
main: true,
},
manyToOneRelation: {
type: FieldType.LINK,
constraints: {
type: "array",
presence: false,
},
fieldName: manyToOneRelationshipInfo.fieldName,
name: "manyToOneRelation",
relationshipType: RelationshipTypes.MANY_TO_ONE,
tableId: manyToOneRelationshipInfo.table._id,
main: true,
},
manyToManyRelation: {
type: FieldType.LINK,
constraints: {
type: "array",
presence: false,
},
fieldName: manyToManyRelationshipInfo.fieldName,
name: "manyToManyRelation",
relationshipType: RelationshipTypes.MANY_TO_MANY,
tableId: manyToManyRelationshipInfo.table._id,
main: true,
},
},
sourceId: postgresDatasource._id,
})
})
afterAll(async () => {
await config.end()
})
afterAll(config.end)
function generateRandomPrimaryRowData() {
return {
@ -151,22 +197,99 @@ describe("row api - postgres", () => {
value: number
}
type ForeignTableInfo = {
table: Table
fieldName: string
relationshipType: RelationshipTypes
}
type ForeignRowsInfo = {
row: Row
relationshipType: RelationshipTypes
}
async function createPrimaryRow(opts: {
rowData: PrimaryRowData
createForeignRow?: boolean
createForeignRows?: {
createOneToMany?: boolean
createManyToOne?: number
createManyToMany?: number
}
}) {
let { rowData } = opts
let foreignRow: Row | undefined
if (opts?.createForeignRow) {
foreignRow = await config.createRow({
tableId: auxPostgresTable._id,
let { rowData } = opts as any
let foreignRows: ForeignRowsInfo[] = []
async function createForeignRow(tableInfo: ForeignTableInfo) {
const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}`
const foreignRow = await config.createRow({
tableId: tableInfo.table._id,
title: generator.name(),
})
rowData = {
...rowData,
[`fk_${auxPostgresTable.name}_foreignField`]: foreignRow.id,
[foreignKey]: foreignRow.id,
}
foreignRows.push({
row: foreignRow,
relationshipType: tableInfo.relationshipType,
})
}
if (opts?.createForeignRows?.createOneToMany) {
const foreignKey = `fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`
const foreignRow = await config.createRow({
tableId: oneToManyRelationshipInfo.table._id,
title: generator.name(),
})
rowData = {
...rowData,
[foreignKey]: foreignRow.id,
}
foreignRows.push({
row: foreignRow,
relationshipType: oneToManyRelationshipInfo.relationshipType,
})
}
for (let i = 0; i < (opts?.createForeignRows?.createManyToOne || 0); i++) {
const foreignRow = await config.createRow({
tableId: manyToOneRelationshipInfo.table._id,
title: generator.name(),
})
rowData = {
...rowData,
[manyToOneRelationshipInfo.fieldName]:
rowData[manyToOneRelationshipInfo.fieldName] || [],
}
rowData[manyToOneRelationshipInfo.fieldName].push(foreignRow._id)
foreignRows.push({
row: foreignRow,
relationshipType: RelationshipTypes.MANY_TO_ONE,
})
}
for (let i = 0; i < (opts?.createForeignRows?.createManyToMany || 0); i++) {
const foreignRow = await config.createRow({
tableId: manyToManyRelationshipInfo.table._id,
title: generator.name(),
})
rowData = {
...rowData,
[manyToManyRelationshipInfo.fieldName]:
rowData[manyToManyRelationshipInfo.fieldName] || [],
}
rowData[manyToManyRelationshipInfo.fieldName].push(foreignRow._id)
foreignRows.push({
row: foreignRow,
relationshipType: RelationshipTypes.MANY_TO_MANY,
})
}
const row = await config.createRow({
@ -174,7 +297,7 @@ describe("row api - postgres", () => {
...rowData,
})
return { row, foreignRow }
return { row, foreignRows }
}
async function createDefaultPgTable() {
@ -198,7 +321,9 @@ describe("row api - postgres", () => {
async function populatePrimaryRows(
count: number,
opts?: {
createForeignRow?: boolean
createOneToMany?: boolean
createManyToOne?: number
createManyToMany?: number
}
) {
return await Promise.all(
@ -210,7 +335,7 @@ describe("row api - postgres", () => {
rowData,
...(await createPrimaryRow({
rowData,
createForeignRow: opts?.createForeignRow,
createForeignRows: opts,
})),
}
})
@ -295,7 +420,7 @@ describe("row api - postgres", () => {
describe("given than a row exists", () => {
let row: Row
beforeEach(async () => {
let rowResponse = _.sample(await populatePrimaryRows(10))!
let rowResponse = _.sample(await populatePrimaryRows(1))!
row = rowResponse.row
})
@ -403,7 +528,7 @@ describe("row api - postgres", () => {
let rows: { row: Row; rowData: PrimaryRowData }[]
beforeEach(async () => {
rows = await populatePrimaryRows(10)
rows = await populatePrimaryRows(5)
})
it("a single row can be retrieved successfully", async () => {
@ -419,34 +544,136 @@ describe("row api - postgres", () => {
describe("given a row with relation data", () => {
let row: Row
let foreignRow: Row
beforeEach(async () => {
let [createdRow] = await populatePrimaryRows(1, {
createForeignRow: true,
let rowData: {
name: string
description: string
value: number
}
let foreignRows: ForeignRowsInfo[]
describe("with all relationship types", () => {
beforeEach(async () => {
let [createdRow] = await populatePrimaryRows(1, {
createOneToMany: true,
createManyToOne: 3,
createManyToMany: 2,
})
row = createdRow.row
rowData = createdRow.rowData
foreignRows = createdRow.foreignRows
})
it("only one to many foreign keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
const one2ManyForeignRows = foreignRows.filter(
x => x.relationshipType === RelationshipTypes.ONE_TO_MANY
)
expect(one2ManyForeignRows).toHaveLength(1)
expect(res.body).toEqual({
...rowData,
id: row.id,
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
one2ManyForeignRows[0].row.id,
})
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
row = createdRow.row
foreignRow = createdRow.foreignRow!
})
it("only foreign keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
expect(res.body).toEqual({
...row,
_id: expect.any(String),
_rev: expect.any(String),
describe("with only one to many", () => {
beforeEach(async () => {
let [createdRow] = await populatePrimaryRows(1, {
createOneToMany: true,
})
row = createdRow.row
rowData = createdRow.rowData
foreignRows = createdRow.foreignRows
})
expect(res.body.foreignField).toBeUndefined()
it("only one to many foreign keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(
res.body[`fk_${auxPostgresTable.name}_foreignField`]
).toBeDefined()
expect(res.body[`fk_${auxPostgresTable.name}_foreignField`]).toBe(
foreignRow.id
)
expect(res.status).toBe(200)
expect(foreignRows).toHaveLength(1)
expect(res.body).toEqual({
...rowData,
id: row.id,
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
foreignRows[0].row.id,
})
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
describe("with only many to one", () => {
beforeEach(async () => {
let [createdRow] = await populatePrimaryRows(1, {
createManyToOne: 3,
})
row = createdRow.row
rowData = createdRow.rowData
foreignRows = createdRow.foreignRows
})
it("only one to many foreign keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
expect(foreignRows).toHaveLength(3)
expect(res.body).toEqual({
...rowData,
id: row.id,
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
})
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
describe("with only many to many", () => {
beforeEach(async () => {
let [createdRow] = await populatePrimaryRows(1, {
createManyToMany: 2,
})
row = createdRow.row
rowData = createdRow.rowData
foreignRows = createdRow.foreignRows
})
it("only one to many foreign keys are retrieved", async () => {
const res = await getRow(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
expect(foreignRows).toHaveLength(2)
expect(res.body).toEqual({
...rowData,
id: row.id,
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
})
expect(res.body[oneToManyRelationshipInfo.fieldName]).toBeUndefined()
})
})
})
})
@ -667,31 +894,74 @@ describe("row api - postgres", () => {
const getAll = (tableId: string | undefined, rowId: string | undefined) =>
makeRequest("get", `/api/${tableId}/${rowId}/enrich`)
describe("given a row with relation data", () => {
let row: Row, foreignRow: Row | undefined
let row: Row, rowData: PrimaryRowData, foreignRows: ForeignRowsInfo[]
beforeEach(async () => {
const rowsInfo = await createPrimaryRow({
rowData: generateRandomPrimaryRowData(),
createForeignRow: true,
describe("with all relationship types", () => {
beforeEach(async () => {
rowData = generateRandomPrimaryRowData()
const rowsInfo = await createPrimaryRow({
rowData,
createForeignRows: {
createOneToMany: true,
createManyToOne: 3,
createManyToMany: 2,
},
})
row = rowsInfo.row
foreignRows = rowsInfo.foreignRows
})
row = rowsInfo.row
foreignRow = rowsInfo.foreignRow
})
it("enrich populates the foreign fields", async () => {
const res = await getAll(primaryPostgresTable._id, row.id)
it("enrich populates the foreign field", async () => {
const res = await getAll(primaryPostgresTable._id, row.id)
expect(res.status).toBe(200)
expect(res.status).toBe(200)
expect(foreignRow).toBeDefined()
expect(res.body).toEqual({
...row,
linkedField: [
{
...foreignRow,
},
],
const foreignRowsByType = _.groupBy(
foreignRows,
x => x.relationshipType
)
expect(res.body).toEqual({
...rowData,
[`fk_${oneToManyRelationshipInfo.table.name}_${oneToManyRelationshipInfo.fieldName}`]:
foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row.id,
[oneToManyRelationshipInfo.fieldName]: [
{
...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row,
_id: expect.any(String),
_rev: expect.any(String),
},
],
[manyToOneRelationshipInfo.fieldName]: [
{
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][0].row,
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id,
},
{
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row,
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id,
},
{
...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row,
[`fk_${manyToOneRelationshipInfo.table.name}_${manyToOneRelationshipInfo.fieldName}`]:
row.id,
},
],
[manyToManyRelationshipInfo.fieldName]: [
{
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row,
},
{
...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row,
},
],
id: row.id,
tableId: row.tableId,
_id: expect.any(String),
_rev: expect.any(String),
})
})
})
})
@ -714,7 +984,7 @@ describe("row api - postgres", () => {
const rowsCount = 6
let rows: {
row: Row
foreignRow: Row | undefined
foreignRows: ForeignRowsInfo[]
rowData: PrimaryRowData
}[]
beforeEach(async () => {

View file

@ -415,9 +415,7 @@ class InternalBuilder {
if (opts.disableReturning) {
return query.insert(parsedBody)
} else {
return query
.insert(parsedBody)
.returning(generateSelectStatement(json, knex))
return query.insert(parsedBody).returning("*")
}
}
@ -502,9 +500,7 @@ class InternalBuilder {
if (opts.disableReturning) {
return query.update(parsedBody)
} else {
return query
.update(parsedBody)
.returning(generateSelectStatement(json, knex))
return query.update(parsedBody).returning("*")
}
}

View file

@ -79,10 +79,6 @@ export default (
return ctx.throw(403, "No user info found")
}
// check general builder stuff, this middleware is a good way
// to find API endpoints which are builder focused
await builderMiddleware(ctx, permType)
// get the resource roles
let resourceRoles: any = []
let otherLevelRoles: any = []
@ -112,6 +108,12 @@ export default (
return ctx.throw(403, "Session not authenticated")
}
// check general builder stuff, this middleware is a good way
// to find API endpoints which are builder focused
if (permType === permissions.PermissionType.BUILDER) {
await builderMiddleware(ctx)
}
try {
// check authorized
await checkAuthorized(ctx, resourceRoles, permType, permLevel)

View file

@ -64,13 +64,18 @@ async function updateAppUpdatedAt(ctx: BBContext) {
})
}
export default async function builder(ctx: BBContext, permType: string) {
export default async function builder(ctx: BBContext) {
const appId = ctx.appId
// this only functions within an app context
if (!appId) {
return
}
const isBuilderApi = permType === permissions.PermissionType.BUILDER
// check authenticated
if (!ctx.isAuthenticated) {
return ctx.throw(403, "Session not authenticated")
}
const referer = ctx.headers["referer"]
const overviewPath = "/builder/portal/overview/"
@ -82,7 +87,7 @@ export default async function builder(ctx: BBContext, permType: string) {
const hasAppId = !referer ? false : referer.includes(appId)
const editingApp = referer ? hasAppId : false
// check this is a builder call and editing
if (!isBuilderApi || !editingApp) {
if (!editingApp) {
return
}
// check locks

View file

@ -1278,14 +1278,14 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d"
integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ==
"@budibase/backend-core@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.10.tgz#c934db86c39280b67819f90f19b2633503a7c563"
integrity sha512-4Y6+DEoCCnlrLtdMSkn07CkJKvkNhu744OgnjvVf+gWhhZAQbsgCNyK62HrUtH4Rnp8Xk9IZ1T59icEH4nbjJQ==
dependencies:
"@budibase/nano" "10.1.1"
"@budibase/pouchdb-replication-stream" "1.2.10"
"@budibase/types" "2.3.18-alpha.0"
"@budibase/types" "2.3.18-alpha.10"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-cloudfront-sign "2.2.0"
@ -1392,13 +1392,13 @@
pouchdb-promise "^6.0.4"
through2 "^2.0.0"
"@budibase/pro@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff"
integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg==
"@budibase/pro@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.10.tgz#fed175d68dfd2219bb969eb96ef5db71fe1798ca"
integrity sha512-knUyW1U/CxrJ6P6Uv1kJC2APTyLUTp2+Zqo7X/w8aLWF1K/cn4mq6/6s6A8cC9xOZXJHNT30ZKzX6SJVrxjU4A==
dependencies:
"@budibase/backend-core" "2.3.18-alpha.0"
"@budibase/types" "2.3.18-alpha.0"
"@budibase/backend-core" "2.3.18-alpha.10"
"@budibase/types" "2.3.18-alpha.10"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -1424,10 +1424,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9"
integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA==
"@budibase/types@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.10.tgz#efc70c66bb59df2eb9ba3292ca14009016ba0d4a"
integrity sha512-31MLknZpi+H08qFxCDoDP2kNf7Nj+QvvvEGYFbMXFQkNLNoGO8cFVIlE70hVgYe0Rxa+mqSMJ5WwFIGIKuYNCw==
"@bull-board/api@3.7.0":
version "3.7.0"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "2.3.18-alpha.0",
"version": "2.3.18-alpha.10",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -22,7 +22,7 @@
"build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
"dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && nodemon",
"test": "jest --coverage --runInBand",
"test": "bash scripts/test.sh",
"test:watch": "jest --watch",
"env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable",
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "2.3.18-alpha.0",
"@budibase/pro": "2.3.18-alpha.0",
"@budibase/string-templates": "2.3.18-alpha.0",
"@budibase/types": "2.3.18-alpha.0",
"@budibase/backend-core": "2.3.18-alpha.10",
"@budibase/pro": "2.3.18-alpha.10",
"@budibase/string-templates": "2.3.18-alpha.10",
"@budibase/types": "2.3.18-alpha.10",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",

View file

@ -0,0 +1,12 @@
#!/bin/bash
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
echo "jest --coverage --runInBand"
jest --coverage --runInBand
else
# --maxWorkers performs better in development
echo "jest --coverage --maxWorkers=2"
jest --coverage --maxWorkers=2
fi

View file

@ -475,14 +475,14 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d"
integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ==
"@budibase/backend-core@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.10.tgz#c934db86c39280b67819f90f19b2633503a7c563"
integrity sha512-4Y6+DEoCCnlrLtdMSkn07CkJKvkNhu744OgnjvVf+gWhhZAQbsgCNyK62HrUtH4Rnp8Xk9IZ1T59icEH4nbjJQ==
dependencies:
"@budibase/nano" "10.1.1"
"@budibase/pouchdb-replication-stream" "1.2.10"
"@budibase/types" "2.3.18-alpha.0"
"@budibase/types" "2.3.18-alpha.10"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-cloudfront-sign "2.2.0"
@ -539,13 +539,13 @@
pouchdb-promise "^6.0.4"
through2 "^2.0.0"
"@budibase/pro@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff"
integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg==
"@budibase/pro@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.10.tgz#fed175d68dfd2219bb969eb96ef5db71fe1798ca"
integrity sha512-knUyW1U/CxrJ6P6Uv1kJC2APTyLUTp2+Zqo7X/w8aLWF1K/cn4mq6/6s6A8cC9xOZXJHNT30ZKzX6SJVrxjU4A==
dependencies:
"@budibase/backend-core" "2.3.18-alpha.0"
"@budibase/types" "2.3.18-alpha.0"
"@budibase/backend-core" "2.3.18-alpha.10"
"@budibase/types" "2.3.18-alpha.10"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -553,10 +553,10 @@
lru-cache "^7.14.1"
node-fetch "^2.6.1"
"@budibase/types@2.3.18-alpha.0":
version "2.3.18-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9"
integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA==
"@budibase/types@2.3.18-alpha.10":
version "2.3.18-alpha.10"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.10.tgz#efc70c66bb59df2eb9ba3292ca14009016ba0d4a"
integrity sha512-31MLknZpi+H08qFxCDoDP2kNf7Nj+QvvvEGYFbMXFQkNLNoGO8cFVIlE70hVgYe0Rxa+mqSMJ5WwFIGIKuYNCw==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"

View file

@ -38,7 +38,7 @@
]
},
"devDependencies": {
"@budibase/types": "1.3.4",
"@budibase/types": "^2.3.17",
"@types/jest": "29.0.0",
"@types/node-fetch": "2.6.2",
"chance": "1.1.8",
@ -53,8 +53,8 @@
"typescript": "4.7.3"
},
"dependencies": {
"@budibase/backend-core": "^2.0.5",
"@budibase/backend-core": "^2.3.17",
"form-data": "^4.0.0",
"node-fetch": "2"
}
}
}

View file

@ -297,27 +297,30 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.5.tgz#e720ad8a0fd0eb0157d8ec332e530b481fd5b912"
integrity sha512-uY/YQgZ1xTm3npzWNRgZQBY/nj2ZxSkGtGbgK4NyWwZzvVUwd9vfNAIdKf7crECMJncH1x4H9TalQoFXb/cmbA==
"@budibase/backend-core@^2.3.17":
version "2.3.17"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5"
integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw==
dependencies:
"@budibase/types" "^2.0.5"
"@budibase/nano" "10.1.1"
"@budibase/types" "^2.3.17"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-cloudfront-sign "2.2.0"
aws-sdk "2.1030.0"
bcrypt "5.0.1"
bcryptjs "2.4.3"
bull "4.10.1"
correlation-id "4.0.0"
dotenv "16.0.1"
emitter-listener "1.1.2"
ioredis "4.28.0"
joi "17.6.0"
jsonwebtoken "8.5.1"
jsonwebtoken "9.0.0"
koa-passport "4.1.4"
lodash "4.17.21"
lodash.isarguments "3.1.0"
node-fetch "2.6.7"
passport-google-auth "1.0.2"
passport-google-oauth "2.0.0"
passport-jwt "4.0.0"
passport-local "1.0.0"
@ -333,15 +336,22 @@
uuid "8.3.2"
zlib "1.0.5"
"@budibase/types@1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.3.4.tgz#25f087b024e843eb372e50c81f8f925fb39f1dfd"
integrity sha512-ndyWs8yeCS7cpZjApDB1HhY6UUM2SRBUgAMCZOZaWABG9JHeCbx7x0e/pA2SZjswdMXqS5WmnEd3br5wuvUzJw==
"@budibase/nano@10.1.1":
version "10.1.1"
resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.1.tgz#36ccda4d9bb64b5ee14dd2b27a295b40739b1038"
integrity sha512-kbMIzMkjVtl+xI0UPwVU0/pn8/ccxTyfzwBz6Z+ZiN2oUSb0fJCe0qwA6o8dxwSa8nZu4MbGAeMJl3CJndmWtA==
dependencies:
"@types/tough-cookie" "^4.0.2"
axios "^1.1.3"
http-cookie-agent "^4.0.2"
node-abort-controller "^3.0.1"
qs "^6.11.0"
tough-cookie "^4.1.2"
"@budibase/types@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.5.tgz#852c86611f237640b59d8dc4ae0c8c5fec491cf1"
integrity sha512-MnnDEB22kbXRsztmHPgvFDSYavpb0qm6H6Y/3UHXKqyFEg/KRpiF1p7lYsN+FAUDAWxpFgI+kp2Yw6gWyA5FLQ==
"@budibase/types@^2.3.17":
version "2.3.17"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995"
integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
@ -660,6 +670,36 @@
semver "^7.3.5"
tar "^6.1.11"
"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.0.tgz#d31a238c943ffc34bab73ad6ce7a6466d65888ef"
integrity sha512-5qpnNHUyyEj9H3sm/4Um/bnx1lrQGhe8iqry/1d+cQYCRd/gzYA0YLeq0ezlk4hKx4vO+dsEsNyeowqRqslwQA==
"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.0.tgz#2f6fbbec3d3f0bbe9c6678c899f1c1a6e25ed980"
integrity sha512-ZphTFFd6SFweNAMKD+QJCrWpgkjf4qBuHltiMkKkD6FFrB3NOTRVmetAGTkJ57pa+s6J0yCH06LujWB9rZe94g==
"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.0.tgz#19875441da50b9aa8f8e726eb097a4cead435a3f"
integrity sha512-NEX6hdSvP4BmVyegaIbrGxvHzHvTzzsPaxXCsUt0mbLbPpEftsvNwaEVKOowXnLoeuGeD4MaqSwL3BUK2elsUA==
"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.0.tgz#3b855ac72cc16e89db2f72adf47ddc964c20a53d"
integrity sha512-ztKVV1dO/sSZyGse0PBCq3Pk1PkYjsA/dsEWE7lfrGoAK3i9HpS2o7XjGQ7V4va6nX+xPPOiuYpQwa4Bi6vlww==
"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.0.tgz#455f1d5bb00e87f78c67711f26e7bff9f1457684"
integrity sha512-9uvdAkZMOPCY7SPRxZLW8XGqBOVNVEhqlgffenN8shA1XR9FWVsSM13nr/oHtNgXg6iVyML7RwWPyqUeThlwxg==
"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.0.tgz#03c6bfcd3acb179ea69546c20d50895b9d623ada"
integrity sha512-Wg0+9615kHKlr9iLVcG5I+/CHnf6w3x5UADRv8Ad16yA0Bu5l9eVOROjV7aHPG6uC8ZPFIVVaoSjDChD+Y0pzg==
"@shopify/jest-koa-mocks@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94"
@ -825,6 +865,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/tough-cookie@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@ -889,7 +934,7 @@ acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
agent-base@6:
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@ -987,18 +1032,18 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
async@~2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
integrity sha512-+g/Ncjbx0JSq2Mk03WQkyKvNh5q9Qvyo/RIqIqnmC5feJY70PNl2ESwZU2BhAB+AZPkHNzzyC2Dq2AS5VnTKhQ==
dependencies:
lodash "^4.14.0"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
aws-cloudfront-sign@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/aws-cloudfront-sign/-/aws-cloudfront-sign-2.2.0.tgz#3910f5a6d0d90fec07f2b4ef8ab07f3eefb5625d"
integrity sha512-qG+rwZMP3KRTPPbVmWY8DlrT56AkA4iVOeo23vkdK2EXeW/brJFN2haSNKzVz+oYhFMEIzVVloeAcrEzuRkuVQ==
dependencies:
lodash "^3.6.0"
aws-sdk@2.1030.0:
version "2.1030.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1030.0.tgz#24a856af3d2b8b37c14a8f59974993661c66fd82"
@ -1046,6 +1091,15 @@ axios@^0.21.1:
dependencies:
follow-redirects "^1.14.0"
axios@^1.1.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.3.tgz#e7011384ba839b885007c9c9fae1ff23dceb295b"
integrity sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-jest@^28.1.3:
version "28.1.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5"
@ -1226,6 +1280,21 @@ buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
bull@4.10.1:
version "4.10.1"
resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f"
integrity sha512-Fp21tRPb2EaZPVfmM+ONZKVz2RA+to+zGgaTLyCKt3JMSU8OOBqK8143OQrnGuGpsyE5G+9FevFAGhdZZfQP2g==
dependencies:
cron-parser "^4.2.1"
debuglog "^1.0.0"
get-port "^5.1.1"
ioredis "^4.28.5"
lodash "^4.17.21"
msgpackr "^1.5.2"
p-timeout "^3.2.0"
semver "^7.3.2"
uuid "^8.3.0"
cache-content-type@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
@ -1234,6 +1303,14 @@ cache-content-type@^1.0.0:
mime-types "^2.1.18"
ylru "^1.2.0"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -1438,11 +1515,25 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
correlation-id@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/correlation-id/-/correlation-id-4.0.0.tgz#c1d3038e5f30d7bfeae5728ff96f27a7506bc2c0"
integrity sha512-WvXtJBlovvOBKqTz/YwWP2gm6CXJZJArfGimp9s/ehmhJMPFbmnPMQe3K60Q9idGNixMvKojMjleyDhZEFdHfg==
dependencies:
uuid "^8.3.1"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cron-parser@^4.2.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.7.1.tgz#1e325a6a18e797a634ada1e2599ece0b6b5ed177"
integrity sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==
dependencies:
luxon "^3.2.1"
cross-spawn@^7.0.0, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -1478,6 +1569,11 @@ debug@4.3.2:
dependencies:
ms "2.1.2"
debuglog@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==
dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
@ -1820,7 +1916,7 @@ follow-redirects@^1.14.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
follow-redirects@^1.14.4:
follow-redirects@^1.14.4, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@ -1919,11 +2015,25 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.3"
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
get-port@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -1953,47 +2063,11 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
google-auth-library@~0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
integrity sha512-KM54Y9GhdAzfXUHmWEoYmaOykSLuMG7W4HvVLYqyogxOyE6px8oSS8W13ngqW0oDGZ915GFW3V6OM6+qcdvPOA==
dependencies:
gtoken "^1.2.1"
jws "^3.1.4"
lodash.noop "^3.0.1"
request "^2.74.0"
google-p12-pem@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177"
integrity sha512-puhMlJ2+E/rgvxWaqgN/nC7x623OAE8MR9vBUqxF0inCE7HoVfCHvTeQ9+BR+rj9KM0fIg6XV6tmbt7XHHssoQ==
dependencies:
node-forge "^0.7.1"
googleapis@^16.0.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576"
integrity sha512-5czmF7xkIlJKc1+/+5tltrI1skoR3HKtkDOld9rk+DOucTpZRjOhCoJzoSjxB3M8rP2tEb1VIr1TPyzR3V2PUQ==
dependencies:
async "~2.1.4"
google-auth-library "~0.10.0"
string-template "~1.0.0"
graceful-fs@^4.2.9:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
gtoken@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8"
integrity sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w==
dependencies:
google-p12-pem "^0.1.0"
jws "^3.0.0"
mime "^1.4.1"
request "^2.72.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -2017,7 +2091,7 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.2:
has-symbols@^1.0.2, has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
@ -2054,6 +2128,13 @@ http-assert@^1.3.0:
deep-equal "~1.0.1"
http-errors "~1.8.0"
http-cookie-agent@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/http-cookie-agent/-/http-cookie-agent-4.0.2.tgz#dcdaae18ed1f7452d81ae4d5cd80b227d6831b69"
integrity sha512-noTmxdH5CuytTnLj/Qv3Z84e/YFq8yLXAw3pqIYZ25Edhb9pQErIAC+ednw40Cic6Le/h9ryph5/TqsvkOaUCw==
dependencies:
agent-base "^6.0.2"
http-errors@^1.6.3, http-errors@~1.8.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
@ -2150,6 +2231,23 @@ ioredis@4.28.0:
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
ioredis@^4.28.5:
version "4.28.5"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f"
integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==
dependencies:
cluster-key-slot "^1.1.0"
debug "^4.3.1"
denque "^1.1.0"
lodash.defaults "^4.2.0"
lodash.flatten "^4.4.0"
lodash.isarguments "^3.1.0"
p-map "^2.1.0"
redis-commands "1.7.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@ -2745,7 +2843,17 @@ json5@^2.2.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0:
jsonwebtoken@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d"
integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==
dependencies:
jws "^3.2.2"
lodash "^4.17.21"
ms "^2.1.1"
semver "^7.3.8"
jsonwebtoken@^8.2.0:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
@ -2780,7 +2888,7 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
@ -3017,11 +3125,6 @@ lodash.memoize@4.x:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
lodash.noop@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c"
integrity sha512-TmYdmu/pebrdTIBDK/FDx9Bmfzs9x0sZG6QIJuMDTqEPfeciLcN13ij+cOd0i9vwJfBtbG9UQ+C7MkXgYxrIJg==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@ -3032,11 +3135,16 @@ lodash.pick@^4.0.0:
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==
lodash@4.17.21, lodash@^4.14.0, lodash@^4.17.21:
lodash@4.17.21, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^3.6.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -3049,6 +3157,11 @@ ltgt@2.2.1, ltgt@^2.1.2:
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==
luxon@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f"
integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==
make-dir@^3.0.0, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -3122,7 +3235,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24,
dependencies:
mime-db "1.52.0"
mime@^1.3.4, mime@^1.4.1:
mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@ -3179,6 +3292,27 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msgpackr-extract@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.0.tgz#5b5c5fbfff25be5ee5b5a82a9cbe02e37f72bed0"
integrity sha512-oy6KCk1+X4Bn5m6Ycq5N1EWl9npqG/cLrE8ga8NX7ZqfqYUUBS08beCQaGq80fjbKBySur0E6x//yZjzNJDt3A==
dependencies:
node-gyp-build-optional-packages "5.0.7"
optionalDependencies:
"@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.0"
"@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.0"
"@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.0"
"@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.0"
"@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.0"
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.0"
msgpackr@^1.5.2:
version "1.8.3"
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.3.tgz#78c1b91359f72707f4abeaca40cc423bd2d75185"
integrity sha512-m2JefwcKNzoHYXkH/5jzHRxAw7XLWsAdvu0FOJ+OLwwozwOV/J6UA62iLkfIMbg7G8+dIuRwgg6oz+QoQ4YkoA==
optionalDependencies:
msgpackr-extract "^3.0.0"
napi-macros@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
@ -3204,6 +3338,11 @@ negotiator@0.6.3:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-abort-controller@^3.0.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
node-addon-api@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -3221,10 +3360,10 @@ node-fetch@2.6.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-forge@^0.7.1:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
node-gyp-build-optional-packages@5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==
node-gyp-build@~4.1.0:
version "4.1.1"
@ -3301,6 +3440,11 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.9.0:
version "1.12.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
on-finished@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@ -3327,6 +3471,11 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@ -3353,6 +3502,13 @@ p-map@^2.1.0:
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
p-timeout@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
dependencies:
p-finally "^1.0.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@ -3373,14 +3529,6 @@ parseurl@^1.3.2, parseurl@^1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
passport-google-auth@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938"
integrity sha512-cfAqna6jZLyMEwUdd4PIwAh2mQKQVEDAaRIaom1pG6h4x4Gwjllf/Jflt3TkR1Sen5Rkvr3l7kSXCWE1EKkh8g==
dependencies:
googleapis "^16.0.0"
passport-strategy "1.x"
passport-google-oauth1@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc"
@ -3443,7 +3591,7 @@ passport-oauth2@1.x.x:
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0:
passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==
@ -3720,6 +3868,11 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@ -3755,6 +3908,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@^6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
@ -3851,7 +4011,7 @@ remove-trailing-slash@^0.1.1:
resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d"
integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==
request@^2.72.0, request@^2.74.0, request@^2.88.0:
request@^2.88.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@ -3974,6 +4134,13 @@ semver@^6.0.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2, semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -4001,6 +4168,15 @@ shimmer@^1.2.0:
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@ -4123,11 +4299,6 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
string-template@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
integrity sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@ -4315,7 +4486,7 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0":
"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
@ -4486,7 +4657,7 @@ uuid@8.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
uuid@8.3.2, uuid@^8.3.2:
uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==