1
0
Fork 0
mirror of synced 2024-07-03 13:30:46 +12:00

Merge branch 'master' of github.com:Budibase/budibase into form-builder

This commit is contained in:
Andrew Kingston 2021-02-02 14:44:19 +00:00
commit e87b384de5
49 changed files with 2479 additions and 330 deletions

View file

@ -86,6 +86,10 @@ Watch "releases" of this repo to get notified of major updates, and give the sta
<img src="https://i.imgur.com/cJpgqm8.png">
</p>
### Stargazers over time
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md#troubleshooting) to clear down your environment.
@ -107,6 +111,8 @@ Budibase wants to make sure anyone can use the tools we develop and we know a lo
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb&region=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
## 🎓 Learning Budibase
@ -154,6 +160,7 @@ If you have a question or would like to talk with other Budibase users, please h
![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

View file

@ -1,8 +1,11 @@
version: "3"
# optional ports are specified throughout for more advanced use cases.
services:
app-service:
restart: always
#build: ./build/server
image: budibase/budibase-apps
ports:
- "${APP_PORT}:4002"
@ -20,6 +23,7 @@ services:
worker-service:
restart: always
#build: ./build/worker
image: budibase/budibase-worker
ports:
- "${WORKER_PORT}:4003"
@ -62,7 +66,7 @@ services:
- ./envoy.yaml:/etc/envoy/envoy.yaml
ports:
- "${MAIN_PORT}:10000"
- "9901:9901"
#- "9901:9901"
depends_on:
- minio-service
- worker-service
@ -77,8 +81,8 @@ services:
- COUCHDB_USER=${COUCH_DB_USER}
ports:
- "${COUCH_DB_PORT}:5984"
- "4369:4369"
- "9100:9100"
#- "4369:4369"
#- "9100:9100"
volumes:
- couchdb_data:/couchdb

View file

@ -20,6 +20,11 @@ static_resources:
route:
cluster: app-service
prefix_rewrite: "/"
# special case for presenting our static self hosting page
- match: { path: "/" }
route:
cluster: app-service
# special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }

View file

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.55.2",
"@budibase/bbui": "^1.56.0",
"@budibase/client": "^0.6.2",
"@budibase/colorpicker": "^1.0.1",
"@budibase/string-templates": "^0.6.2",

View file

@ -2,6 +2,7 @@ import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore"
import { findComponentPath } from "./storeUtils"
import { makePropSafe } from "@budibase/string-templates"
import { TableNames } from "../constants"
// Regex to match all instances of template strings
@ -128,7 +129,9 @@ export const getContextBindings = (rootComponent, componentId) => {
contextBindings.push({
type: "context",
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey
)}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`,
fieldSchema,
providerId: component._id,
@ -197,43 +200,52 @@ export const getSchemaForDatasource = datasource => {
}
/**
* Converts a readable data binding into a runtime data binding
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
const convertFrom =
convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding"
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const convertFromProps = bindableProperties
.map(el => el[convertFrom])
.sort((a, b) => {
return b.length - a.length
})
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
let result = textWithBindings
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ readableBinding }) => {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
for (let boundValue of boundValues) {
let newBoundValue = boundValue
for (let from of convertFromProps) {
if (newBoundValue.includes(from)) {
const binding = bindableProperties.find(el => el[convertFrom] === from)
newBoundValue = newBoundValue.replace(from, binding[convertTo])
}
}
})
result = result.replace(boundValue, newBoundValue)
}
return result
}
/**
* Converts a readable data binding into a runtime data binding
*/
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
return bindingReplacement(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
}
/**
* Converts a runtime data binding into a readable data binding
*/
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
let result = textWithBindings
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ runtimeBinding }) => {
return boundValue === `{{ ${runtimeBinding} }}`
})
// Show invalid bindings as invalid rather than a long ID
result = result.replace(
boundValue,
`{{ ${binding?.readableBinding ?? "Invalid binding"} }}`
)
})
return result
return bindingReplacement(
bindableProperties,
textWithBindings,
"readableBinding"
)
}

View file

@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl"
import { rowListUrl } from "./rowListScreen"
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import { makePropSafe } from "@budibase/string-templates"
import {
makeBreadcrumbContainer,
makeTitleContainer,
@ -58,8 +59,8 @@ function generateTitleContainer(table, title, formId) {
onClick: [
{
parameters: {
rowId: `{{ ${formId}._id }}`,
revId: `{{ ${formId}._rev }}`,
rowId: `{{ ${makePropSafe(formId)}._id }}`,
revId: `{{ ${makePropSafe(formId)}._rev }}`,
tableId: table._id,
},
"##eventHandlerType": "Delete Row",
@ -113,7 +114,7 @@ const createScreen = table => {
const formId = form._json._id
const rowDetailId = screen._json.props._id
const heading = table.primaryDisplay
? `{{ ${rowDetailId}.${table.primaryDisplay} }}`
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}`
: null
form
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))

View file

