Merge branch 'master' into budi-7786-options-picker-dropdown-opens-above-even-though-it-is-at-the

This commit is contained in:
Mel O'Hagan 2024-01-24 12:48:46 +00:00
commit 969c600201
18 changed files with 265 additions and 98 deletions

View File

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

View File

@ -184,8 +184,9 @@
} }
if ( if (
(idx === 0 && automation.trigger?.event === "row:update") || idx === 0 &&
automation.trigger?.event === "row:save" (automation.trigger?.event === "row:update" ||
automation.trigger?.event === "row:save")
) { ) {
if (name !== "id" && name !== "revision") return `trigger.row.${name}` if (name !== "id" && name !== "revision") return `trigger.row.${name}`
} }

View File

@ -1,11 +1,27 @@
import { PlanType } from "@budibase/types" import { PlanType } from "@budibase/types"
export function getFormattedPlanName(userPlanType) { export function getFormattedPlanName(userPlanType) {
let planName = "Free" let planName
if (userPlanType === PlanType.PREMIUM_PLUS) { switch (userPlanType) {
planName = "Premium" case PlanType.PRO:
} else if (userPlanType === PlanType.ENTERPRISE_BASIC) { planName = "Pro"
planName = "Enterprise" break
case PlanType.TEAM:
planName = "Team"
break
case PlanType.PREMIUM:
case PlanType.PREMIUM_PLUS:
planName = "Premium"
break
case PlanType.BUSINESS:
planName = "Business"
break
case PlanType.ENTERPRISE_BASIC:
case PlanType.ENTERPRISE:
planName = "Enterprise"
break
default:
planName = "Free" // Default to "Free" if the type is not explicitly handled
} }
return `${planName} Plan` return `${planName} Plan`
} }

View File

@ -15,9 +15,9 @@
<Content showMobileNav> <Content showMobileNav>
<SideNav slot="side-nav"> <SideNav slot="side-nav">
<SideNavItem <SideNavItem
text="Automation History" text="Automations"
url={$url("./automation-history")} url={$url("./automations")}
active={$isActive("./automation-history")} active={$isActive("./automations")}
/> />
<SideNavItem <SideNavItem
text="Backups" text="Backups"

View File

@ -8,6 +8,8 @@
Body, Body,
Heading, Heading,
Divider, Divider,
Toggle,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte" import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import StatusRenderer from "./_components/StatusRenderer.svelte" import StatusRenderer from "./_components/StatusRenderer.svelte"
@ -16,7 +18,7 @@
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { getContext, onDestroy, onMount } from "svelte" import { getContext, onDestroy, onMount } from "svelte"
import dayjs from "dayjs" import dayjs from "dayjs"
import { auth, licensing, admin } from "stores/portal" import { auth, licensing, admin, apps } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import Portal from "svelte-portal" import Portal from "svelte-portal"
@ -35,9 +37,13 @@
let timeRange = null let timeRange = null
let loaded = false let loaded = false
$: app = $apps.find(app => app.devId === $store.appId?.includes(app.appId))
$: licensePlan = $auth.user?.license?.plan $: licensePlan = $auth.user?.license?.plan
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchLogs(automationId, status, page, timeRange) $: fetchLogs(automationId, status, page, timeRange)
$: isCloud = $admin.cloud
$: chainAutomations = app?.automations?.chainAutomations ?? !isCloud
const timeOptions = [ const timeOptions = [
{ value: "90-d", label: "Past 90 days" }, { value: "90-d", label: "Past 90 days" },
@ -124,6 +130,18 @@
sidePanel.open() sidePanel.open()
} }
async function save({ detail }) {
try {
await apps.update($store.appId, {
automations: {
chainAutomations: detail,
},
})
} catch (error) {
notifications.error("Error updating automation chaining setting")
}
}
onMount(async () => { onMount(async () => {
await automationStore.actions.fetch() await automationStore.actions.fetch()
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
@ -150,11 +168,30 @@
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading>Automation History</Heading> <Heading>Automations</Heading>
<Body>View the automations your app has executed</Body> <Body size="S">See your automation history and edit advanced settings</Body>
</Layout> </Layout>
<Divider /> <Divider />
<Layout gap="XS" noPadding>
<Heading size="XS">Chain automations</Heading>
<Body size="S">Allow automations to trigger from other automations</Body>
<div class="setting-spacing">
<Toggle
text={"Enable chaining"}
on:change={e => {
save(e)
}}
value={chainAutomations}
/>
</div>
</Layout>
<Divider />
<Layout gap="XS" noPadding>
<Heading size="XS">History</Heading>
<Body size="S">Free plan stores up to 1 day of automation history</Body>
</Layout>
<div class="controls"> <div class="controls">
<div class="search"> <div class="search">
<div class="select"> <div class="select">
@ -237,6 +274,9 @@
{/if} {/if}
<style> <style>
.setting-spacing {
padding-top: var(--spacing-s);
}
.controls { .controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,5 +1,5 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
$redirect("../settings/automation-history") $redirect("../settings/automations")
</script> </script>

View File

@ -1,18 +1,11 @@
import { rowEmission, tableEmission } from "./utils" import { rowEmission, tableEmission } from "./utils"
import mainEmitter from "./index" import mainEmitter from "./index"
import env from "../environment" import env from "../environment"
import { Table, Row } from "@budibase/types" import { Table, Row, DocumentType, App } from "@budibase/types"
import { context } from "@budibase/backend-core"
// max number of automations that can chain on top of each other const MAX_AUTOMATIONS_ALLOWED = 5
// TODO: in future make this configurable at the automation level
const MAX_AUTOMATION_CHAIN = env.SELF_HOSTED ? 5 : 0
/**
* Special emitter which takes the count of automation runs which have occurred and blocks an
* automation from running if it has reached the maximum number of chained automations runs.
* This essentially "fakes" the normal emitter to add some functionality in-between to stop automations
* from getting stuck endlessly chaining.
*/
class AutomationEmitter { class AutomationEmitter {
chainCount: number chainCount: number
metadata: { automationChainCount: number } metadata: { automationChainCount: number }
@ -24,7 +17,23 @@ class AutomationEmitter {
} }
} }
emitRow(eventName: string, appId: string, row: Row, table?: Table) { async getMaxAutomationChain() {
const db = context.getAppDB()
const appMetadata = await db.get<App>(DocumentType.APP_METADATA)
let chainAutomations = appMetadata?.automations?.chainAutomations
if (chainAutomations === true) {
return MAX_AUTOMATIONS_ALLOWED
} else if (chainAutomations === undefined && env.SELF_HOSTED) {
return MAX_AUTOMATIONS_ALLOWED
} else {
return 0
}
}
async emitRow(eventName: string, appId: string, row: Row, table?: Table) {
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
// don't emit even if we've reached max automation chain // don't emit even if we've reached max automation chain
if (this.chainCount >= MAX_AUTOMATION_CHAIN) { if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
return return
@ -39,9 +48,11 @@ class AutomationEmitter {
}) })
} }
emitTable(eventName: string, appId: string, table?: Table) { async emitTable(eventName: string, appId: string, table?: Table) {
let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
// don't emit even if we've reached max automation chain // don't emit even if we've reached max automation chain
if (this.chainCount > MAX_AUTOMATION_CHAIN) { if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
return return
} }

View File

@ -48,6 +48,9 @@ async function checkResponse(
let error let error
try { try {
error = await response.json() error = await response.json()
if (!error.message) {
error = JSON.stringify(error)
}
} catch (err) { } catch (err) {
error = await response.text() error = await response.text()
} }

View File

@ -137,7 +137,7 @@
"n" "n"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{ after [1, 2, 3] 1}} -> [3]", "example": "{{ after ['a', 'b', 'c', 'd'] 2}} -> ['c', 'd']",
"description": "<p>Returns all of the items in an array after the specified index. Opposite of <a href=\"#before\">before</a>.</p>\n" "description": "<p>Returns all of the items in an array after the specified index. Opposite of <a href=\"#before\">before</a>.</p>\n"
}, },
"arrayify": { "arrayify": {
@ -154,7 +154,7 @@
"n" "n"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{ before [1, 2, 3] 2}} -> [1, 2]", "example": "{{ before ['a', 'b', 'c', 'd'] 3}} -> ['a', 'b']",
"description": "<p>Return all of the items in the collection before the specified count. Opposite of <a href=\"#after\">after</a>.</p>\n" "description": "<p>Return all of the items in the collection before the specified count. Opposite of <a href=\"#after\">after</a>.</p>\n"
}, },
"eachIndex": { "eachIndex": {
@ -182,7 +182,7 @@
"n" "n"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{first [1, 2, 3, 4] 2}} -> [1, 2]", "example": "{{first [1, 2, 3, 4] 2}} -> 1,2",
"description": "<p>Returns the first item, or first <code>n</code> items of an array.</p>\n" "description": "<p>Returns the first item, or first <code>n</code> items of an array.</p>\n"
}, },
"forEach": { "forEach": {
@ -200,7 +200,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> 2 exists", "example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> ' 2 exists '",
"description": "<p>Block helper that renders the block if an array has the given <code>value</code>. Optionally specify an inverse block to render when the array does not have the given value.</p>\n" "description": "<p>Block helper that renders the block if an array has the given <code>value</code>. Optionally specify an inverse block to render when the array does not have the given value.</p>\n"
}, },
"isArray": { "isArray": {
@ -226,7 +226,7 @@
"separator" "separator"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{join [1, 2, 3]}} -> '1, 2, 3'", "example": "{{join [1, 2, 3]}} -> 1, 2, 3",
"description": "<p>Join all elements of array into a string, optionally using a given separator.</p>\n" "description": "<p>Join all elements of array into a string, optionally using a given separator.</p>\n"
}, },
"equalsLength": { "equalsLength": {
@ -236,7 +236,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{equalsLength '[1,2,3]' 3}} -> true", "example": "{{equalsLength [1, 2, 3] 3}} -> true",
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n" "description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
}, },
"last": { "last": {
@ -253,7 +253,7 @@
"value" "value"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{length '[1, 2, 3]'}} -> 3", "example": "{{length [1, 2, 3]}} -> 3",
"description": "<p>Returns the length of the given string or array.</p>\n" "description": "<p>Returns the length of the given string or array.</p>\n"
}, },
"lengthEqual": { "lengthEqual": {
@ -263,7 +263,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{equalsLength '[1,2,3]' 3}} -> true", "example": "{{equalsLength [1, 2, 3] 3}} -> true",
"description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n" "description": "<p>Returns true if the the length of the given <code>value</code> is equal to the given <code>length</code>. Can be used as a block or inline helper.</p>\n"
}, },
"map": { "map": {
@ -299,7 +299,7 @@
"provided" "provided"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#some [1, 'b', 3] isString}} string found {{else}} No string found {{/some}} -> string found", "example": "{{#some [1, \"b\", 3] isString}} string found {{else}} No string found {{/some}} -> ' string found '",
"description": "<p>Block helper that returns the block if the callback returns true for some value in the given array.</p>\n" "description": "<p>Block helper that returns the block if the callback returns true for some value in the given array.</p>\n"
}, },
"sort": { "sort": {
@ -317,7 +317,7 @@
"props" "props"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{ sortBy [{a: 'zzz'}, {a: 'aaa'}] 'a' }} -> [{'a':'aaa'}, {'a':'zzz'}]", "example": "{{ sortBy [{'a': 'zzz'}, {'a': 'aaa'}] 'a' }} -> [{'a':'aaa'},{'a':'zzz'}]",
"description": "<p>Sort an <code>array</code>. If an array of objects is passed, you may optionally pass a <code>key</code> to sort on as the second argument. You may alternatively pass a sorting function as the second argument.</p>\n" "description": "<p>Sort an <code>array</code>. If an array of objects is passed, you may optionally pass a <code>key</code> to sort on as the second argument. You may alternatively pass a sorting function as the second argument.</p>\n"
}, },
"withAfter": { "withAfter": {
@ -347,7 +347,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{ withFirst [1, 2, 3] }} {{this}} {{/withFirst}}", "example": "{{#withFirst [1, 2, 3] }}{{this}}{{/withFirst}} -> 1",
"description": "<p>Use the first item in a collection inside a handlebars block expression. Opposite of <a href=\"#withLast\">withLast</a>.</p>\n" "description": "<p>Use the first item in a collection inside a handlebars block expression. Opposite of <a href=\"#withLast\">withLast</a>.</p>\n"
}, },
"withGroup": { "withGroup": {
@ -357,7 +357,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#withGroup [1, 2, 3, 4] 2}} {{#each this}} {{.}} {{each}} <br> {{/withGroup}} -> 1,2<br> 3,4<br>", "example": "{{#withGroup [1, 2, 3, 4] 2}}{{#each this}}{{.}}{{/each}}<br>{{/withGroup}} -> 12<br>34<br>",
"description": "<p>Block helper that groups array elements by given group <code>size</code>.</p>\n" "description": "<p>Block helper that groups array elements by given group <code>size</code>.</p>\n"
}, },
"withLast": { "withLast": {
@ -367,7 +367,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#withLast [1, 2, 3, 4]}} {{this}} {{/withLast}} -> 4", "example": "{{#withLast [1, 2, 3, 4]}}{{this}}{{/withLast}} -> 4",
"description": "<p>Use the last item or <code>n</code> items in an array as context inside a block. Opposite of <a href=\"#withFirst\">withFirst</a>.</p>\n" "description": "<p>Use the last item or <code>n</code> items in an array as context inside a block. Opposite of <a href=\"#withFirst\">withFirst</a>.</p>\n"
}, },
"withSort": { "withSort": {
@ -377,7 +377,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#withSort ['b', 'a', 'c']}} {{this}} {{/withSort}} -> abc", "example": "{{#withSort ['b', 'a', 'c']}}{{this}}{{/withSort}} -> abc",
"description": "<p>Block helper that sorts a collection and exposes the sorted collection as context inside the block.</p>\n" "description": "<p>Block helper that sorts a collection and exposes the sorted collection as context inside the block.</p>\n"
}, },
"unique": { "unique": {
@ -386,7 +386,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }} {{.}} {{/each}} -> acbe", "example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }}{{.}}{{/each}} -> acbe",
"description": "<p>Block helper that return an array with all duplicate values removed. Best used along with a <a href=\"#each\">each</a> helper.</p>\n" "description": "<p>Block helper that return an array with all duplicate values removed. Best used along with a <a href=\"#each\">each</a> helper.</p>\n"
} }
}, },
@ -396,7 +396,7 @@
"number" "number"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ bytes 1386 }} -> 1.4Kb", "example": "{{ bytes 1386 1 }} -> 1.4 kB",
"description": "<p>Format a number to it&#39;s equivalent in bytes. If a string is passed, it&#39;s length will be formatted and returned. <strong>Examples:</strong> - <code>&#39;foo&#39; =&gt; 3 B</code> - <code>13661855 =&gt; 13.66 MB</code> - <code>825399 =&gt; 825.39 kB</code> - <code>1396 =&gt; 1.4 kB</code></p>\n" "description": "<p>Format a number to it&#39;s equivalent in bytes. If a string is passed, it&#39;s length will be formatted and returned. <strong>Examples:</strong> - <code>&#39;foo&#39; =&gt; 3 B</code> - <code>13661855 =&gt; 13.66 MB</code> - <code>825399 =&gt; 825.39 kB</code> - <code>1396 =&gt; 1.4 kB</code></p>\n"
}, },
"addCommas": { "addCommas": {
@ -430,7 +430,7 @@
"fractionDigits" "fractionDigits"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{ toExponential 10123 2 }} -> 101e+4", "example": "{{ toExponential 10123 2 }} -> 1.01e+4",
"description": "<p>Returns a string representing the given number in exponential notation.</p>\n" "description": "<p>Returns a string representing the given number in exponential notation.</p>\n"
}, },
"toFixed": { "toFixed": {
@ -472,7 +472,7 @@
"str" "str"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ encodeURI 'https://myurl?Hello There' }} -> https://myurl?Hello%20There", "example": "{{ encodeURI 'https://myurl?Hello There' }} -> https%3A%2F%2Fmyurl%3FHello%20There",
"description": "<p>Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.</p>\n" "description": "<p>Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.</p>\n"
}, },
"escape": { "escape": {
@ -480,7 +480,7 @@
"str" "str"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ escape 'https://myurl?Hello+There' }} -> https://myurl?Hello%20There", "example": "{{ escape 'https://myurl?Hello+There' }} -> https%3A%2F%2Fmyurl%3FHello%2BThere",
"description": "<p>Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.</p>\n" "description": "<p>Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.</p>\n"
}, },
"decodeURI": { "decodeURI": {
@ -488,7 +488,7 @@
"str" "str"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?=Hello There", "example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?Hello There",
"description": "<p>Decode a Uniform Resource Identifier (URI) component.</p>\n" "description": "<p>Decode a Uniform Resource Identifier (URI) component.</p>\n"
}, },
"urlResolve": { "urlResolve": {
@ -513,7 +513,7 @@
"url" "url"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ stripQueryString 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'", "example": "{{ stripQuerystring 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'",
"description": "<p>Strip the query string from the given <code>url</code>.</p>\n" "description": "<p>Strip the query string from the given <code>url</code>.</p>\n"
}, },
"stripProtocol": { "stripProtocol": {
@ -521,7 +521,7 @@
"str" "str"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ stripProtocol 'https://myurl/api/test' }} -> 'myurl/api/test'", "example": "{{ stripProtocol 'https://myurl/api/test' }} -> '//myurl/api/test'",
"description": "<p>Strip protocol from a <code>url</code>. Useful for displaying media that may have an &#39;http&#39; protocol on secure connections.</p>\n" "description": "<p>Strip protocol from a <code>url</code>. Useful for displaying media that may have an &#39;http&#39; protocol on secure connections.</p>\n"
} }
}, },
@ -573,7 +573,7 @@
"string" "string"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{ chop ' ABC '}} -> 'ABC'", "example": "{{ chop ' ABC '}} -> ABC",
"description": "<p>Like trim, but removes both extraneous whitespace <strong>and non-word characters</strong> from the beginning and end of a string.</p>\n" "description": "<p>Like trim, but removes both extraneous whitespace <strong>and non-word characters</strong> from the beginning and end of a string.</p>\n"
}, },
"dashcase": { "dashcase": {
@ -606,7 +606,7 @@
"length" "length"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{ellipsis 'foo bar baz', 7}} -> foo bar…", "example": "{{ellipsis 'foo bar baz' 7}} -> foo bar…",
"description": "<p>Truncates a string to the specified <code>length</code>, and appends it with an elipsis, <code>…</code>.</p>\n" "description": "<p>Truncates a string to the specified <code>length</code>, and appends it with an elipsis, <code>…</code>.</p>\n"
}, },
"hyphenate": { "hyphenate": {
@ -675,14 +675,6 @@
"example": "{{prepend 'bar' 'foo-'}} -> foo-bar", "example": "{{prepend 'bar' 'foo-'}} -> foo-bar",
"description": "<p>Prepends the given <code>string</code> with the specified <code>prefix</code>.</p>\n" "description": "<p>Prepends the given <code>string</code> with the specified <code>prefix</code>.</p>\n"
}, },
"raw": {
"args": [
"options"
],
"numArgs": 1,
"example": "{{{{#raw}}}} {{foo}} {{{{/raw}}}} -> {{foo}}",
"description": "<p>Render a block without processing mustache templates inside the block.</p>\n"
},
"remove": { "remove": {
"args": [ "args": [
"str", "str",
@ -698,7 +690,7 @@
"substring" "substring"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{remove 'a b a b a b' 'a'}} -> b a b a b", "example": "{{removeFirst 'a b a b a b' 'a'}} -> ' b a b a b'",
"description": "<p>Remove the first occurrence of <code>substring</code> from the given <code>str</code>.</p>\n" "description": "<p>Remove the first occurrence of <code>substring</code> from the given <code>str</code>.</p>\n"
}, },
"replace": { "replace": {
@ -718,7 +710,7 @@
"b" "b"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{replace 'a b a b a b' 'a' 'z'}} -> z b a b a b", "example": "{{replaceFirst 'a b a b a b' 'a' 'z'}} -> z b a b a b",
"description": "<p>Replace the first occurrence of substring <code>a</code> with substring <code>b</code>.</p>\n" "description": "<p>Replace the first occurrence of substring <code>a</code> with substring <code>b</code>.</p>\n"
}, },
"sentence": { "sentence": {
@ -752,7 +744,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#startsWith 'Goodbye' 'Hello, world!'}} Yep {{else}} Nope {{/startsWith}} -> Nope", "example": "{{#startsWith 'Goodbye' 'Hello, world!'}}Yep{{else}}Nope{{/startsWith}} -> Nope",
"description": "<p>Tests whether a string begins with the given prefix.</p>\n" "description": "<p>Tests whether a string begins with the given prefix.</p>\n"
}, },
"titleize": { "titleize": {
@ -760,7 +752,7 @@
"str" "str"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{#titleize 'this is title case' }} -> This Is Title Case", "example": "{{titleize 'this is title case' }} -> This Is Title Case",
"description": "<p>Title case the given string.</p>\n" "description": "<p>Title case the given string.</p>\n"
}, },
"trim": { "trim": {
@ -784,7 +776,7 @@
"string" "string"
], ],
"numArgs": 1, "numArgs": 1,
"example": "{{trimRight ' ABC ' }} -> ' ABC '", "example": "{{trimRight ' ABC ' }} -> ' ABC'",
"description": "<p>Removes extraneous whitespace from the end of a string.</p>\n" "description": "<p>Removes extraneous whitespace from the end of a string.</p>\n"
}, },
"truncate": { "truncate": {
@ -804,7 +796,7 @@
"suffix" "suffix"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{truncateWords 'foo bar baz' 1 }} -> foo", "example": "{{truncateWords 'foo bar baz' 1 }} -> foo",
"description": "<p>Truncate a string to have the specified number of words. Also see <a href=\"#truncate\">truncate</a>.</p>\n" "description": "<p>Truncate a string to have the specified number of words. Also see <a href=\"#truncate\">truncate</a>.</p>\n"
}, },
"upcase": { "upcase": {
@ -844,7 +836,7 @@
"options" "options"
], ],
"numArgs": 4, "numArgs": 4,
"example": "{{compare 10 '<' 5 }} -> true", "example": "{{compare 10 '<' 5 }} -> false",
"description": "<p>Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.</p>\n" "description": "<p>Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.</p>\n"
}, },
"contains": { "contains": {
@ -874,7 +866,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#eq 3 3}} equal{{else}} not equal{{/eq}} -> equal", "example": "{{#eq 3 3}}equal{{else}}not equal{{/eq}} -> equal",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n"
}, },
"gt": { "gt": {
@ -884,7 +876,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> greater than", "example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> ' greater than'",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n"
}, },
"gte": { "gte": {
@ -894,7 +886,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> greater than or equal", "example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> ' greater than or equal'",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>greater than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n"
}, },
"has": { "has": {
@ -904,7 +896,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#has 'foobar' 'foo'}} has it{{else}} doesn't{{/has}} -> has it", "example": "{{#has 'foobar' 'foo'}}has it{{else}}doesn't{{/has}} -> has it",
"description": "<p>Block helper that renders a block if <code>value</code> has <code>pattern</code>. If an inverse block is specified it will be rendered when falsy.</p>\n" "description": "<p>Block helper that renders a block if <code>value</code> has <code>pattern</code>. If an inverse block is specified it will be rendered when falsy.</p>\n"
}, },
"isFalsey": { "isFalsey": {
@ -931,7 +923,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> even", "example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> ' even '",
"description": "<p>Return true if the given value is an even number.</p>\n" "description": "<p>Return true if the given value is an even number.</p>\n"
}, },
"ifNth": { "ifNth": {
@ -941,8 +933,8 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#ifNth 10 2}} remainder {{else}} no remainder {{/ifNth}} -> remainder", "example": "{{#ifNth 2 10}}remainder{{else}}no remainder{{/ifNth}} -> remainder",
"description": "<p>Conditionally renders a block if the remainder is zero when <code>a</code> operand is divided by <code>b</code>. If an inverse block is specified it will be rendered when the remainder is <strong>not zero</strong>.</p>\n" "description": "<p>Conditionally renders a block if the remainder is zero when <code>b</code> operand is divided by <code>a</code>. If an inverse block is specified it will be rendered when the remainder is <strong>not zero</strong>.</p>\n"
}, },
"ifOdd": { "ifOdd": {
"args": [ "args": [
@ -950,7 +942,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#ifOdd 3}} odd {{else}} even {{/ifOdd}} -> odd", "example": "{{#ifOdd 3}}odd{{else}}even{{/ifOdd}} -> odd",
"description": "<p>Block helper that renders a block if <code>value</code> is <strong>an odd number</strong>. If an inverse block is specified it will be rendered when falsy.</p>\n" "description": "<p>Block helper that renders a block if <code>value</code> is <strong>an odd number</strong>. If an inverse block is specified it will be rendered when falsy.</p>\n"
}, },
"is": { "is": {
@ -960,7 +952,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#is 3 3}} is {{else}} is not {{/is}} -> is", "example": "{{#is 3 3}} is {{else}} is not {{/is}} -> ' is '",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#eq\">eq</a> but does not do strict equality.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#eq\">eq</a> but does not do strict equality.</p>\n"
}, },
"isnt": { "isnt": {
@ -970,7 +962,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> is", "example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> ' is '",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>not equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#unlesseq\">unlessEq</a> but does not use strict equality for comparisons.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>not equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. Similar to <a href=\"#unlesseq\">unlessEq</a> but does not use strict equality for comparisons.</p>\n"
}, },
"lt": { "lt": {
@ -979,7 +971,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> less than", "example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> ' less than '",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n"
}, },
"lte": { "lte": {
@ -989,7 +981,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> less than or equal", "example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> ' less than or equal '",
"description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n" "description": "<p>Block helper that renders a block if <code>a</code> is <strong>less than or equal to</strong> <code>b</code>. If an inverse block is specified it will be rendered when falsy. You may optionally use the <code>compare=&#39;&#39;</code> hash argument for the second value.</p>\n"
}, },
"neither": { "neither": {
@ -999,7 +991,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#neither null null}} both falsey {{else}} both not falsey {{/neither}} -> both falsey", "example": "{{#neither null null}}both falsey{{else}}both not falsey{{/neither}} -> both falsey",
"description": "<p>Block helper that renders a block if <strong>neither of</strong> the given values are truthy. If an inverse block is specified it will be rendered when falsy.</p>\n" "description": "<p>Block helper that renders a block if <strong>neither of</strong> the given values are truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
}, },
"not": { "not": {
@ -1008,7 +1000,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#not undefined }} falsey {{else}} not falsey {{/not}} -> falsey", "example": "{{#not undefined }}falsey{{else}}not falsey{{/not}} -> falsey",
"description": "<p>Returns true if <code>val</code> is falsey. Works as a block or inline helper.</p>\n" "description": "<p>Returns true if <code>val</code> is falsey. Works as a block or inline helper.</p>\n"
}, },
"or": { "or": {
@ -1017,7 +1009,7 @@
"options" "options"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> at least one truthy", "example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> ' at least one truthy '",
"description": "<p>Block helper that renders a block if <strong>any of</strong> the given values is truthy. If an inverse block is specified it will be rendered when falsy.</p>\n" "description": "<p>Block helper that renders a block if <strong>any of</strong> the given values is truthy. If an inverse block is specified it will be rendered when falsy.</p>\n"
}, },
"unlessEq": { "unlessEq": {
@ -1027,7 +1019,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> not equal", "example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> ' not equal '",
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is equal to <code>b</code></strong>.</p>\n" "description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is equal to <code>b</code></strong>.</p>\n"
}, },
"unlessGt": { "unlessGt": {
@ -1037,7 +1029,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> greater than", "example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> ' greater than '",
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than <code>b</code></strong>.</p>\n" "description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than <code>b</code></strong>.</p>\n"
}, },
"unlessLt": { "unlessLt": {
@ -1047,7 +1039,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#unlessLt 20 1 }} greater than or equal {{else}} less than {{/unlessLt}} -> greater than or equal", "example": "{{#unlessLt 20 1 }}greater than or equal{{else}}less than{{/unlessLt}} -> greater than or equal",
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than <code>b</code></strong>.</p>\n" "description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than <code>b</code></strong>.</p>\n"
}, },
"unlessGteq": { "unlessGteq": {
@ -1057,7 +1049,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#unlessGteq 20 1 }} less than {{else}} greater than or equal to {{/unlessGteq}} -> greater than or equal to", "example": "{{#unlessGteq 20 1 }} less than {{else}}greater than or equal to{{/unlessGteq}} -> greater than or equal to",
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than or equal to <code>b</code></strong>.</p>\n" "description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is greater than or equal to <code>b</code></strong>.</p>\n"
}, },
"unlessLteq": { "unlessLteq": {
@ -1067,7 +1059,7 @@
"options" "options"
], ],
"numArgs": 3, "numArgs": 3,
"example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> greater than", "example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> ' greater than '",
"description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than or equal to <code>b</code></strong>.</p>\n" "description": "<p>Block helper that always renders the inverse block <strong>unless <code>a</code> is less than or equal to <code>b</code></strong>.</p>\n"
} }
}, },
@ -1204,7 +1196,7 @@
"durationType" "durationType"
], ],
"numArgs": 2, "numArgs": 2,
"example": "{{duration timeLeft \"seconds\"}} -> a few seconds", "example": "{{duration 8 \"seconds\"}} -> a few seconds",
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n" "description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
} }
} }