@ -3,6 +3,7 @@
import {
TextArea,
Label,
Input,
Heading,
Body,
Spacer,
@ -10,6 +11,9 @@
Popover,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions"
const dispatch = createEventDispatcher()
export let value = ""
@ -18,9 +22,14 @@
export let align
export let popover = null
let helpers = handlebarsCompletions()
let getCaretPosition
let validity = true
let search = ""
$: categories = Object.entries(groupBy("category", bindings))
$: value && checkValid()
$: searchRgx = new RegExp(search, "ig")
function onClickBinding(binding) {
const position = getCaretPosition()
@ -34,18 +43,27 @@
value += toAdd
}
}
function checkValid() {
validity = isValid(value)
}
</script>
<Popover {anchor} {align} bind:this={popover}>
<div class="container">
<div class="bindings">
<Heading small>Available bindings</Heading>
<Spacer medium />
<Input extraThin placeholder="Search" bind:value={search} />
<Spacer medium />
<div class="bindings__wrapper">
<div class="bindings__list">
{#each categories as [categoryName, bindings]}
<Heading extraSmall>{categoryName}</Heading>
<Spacer extraSmall />
{#each bindings as binding}
{#each bindings.filter(binding =>
binding.label.match(searchRgx)
) as binding}
<div class="binding" on:click={() => onClickBinding(binding)}>
<span class="binding__label">{binding.label}</span>
<span class="binding__type">{binding.type}</span>
@ -56,6 +74,17 @@
</div>
{/each}
{/each}
<Heading extraSmall>Helpers</Heading>
<Spacer extraSmall />
{#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper}
<div class="binding" on:click={() => onClickBinding(helper)}>
<span class="binding__label">{helper.label}</span>
<br />
<div class="binding__description">
{@html helper.description || ''}
</div>
</div>
{/each}
</div>
</div>
</div>
@ -70,11 +99,20 @@
bind:getCaretPosition
bind:value
placeholder="Add options from the left, type text, or do both" />
{#if !validity}
<p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a>
for more details.
</p>
{/if}
<div class="controls">
<a href="https://docs.budibase.com/design/binding">
<Body small>Learn more about binding</Body>
</a>
<Button on:click={popover.hide} primary>Done</Button>
<Button on:click={popover.hide} disabled={!validity} primary>
Done
</Button>
</div>
</div>
</div>
@ -152,4 +190,14 @@
align-items: center;
margin-top: var(--spacing-m);
}
.syntax-error {
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
</style>

View file

@ -9,7 +9,7 @@
let permissions = []
let selectedRole = {}
let errors = []
let builtInRoles = ['Admin', 'Power', 'Basic', 'Public']
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
$: selectedRoleId = selectedRole._id
$: otherRoles = $backendUiStore.roles.filter(
role => role._id !== selectedRoleId
@ -103,7 +103,11 @@
{/each}
</Select>
{#if selectedRole}
<Input label="Name" bind:value={selectedRole.name} thin disabled={builtInRoles.includes(selectedRole.name)}/>
<Input
label="Name"
bind:value={selectedRole.name}
thin
disabled={builtInRoles.includes(selectedRole.name)} />
<Select
thin
secondary

View file

@ -1,6 +1,7 @@
<script>
import api from "builderStore/api"
import { Button, Select } from "@budibase/bbui"
import download from "downloadjs"
const FORMATS = [
{

View file

@ -41,7 +41,7 @@
.icon {
right: 2px;
top: 2px;
top: 26px;
bottom: 2px;
position: absolute;
align-items: center;

View file

@ -1,59 +1,127 @@
<script>
import groupBy from "lodash/fp/groupBy"
import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
import { Input, TextArea, Heading, Spacer, Label } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import {
getBindableProperties,
readableToRuntimeBinding,
} from "builderStore/dataBinding"
import { currentAsset, store } from "../../../builderStore"
import { handlebarsCompletions } from "constants/completions"
const dispatch = createEventDispatcher()
export let bindableProperties
export let value = ""
export let bindingDrawer
export let valid = true
let originalValue = value
let helpers = handlebarsCompletions()
let getCaretPosition
let search = ""
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: dispatch("update", value)
$: ({ instance, context } = groupBy("type", bindableProperties))
$: searchRgx = new RegExp(search, "ig")
function checkValid() {
// TODO: need to convert the value to the runtime binding
const runtimeBinding = readableToRuntimeBinding(bindableProperties, value)
valid = isValid(runtimeBinding)
}
function addToText(readableBinding) {
value = `${value || ""}{{ ${readableBinding} }}`
const position = getCaretPosition()
const toAdd = `{{ ${readableBinding} }}`
if (position.start) {
value =
value.substring(0, position.start) +
toAdd +
value.substring(position.end, value.length)
} else {
value += toAdd
}
}
let originalValue = value
$: dispatch("update", value)
export function cancel() {
dispatch("update", originalValue)
bindingDrawer.close()
}
$: ({ instance, context } = groupBy("type", bindableProperties))
function updateValue({ detail }) {
value = detail.value
}
</script>
<div class="drawer-contents">
<div class="container" data-cy="binding-dropdown-modal">
<div class="list">
<Input extraThin placeholder="Search" bind:value={search} />
<Spacer medium />
{#if context}
<Heading extraSmall>Columns</Heading>
<Spacer small />
<ul>
{#each context as { readableBinding }}
{#each context.filter(context =>
context.readableBinding.match(searchRgx)
) as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each}
</ul>
{/if}
<Spacer small />
{#if instance}
<Heading extraSmall>Components</Heading>
<Spacer small />
<ul>
{#each instance as { readableBinding }}
{#each instance.filter(instance =>
instance.readableBinding.match(searchRgx)
) as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each}
</ul>
{/if}
<Spacer small />
<Heading extraSmall>Helpers</Heading>
<Spacer small />
<ul>
{#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper}
<li on:click={() => addToText(helper.text)}>
<div>
<Label extraSmall>{helper.displayText}</Label>
<div class="description">
{@html helper.description}
</div>
</div>
</li>
{/each}
</ul>
</div>
<div class="text">
<TextArea
bind:getCaretPosition
thin
bind:value
placeholder="Add text, or click the objects on the left to add them to
the textbox." />
{#if !valid}
<p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a>
for more details.
</p>
{/if}
</div>
</div>
</div>
@ -114,4 +182,27 @@
height: 40vh;
overflow-y: auto;
}
.syntax-error {
padding-top: var(--spacing-m);
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
.description :global(p) {
color: var(--grey-7);
}
.description :global(p:hover) {
color: var(--ink);
}
.description :global(p a) {
color: var(--grey-7);
}
</style>

View file

@ -21,6 +21,7 @@
let bindingDrawer
let temporaryBindableValue = value
let anchor
let valid
$: bindableProperties = getBindableProperties(
$currentAsset.props,
@ -79,7 +80,7 @@
class="icon"
data-cy={`${key}-binding-button`}
on:click={bindingDrawer.show}>
<Icon name="edit" />
<Icon name="lightning" />
</div>
{/if}
</div>
@ -90,10 +91,11 @@
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
bind:valid
value={safeValue}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
@ -142,7 +144,7 @@
border-top-right-radius: var(--border-radius-m);
border-bottom-right-radius: var(--border-radius-m);
color: var(--grey-7);
font-size: 16px;
font-size: 14px;
}
.icon:hover {
color: var(--ink);

View file

@ -2,6 +2,7 @@
import CodeMirror from "./codemirror"
import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "builderStore"
import { handlebarsCompletions } from "constants/completions"
const dispatch = createEventDispatcher()
@ -15,6 +16,14 @@
export let lineNumbers = true
export let tab = true
export let mode
// export let parameters = []
let completions = handlebarsCompletions()
// $: completions = parameters.map(param => ({
// text: `{{ ${param.name} }}`,
// displayText: param.name,
// }))
let width
let height
@ -109,6 +118,7 @@
mode: modes[mode] || {
name: mode,
},
readOnly,
autoCloseBrackets: true,
autoCloseTags: true,
@ -136,6 +146,18 @@
}
})
// editor.on("cursorActivity", function() {
// editor.showHint({
// hint: function() {
// return {
// from: editor.getDoc().getCursor(),
// to: editor.getDoc().getCursor(),
// list: completions,
// }
// },
// })
// })
if (first) await sleep(50)
editor.refresh()

View file

@ -1,10 +1,12 @@
import CodeMirror from "codemirror"
import "codemirror/lib/codemirror.css"
import "codemirror/theme/tomorrow-night-eighties.css"
import "codemirror/addon/hint/show-hint.css"
import "codemirror/theme/neo.css"
import "codemirror/mode/sql/sql"
import "codemirror/mode/css/css"
import "codemirror/mode/handlebars/handlebars"
import "codemirror/mode/javascript/javascript"
import "codemirror/addon/hint/show-hint"
export default CodeMirror

View file

@ -28,14 +28,16 @@
mode="sql"
on:change={updateQuery}
readOnly={!editable}
value={query.fields.sql} />
value={query.fields.sql}
parameters={query.parameters} />
{:else if schema.type === QueryTypes.JSON}
<Editor
label="Query"
mode="json"
on:change={updateQuery}
readOnly={!editable}
value={query.fields.json} />
value={query.fields.json}
parameters={query.parameters} />
{:else if schema.type === QueryTypes.FIELDS}
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
{/if}

View file

@ -0,0 +1,15 @@
import { getManifest } from "@budibase/string-templates"
export function handlebarsCompletions() {
const manifest = getManifest()
return Object.keys(manifest).flatMap(key =>
Object.entries(manifest[key]).map(([helperName, helperConfig]) => ({
text: helperName,
path: helperName,
label: helperName,
displayText: helperName,
description: helperConfig.description,
}))
)
}

View file

@ -842,10 +842,10 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@budibase/bbui@^1.55.2":
version "1.55.2"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.55.2.tgz#be636e8b86b7e516a08eb626bb50c4b1f9774bf8"
integrity sha512-CevH/olxDFIko9uwYUpUTevPmpShrLwJSR7+wn/JetZERwhTwbLhOXzpsyXaK226qQ8vWhm0U31HRSKI1HwDDg==
"@budibase/bbui@^1.56.0":
version "1.56.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.56.0.tgz#91376f11b43706fd380291e9a5283760996eb62b"
integrity sha512-OEFC7MapbJk7Bd7oo79cVOq9BIcK1x8XPHLC1lB2N4hts37IygzB4Egg6JZBD7rh7CqU8ppc4W7wkfQbaXEO1Q==
dependencies:
markdown-it "^12.0.2"
quill "^1.3.7"

View file

@ -26,7 +26,7 @@
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-terser": "^4.0.4",
"rollup-plugin-terser": "^7.0.2",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"
},

View file

@ -2,6 +2,7 @@ import commonjs from "@rollup/plugin-commonjs"
import resolve from "@rollup/plugin-node-resolve"
import builtins from "rollup-plugin-node-builtins"
import svelte from "rollup-plugin-svelte"
import { terser } from "rollup-plugin-terser"
const production = !process.env.ROLLUP_WATCH
@ -25,6 +26,7 @@ export default {
}),
commonjs(),
builtins(),
production && terser(),
],
watch: {
clearScreen: false,

View file

@ -9,6 +9,13 @@
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/code-frame@^7.10.4":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/helper-validator-identifier@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
@ -373,7 +380,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.19.0:
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -847,6 +854,11 @@ has-flag@^3.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
has-flag@^4.0.0:
version "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.0, has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
@ -1097,13 +1109,14 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
jest-worker@^24.0.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
jest-worker@^26.2.1:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
dependencies:
"@types/node" "*"
merge-stream "^2.0.0"
supports-color "^6.1.0"
supports-color "^7.0.0"
js-tokens@^4.0.0:
version "4.0.0"
@ -1564,7 +1577,7 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@ -1744,15 +1757,15 @@ rollup-plugin-svelte@^6.1.1:
rollup-pluginutils "^2.8.2"
sourcemap-codec "^1.4.8"
rollup-plugin-terser@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz#6f661ef284fa7c27963d242601691dc3d23f994e"
integrity sha512-wPANT5XKVJJ8RDUN0+wIr7UPd0lIXBo4UdJ59VmlPCtlFsE20AM+14pe+tk7YunCsWEiuzkDBY3QIkSCjtrPXg==
rollup-plugin-terser@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
dependencies:
"@babel/code-frame" "^7.0.0"
jest-worker "^24.0.0"
serialize-javascript "^1.6.1"
terser "^3.14.1"
"@babel/code-frame" "^7.10.4"
jest-worker "^26.2.1"
serialize-javascript "^4.0.0"
terser "^5.0.0"
rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
version "2.8.2"
@ -1795,10 +1808,12 @@ semver@~2.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
integrity sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=
serialize-javascript@^1.6.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
dependencies:
randombytes "^2.1.0"
sha.js@^2.4.0, sha.js@^2.4.8:
version "2.4.11"
@ -1823,7 +1838,7 @@ side-channel@^1.0.2:
es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0"
source-map-support@~0.5.10:
source-map-support@~0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@ -1836,6 +1851,11 @@ source-map@^0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@~0.7.2:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
@ -1908,12 +1928,12 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
supports-color@^7.0.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^3.0.0"
has-flag "^4.0.0"
svelte-jester@^1.0.6:
version "1.1.5"
@ -1939,14 +1959,14 @@ symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
terser@^3.14.1:
version "3.17.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
terser@^5.0.0:
version "5.5.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289"
integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==
dependencies:
commander "^2.19.0"
source-map "~0.6.1"
source-map-support "~0.5.10"
commander "^2.20.0"
source-map "~0.7.2"
source-map-support "~0.5.19"
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
version "2.5.0"

View file

@ -45,7 +45,7 @@ exports.authenticate = async ctx => {
expiresIn: "1 day",
})
setCookie(ctx, appId, token)
setCookie(ctx, token, appId)
delete dbUser.password
ctx.body = {

View file

@ -49,6 +49,17 @@ exports.serveBuilder = async function(ctx) {
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
}
exports.serveSelfHostPage = async function(ctx) {
const logo = fs.readFileSync(resolve(__dirname, "selfhost/logo.svg"), "utf8")
const hostingHbs = fs.readFileSync(
resolve(__dirname, "selfhost/index.hbs"),
"utf8"
)
ctx.body = await processString(hostingHbs, {
logo,
})
}
exports.uploadFile = async function(ctx) {
let files
files =

View file

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Budibase self hosting</title>
<style>
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
height: 100%;
width: 100%;
margin: 0;
padding: 0;
background: #fafafa;
}
.main {
padding: 0 20px;
margin: 30px auto;
width: 60%;
}
h2 {
font-size: clamp(24px, 1.5vw, 30px);
text-align: center;
line-height: 1.3;
font-weight: bold;
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
}
.card {
display: grid;
background-color: #222222;
grid-template-columns: 1fr;
align-items: center;
padding: 2.5rem 1.75rem;
border-radius: 12px;
color: white;
}
.card h3 {
margin: 0;
font-size: 24px;
font-family: sans-serif;
font-weight: 600;
}
.card h3 b {
text-wrap: normal;
font-size: 36px;
padding-right: 14px;
}
.card p {
color: #ffffff;
opacity: 0.8;
font-size: 18px;
text-align: left;
line-height: 1.3;
margin-top: 1rem;
}
.logo {
width: 60px;
height: 60px;
margin: auto;
}
.top-text {
text-align: center;
color: #707070;
margin: 0 0 1.5rem 0;
}
.button {
cursor: pointer;
display: block;
background: #4285f4;
color: white;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
border-radius: 6px;
max-width: 120px;
text-align: center;
transition: 200ms background ease;
text-decoration: none;
}
.info {
background: #f5f5f5;
padding: 1rem 1rem 1rem 1rem;
border: #ccc 1px solid;
border-radius: 6px;
margin-top: 40px;
display: flex;
align-items: center;
}
.info p {
margin-left: 20px;
color: #222222;
font-family: sans-serif;
}
.info p {
margin-right: 20px;
}
.info svg {
margin-left: 20px;
}
.info a {
color: #4285f4;
}
</style>
</head>
<body>
<div class="main">
<div class="logo">
{{logo}}
</div>
<h2>Get started with Budibase Self Hosting</h2>
<p class="top-text">Use the address <b id="url"></b> in your Builder</p>
<div class="card-grid">
<div class="card">
<h3><b>📚</b>Documentation</h3>
<p>
Find out more about your self hosted platform.
</p>
<a class="button"
href="https://docs.budibase.com/self-hosting/introduction-to-self-hosting">
Documentation
</a>
</div>
<div class="card">
<h3><b>💻</b>Next steps</h3>
<p>
Find out how to make use of your self hosted Budibase platform.
</p>
<a class="button"
href="https://docs.budibase.com/self-hosting/builder-settings">
Next steps
</a>
</div>
</div>
<div class="info">
<svg preserveAspectRatio="xMidYMid meet" height="28px" width="28px" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" stroke="none" class="icon-7f6730be--text-3f89f380">
<g>
<path d="M12.2 8.98c.06-.01.12-.03.18-.06.06-.02.12-.05.18-.09l.15-.12c.18-.19.29-.45.29-.71 0-.06-.01-.13-.02-.19a.603.603 0 0 0-.06-.19.757.757 0 0 0-.09-.18c-.03-.05-.08-.1-.12-.15-.28-.27-.72-.37-1.09-.21-.13.05-.23.12-.33.21-.04.05-.09.1-.12.15-.04.06-.07.12-.09.18-.03.06-.05.12-.06.19-.01.06-.02.13-.02.19 0 .26.11.52.29.71.1.09.2.16.33.21.12.05.25.08.38.08.06 0 .13-.01.2-.02M13 16v-4a1 1 0 1 0-2 0v4a1 1 0 1 0 2 0M12 3c-4.962 0-9 4.038-9 9 0 4.963 4.038 9 9 9 4.963 0 9-4.037 9-9 0-4.962-4.037-9-9-9m0 20C5.935 23 1 18.065 1 12S5.935 1 12 1c6.066 0 11 4.935 11 11s-4.934 11-11 11" fill-rule="evenodd">
</path>
</g>
</svg>
<p>A <b>Hosting Key</b> will also be required, this can be found in your hosting properties, info found <a href="https://docs.budibase.com/self-hosting/hosting-settings">here</a>.</p>
</div>
</div>
<script>
window.addEventListener("load", () => {
let url = document.URL.split("//")[1]
if (url.substring(url.length - 1) === "/") {
url = url.substring(0, url.length - 1)
}
document.getElementById("url").innerHTML = url
})
</script>
</body>
</html>

View file

@ -0,0 +1,17 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 48 48" xml:space="preserve">
<style>
.st0{fill:#393c44}.st1{fill:#fff}
</style>
<path class="st0" d="M-152.17-24.17H-56V72h-96.17z"/>
<path class="st1" d="M-83.19 48h-41.79c-1.76 0-3.19-1.43-3.19-3.19V3.02c0-1.76 1.43-3.19 3.19-3.19h41.79c1.76 0 3.19 1.43 3.19 3.19v41.79c0 1.76-1.43 3.19-3.19 3.19z"/>
<path class="st0" d="M-99.62 12.57v9.94c1.15-1.21 2.59-1.81 4.32-1.81 1.03 0 1.97.19 2.82.58.86.39 1.59.91 2.19 1.57.6.66 1.08 1.43 1.42 2.32.34.89.51 1.84.51 2.85 0 1.03-.18 1.99-.53 2.89-.35.9-.84 1.68-1.47 2.35-.63.67-1.37 1.19-2.23 1.58-.86.39-1.78.58-2.77.58-1.8 0-3.22-.66-4.27-1.97V35h-4.89V12.57h4.9zm6.16 15.54c0-.43-.08-.84-.24-1.23-.16-.39-.39-.72-.68-1.01-.29-.29-.62-.52-1-.69-.38-.17-.79-.26-1.24-.26-.43 0-.84.08-1.22.24-.38.16-.71.39-.99.68-.28.29-.5.63-.68 1.01-.17.39-.26.8-.26 1.23 0 .43.08.84.24 1.22.16.38.39.71.68.99.29.28.63.5 1.01.68.39.17.8.26 1.23.26.43 0 .84-.08 1.22-.24.38-.16.71-.39.99-.68.28-.29.5-.62.68-1 .17-.39.26-.79.26-1.2z"/>
<path class="st0" d="M-114.76 12.57v9.94c1.15-1.21 2.59-1.81 4.32-1.81 1.03 0 1.97.19 2.82.58.86.39 1.59.91 2.19 1.57.6.66 1.08 1.43 1.42 2.32.34.89.51 1.84.51 2.85 0 1.03-.18 1.99-.53 2.89-.35.9-.84 1.68-1.47 2.35-.63.67-1.37 1.19-2.23 1.58-.86.39-1.78.58-2.77.58-1.8 0-3.22-.66-4.27-1.97V35h-4.89V12.57h4.9zm6.16 15.54c0-.43-.08-.84-.24-1.23-.16-.39-.39-.72-.68-1.01-.29-.29-.62-.52-1-.69-.38-.17-.79-.26-1.24-.26-.43 0-.84.08-1.22.24-.38.16-.71.39-.99.68-.28.29-.5.63-.68 1.01-.17.39-.26.8-.26 1.23 0 .43.08.84.24 1.22.16.38.39.71.68.99.29.28.63.5 1.01.68.39.17.8.26 1.23.26.43 0 .84-.08 1.22-.24.38-.16.71-.39.99-.68.28-.29.5-.62.68-1 .18-.39.26-.79.26-1.2z"/>
<path d="M44.81 159H3.02c-1.76 0-3.19-1.43-3.19-3.19v-41.79c0-1.76 1.43-3.19 3.19-3.19h41.79c1.76 0 3.19 1.43 3.19 3.19v41.79c0 1.76-1.43 3.19-3.19 3.19z" fill="#4285f4"/>
<path class="st1" d="M28.38 123.57v9.94c1.15-1.21 2.59-1.81 4.32-1.81 1.03 0 1.97.19 2.82.58.86.39 1.59.91 2.19 1.57.6.66 1.08 1.43 1.42 2.32.34.89.51 1.84.51 2.85 0 1.03-.18 1.99-.53 2.89-.35.9-.84 1.68-1.47 2.35-.63.67-1.37 1.19-2.23 1.58-.86.39-1.78.58-2.77.58-1.8 0-3.22-.66-4.27-1.97V146h-4.89v-22.43h4.9zm6.16 15.54c0-.43-.08-.84-.24-1.23-.16-.39-.39-.72-.68-1.01-.29-.29-.62-.52-1-.69-.38-.17-.79-.26-1.24-.26-.43 0-.84.08-1.22.24-.38.16-.71.39-.99.68-.28.29-.5.63-.68 1.01-.17.39-.26.8-.26 1.23 0 .43.08.84.24 1.22.16.38.39.71.68.99.29.28.63.5 1.01.68.39.17.8.26 1.23.26.43 0 .84-.08 1.22-.24.38-.16.71-.39.99-.68.28-.29.5-.62.68-1 .17-.39.26-.79.26-1.2z"/>
<path class="st1" d="M13.24 123.57v9.94c1.15-1.21 2.59-1.81 4.32-1.81 1.03 0 1.97.19 2.82.58.86.39 1.59.91 2.19 1.57.6.66 1.08 1.43 1.42 2.32.34.89.51 1.84.51 2.85 0 1.03-.18 1.99-.53 2.89-.35.9-.84 1.68-1.47 2.35-.63.67-1.37 1.19-2.23 1.58-.86.39-1.78.58-2.77.58-1.8 0-3.22-.66-4.27-1.97V146H8.35v-22.43h4.89zm6.16 15.54c0-.43-.08-.84-.24-1.23-.16-.39-.39-.72-.68-1.01-.29-.29-.62-.52-1-.69-.38-.17-.79-.26-1.24-.26-.43 0-.84.08-1.22.24-.38.16-.71.39-.99.68-.28.29-.5.63-.68 1.01-.17.39-.26.8-.26 1.23 0 .43.08.84.24 1.22.16.38.39.71.68.99.29.28.63.5 1.01.68.39.17.8.26 1.23.26.43 0 .84-.08 1.22-.24.38-.16.71-.39.99-.68.28-.29.5-.62.68-1 .18-.39.26-.79.26-1.2z"/>
<g>
<path class="st0" d="M44 48H4c-2.21 0-4-1.79-4-4V4c0-2.21 1.79-4 4-4h40c2.21 0 4 1.79 4 4v40c0 2.21-1.79 4-4 4z"/>
<path class="st1" d="M28.48 12v10.44c1.18-1.27 2.65-1.9 4.42-1.9 1.05 0 2.01.2 2.89.61.87.41 1.62.96 2.24 1.65.62.69 1.1 1.5 1.45 2.44.35.94.52 1.93.52 2.99 0 1.08-.18 2.09-.54 3.04-.36.95-.86 1.77-1.51 2.47-.64.7-1.4 1.25-2.28 1.66-.87.4-1.81.6-2.83.6-1.84 0-3.3-.69-4.37-2.07v1.62h-5V12h5.01zm6.3 16.31c0-.45-.08-.88-.25-1.29-.17-.41-.4-.76-.69-1.06-.3-.3-.64-.54-1.02-.72-.39-.18-.81-.27-1.27-.27-.44 0-.86.09-1.24.26-.39.17-.72.41-1.01.71-.29.3-.52.66-.69 1.06-.18.41-.26.84-.26 1.29s.08.88.25 1.28c.17.4.4.74.69 1.04.29.29.64.53 1.04.71.4.18.82.27 1.26.27.44 0 .86-.09 1.24-.26.39-.17.72-.41 1.01-.71.29-.3.52-.65.69-1.05.16-.41.25-.82.25-1.26z"/>
<path class="st1" d="M13 12v10.44c1.18-1.27 2.65-1.9 4.42-1.9 1.05 0 2.01.2 2.89.61.87.41 1.62.96 2.24 1.65.62.69 1.1 1.5 1.45 2.44.35.94.52 1.93.52 2.99 0 1.08-.18 2.09-.54 3.04-.36.95-.86 1.77-1.51 2.47-.64.7-1.4 1.25-2.28 1.66-.87.4-1.81.6-2.82.6-1.84 0-3.3-.69-4.37-2.07v1.62H8V12h5zm6.3 16.31c0-.45-.08-.88-.25-1.29-.17-.41-.4-.76-.69-1.06-.3-.3-.64-.54-1.02-.72-.39-.18-.81-.27-1.27-.27-.44 0-.86.09-1.24.26-.39.17-.72.41-1.01.71-.29.3-.52.66-.69 1.06-.18.41-.26.84-.26 1.29s.08.88.25 1.28c.17.4.4.74.69 1.04.29.29.64.53 1.04.71.4.18.82.27 1.26.27.44 0 .86-.09 1.24-.26.39-.17.72-.41 1.01-.71.29-.3.52-.65.69-1.05.16-.41.25-.82.25-1.26z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -65,6 +65,8 @@ for (let route of mainRoutes) {
router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods())
router.redirect("/", "/_builder")
if (!env.SELF_HOSTED && !env.CLOUD) {
router.redirect("/", "/_builder")
}
module.exports = router

View file

@ -23,6 +23,10 @@ if (env.NODE_ENV !== "production") {
router.get("/_builder/:file*", controller.serveBuilder)
}
if (env.SELF_HOSTED) {
router.get("/", controller.serveSelfHostPage)
}
router
.post(
"/api/attachments/process",

View file

@ -2,7 +2,13 @@ const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const { getRole, BUILTIN_ROLES } = require("../utilities/security/roles")
const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie, isClient } = require("../utilities")
const {
getAppId,
getCookieName,
clearCookie,
setCookie,
isClient,
} = require("../utilities")
module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") {
@ -15,16 +21,18 @@ module.exports = async (ctx, next) => {
let appId = getAppId(ctx)
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
if (appId && cookieAppId !== appId) {
setCookie(ctx, "currentapp", appId)
setCookie(ctx, appId, "currentapp")
} else if (cookieAppId) {
appId = cookieAppId
}
let token = ctx.cookies.get(getCookieName(appId))
let authType = AuthTypes.APP
if (!token && !isClient(ctx)) {
authType = AuthTypes.BUILDER
let token, authType
if (!isClient(ctx)) {
token = ctx.cookies.get(getCookieName())
authType = AuthTypes.BUILDER
}
if (!token && appId) {
token = ctx.cookies.get(getCookieName(appId))
authType = AuthTypes.APP
}
if (!token) {
@ -49,9 +57,13 @@ module.exports = async (ctx, next) => {
role: await getRole(appId, jwtPayload.roleId),
}
} catch (err) {
// TODO - this can happen if the JWT secret is changed and can never login
// TODO: wipe cookies if they exist
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
if (authType === AuthTypes.BUILDER) {
clearCookie(ctx)
ctx.status = 200
return
} else {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
}
}
await next()

View file

@ -3,7 +3,7 @@ const env = require("../../environment")
const CouchDB = require("../../db")
const jwt = require("jsonwebtoken")
const { DocumentTypes, SEPARATOR } = require("../../db/utils")
const { setCookie } = require("../index")
const { setCookie, clearCookie } = require("../index")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
module.exports = async (ctx, appId, version) => {
@ -20,13 +20,13 @@ module.exports = async (ctx, appId, version) => {
})
// set the builder token
setCookie(ctx, "builder", token)
setCookie(ctx, "currentapp", appId)
setCookie(ctx, token, "builder")
setCookie(ctx, appId, "currentapp")
// need to clear all app tokens or else unable to use the app in the builder
let allDbNames = await CouchDB.allDbs()
allDbNames.map(dbName => {
if (dbName.startsWith(APP_PREFIX)) {
setCookie(ctx, dbName, "")
clearCookie(ctx, dbName)
}
})
}

View file

@ -111,16 +111,28 @@ exports.getCookieName = (name = "builder") => {
* @param {string} name The name of the cookie to set.
* @param {string|object} value The value of cookie which will be set.
*/
exports.setCookie = (ctx, name, value) => {
exports.setCookie = (ctx, value, name = "builder") => {
const expires = new Date()
expires.setDate(expires.getDate() + 1)
ctx.cookies.set(exports.getCookieName(name), value, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
const cookieName = exports.getCookieName(name)
if (!value) {
ctx.cookies.set(cookieName)
} else {
ctx.cookies.set(cookieName, value, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
}
}
/**
* Utility function, simply calls setCookie with an empty string for value
*/
exports.clearCookie = (ctx, name) => {
exports.setCookie(ctx, "", name)
}
exports.isClient = ctx => {

View file

@ -132,18 +132,8 @@
},
{
"type": "text",
"label": "Text 1",
"key": "text1"
},
{
"type": "text",
"label": "Text 2",
"key": "text2"
},
{
"type": "text",
"label": "Text 3",
"key": "text3"
"label": "Subheading",
"key": "subheading"
},
{
"type": "screen",

View file

@ -6,9 +6,7 @@
export let imageUrl = ""
export let heading = ""
export let text1 = ""
export let text2 = ""
export let text3 = ""
export let subheading = ""
export let destinationUrl = ""
$: showImage = !!imageUrl
@ -16,16 +14,16 @@
<div class="container" use:styleable={$component.styles}>
<a href={destinationUrl}>
<div class="content">
<div class="stackedlist">
{#if showImage}
<div class="image-block">
<img class="image" src={imageUrl} alt="" />
</div>
{/if}
<h2 class="heading">{heading}</h2>
<h4 class="text">{text1}</h4>
<h4 class="text">{text2}</h4>
<h4 class="text3">{text3}</h4>
<div class="content">
<div class="heading">{heading}</div>
<div class="subheading">{subheading}</div>
</div>
</div>
</a>
</div>
@ -34,61 +32,51 @@
a {
text-decoration: none;
color: inherit;
cursor: pointer;
}
.container {
padding: 20px;
padding: 1rem 1.5rem;
min-height: 3rem;
display: block;
align-items: flex-start;
}
.stackedlist {
display: flex;
flex-direction: row;
overflow: hidden;
max-width: 100%;
}
.subheading {
opacity: 0.6;
}
.content {
display: grid;
grid-template-columns: 120px 300px 1fr 1fr 1fr;
align-items: center;
gap: 20px;
min-height: 80px;
}
@media (max-width: 800px) {
.content {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
}
.image-block {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.image {
padding: 2px;
max-width: 80px;
max-height: 80px;
margin-right: 20px;
min-width: 0;
max-width: 100%;
flex: 1 1 auto;
}
.heading {
font-size: 24px;
margin: 0;
min-width: 200px;
font-weight: 600;
}
.text {
font-size: 16px;
font-weight: 400;
.image-block {
display: flex;
flex: 0 0 auto;
margin-right: 1.5rem;
color: inherit;
text-decoration: none;
}
.text3 {
text-align: end;
font-size: 16px;
font-weight: 400;
}
@media (max-width: 800px) {
.text3 {
text-align: start;
}
.image {
display: block;
overflow: hidden;
width: 3rem;
max-width: 100%;
-webkit-user-select: none;
user-select: none;
border-radius: 99px;
}
</style>

View file

@ -0,0 +1,92 @@
# String templating
This package provides a common system for string templating across the Budibase Builder, client and server.
The templating is provided through the use of [Handlebars](https://handlebarsjs.com/) an extension of Mustache
which is capable of carrying out logic. We have also extended the base Handlebars functionality through the use
of a set of helpers provided through the [handlebars-helpers](https://github.com/helpers/handlebars-helpers) package.
We have not implemented all the helpers provided by the helpers package as some of them provide functionality
we felt would not be beneficial. The following collections of helpers have been implemented:
1. [Math](https://github.com/helpers/handlebars-helpers/tree/master#math) - a set of useful helpers for
carrying out logic pertaining to numbers e.g. `avg`, `add`, `abs` and so on.
2. [Array](https://github.com/helpers/handlebars-helpers/tree/master#array) - some very specific helpers
for use with arrays, useful for example in automations. Helpers like `first`, `last`, `after` and `join`
can be useful for getting particular portions of arrays or turning them into strings.
3. [Number](https://github.com/helpers/handlebars-helpers/tree/master#number) - unlike the math helpers these
are useful for converting numbers into useful formats for display, e.g. `bytes`, `addCommas` and `toPrecision`.
4. [URL](https://github.com/helpers/handlebars-helpers/tree/master#url) - very specific helpers for dealing with URLs,
such as `encodeURI`, `escape`, `stripQueryString` and `stripProtocol`. These are primarily useful
for building up particular URLs to hit as say part of an automation.
5. [String](https://github.com/helpers/handlebars-helpers/tree/master#string) - these helpers are useful for building
strings and preparing them for display, e.g. `append`, `camelcase`, `capitalize` and `ellipsis`.
6. [Comparison](https://github.com/helpers/handlebars-helpers/tree/master#comparison) - these helpers are mainly for
building strings when particular conditions are met, for example `and`, `or`, `gt`, `lt`, `not` and so on. This is a very
extensive set of helpers but is mostly as would be expected from a set of logical operators.
7. [Date](https://github.com/helpers/helper-date) - last but certainly not least is a moment based date helper, which can
format ISO/timestamp based dates into something human-readable. An example of this would be `{{date dateProperty "DD-MM-YYYY"}}`.
## Date formatting
This package uses the standard method for formatting date times, using the following syntax:
| Input | Example | Description |
| ----- | ------- | ----------- |
| YYYY | 2014 | 4 or 2 digit year. Note: Only 4 digit can be parsed on strict mode |
| YY | 14 | 2 digit year |
| Y | -25 | Year with any number of digits and sign |
| Q | 1..4 | Quarter of year. Sets month to first month in quarter. |
| M MM | 1..12 | Month number |
| MMM MMMM | Jan..December | Month name in locale set by moment.locale() |
| D DD | 1..31 | Day of month |
| Do | 1st..31st | Day of month with ordinal |
| DDD DDDD | 1..365 | Day of year |
| X | 1410715640.579 | Unix timestamp |
| x | 1410715640579 | Unix ms timestamp |
## Template format
There are two main ways that the templating system can be used, the first is very similar to that which
would be produced by Mustache - a single statement:
```
Hello I'm building a {{uppercase adjective}} string with Handlebars!
```
In the statement above provided a context of `{adjective: "cool"}` will produce a string of `Hello I'm building a COOL string with Handlebars!`.
Here we can see an example of how string helpers can be used to make a string exactly as we need it. These statements are relatively
simple; we can also stack helpers as such: `{{ uppercase (remove string "bad") }}` with the use of parenthesis.
The other type of statement that can be made with the templating system is conditional ones, that appear as such:
```
Hello I'm building a {{ #gte score "50" }}Great{{ else }}Bad{{ /gte }} string with Handlebars!
```
In this string we can see that the string `Great` or `Bad` will be inserted depending on the state of the
`score` context variable. The comparison, string and array helpers all provide some conditional operators which can be used
in this way. There will also be some operators which will be built with a very similar syntax but will produce an
iterative operation, like a for each - an example of this would be the `forEach` array helper.
## Usage
Usage of this templating package is through one of the primary functions provided by the package - these functions are
as follows:
1. `processString` - `async (string, object)` - given a template string and a context object this will build a string
using our pre-processors, post-processors and handlebars.
2. `processObject` - `async (object, object)` - carries out the functionality provided by `processString` for any string
inside the given object. This will recurse deeply into the provided object so for very large objects this could be slow.
3. `processStringSync` - `(string, object)` - a reduced functionality processing of strings which is synchronous, like
functions provided by Node (e.g. `readdirSync`)
4. `processObjectSync` - `(object, object)` - as with the sync'd string, recurses an object to process it synchronously.
5. `makePropSafe` - `(string)` - some properties cannot be handled by Handlebars, for example `Table 1` is not valid due
to spaces found in the property name. This will update the property name to `[Table 1]` wrapping it in literal
specifiers so that it is safe for use in Handlebars. Ideally this function should be called for every level of an object
being accessed, for example `[Table 1].[property name]` is the syntax that is required for Handlebars.
6. `isValid` - `(string)` - checks the given string for any templates and provides a boolean stating whether it is a valid
template.
7. `getManifest` - returns the manifest JSON which has been generated for the helpers, describing them and their params.
## Development
This library is built with [Rollup](https://rollupjs.org/guide/en/) as many of the packages built by Budibase are. We have
built the string templating package as a UMD so that it can be used by Node and Browser based applications. This package also
builds Typescript stubs which when making use of the library will be used by your IDE to provide code completion. The following
commands are provided for development purposes:
1. `yarn build` - will build the Typescript stubs and the bundle into the `dist` directory.
2. `yarn test` - runs the test suite which will check various helpers are still functioning as
expected and a few expected use cases.
3. `yarn dev:builder` - an internal command which is used by lerna to watch and build any changes
to the package as part of the main `yarn dev` of the repo.
It is also important to note this package is managed in the same manner as all other in the mono-repo,
through lerna.

View file

@ -0,0 +1,986 @@
{
"math": {
"abs": {
"args": [
"a"
],
"numArgs": 1,
"description": "<p>Return the magnitude of <code>a</code>.</p>\n"
},
"add": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Return the sum of <code>a</code> plus <code>b</code>.</p>\n"
},
"avg": {
"args": [
"array"
],
"numArgs": 1,
"description": "<p>Returns the average of all numbers in the given array.</p>\n"
},
"ceil": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Get the <code>Math.ceil()</code> of the given value.</p>\n"
},
"divide": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Divide <code>a</code> by <code>b</code></p>\n"
},
"floor": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Get the <code>Math.floor()</code> of the given value.</p>\n"
},
"minus": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Return the product of <code>a</code> minus <code>b</code>.</p>\n"
},
"modulo": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Get the remainder of a division operation.</p>\n"
},
"multiply": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Return the product of <code>a</code> times <code>b</code>.</p>\n"
},
"plus": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Add <code>a</code> by <code>b</code>.</p>\n"
},
"random": {
"args": [
"min",
"max"
],
"numArgs": 2,
"description": "<p>Generate a random number between two values</p>\n"
},
"remainder": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Get the remainder when <code>a</code> is divided by <code>b</code>.</p>\n"
},
"round": {
"args": [
"number"
],
"numArgs": 1,
"description": "<p>Round the given number.</p>\n"
},
"subtract": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Return the product of <code>a</code> minus <code>b</code>.</p>\n"
},
"sum": {
"args": [
"array"
],
"numArgs": 1,
"description": "<p>Returns the sum of all numbers in the given array.</p>\n"
},
"times": {
"args": [
"a",
"b"
],
"numArgs": 2,
"description": "<p>Multiply number <code>a</code> by number <code>b</code>.</p>\n"
}
},
"array": {
"after": {
"args": [
"array",
"n"
],
"numArgs": 2,
"description": "<p>Returns all of the items in an array after the specified index. Opposite of <a href=\"#before\">before</a>.</p>\n"
},
"arrayify": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Cast the given <code>value</code> to an array.</p>\n"
},
"before": {
"args": [
"array",
"n"
],
"numArgs": 2,
"description": "<p>Return all of the items in the collection before the specified count. Opposite of <a href=\"#after\">after</a>.</p>\n"
},
"eachIndex": {
"args": [
"array",
"options"
],
"numArgs": 2,
"description": "<p>Iterates the array, listing an item and the index of it.</p>\n"
},
"filter": {
"args": [
"array",
"value",
"options"
],
"numArgs": 3,
"description": "<p>Block helper that filters the given array and renders the block for values that evaluate to <code>true</code>, otherwise the inverse block is returned.</p>\n"
},
"first": {
"args": [
"array",
"n"
],
"numArgs": 2,
"description": "<p>Returns the first item, or first <code>n</code> items of an array.</p>\n"
},
"forEach": {
"args": [
"array"
],
"numArgs": 1,
"description": "<p>Iterates over each item in an array and exposes the current item in the array as context to the inner block. In addition to the current array item, the helper exposes the following variables to the inner block: - <code>index</code> - <code>total</code> - <code>isFirst</code> - <code>isLast</code> Also, <code>@index</code> is exposed as a private variable, and additional private variables may be defined as hash arguments.</p>\n"
},
"inArray": {
"args": [
"array",
"value",
"options"
],
"numArgs": 3,
"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": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Returns true if <code>value</code> is an es5 array.</p>\n"
},
"itemAt": {
"args": [
"array",
"idx"
],
"numArgs": 2,
"description": "<p>Returns the item from <code>array</code> at index <code>idx</code>.</p>\n"
},
"join": {
"args": [
"array",
"separator"
],
"numArgs": 2,
"description": "<p>Join all elements of array into a string, optionally using a given separator.</p>\n"
},
"equalsLength": {
"args": [
"value",
"length",
"options"
],
"numArgs": 3,
"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": {
"args": [
"value",
"n"
],
"numArgs": 2,
"description": "<p>Returns the last item, or last <code>n</code> items of an array or string. Opposite of <a href=\"#first\">first</a>.</p>\n"
},
"length": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Returns the length of the given string or array.</p>\n"
},
"lengthEqual": {
"args": [
"value",
"length",
"options"
],
"numArgs": 3,
"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": {
"args": [
"array",
"fn"
],
"numArgs": 2,
"description": "<p>Returns a new array, created by calling <code>function</code> on each element of the given <code>array</code>. For example,</p>\n"
},
"pluck": {
"args": [
"collection",
"prop"
],
"numArgs": 2,
"description": "<p>Map over the given object or array or objects and create an array of values from the given <code>prop</code>. Dot-notation may be used (as a string) to get nested properties.</p>\n"
},
"reverse": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Reverse the elements in an array, or the characters in a string.</p>\n"
},
"some": {
"args": [
"array",
"iter",
"provided"
],
"numArgs": 3,
"description": "<p>Block helper that returns the block if the callback returns true for some value in the given array.</p>\n"
},
"sort": {
"args": [
"array",
"key"
],
"numArgs": 2,
"description": "<p>Sort the given <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"
},
"sortBy": {
"args": [
"array",
"props"
],
"numArgs": 2,
"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": {
"args": [
"array",
"idx",
"options"
],
"numArgs": 3,
"description": "<p>Use the items in the array <em>after</em> the specified index as context inside a block. Opposite of <a href=\"#withBefore\">withBefore</a>.</p>\n"
},
"withBefore": {
"args": [
"array",
"idx",
"options"
],
"numArgs": 3,
"description": "<p>Use the items in the array <em>before</em> the specified index as context inside a block. Opposite of <a href=\"#withAfter\">withAfter</a>.</p>\n"
},
"withFirst": {
"args": [
"array",
"idx",
"options"
],
"numArgs": 3,
"description": "<p>Use the first item in a collection inside a handlebars block expression. Opposite of <a href=\"#withLast\">withLast</a>.</p>\n"
},
"withGroup": {
"args": [
"array",
"size",
"options"
],
"numArgs": 3,
"description": "<p>Block helper that groups array elements by given group <code>size</code>.</p>\n"
},
"withLast": {
"args": [
"array",
"idx",
"options"
],
"numArgs": 3,
"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": {
"args": [
"array",
"prop",
"options"
],
"numArgs": 3,
"description": "<p>Block helper that sorts a collection and exposes the sorted collection as context inside the block.</p>\n"
},
"unique": {
"args": [
"array",
"options"
],
"numArgs": 2,
"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"
}
},
"number": {
"bytes": {
"args": [
"number"
],
"numArgs": 1,
"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": {
"args": [
"num"
],
"numArgs": 1,
"description": "<p>Add commas to numbers</p>\n"
},
"phoneNumber": {
"args": [
"num"
],
"numArgs": 1,
"description": "<p>Convert a string or number to a formatted phone number.</p>\n"
},
"toAbbr": {
"args": [
"number",
"precision"
],
"numArgs": 2,
"description": "<p>Abbreviate numbers to the given number of <code>precision</code>. This for general numbers, not size in bytes.</p>\n"
},
"toExponential": {
"args": [
"number",
"fractionDigits"
],
"numArgs": 2,
"description": "<p>Returns a string representing the given number in exponential notation.</p>\n"
},
"toFixed": {
"args": [
"number",
"digits"
],
"numArgs": 2,
"description": "<p>Formats the given number using fixed-point notation.</p>\n"
},
"toFloat": {
"args": [
"number"
],
"numArgs": 1,
"description": "<p>Convert input to a float.</p>\n"
},
"toInt": {
"args": [
"number"
],
"numArgs": 1,
"description": "<p>Convert input to an integer.</p>\n"
},
"toPrecision": {
"args": [
"number",
"precision"
],
"numArgs": 2,
"description": "<p>Returns a string representing the <code>Number</code> object to the specified precision.</p>\n"
}
},
"url": {
"encodeURI": {
"args": [
"str"
],
"numArgs": 1,
"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": {
"args": [
"str"
],
"numArgs": 1,
"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": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Decode a Uniform Resource Identifier (URI) component.</p>\n"
},
"url_encode": {
"args": [],
"numArgs": 0,
"description": "<p>Alias for <a href=\"#encodeuri\">encodeURI</a>.</p>\n"
},
"url_decode": {
"args": [],
"numArgs": 0,
"description": "<p>Alias for <a href=\"#decodeuri\">decodeURI</a>.</p>\n"
},
"urlResolve": {
"args": [
"base",
"href"
],
"numArgs": 2,
"description": "<p>Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.</p>\n"
},
"urlParse": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Parses a <code>url</code> string into an object.</p>\n"
},
"stripQuerystring": {
"args": [
"url"
],
"numArgs": 1,
"description": "<p>Strip the query string from the given <code>url</code>.</p>\n"
},
"stripProtocol": {
"args": [
"str"
],
"numArgs": 1,
"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"
}
},
"string": {
"append": {
"args": [
"str",
"suffix"
],
"numArgs": 2,
"description": "<p>Append the specified <code>suffix</code> to the given string.</p>\n"
},
"camelcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>camelCase the characters in the given <code>string</code>.</p>\n"
},
"capitalize": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Capitalize the first word in a sentence.</p>\n"
},
"capitalizeAll": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Capitalize all words in a string.</p>\n"
},
"center": {
"args": [
"str",
"spaces"
],
"numArgs": 2,
"description": "<p>Center a string using non-breaking spaces</p>\n"
},
"chop": {
"args": [
"string"
],
"numArgs": 1,
"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": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>dash-case the characters in <code>string</code>. Replaces non-word characters and periods with hyphens.</p>\n"
},
"dotcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>dot.case the characters in <code>string</code>.</p>\n"
},
"downcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Lowercase all of the characters in the given string. Alias for <a href=\"#lowercase\">lowercase</a>.</p>\n"
},
"ellipsis": {
"args": [
"str",
"length"
],
"numArgs": 2,
"description": "<p>Truncates a string to the specified <code>length</code>, and appends it with an elipsis, <code>…</code>.</p>\n"
},
"hyphenate": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Replace spaces in a string with hyphens.</p>\n"
},
"isString": {
"args": [
"value"
],
"numArgs": 1,
"description": "<p>Return true if <code>value</code> is a string.</p>\n"
},
"lowercase": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Lowercase all characters in the given string.</p>\n"
},
"occurrences": {
"args": [
"str",
"substring"
],
"numArgs": 2,
"description": "<p>Return the number of occurrences of <code>substring</code> within the given <code>string</code>.</p>\n"
},
"pascalcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>PascalCase the characters in <code>string</code>.</p>\n"
},
"pathcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>path/case the characters in <code>string</code>.</p>\n"
},
"plusify": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Replace spaces in the given string with pluses.</p>\n"
},
"prepend": {
"args": [
"str",
"prefix"
],
"numArgs": 2,
"description": "<p>Prepends the given <code>string</code> with the specified <code>prefix</code>.</p>\n"
},
"raw": {
"args": [
"options"
],
"numArgs": 1,
"description": "<p>Render a block without processing mustache templates inside the block.</p>\n"
},
"remove": {
"args": [
"str",
"substring"
],
"numArgs": 2,
"description": "<p>Remove all occurrences of <code>substring</code> from the given <code>str</code>.</p>\n"
},
"removeFirst": {
"args": [
"str",
"substring"
],
"numArgs": 2,
"description": "<p>Remove the first occurrence of <code>substring</code> from the given <code>str</code>.</p>\n"
},
"replace": {
"args": [
"str",
"a",
"b"
],
"numArgs": 3,
"description": "<p>Replace all occurrences of substring <code>a</code> with substring <code>b</code>.</p>\n"
},
"replaceFirst": {
"args": [
"str",
"a",
"b"
],
"numArgs": 3,
"description": "<p>Replace the first occurrence of substring <code>a</code> with substring <code>b</code>.</p>\n"
},
"sentence": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Sentence case the given string</p>\n"
},
"snakecase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>snake_case the characters in the given <code>string</code>.</p>\n"
},
"split": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Split <code>string</code> by the given <code>character</code>.</p>\n"
},
"startsWith": {
"args": [
"prefix",
"testString",
"options"
],
"numArgs": 3,
"description": "<p>Tests whether a string begins with the given prefix.</p>\n"
},
"titleize": {
"args": [
"str"
],
"numArgs": 1,
"description": "<p>Title case the given string.</p>\n"
},
"trim": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Removes extraneous whitespace from the beginning and end of a string.</p>\n"
},
"trimLeft": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Removes extraneous whitespace from the beginning of a string.</p>\n"
},
"trimRight": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Removes extraneous whitespace from the end of a string.</p>\n"
},
"truncate": {
"args": [
"str",
"limit",
"suffix"
],
"numArgs": 3,
"description": "<p>Truncate a string to the specified <code>length</code>. Also see <a href=\"#ellipsis\">ellipsis</a>.</p>\n"
},
"truncateWords": {
"args": [
"str",
"limit",
"suffix"
],
"numArgs": 3,
"description": "<p>Truncate a string to have the specified number of words. Also see <a href=\"#truncate\">truncate</a>.</p>\n"
},
"upcase": {
"args": [
"string"
],
"numArgs": 1,
"description": "<p>Uppercase all of the characters in the given string. Alias for <a href=\"#uppercase\">uppercase</a>.</p>\n"
},
"uppercase": {
"args": [
"str",
"options"
],
"numArgs": 2,
"description": "<p>Uppercase all of the characters in the given string. If used as a block helper it will uppercase the entire block. This helper does not support inverse blocks.</p>\n"
}
},
"comparison": {
"and": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"description": "<p>Helper that renders the block if <strong>both</strong> of the given values are truthy. If an inverse block is specified it will be rendered when falsy. Works as a block helper, inline helper or subexpression.</p>\n"
},
"compare": {
"args": [
"a",
"operator",
"b",
"options"
],
"numArgs": 4,
"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": {
"args": [
"collection",
"value",
"[startIndex=0]",
"options"
],
"numArgs": 4,
"description": "<p>Block helper that renders the block if <code>collection</code> has the given <code>value</code>, using strict equality (<code>===</code>) for comparison, otherwise the inverse block is rendered (if specified). If a <code>startIndex</code> is specified and is negative, it is used as the offset from the end of the collection.</p>\n"
},
"default": {
"args": [
"value",
"defaultValue"
],
"numArgs": 2,
"description": "<p>Returns the first value that is not undefined, otherwise the &quot;default&quot; value is returned.</p>\n"
},
"eq": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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=&quot;&quot;</code> hash argument for the second value.</p>\n"
},
"gt": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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=&quot;&quot;</code> hash argument for the second value.</p>\n"
},
"gte": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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=&quot;&quot;</code> hash argument for the second value.</p>\n"
},
"has": {
"args": [
"val",
"pattern",
"options"
],
"numArgs": 3,
"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": {
"args": [
"val",
"options"
],
"numArgs": 2,
"description": "<p>Returns true if the given <code>value</code> is falsey. Uses the [falsey][] library for comparisons. Please see that library for more information or to report bugs with this helper.</p>\n"
},
"isTruthy": {
"args": [
"val",
"options"
],
"numArgs": 2,
"description": "<p>Returns true if the given <code>value</code> is truthy. Uses the [falsey][] library for comparisons. Please see that library for more information or to report bugs with this helper.</p>\n"
},
"ifEven": {
"args": [
"number",
"options"
],
"numArgs": 2,
"description": "<p>Return true if the given value is an even number.</p>\n"
},
"ifNth": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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"
},
"ifOdd": {
"args": [
"value",
"options"
],
"numArgs": 2,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"context",
"options"
],
"numArgs": 2,
"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=&quot;&quot;</code> hash argument for the second value.</p>\n"
},
"lte": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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=&quot;&quot;</code> hash argument for the second value.</p>\n"
},
"neither": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"val",
"options"
],
"numArgs": 2,
"description": "<p>Returns true if <code>val</code> is falsey. Works as a block or inline helper.</p>\n"
},
"or": {
"args": [
"arguments",
"options"
],
"numArgs": 2,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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": {
"args": [
"a",
"b",
"options"
],
"numArgs": 3,
"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"
}
},
"date": {
"date": {
"args": [
"datetime",
"format"
],
"numArgs": 2,
"example": "{{date now \"YYYY\"}}",
"description": "<p>Format a date using moment.js data formatting.</p>\n"
}
}
}

View file

@ -2,30 +2,35 @@
"name": "@budibase/string-templates",
"version": "0.6.2",
"description": "Handlebars wrapper for Budibase templating.",
"main": "dist/bundle.js",
"module": "dist/bundle.js",
"main": "src/index.js",
"module": "src/index.js",
"browser": "dist/bundle.js",
"license": "AGPL-3.0",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc && rollup -c",
"dev:builder": "tsc && rollup -cw",
"test": "jest"
"test": "jest",
"manifest": "node ./scripts/gen-collection-info.js"
},
"dependencies": {
"@budibase/handlebars-helpers": "^0.11.1",
"handlebars": "^4.7.6",
"handlebars-helpers": "^0.10.0",
"handlebars-utils": "^1.0.6",
"helper-date": "^1.0.1",
"lodash": "^4.17.20"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0",
"doctrine": "^3.0.0",
"jest": "^26.6.3",
"marked": "^1.2.8",
"rollup": "^2.36.2",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.1.3"
}
}

View file

@ -1,28 +1,32 @@
import commonjs from "rollup-plugin-commonjs"
import commonjs from "@rollup/plugin-commonjs"
import resolve from "rollup-plugin-node-resolve"
import builtins from "rollup-plugin-node-builtins"
import globals from "rollup-plugin-node-globals"
import json from "@rollup/plugin-json"
import { terser } from "rollup-plugin-terser"
const production = !process.env.ROLLUP_WATCH
export default {
input: "src/index.js",
input: "src/esIndex.js",
output: [
{
sourcemap: true,
format: "umd",
format: "esm",
file: "./dist/bundle.js",
name: "string-templates",
name: "templates",
exports: "named",
},
],
plugins: [
resolve({
mainFields: ["module", "main"],
preferBuiltins: true,
browser: true,
}),
commonjs(),
globals(),
builtins(),
production && terser(),
json(),
],
}