View File

@ -25,7 +25,7 @@
"manifest": "node ./scripts/gen-collection-info.js" "manifest": "node ./scripts/gen-collection-info.js"
}, },
"dependencies": { "dependencies": {
"@budibase/handlebars-helpers": "^0.12.0", "@budibase/handlebars-helpers": "^0.13.0",
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View File

@ -36,7 +36,7 @@ const ADDED_HELPERS = {
duration: { duration: {
args: ["time", "durationType"], args: ["time", "durationType"],
numArgs: 2, numArgs: 2,
example: '{{duration timeLeft "seconds"}} -> a few seconds', example: '{{duration 8 "seconds"}} -> a few seconds',
description: description:
"Produce a humanized duration left/until given an amount of time and the type of time measurement.", "Produce a humanized duration left/until given an amount of time and the type of time measurement.",
}, },
@ -118,6 +118,8 @@ function getCommentInfo(file, func) {
return docs return docs
} }
const excludeFunctions = { string: ["raw"] }
/** /**
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them. * This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
*/ */
@ -136,7 +138,8 @@ function run() {
// skip built in functions and ones seen already // skip built in functions and ones seen already
if ( if (
HelperFunctionBuiltin.indexOf(name) !== -1 || HelperFunctionBuiltin.indexOf(name) !== -1 ||
foundNames.indexOf(name) !== -1 foundNames.indexOf(name) !== -1 ||
excludeFunctions[collection]?.includes(name)
) { ) {
continue continue
} }

View File

@ -61,10 +61,10 @@ describe("test the array helpers", () => {
}) })
it("should allow use of the before helper", async () => { it("should allow use of the before helper", async () => {
const output = await processString("{{before array 2}}", { const output = await processString("{{before array 3}}", {
array, array,
}) })
expect(output).toBe("hi,person,how") expect(output).toBe("hi,person")
}) })
it("should allow use of the filter helper", async () => { it("should allow use of the filter helper", async () => {

View File

@ -0,0 +1,96 @@
jest.mock("@budibase/handlebars-helpers/lib/math", () => {
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math")
return {
...actual,
random: () => 10,
}
})
jest.mock("@budibase/handlebars-helpers/lib/uuid", () => {
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/uuid")
return {
...actual,
uuid: () => "f34ebc66-93bd-4f7c-b79b-92b5569138bc",
}
})
const fs = require("fs")
const { processString } = require("../src/index.cjs")
const tk = require("timekeeper")
tk.freeze("2021-01-21T12:00:00")
const manifest = JSON.parse(
fs.readFileSync(require.resolve("../manifest.json"), "utf8")
)
const collections = Object.keys(manifest)
const examples = collections.reduce((acc, collection) => {
const functions = Object.keys(manifest[collection]).filter(
fnc => manifest[collection][fnc].example
)
if (functions.length) {
acc[collection] = functions
}
return acc
}, {})
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
}
function tryParseJson(str) {
if (typeof str !== "string") {
return
}
try {
return JSON.parse(str.replace(/\'/g, '"'))
} catch (e) {
return
}
}
describe("manifest", () => {
describe("examples are valid", () => {
describe.each(Object.keys(examples))("%s", collection => {
it.each(examples[collection])("%s", async func => {
const example = manifest[collection][func].example
let [hbs, js] = example.split("->").map(x => x.trim())
const context = {
double: i => i * 2,
isString: x => typeof x === "string",
}
const arrays = hbs.match(/\[[^/\]]+\]/)
arrays?.forEach((arrayString, i) => {
hbs = hbs.replace(new RegExp(escapeRegExp(arrayString)), `array${i}`)
context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"'))
})
if (js === undefined) {
// The function has no return value
return
}
let result = await processString(hbs, context)
// Trim 's
js = js.replace(/^\'|\'$/g, "")
if ((parsedExpected = tryParseJson(js))) {
if (Array.isArray(parsedExpected)) {
if (typeof parsedExpected[0] === "object") {
js = JSON.stringify(parsedExpected)
} else {
js = parsedExpected.join(",")
}
}
}
result = result.replace(/&nbsp;/g, " ")
expect(result).toEqual(js)
})
})
})
})

View File

@ -23,6 +23,7 @@ export interface App extends Document {
automationErrors?: AppMetadataErrors automationErrors?: AppMetadataErrors
icon?: AppIcon icon?: AppIcon
features?: AppFeatures features?: AppFeatures
automations?: AutomationSettings
} }
export interface AppInstance { export interface AppInstance {
@ -68,3 +69,7 @@ export interface AppFeatures {
componentValidation?: boolean componentValidation?: boolean
disableUserMetadata?: boolean disableUserMetadata?: boolean
} }
export interface AutomationSettings {
chainAutomations?: boolean
}

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
yarn build --scope @budibase/server --scope @budibase/worker yarn build --scope @budibase/server --scope @budibase/worker
version=$(./scripts/getCurrentVersion.sh) version=$(./scripts/getCurrentVersion.sh)
docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version . docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version --build-arg TARGETBUILD=single .

View File

@ -2031,10 +2031,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/handlebars-helpers@^0.12.0": "@budibase/handlebars-helpers@^0.13.0":
version "0.12.0" version "0.13.0"
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.12.0.tgz#dcc4ba8d796a611474e3495b1142c56b470ca67d" resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.0.tgz#224333d14e3900b7dacf48286af1e624a9fd62ea"
integrity sha512-JjGboau7KMdrVSO8gGJzgo1ACSeD4BxN46vidIx9hvdrEXy+v1x2bfQZMaq/c7Dv+V1vyq7c006XwxR1bpfARg== integrity sha512-g8+sFrMNxsIDnK+MmdUICTVGr6ReUFtnPp9hJX0VZwz1pN3Ynolpk/Qbu6rEWAvoU1sEqY1mXr9uo/+kEfeGbQ==
dependencies: dependencies:
get-object "^0.2.0" get-object "^0.2.0"
get-value "^3.0.1" get-value "^3.0.1"