View file

@ -0,0 +1,155 @@
const HELPER_LIBRARY = "@budibase/handlebars-helpers"
const helpers = require(HELPER_LIBRARY)
const { HelperFunctionBuiltin } = require("../src/helpers/constants")
const fs = require("fs")
const doctrine = require("doctrine")
const marked = require("marked")
const DIRECTORY = fs.existsSync("node_modules") ? "." : ".."
const FILENAME = `${DIRECTORY}/manifest.json`
/**
* full list of supported helpers can be found here:
* https://github.com/helpers/handlebars-helpers
*/
const COLLECTIONS = ["math", "array", "number", "url", "string", "comparison"]
const outputJSON = {}
function fixSpecialCases(name, obj) {
const args = obj.args
if (name === "ifNth") {
args[0] = "a"
args[1] = "b"
}
if (name === "eachIndex") {
obj.description = "Iterates the array, listing an item and the index of it."
}
if (name === "toFloat") {
obj.description = "Convert input to a float."
}
if (name === "toInt") {
obj.description = "Convert input to an integer."
}
return obj
}
function lookForward(lines, funcLines, idx) {
const funcLen = funcLines.length
for (let i = idx, j = 0; i < idx + funcLen; ++i, j++) {
if (!lines[i].includes(funcLines[j])) {
return false
}
}
return true
}
function getCommentInfo(file, func) {
const lines = file.split("\n")
const funcLines = func.split("\n")
let comment = null
for (let idx = 0; idx < lines.length; ++idx) {
// from here work back until we have the comment
if (lookForward(lines, funcLines, idx)) {
let fromIdx = idx
let start = 0,
end = 0
do {
if (lines[fromIdx].includes("*/")) {
end = fromIdx
} else if (lines[fromIdx].includes("/*")) {
start = fromIdx
}
if (start && end) {
break
}
fromIdx--
} while (fromIdx > 0)
comment = lines.slice(start, end + 1).join("\n")
}
}
if (comment == null) {
return { description: "" }
}
const docs = doctrine.parse(comment, { unwrap: true })
// some hacky fixes
docs.description = docs.description.replace(/\n/g, " ")
docs.description = docs.description.replace(/[ ]{2,}/g, " ")
docs.description = docs.description.replace(/is is/g, "is")
const examples = docs.tags
.filter(el => el.title === "example")
.map(el => el.description)
const blocks = docs.description.split("```")
if (examples.length > 0) {
docs.example = examples.join(" ")
}
docs.description = blocks[0].trim()
return docs
}
/**
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
*/
function run() {
const foundNames = []
for (let collection of COLLECTIONS) {
const collectionFile = fs.readFileSync(
`${DIRECTORY}/node_modules/${HELPER_LIBRARY}/lib/${collection}.js`,
"utf8"
)
const collectionInfo = {}
// collect information about helper
let hbsHelperInfo = helpers[collection]()
for (let entry of Object.entries(hbsHelperInfo)) {
const name = entry[0]
// skip built in functions and ones seen already
if (
HelperFunctionBuiltin.indexOf(name) !== -1 ||
foundNames.indexOf(name) !== -1
) {
continue
}
foundNames.push(name)
// this is ridiculous, but it parse the function header
const fnc = entry[1].toString()
const jsDocInfo = getCommentInfo(collectionFile, fnc)
let args = jsDocInfo.tags
.filter(tag => tag.title === "param")
.map(
tag =>
tag.description &&
tag.description
.replace(/`/g, "")
.split(" ")[0]
.trim()
)
collectionInfo[name] = fixSpecialCases(name, {
args,
numArgs: args.length,
example: jsDocInfo.example || undefined,
description: jsDocInfo.description,
})
}
outputJSON[collection] = collectionInfo
}
// add the date helper
outputJSON["date"] = {
date: {
args: ["datetime", "format"],
numArgs: 2,
example: '{{date now "YYYY"}}',
description: "Format a date using moment.js data formatting.",
},
}
// convert all markdown to HTML
for (let collection of Object.values(outputJSON)) {
for (let helper of Object.values(collection)) {
helper.description = marked(helper.description)
}
}
fs.writeFileSync(FILENAME, JSON.stringify(outputJSON, null, 2))
}
run()

View file

@ -0,0 +1,12 @@
import templates from "./index"
/**
* This file is simply an entrypoint for rollup - makes a lot of cjs problems go away
*/
export const isValid = templates.isValid
export const makePropSafe = templates.makePropSafe
export const getManifest = templates.getManifest
export const processStringSync = templates.processStringSync
export const processObjectSync = templates.processObjectSync
export const processString = templates.processString
export const processObject = templates.processObject

View file

@ -18,4 +18,7 @@ module.exports.HelperFunctionBuiltin = [
module.exports.HelperFunctionNames = {
OBJECT: "object",
ALL: "all",
LITERAL: "literal",
}
module.exports.LITERAL_MARKER = "%LITERAL%"

View file

@ -1,10 +1,10 @@
const helpers = require("handlebars-helpers")
const helpers = require("@budibase/handlebars-helpers")
const dateHelper = require("helper-date")
const { HelperFunctionBuiltin } = require("./constants")
/**
* full list of supported helpers can be found here:
* https://github.com/helpers/handlebars-helpers
* https://github.com/Budibase/handlebars-helpers
*/
const EXTERNAL_FUNCTION_COLLECTIONS = [
@ -13,7 +13,7 @@ const EXTERNAL_FUNCTION_COLLECTIONS = [
"number",
"url",
"string",
"markdown",
"comparison",
]
const DATE_NAME = "date"

View file

@ -1,7 +1,11 @@
const Helper = require("./Helper")
const { SafeString } = require("handlebars")
const externalHandlebars = require("./external")
const { HelperFunctionNames, HelperFunctionBuiltin } = require("./constants")
const {
HelperFunctionNames,
HelperFunctionBuiltin,
LITERAL_MARKER,
} = require("./constants")
const HTML_SWAPS = {
"<": "&lt;",
@ -27,6 +31,12 @@ const HELPERS = [
return HTML_SWAPS[tag] || tag
})
}),
// adds a note for post-processor
new Helper(HelperFunctionNames.LITERAL, value => {
const type = typeof value
const outputVal = type === "object" ? JSON.stringify(value) : value
return `{{-${LITERAL_MARKER}-${type}-${outputVal}-}}`
}),
]
module.exports.HelperNames = () => {

View file

@ -2,7 +2,8 @@ const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index")
const processors = require("./processors")
const { cloneDeep } = require("lodash/fp")
const { removeNull } = require("./utilities")
const { removeNull, addConstants } = require("./utilities")
const manifest = require("../manifest.json")
const hbsInstance = handlebars.create()
registerAll(hbsInstance)
@ -82,24 +83,52 @@ module.exports.processObjectSync = (object, context) => {
* @returns {string} The enriched string, all templates should have been replaced if they can be.
*/
module.exports.processStringSync = (string, context) => {
const clonedContext = removeNull(cloneDeep(context))
let clonedContext = removeNull(cloneDeep(context))
clonedContext = addConstants(clonedContext)
// remove any null/undefined properties
if (typeof string !== "string") {
throw "Cannot process non-string types."
}
let template
string = processors.preprocess(string)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
template = hbsInstance.compile(string)
const template = hbsInstance.compile(string)
return processors.postprocess(template(clonedContext))
}
/**
* Errors can occur if a user of this library attempts to use a helper that has not been added to the system, these errors
* can be captured to alert the user of the mistake.
* @param {function} handler a function which will be called every time an error occurs when processing a handlebars
* statement.
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
* @param {string} property The property which is to be wrapped.
* @returns {string} The wrapped property ready to be added to a templating string.
*/
module.exports.errorEvents = handler => {
hbsInstance.registerHelper("helperMissing", handler)
module.exports.makePropSafe = property => {
return `[${property}]`.replace("[[", "[").replace("]]", "]")
}
/**
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
* @returns {boolean} Whether or not the input string is valid.
*/
module.exports.isValid = string => {
const specialCases = ["isNumber", "expected a number"]
// don't really need a real context to check if its valid
const context = {}
try {
hbsInstance.compile(processors.preprocess(string, false))(context)
return true
} catch (err) {
const msg = err ? err.message : ""
const foundCase = specialCases.find(spCase => msg.includes(spCase))
// special case for maths functions - don't have inputs yet
return !!foundCase
}
}
/**
* We have generated a static manifest file from the helpers that this string templating package makes use of.
* This manifest provides information about each of the helpers and how it can be used.
* @returns The manifest JSON which has been generated from the helpers.
*/
module.exports.getManifest = () => {
return manifest
}

View file

@ -2,23 +2,35 @@ const { FIND_HBS_REGEX } = require("../utilities")
const preprocessor = require("./preprocessor")
const postprocessor = require("./postprocessor")
function process(string, processors) {
function process(output, processors) {
for (let processor of processors) {
// if a literal statement has occurred stop
if (typeof output !== "string") {
break
}
// re-run search each time incase previous processor updated/removed a match
let regex = new RegExp(FIND_HBS_REGEX)
let matches = string.match(regex)
let regexp = new RegExp(FIND_HBS_REGEX)
let matches = output.match(regexp)
if (matches == null) {
continue
}
for (let match of matches) {
string = processor.process(string, match)
output = processor.process(output, match)
}
}
return string
return output
}
module.exports.preprocess = string => {
return process(string, preprocessor.processors)
module.exports.preprocess = (string, finalise = true) => {
let processors = preprocessor.processors
// the pre-processor finalisation stops handlebars from ever throwing an error
// might want to pre-process for other benefits but still want to see errors
if (!finalise) {
processors = processors.filter(
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
)
}
return process(string, processors)
}
module.exports.postprocess = string => {

View file

@ -1,9 +1,43 @@
const { LITERAL_MARKER } = require("../helpers/constants")
const PostProcessorNames = {
CONVERT_LITERALS: "convert-literals",
}
/* eslint-disable no-unused-vars */
class Postprocessor {
constructor(name, fn) {
this.name = name
this.fn = fn
}
process(statement) {
return this.fn(statement)
}
}
module.exports.processors = []
module.exports.processors = [
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
if (!statement.includes(LITERAL_MARKER)) {
return statement
}
const components = statement.split("-")
// pop and shift remove the empty array elements from the first and last dash
components.pop()
components.shift()
const type = components[1]
const value = components[2]
switch (type) {
case "string":
return value
case "number":
return parseFloat(value)
case "boolean":
return value === "true"
case "object":
return JSON.parse(value)
}
return value
}),
]

View file

@ -1,9 +1,11 @@
const { HelperNames } = require("../helpers")
const { swapStrings, isAlphaNumeric } = require("../utilities")
const FUNCTION_CASES = ["#", "else", "/"]
const PreprocessorNames = {
SWAP_TO_DOT: "swap-to-dot-notation",
HANDLE_SPACES: "handle-spaces-in-properties",
FIX_FUNCTIONS: "fix-functions",
FINALISE: "finalise",
}
@ -37,42 +39,16 @@ module.exports.processors = [
return statement
}),
new Preprocessor(PreprocessorNames.HANDLE_SPACES, statement => {
// exclude helpers and brackets, regex will only find double brackets
const exclusions = HelperNames()
// find all the parts split by spaces
const splitBySpaces = statement
.split(" ")
.filter(el => el !== "{{" && el !== "}}")
// remove braces if they are found and weren't spaced out
splitBySpaces[0] = splitBySpaces[0].replace("{", "")
splitBySpaces[splitBySpaces.length - 1] = splitBySpaces[
splitBySpaces.length - 1
].replace("}", "")
// remove the excluded elements
const propertyParts = splitBySpaces.filter(
part => exclusions.indexOf(part) === -1
)
// rebuild to get the full property
const fullProperty = propertyParts.join(" ")
// now work out the dot notation layers and split them up
const propertyLayers = fullProperty.split(".")
// find the layers which need to be wrapped and wrap them
for (let layer of propertyLayers) {
if (layer.indexOf(" ") !== -1) {
statement = swapStrings(
statement,
statement.indexOf(layer),
layer.length,
`[${layer}]`
)
}
new Preprocessor(PreprocessorNames.FIX_FUNCTIONS, statement => {
for (let specialCase of FUNCTION_CASES) {
const toFind = `{ ${specialCase}`,
replacement = `{${specialCase}`
statement = statement.replace(new RegExp(toFind, "g"), replacement)
}
// remove the edge case of double brackets being entered (in-case user already has specified)
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
return statement
}),
new Preprocessor(Preprocessor.FINALISE, statement => {
new Preprocessor(PreprocessorNames.FINALISE, statement => {
let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1)
@ -81,9 +57,17 @@ module.exports.processors = [
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
}
const possibleHelper = insideStatement.split(" ")[0]
if (HelperNames().some(option => possibleHelper === option)) {
// function helpers can't be wrapped
for (let specialCase of FUNCTION_CASES) {
if (possibleHelper.includes(specialCase)) {
return statement
}
}
if (HelperNames().some(option => possibleHelper.includes(option))) {
insideStatement = `(${insideStatement})`
}
return `{{ all ${insideStatement} }}`
}),
]
module.exports.PreprocessorNames = PreprocessorNames

View file

@ -1,6 +1,7 @@
const _ = require("lodash")
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
module.exports.FIND_HBS_REGEX = /{{([^{}])+}}/g
module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g
module.exports.isAlphaNumeric = char => {
return char.match(ALPHA_NUMERIC_REGEX)
@ -12,12 +13,22 @@ module.exports.swapStrings = (string, start, length, swap) => {
// removes null and undefined
module.exports.removeNull = obj => {
return Object.fromEntries(
Object.entries(obj)
.filter(entry => entry[1] != null)
.map(([key, value]) => [
key,
value === Object(value) ? module.exports.removeNull(value) : value,
])
)
obj = _(obj)
.omitBy(_.isUndefined)
.omitBy(_.isNull)
.value()
for (let [key, value] of Object.entries(obj)) {
// only objects
if (typeof value === "object" && !Array.isArray(value)) {
obj[key] = module.exports.removeNull(value)
}
}
return obj
}
module.exports.addConstants = obj => {
if (obj.now == null) {
obj.now = new Date()
}
return obj
}

View file

@ -1,6 +1,9 @@
const {
processObject,
processString,
isValid,
makePropSafe,
getManifest,
} = require("../src/index")
describe("Test that the string processing works correctly", () => {
@ -81,4 +84,29 @@ describe("Test that the object processing works correctly", () => {
}
expect(error).not.toBeNull()
})
})
describe("check the utility functions", () => {
it("should return false for an invalid template string", () => {
const valid = isValid("{{ table1.thing prop }}")
expect(valid).toBe(false)
})
it("should state this template is valid", () => {
const valid = isValid("{{ thing }}")
expect(valid).toBe(true)
})
it("should make a property safe", () => {
const property = makePropSafe("thing")
expect(property).toEqual("[thing]")
})
})
describe("check manifest", () => {
it("should be able to retrieve the manifest", () => {
const manifest = getManifest()
expect(manifest.math).not.toBeNull()
expect(manifest.math.abs.description).toBe("<p>Return the magnitude of <code>a</code>.</p>\n")
})
})

View file

@ -18,14 +18,14 @@ describe("Handling context properties with spaces in their name", () => {
})
it("should be able to handle a property with a space in its name", async () => {
const output = await processString("hello my name is {{ person name }}", {
const output = await processString("hello my name is {{ [person name] }}", {
"person name": "Mike",
})
expect(output).toBe("hello my name is Mike")
})
it("should be able to handle an object with layers that requires escaping", async () => {
const output = await processString("testcase {{ testing.test case }}", {
const output = await processString("testcase {{ testing.[test case] }}", {
testing: {
"test case": 1
}
@ -44,8 +44,19 @@ describe("attempt some complex problems", () => {
},
},
}
const hbs = "{{ New Repeater.Get Actors.first_name }} {{ New Repeater.Get Actors.last_name }}"
const hbs = "{{ [New Repeater].[Get Actors].[first_name] }} {{ [New Repeater].[Get Actors].[last_name] }}"
const output = await processString(hbs, context)
expect(output).toBe("Bob Bobert")
})
it("should be able to process an odd string produced by builder", async () => {
const context = {
"c306d140d7e854f388bae056db380a0eb": {
"test prop": "test",
}
}
const hbs = "null{{ [c306d140d7e854f388bae056db380a0eb].[test prop] }}"
const output = await processString(hbs, context)
expect(output).toBe("nulltest")
})
})

View file

@ -1,5 +1,6 @@
const {
processString,
isValid,
} = require("../src/index")
describe("test the custom helpers we have applied", () => {
@ -9,5 +10,295 @@ describe("test the custom helpers we have applied", () => {
})
expect(output).toBe("object is {\"a\":1}")
})
})
describe("test the math helpers", () => {
it("should be able to produce an absolute", async () => {
const output = await processString("{{abs a}}", {
a: -10,
})
expect(parseInt(output)).toBe(10)
})
it("should be able to add", async () => {
const output = await processString("{{add a b}}", {
a: 10,
b: 10,
})
expect(parseInt(output)).toBe(20)
})
it("should be able to average", async () => {
const output = await processString("{{avg a b c}}", {
a: 1,
b: 2,
c: 3,
})
expect(parseInt(output)).toBe(2)
})
it("should be able to times", async () => {
const output = await processString("{{times a b}}", {
a: 5,
b: 5,
})
expect(parseInt(output)).toBe(25)
})
})
describe("test the array helpers", () => {
const array = ["hi", "person", "how", "are", "you"]
it("should allow use of the after helper", async () => {
const output = await processString("{{after array 1}}", {
array,
})
expect(output).toBe("person,how,are,you")
})
it("should allow use of the before helper", async () => {
const output = await processString("{{before array 2}}", {
array,
})
expect(output).toBe("hi,person,how")
})
it("should allow use of the filter helper", async () => {
const output = await processString("{{#filter array \"person\"}}THING{{else}}OTHER{{/filter}}", {
array,
})
expect(output).toBe("THING")
})
it("should allow use of the itemAt helper", async () => {
const output = await processString("{{itemAt array 1}}", {
array,
})
expect(output).toBe("person")
})
it("should allow use of the join helper", async () => {
const output = await processString("{{join array \"-\"}}", {
array,
})
expect(output).toBe("hi-person-how-are-you")
})
it("should allow use of the sort helper", async () => {
const output = await processString("{{sort array}}", {
array: ["d", "a", "c", "e"]
})
expect(output).toBe("a,c,d,e")
})
it("should allow use of the unique helper", async () => {
const output = await processString("{{unique array}}", {
array: ["a", "a", "b"]
})
expect(output).toBe("a,b")
})
})
describe("test the number helpers", () => {
it("should allow use of the addCommas helper", async () => {
const output = await processString("{{ addCommas number }}", {
number: 10000000
})
expect(output).toBe("10,000,000")
})
it("should allow use of the phoneNumber helper", async () => {
const output = await processString("{{ phoneNumber number }}", {
number: 4490102030,
})
expect(output).toBe("(449) 010-2030")
})
it("should allow use of the toPrecision helper", async () => {
const output = await processString("{{ toPrecision number 2 }}", {
number: 1.222222222,
})
expect(output).toBe("1.2")
})
it("should allow use of the bytes helper", async () => {
const output = await processString("{{ bytes number }}", {
number: 1000000,
})
expect(output).toBe("1 MB")
})
})
describe("test the url helpers", () => {
const url = "http://example.com?query=1"
it("should allow use of the stripQueryString helper", async () => {
const output = await processString('{{stripQuerystring url }}', {
url,
})
expect(output).toBe("http://example.com")
})
it("should allow use of the stripProtocol helper", async () => {
const output = await processString("{{ stripProtocol url }}", {
url,
})
expect(output).toBe("//example.com/?query=1")
})
it("should allow use of the urlParse helper", async () => {
const output = await processString("{{ object ( urlParse url ) }}", {
url,
})
expect(output).toBe("{\"protocol\":\"http:\",\"slashes\":true,\"auth\":null,\"host\":\"example.com\"," +
"\"port\":null,\"hostname\":\"example.com\",\"hash\":null,\"search\":\"?query=1\"," +
"\"query\":\"query=1\",\"pathname\":\"/\",\"path\":\"/?query=1\"," +
"\"href\":\"http://example.com/?query=1\"}")
})
})
describe("test the date helpers", () => {
it("should allow use of the date helper", async () => {
const date = new Date(1611577535000)
const output = await processString("{{ date time 'YYYY-MM-DD' }}", {
time: date.toISOString(),
})
expect(output).toBe("2021-01-25")
})
it("should allow use of the date helper with now time", async () => {
const date = new Date()
const output = await processString("{{ date now 'DD' }}", {})
expect(parseInt(output)).toBe(date.getDate())
})
})
describe("test the string helpers", () => {
it("should allow use of the append helper", async () => {
const output = await processString("{{ append filename '.txt' }}", {
filename: "yummy",
})
expect(output).toBe("yummy.txt")
})
it("should allow use of the camelcase helper", async () => {
const output = await processString("{{ camelcase camel }}", {
camel: "testing this thing",
})
expect(output).toBe("testingThisThing")
})
it("should allow use of the capitalize helper", async () => {
const output = await processString("{{ capitalize string }}", {
string: "this is a string",
})
expect(output).toBe("This is a string")
})
it("should allow use of the capitalizeAll helper", async () => {
const output = await processString("{{ capitalizeAll string }}", {
string: "this is a string",
})
expect(output).toBe("This Is A String")
})
it("should allow use of the replace helper", async () => {
const output = await processString("{{ replace string 'Mike' name }}", {
string: "Hello my name is Mike",
name: "David",
})
expect(output).toBe("Hello my name is David")
})
it("should allow use of the split helper", async () => {
const output = await processString("{{ first ( split string ' ' ) }}", {
string: "this is a string",
})
expect(output).toBe("this")
})
it("should allow use of the remove helper", async () => {
const output = await processString("{{ remove string 'string' }}", {
string: "this is a string",
})
expect(output).toBe("this is a ")
})
it("should allow use of the startsWith helper", async () => {
const output = await processString("{{ #startsWith 'Hello' string }}Hi!{{ else }}Goodbye!{{ /startsWith }}", {
string: "Hello my name is Mike",
})
expect(output).toBe("Hi!")
})
})
describe("test the comparison helpers", () => {
async function compare(func, a, b) {
const output = await processString(`{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`, {
a,
b,
})
expect(output).toBe("Success")
}
it("should allow use of the lt helper", async () => {
await compare("lt", 10, 15)
})
it("should allow use of the gt helper", async () => {
await compare("gt", 15, 10)
})
it("should allow use of the and helper", async () => {
await compare("and", true, true)
})
it("should allow use of the or helper", async () => {
await compare("or", false, true)
})
it("should allow use of gte with a literal value", async () => {
const output = await processString(`{{ #gte a "50" }}s{{ else }}f{{ /gte }}`, {
a: 51,
})
expect(output).toBe("s")
})
})
describe("Test the literal helper", () => {
it("should allow use of the literal specifier for a number", async () => {
const output = await processString(`{{literal a}}`, {
a: 51,
})
expect(output).toBe(51)
})
it("should allow use of the literal specifier for an object", async () => {
const output = await processString(`{{literal a}}`, {
a: {b: 1},
})
expect(output.b).toBe(1)
})
})
describe("Cover a few complex use cases", () => {
it("should allow use of three different collection helpers", async () => {
const output = await processString(`{{ join ( after ( split "My name is: Joe Smith" " " ) 3 ) " " }}`, {})
expect(output).toBe("Joe Smith")
})
it("should allow a complex array case", async () => {
const output = await processString("{{ last ( sort ( unique array ) ) }}", {
array: ["a", "a", "d", "c", "e"]
})
expect(output).toBe("e")
})
it("should make sure case is valid", () => {
const validity = isValid("{{ avg [c355ec2b422e54f988ae553c8acd811ea].[a] [c355ec2b422e54f988ae553c8acd811ea].[b] }}")
expect(validity).toBe(true)
})
it("should be able to solve an example from docs", async () => {
const output = await processString(`{{first ( split "a-b-c" "-") 2}}`, {})
expect(output).toBe(`a,b`)
})
})

View file

@ -270,6 +270,38 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/handlebars-helpers@^0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.1.tgz#fe8672612fb4ad8fd3bad338ee15759f45e421eb"
integrity sha512-s72LhOhlHk43QYGVcqjogaGa4h4e6y6X1AfshqWBWceTRYBs/9tUnoYDUiRJ/Mt3WoTmx8Hj5GOZekSqrUsOFQ==
dependencies:
arr-flatten "^1.1.0"
array-sort "^0.1.4"
define-property "^1.0.0"
extend-shallow "^3.0.2"
"falsey" "^0.3.2"
for-in "^1.0.2"
for-own "^1.0.0"
get-object "^0.2.0"
get-value "^2.0.6"
handlebars "^4.0.11"
handlebars-utils "^1.0.6"
has-value "^1.0.0"
helper-date "^1.0.1"
helper-markdown "^1.0.0"
helper-md "^0.2.2"
html-tag "^2.0.0"
is-even "^1.0.0"
is-glob "^4.0.0"
is-number "^4.0.0"
kind-of "^6.0.0"
logging-helpers "^1.0.0"
micromatch "^3.1.4"
relative "^3.0.2"
striptags "^3.1.0"
to-gfm-code-block "^0.1.1"
year "^0.2.1"
"@cnakazawa/watch@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@ -465,6 +497,19 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@rollup/plugin-commonjs@^17.1.0":
version "17.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d"
integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==
dependencies:
"@rollup/pluginutils" "^3.1.0"
commondir "^1.0.1"
estree-walker "^2.0.1"
glob "^7.1.6"
is-reference "^1.2.1"
magic-string "^0.25.7"
resolve "^1.17.0"
"@rollup/plugin-json@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
@ -472,7 +517,7 @@
dependencies:
"@rollup/pluginutils" "^3.0.8"
"@rollup/pluginutils@^3.0.8":
"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
@ -1389,6 +1434,16 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -1441,16 +1496,6 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.5.3"
create-frame@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/create-frame/-/create-frame-1.0.0.tgz#8b95f2691e3249b6080443e33d0bad9f8f6975aa"
integrity sha1-i5XyaR4ySbYIBEPjPQutn49pdao=
dependencies:
define-property "^0.2.5"
extend-shallow "^2.0.1"
isobject "^3.0.0"
lazy-cache "^2.0.2"
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
@ -1665,6 +1710,13 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
domexception@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@ -1781,6 +1833,11 @@ estree-walker@^1.0.1:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@ -2074,7 +2131,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@ -2110,48 +2167,6 @@ gulp-header@^1.7.1:
lodash.template "^4.4.0"
through2 "^2.0.0"
handlebars-helper-create-frame@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/handlebars-helper-create-frame/-/handlebars-helper-create-frame-0.1.0.tgz#8aa51d10aeb6408fcc6605d40d77356288487a03"
integrity sha1-iqUdEK62QI/MZgXUDXc1YohIegM=
dependencies:
create-frame "^1.0.0"
isobject "^3.0.0"
handlebars-helpers@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/handlebars-helpers/-/handlebars-helpers-0.10.0.tgz#663d49e718928eafbead1473419ed7bc24bcd45a"
integrity sha512-QiyhQz58u/DbuV41VnfpE0nhy6YCH4vB514ajysV8SoKmP+DxU+pR+fahVyNECHj+jiwEN2VrvxD/34/yHaLUg==
dependencies:
arr-flatten "^1.1.0"
array-sort "^0.1.4"
create-frame "^1.0.0"
define-property "^1.0.0"
"falsey" "^0.3.2"
for-in "^1.0.2"
for-own "^1.0.0"
get-object "^0.2.0"
get-value "^2.0.6"
handlebars "^4.0.11"
handlebars-helper-create-frame "^0.1.0"
handlebars-utils "^1.0.6"
has-value "^1.0.0"
helper-date "^1.0.1"
helper-markdown "^1.0.0"
helper-md "^0.2.2"
html-tag "^2.0.0"
is-even "^1.0.0"
is-glob "^4.0.0"
is-number "^4.0.0"
kind-of "^6.0.0"
lazy-cache "^2.0.2"
logging-helpers "^1.0.0"
micromatch "^3.1.4"
relative "^3.0.2"
striptags "^3.1.0"
to-gfm-code-block "^0.1.1"
year "^0.2.1"
handlebars-utils@^1.0.2, handlebars-utils@^1.0.4, handlebars-utils@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9"
@ -2553,7 +2568,7 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
is-reference@^1.1.2:
is-reference@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
@ -3037,7 +3052,7 @@ jest-watcher@^26.6.2:
jest-util "^26.6.2"
string-length "^4.0.1"
jest-worker@^26.6.2:
jest-worker@^26.2.1, jest-worker@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
@ -3176,7 +3191,7 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lazy-cache@^2.0.1, lazy-cache@^2.0.2:
lazy-cache@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=
@ -3371,7 +3386,7 @@ magic-string@^0.22.5:
dependencies:
vlq "^0.2.2"
magic-string@^0.25.2:
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
@ -3404,6 +3419,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
marked@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.8.tgz#5008ece15cfa43e653e85845f3525af4beb6bdd4"
integrity sha512-lzmFjGnzWHkmbk85q/ILZjFoHHJIQGF+SxGEfIdGk/XhiTPhqGs37gbru6Kkd48diJnEyYwnG67nru0Z2gQtuQ==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -3886,7 +3906,7 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@ -4074,7 +4094,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.18.1:
resolve@^1.10.0, resolve@^1.11.1, resolve@^1.17.0, resolve@^1.18.1:
version "1.19.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
@ -4102,17 +4122,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rollup-plugin-commonjs@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb"
integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==
dependencies:
estree-walker "^0.6.1"
is-reference "^1.1.2"
magic-string "^0.25.2"
resolve "^1.11.0"
rollup-pluginutils "^2.8.1"
rollup-plugin-node-builtins@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-node-builtins/-/rollup-plugin-node-builtins-2.1.2.tgz#24a1fed4a43257b6b64371d8abc6ce1ab14597e9"
@ -4146,6 +4155,16 @@ rollup-plugin-node-resolve@^5.2.0:
resolve "^1.11.1"
rollup-pluginutils "^2.8.1"
rollup-plugin-terser@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
dependencies:
"@babel/code-frame" "^7.10.4"
jest-worker "^26.2.1"
serialize-javascript "^4.0.0"
terser "^5.0.0"
rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.8.1:
version "2.8.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
@ -4236,6 +4255,13 @@ semver@~2.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
integrity sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
dependencies:
randombytes "^2.1.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -4351,7 +4377,7 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
source-map-support@^0.5.6:
source-map-support@^0.5.6, source-map-support@~0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@ -4374,7 +4400,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3:
source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@ -4565,6 +4591,15 @@ terminal-link@^2.0.0:
ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0"
terser@^5.0.0:
version "5.5.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289"
integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==
dependencies:
commander "^2.20.0"
source-map "~0.7.2"
source-map-support "~0.5.19"
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@ -4732,9 +4767,9 @@ typescript@^4.1.3:
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
uglify-js@^3.1.4:
version "3.12.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.4.tgz#93de48bb76bb3ec0fc36563f871ba46e2ee5c7ee"
integrity sha512-L5i5jg/SHkEqzN18gQMTWsZk3KelRsfD1wUVNqtq0kzqWQqcJjyL8yc1o8hJgRrWqrAl2mUFbhfznEIoi7zi2A==
version "3.12.6"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.6.tgz#f884584fcc42e10bca70db5cb32e8625c2c42535"
integrity sha512-aqWHe3DfQmZUDGWBbabZ2eQnJlQd1fKlMUu7gV+MiTuDzdgDw31bI3wA2jLLsV/hNcDP26IfyEgSVoft5+0SVw==
union-value@^1.0.0:
version "1.0.1"