merge master
This commit is contained in:
commit
e26e628ae7
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -32,4 +32,4 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome": "^1.1.8"
|
"@fortawesome/fontawesome": "^1.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,7 +28,7 @@ context("Create a automation", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create action
|
// Create action
|
||||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
cy.get("[data-cy=CREATE_RECORD]").click()
|
||||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||||
cy.get("select")
|
cy.get("select")
|
||||||
.first()
|
.first()
|
||||||
|
|
|
@ -8,7 +8,7 @@ context('Create a Table', () => {
|
||||||
cy.createTable('dog')
|
cy.createTable('dog')
|
||||||
|
|
||||||
// Check if Table exists
|
// Check if Table exists
|
||||||
cy.get('.title').should('have.text', 'dog')
|
cy.get('.title').should('contain.text', 'dog')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds a new column to the table', () => {
|
it('adds a new column to the table', () => {
|
||||||
|
|
|
@ -47,8 +47,9 @@ context('Create a View', () => {
|
||||||
|
|
||||||
it('creates a stats calculation view based on age', () => {
|
it('creates a stats calculation view based on age', () => {
|
||||||
cy.contains("Calculate").click()
|
cy.contains("Calculate").click()
|
||||||
cy.get(".menu-container").find("select").first().select("Statistics")
|
// we may reinstate this - have commented this dropdown for now as there is only one option
|
||||||
cy.get(".menu-container").find("select").eq(1).select("age")
|
//cy.get(".menu-container").find("select").first().select("Statistics")
|
||||||
|
cy.get(".menu-container").find("select").eq(0).select("age")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("thead th").should(($headers) => {
|
cy.get("thead th").should(($headers) => {
|
||||||
expect($headers).to.have.length(7)
|
expect($headers).to.have.length(7)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.34.6",
|
"@budibase/bbui": "^1.34.6",
|
||||||
"@budibase/client": "^0.1.22",
|
"@budibase/client": "^0.1.25",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
import fsort from "fast-sort"
|
import fsort from "fast-sort"
|
||||||
import getOr from "lodash/fp/getOr"
|
import getOr from "lodash/fp/getOr"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
import LinkedRecord from "./LinkedRecord.svelte"
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
@ -26,14 +27,17 @@
|
||||||
let headers = []
|
let headers = []
|
||||||
let currentPage = 0
|
let currentPage = 0
|
||||||
let search
|
let search
|
||||||
|
let loading
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (
|
if (
|
||||||
$backendUiStore.selectedView &&
|
$backendUiStore.selectedView &&
|
||||||
$backendUiStore.selectedView.name.startsWith("all_")
|
$backendUiStore.selectedView.name.startsWith("all_")
|
||||||
) {
|
) {
|
||||||
|
loading = true
|
||||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
||||||
data = records || []
|
data = records || []
|
||||||
|
loading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +64,14 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="table-controls">
|
<div class="table-controls">
|
||||||
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
|
<h2 class="title">
|
||||||
|
<span>{$backendUiStore.selectedModel.name}</span>
|
||||||
|
{#if loading}
|
||||||
|
<div transition:fade>
|
||||||
|
<Spinner size="10" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<ColumnPopover />
|
<ColumnPopover />
|
||||||
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
|
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
|
||||||
|
@ -123,6 +134,12 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title > span {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
import LinkedRecord from "./LinkedRecord.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
|
|
@ -24,11 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let originalName = field.name
|
let originalName = field.name
|
||||||
|
$: required = field && field.constraints && field.constraints.presence
|
||||||
$: required =
|
|
||||||
field.constraints &&
|
|
||||||
field.constraints.presence &&
|
|
||||||
!field.constraints.presence.allowEmpty
|
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
|
@ -50,6 +46,14 @@
|
||||||
field.type = type
|
field.type = type
|
||||||
field.constraints = constraints
|
field.constraints = constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPresence = required => (required ? { allowEmpty: false } : false)
|
||||||
|
|
||||||
|
const requiredChanged = ev => {
|
||||||
|
const req = ev.target.checked
|
||||||
|
field.constraints.presence = req ? { allowEmpty: false } : false
|
||||||
|
required = req
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
@ -68,10 +72,7 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Required</label>
|
<label>Required</label>
|
||||||
<input
|
<input type="checkbox" checked={required} on:change={requiredChanged} />
|
||||||
type="checkbox"
|
|
||||||
bind:checked={required}
|
|
||||||
on:change={() => (field.constraints.presence.allowEmpty = required)} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if field.type === 'string' && field.constraints}
|
{#if field.type === 'string' && field.constraints}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const CALCULATIONS = [
|
const CALCULATIONS = [
|
||||||
|
@ -34,6 +33,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
|
if (!view.calculation) view.calculation = "stats"
|
||||||
backendUiStore.actions.views.save(view)
|
backendUiStore.actions.views.save(view)
|
||||||
notifier.success(`View ${view.name} saved.`)
|
notifier.success(`View ${view.name} saved.`)
|
||||||
analytics.captureEvent("Added View Calculate", { field: view.field })
|
analytics.captureEvent("Added View Calculate", { field: view.field })
|
||||||
|
@ -50,14 +50,15 @@
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
<h5>Calculate</h5>
|
<h5>Calculate</h5>
|
||||||
<div class="input-group-row">
|
<div class="input-group-row">
|
||||||
<p>The</p>
|
<!-- <p>The</p>
|
||||||
<Select secondary thin bind:value={view.calculation}>
|
<Select secondary thin bind:value={view.calculation}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each CALCULATIONS as calculation}
|
{#each CALCULATIONS as calculation}
|
||||||
<option value={calculation.key}>{calculation.name}</option>
|
<option value={calculation.key}>{calculation.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
<p>of</p>
|
<p>of</p> -->
|
||||||
|
<p>The statistics of</p>
|
||||||
<Select secondary thin bind:value={view.field}>
|
<Select secondary thin bind:value={view.field}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
|
@ -86,7 +87,7 @@
|
||||||
|
|
||||||
.input-group-row {
|
.input-group-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50px 1fr 20px 1fr;
|
grid-template-columns: auto 1fr 20px 1fr;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const CONDITIONS = [
|
const CONDITIONS = [
|
||||||
|
@ -81,11 +81,38 @@
|
||||||
|
|
||||||
function isMultipleChoice(field) {
|
function isMultipleChoice(field) {
|
||||||
return (
|
return (
|
||||||
viewModel.schema[field].constraints &&
|
(viewModel.schema[field].constraints &&
|
||||||
viewModel.schema[field].constraints.inclusion &&
|
viewModel.schema[field].constraints.inclusion &&
|
||||||
viewModel.schema[field].constraints.inclusion.length
|
viewModel.schema[field].constraints.inclusion.length) ||
|
||||||
|
viewModel.schema[field].type === "boolean"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldOptions(field) {
|
||||||
|
return viewModel.schema[field].type === "string"
|
||||||
|
? viewModel.schema[field].constraints.inclusion
|
||||||
|
: [true, false]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDate(field) {
|
||||||
|
return viewModel.schema[field].type === "datetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumber(field) {
|
||||||
|
return viewModel.schema[field].type === "number"
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldChanged = filter => ev => {
|
||||||
|
// reset if type changed
|
||||||
|
if (
|
||||||
|
filter.key &&
|
||||||
|
ev.target.value &&
|
||||||
|
viewModel.schema[filter.key].type !==
|
||||||
|
viewModel.schema[ev.target.value].type
|
||||||
|
) {
|
||||||
|
filter.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
|
@ -112,7 +139,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
<Select secondary thin bind:value={filter.key}>
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
bind:value={filter.key}
|
||||||
|
on:change={fieldChanged(filter)}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<option value={field}>{field}</option>
|
<option value={field}>{field}</option>
|
||||||
|
@ -126,10 +157,21 @@
|
||||||
</Select>
|
</Select>
|
||||||
{#if filter.key && isMultipleChoice(filter.key)}
|
{#if filter.key && isMultipleChoice(filter.key)}
|
||||||
<Select secondary thin bind:value={filter.value}>
|
<Select secondary thin bind:value={filter.value}>
|
||||||
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
<option value="">Choose an option</option>
|
||||||
<option value={option}>{option}</option>
|
{#each fieldOptions(filter.key) as option}
|
||||||
|
<option value={option}>{option.toString()}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{:else if filter.key && isDate(filter.key)}
|
||||||
|
<DatePicker
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]} />
|
||||||
|
{:else if filter.key && isNumber(filter.key)}
|
||||||
|
<Input
|
||||||
|
thin
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]}
|
||||||
|
type="number" />
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
thin
|
thin
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
|
|
||||||
const CALCULATIONS = [
|
const CALCULATIONS = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
|
|
|
@ -2,25 +2,41 @@
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Dropzone,
|
||||||
|
Spacer,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import TableDataImport from "./TableDataImport.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
export let table
|
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
let name
|
let name
|
||||||
|
let dataImport
|
||||||
|
let loading
|
||||||
|
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
|
loading = true
|
||||||
const model = await backendUiStore.actions.models.save({
|
const model = await backendUiStore.actions.models.save({
|
||||||
name,
|
name,
|
||||||
schema: {},
|
schema: dataImport.schema || {},
|
||||||
|
dataImport,
|
||||||
})
|
})
|
||||||
notifier.success(`Table ${name} created successfully.`)
|
notifier.success(`Table ${name} created successfully.`)
|
||||||
$goto(`./model/${model._id}`)
|
$goto(`./model/${model._id}`)
|
||||||
|
analytics.captureEvent("Table Created", { name })
|
||||||
name = ""
|
name = ""
|
||||||
dropdown.hide()
|
dropdown.hide()
|
||||||
analytics.captureEvent("Table Created", { name })
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClosed = () => {
|
const onClosed = () => {
|
||||||
|
@ -35,25 +51,38 @@
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h5>Create Table</h5>
|
<h5>Create Table</h5>
|
||||||
|
<Label grey extraSmall>Name</Label>
|
||||||
<Input
|
<Input
|
||||||
data-cy="table-name-input"
|
data-cy="table-name-input"
|
||||||
placeholder="Table Name"
|
placeholder="Table Name"
|
||||||
thin
|
thin
|
||||||
bind:value={name} />
|
bind:value={name} />
|
||||||
|
<Spacer medium />
|
||||||
|
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||||
|
<TableDataImport bind:dataImport />
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div class="button-margin-3">
|
<div class="button-margin-3">
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-margin-4">
|
<div class="button-margin-4">
|
||||||
<Button primary on:click={saveTable}>Save</Button>
|
<Button
|
||||||
|
disabled={!name || (dataImport && !dataImport.valid)}
|
||||||
|
primary
|
||||||
|
on:click={saveTable}>
|
||||||
|
<span style={`margin-right: ${loading ? '10px' : 0};`}>Save</span>
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="10" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h5 {
|
h5 {
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-m);
|
||||||
|
margin-top: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
<script>
|
||||||
|
import { Heading, Body, Button, Select } from "@budibase/bbui"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const BYTES_IN_KB = 1000
|
||||||
|
const BYTES_IN_MB = 1000000
|
||||||
|
const FILE_SIZE_LIMIT = BYTES_IN_MB * 1
|
||||||
|
|
||||||
|
export let files = []
|
||||||
|
export let dataImport = {
|
||||||
|
valid: true,
|
||||||
|
schema: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let parseResult
|
||||||
|
|
||||||
|
$: schema = parseResult && parseResult.schema
|
||||||
|
$: valid =
|
||||||
|
!schema || Object.keys(schema).every(column => schema[column].success)
|
||||||
|
$: dataImport = {
|
||||||
|
valid,
|
||||||
|
schema: buildModelSchema(schema),
|
||||||
|
path: files[0] && files[0].path,
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelSchema(schema) {
|
||||||
|
const modelSchema = {}
|
||||||
|
for (let key in schema) {
|
||||||
|
const type = schema[key].type
|
||||||
|
|
||||||
|
if (type === "omit") continue
|
||||||
|
|
||||||
|
modelSchema[key] = {
|
||||||
|
name: key,
|
||||||
|
type,
|
||||||
|
constraints: FIELDS[type.toUpperCase()].constraints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modelSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateCSV() {
|
||||||
|
const response = await api.post("/api/models/csv/validate", {
|
||||||
|
file: files[0],
|
||||||
|
schema: schema || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
parseResult = await response.json()
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
notifier.danger("CSV Invalid, please try another CSV file")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFile(evt) {
|
||||||
|
const fileArray = Array.from(evt.target.files)
|
||||||
|
const filesToProcess = fileArray.map(({ name, path, size }) => ({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
size,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (filesToProcess.some(file => file.size >= FILE_SIZE_LIMIT)) {
|
||||||
|
notifier.danger(
|
||||||
|
`Files cannot exceed ${FILE_SIZE_LIMIT /
|
||||||
|
BYTES_IN_MB}MB. Please try again with smaller files.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files = filesToProcess
|
||||||
|
|
||||||
|
await validateCSV()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function omitColumn(columnName) {
|
||||||
|
schema[columnName].type = "omit"
|
||||||
|
await validateCSV()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTypeChange = column => evt => {
|
||||||
|
schema[column].type = evt.target.value
|
||||||
|
validateCSV()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropzone">
|
||||||
|
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||||
|
<label for="file-upload" class:uploaded={files[0]}>
|
||||||
|
{#if files[0]}{files[0].name}{:else}Upload{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="schema-fields">
|
||||||
|
{#if schema}
|
||||||
|
{#each Object.keys(schema).filter(key => schema[key].type !== 'omit') as columnName}
|
||||||
|
<div class="field">
|
||||||
|
<span>{columnName}</span>
|
||||||
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
bind:value={schema[columnName].type}
|
||||||
|
on:change={handleTypeChange(columnName)}>
|
||||||
|
<option value={'string'}>Text</option>
|
||||||
|
<option value={'number'}>Number</option>
|
||||||
|
<option value={'datetime'}>Date</option>
|
||||||
|
</Select>
|
||||||
|
<span class="field-status" class:error={!schema[columnName].success}>
|
||||||
|
{schema[columnName].success ? 'Success' : 'Failure'}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="omit-button ri-close-circle-fill"
|
||||||
|
on:click={() => omitColumn(columnName)} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropzone {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-status {
|
||||||
|
color: var(--green);
|
||||||
|
justify-self: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: var(--spacing-s) var(--spacing-l);
|
||||||
|
transition: all 0.2s ease 0s;
|
||||||
|
display: inline-flex;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
min-width: auto;
|
||||||
|
outline: none;
|
||||||
|
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.omit-button {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
justify-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,7 +5,7 @@
|
||||||
import posthog from "posthog-js"
|
import posthog from "posthog-js"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let keys = { budibase: "", sendGrid: "" }
|
let keys = { budibase: "" }
|
||||||
|
|
||||||
async function updateKey([key, value]) {
|
async function updateKey([key, value]) {
|
||||||
if (key === "budibase") {
|
if (key === "budibase") {
|
||||||
|
@ -42,14 +42,6 @@
|
||||||
value={keys.budibase}
|
value={keys.budibase}
|
||||||
label="Budibase" />
|
label="Budibase" />
|
||||||
</div>
|
</div>
|
||||||
<div class="background">
|
|
||||||
<Input
|
|
||||||
on:save={e => updateKey(['sendgrid', e.detail])}
|
|
||||||
thin
|
|
||||||
edit
|
|
||||||
value={keys.sendgrid}
|
|
||||||
label="Sendgrid" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,20 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import AppCard from "./AppCard.svelte"
|
import AppCard from "./AppCard.svelte"
|
||||||
export let apps
|
import { Heading } from "@budibase/bbui"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import { get } from "builderStore/api"
|
||||||
|
|
||||||
|
let promise = getApps()
|
||||||
|
|
||||||
|
async function getApps() {
|
||||||
|
const res = await get("/api/applications")
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return json
|
||||||
|
} else {
|
||||||
|
throw new Error(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="inner">
|
<Heading medium black>Your Apps</Heading>
|
||||||
<div>
|
{#await promise}
|
||||||
|
<div class="spinner-container">
|
||||||
|
<Spinner size="30" />
|
||||||
|
</div>
|
||||||
|
{:then apps}
|
||||||
|
<div class="inner">
|
||||||
<div>
|
<div>
|
||||||
<div class="apps">
|
<div>
|
||||||
{#each apps as app}
|
<div class="apps">
|
||||||
<AppCard {...app} />
|
{#each apps as app}
|
||||||
{/each}
|
<AppCard {...app} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:catch err}
|
||||||
|
<h1 style="color:red">{err}</h1>
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
const createAppStore = writable({ currentStep: 0, values: {} })
|
const createAppStore = writable({ currentStep: 0, values: {} })
|
||||||
|
|
||||||
export let hasKey
|
export let hasKey
|
||||||
|
export let template
|
||||||
|
|
||||||
let isApiKeyValid
|
let isApiKeyValid
|
||||||
let lastApiKey
|
let lastApiKey
|
||||||
|
@ -142,11 +143,13 @@
|
||||||
// Create App
|
// Create App
|
||||||
const appResp = await post("/api/applications", {
|
const appResp = await post("/api/applications", {
|
||||||
name: $createAppStore.values.applicationName,
|
name: $createAppStore.values.applicationName,
|
||||||
|
template,
|
||||||
})
|
})
|
||||||
const appJson = await appResp.json()
|
const appJson = await appResp.json()
|
||||||
analytics.captureEvent("App Created", {
|
analytics.captureEvent("App Created", {
|
||||||
name,
|
name: $createAppStore.values.applicationName,
|
||||||
appId: appJson._id,
|
appId: appJson._id,
|
||||||
|
template,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select Correct Application/DB in prep for creating user
|
// Select Correct Application/DB in prep for creating user
|
||||||
|
@ -222,6 +225,7 @@
|
||||||
<div class:hidden={$createAppStore.currentStep !== i}>
|
<div class:hidden={$createAppStore.currentStep !== i}>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={step.component}
|
this={step.component}
|
||||||
|
{template}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
options={step.options}
|
options={step.options}
|
||||||
name={step.name} />
|
name={step.name} />
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
on:input={() => (blurred.api = true)}
|
on:input={() => (blurred.api = true)}
|
||||||
label="API Key"
|
label="API Key"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
placeholder="Enter your API Key"
|
placeholder="Use command-V to paste your API Key"
|
||||||
type="password"
|
type="password"
|
||||||
error={blurred.api && validationErrors.apiKey} />
|
error={blurred.api && validationErrors.apiKey} />
|
||||||
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input } from "@budibase/bbui"
|
import { Input, Heading, Body } from "@budibase/bbui"
|
||||||
export let validationErrors
|
export let validationErrors
|
||||||
|
export let template
|
||||||
|
|
||||||
let blurred = { appName: false }
|
let blurred = { appName: false }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if template}
|
||||||
|
<Heading small black>Selected Template</Heading>
|
||||||
|
<Body>{template.name}</Body>
|
||||||
|
{/if}
|
||||||
<h2>Create your web app</h2>
|
<h2>Create your web app</h2>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
type="pasword"
|
type="password"
|
||||||
error={blurred.password && validationErrors.password} />
|
error={blurred.password && validationErrors.password} />
|
||||||
<Select secondary name="accessLevelId">
|
<Select secondary name="accessLevelId">
|
||||||
<option value="ADMIN">Admin</option>
|
<option value="ADMIN">Admin</option>
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Heading, Body } from "@budibase/bbui"
|
||||||
|
import AppCard from "./AppCard.svelte"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
export let onSelect
|
||||||
|
|
||||||
|
async function fetchTemplates() {
|
||||||
|
const response = await api.get("/api/templates?type=app")
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
let templatesPromise = fetchTemplates()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Heading medium black>Start With a Template</Heading>
|
||||||
|
{#await templatesPromise}
|
||||||
|
<div class="spinner-container">
|
||||||
|
<Spinner size="30" />
|
||||||
|
</div>
|
||||||
|
{:then templates}
|
||||||
|
<div class="templates">
|
||||||
|
{#each templates as template}
|
||||||
|
<div class="templates-card">
|
||||||
|
<Heading black medium>{template.name}</Heading>
|
||||||
|
<Body medium grey>{template.category}</Body>
|
||||||
|
<Body small black>{template.description}</Body>
|
||||||
|
<div>
|
||||||
|
<img src={template.image} width="300" />
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<Button secondary on:click={() => onSelect(template)}>
|
||||||
|
Create {template.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:catch err}
|
||||||
|
<h1 style="color:red">{err}</h1>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.templates {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
grid-gap: var(--layout-m);
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-card {
|
||||||
|
background-color: var(--white);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
border: var(--border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
margin: 20px 80px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,7 +6,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NUMBER: {
|
NUMBER: {
|
||||||
|
@ -15,7 +15,7 @@ export const FIELDS = {
|
||||||
type: "number",
|
type: "number",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "number",
|
type: "number",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@ export const FIELDS = {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// OPTIONS: {
|
// OPTIONS: {
|
||||||
|
@ -44,7 +44,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
datetime: {
|
datetime: {
|
||||||
latest: "",
|
latest: "",
|
||||||
earliest: "",
|
earliest: "",
|
||||||
|
@ -57,7 +57,7 @@ export const FIELDS = {
|
||||||
type: "attachment",
|
type: "attachment",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// LINKED_FIELDS: {
|
// LINKED_FIELDS: {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import * as api from "components/database/DataTable/api"
|
import * as api from "components/database/DataTable/api"
|
||||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import * as api from "components/database/DataTable/api"
|
import * as api from "components/database/DataTable/api"
|
||||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
|
|
@ -5,25 +5,12 @@
|
||||||
import AppList from "components/start/AppList.svelte"
|
import AppList from "components/start/AppList.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import { get } from "builderStore/api"
|
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
import TemplateList from "components/start/TemplateList.svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let promise = getApps()
|
|
||||||
|
|
||||||
async function getApps() {
|
|
||||||
const res = await get("/api/applications")
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return json
|
|
||||||
} else {
|
|
||||||
throw new Error(json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasKey
|
let hasKey
|
||||||
|
|
||||||
async function fetchKeys() {
|
async function fetchKeys() {
|
||||||
|
@ -47,11 +34,12 @@
|
||||||
// Handle create app modal
|
// Handle create app modal
|
||||||
const { open } = getContext("simple-modal")
|
const { open } = getContext("simple-modal")
|
||||||
|
|
||||||
const showCreateAppModal = () => {
|
const showCreateAppModal = template => {
|
||||||
open(
|
open(
|
||||||
CreateAppModal,
|
CreateAppModal,
|
||||||
{
|
{
|
||||||
hasKey,
|
hasKey,
|
||||||
|
template,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
|
@ -68,7 +56,7 @@
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="welcome">Welcome to the Budibase Beta</div>
|
<div class="welcome">Welcome to the Budibase Beta</div>
|
||||||
<Button primary purple on:click={showCreateAppModal}>
|
<Button primary purple on:click={() => showCreateAppModal()}>
|
||||||
Create New Web App
|
Create New Web App
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,15 +68,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#await promise}
|
<TemplateList onSelect={showCreateAppModal} />
|
||||||
<div class="spinner-container">
|
<AppList />
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{:then result}
|
|
||||||
<AppList apps={result} />
|
|
||||||
{:catch err}
|
|
||||||
<h1 style="color:red">{err}</h1>
|
|
||||||
{/await}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.header {
|
.header {
|
||||||
|
@ -127,12 +108,4 @@
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-container {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "budibase",
|
"name": "budibase",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"description": "Budibase CLI",
|
"description": "Budibase CLI",
|
||||||
"repository": "https://github.com/Budibase/Budibase",
|
"repository": "https://github.com/Budibase/Budibase",
|
||||||
"homepage": "https://www.budibase.com",
|
"homepage": "https://www.budibase.com",
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/server": "^0.1.22",
|
"@budibase/server": "^0.1.25",
|
||||||
"@inquirer/password": "^0.0.6-alpha.0",
|
"@inquirer/password": "^0.0.6-alpha.0",
|
||||||
"chalk": "^2.4.2",
|
"chalk": "^2.4.2",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
"module": "dist/budibase-client.esm.mjs",
|
"module": "dist/budibase-client.esm.mjs",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import api from "../api"
|
||||||
import renderTemplateString from "./renderTemplateString"
|
import renderTemplateString from "./renderTemplateString"
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
@ -5,6 +6,9 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
export const eventHandlers = routeTo => {
|
export const eventHandlers = routeTo => {
|
||||||
const handlers = {
|
const handlers = {
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
"Navigate To": param => routeTo(param && param.url),
|
||||||
|
"Create Record": api.createRecord,
|
||||||
|
"Update Record": api.updateRecord,
|
||||||
|
"Trigger Workflow": api.triggerWorkflow,
|
||||||
}
|
}
|
||||||
|
|
||||||
// when an event is called, this is what gets run
|
// when an event is called, this is what gets run
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 201 KiB |
Binary file not shown.
Before Width: | Height: | Size: 105 KiB |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/electron.js",
|
"main": "src/electron.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -42,13 +42,14 @@
|
||||||
"author": "Michael Shanks",
|
"author": "Michael Shanks",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/client": "^0.1.22",
|
"@budibase/client": "^0.1.25",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sendgrid/mail": "^7.1.1",
|
"@sendgrid/mail": "^7.1.1",
|
||||||
"@sentry/node": "^5.19.2",
|
"@sentry/node": "^5.19.2",
|
||||||
"aws-sdk": "^2.706.0",
|
"aws-sdk": "^2.706.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chmodr": "^1.2.0",
|
"chmodr": "^1.2.0",
|
||||||
|
"csvtojson": "^2.0.10",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
"electron-is-dev": "^1.2.0",
|
"electron-is-dev": "^1.2.0",
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
"pino-pretty": "^4.0.0",
|
"pino-pretty": "^4.0.0",
|
||||||
"pouchdb": "^7.2.1",
|
"pouchdb": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"sharp": "^0.26.0",
|
"sharp": "^0.26.0",
|
||||||
"squirrelly": "^7.5.0",
|
"squirrelly": "^7.5.0",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.0",
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const { exportTemplateFromApp } = require("../src/utilities/templates")
|
||||||
|
const yargs = require("yargs")
|
||||||
|
|
||||||
|
// Script to export a chosen budibase app into a package
|
||||||
|
// Usage: ./scripts/exportAppTemplate.js export --name=Funky --instanceId=someInstanceId --appId=appId
|
||||||
|
|
||||||
|
yargs
|
||||||
|
.command(
|
||||||
|
"export",
|
||||||
|
"Export an existing budibase application to the .budibase/templates directory",
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
description: "The name of the newly exported template",
|
||||||
|
alias: "n",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
instanceId: {
|
||||||
|
description: "The instanceId to dump the database for",
|
||||||
|
alias: "inst",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
appId: {
|
||||||
|
description: "The appId of the application you want to export",
|
||||||
|
alias: "app",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async args => {
|
||||||
|
console.log("Exporting app..")
|
||||||
|
const exportPath = await exportTemplateFromApp({
|
||||||
|
templateName: args.name,
|
||||||
|
instanceId: args.instanceId,
|
||||||
|
appId: args.appId,
|
||||||
|
})
|
||||||
|
console.log(`Template ${args.name} exported to ${exportPath}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.help()
|
||||||
|
.alias("help", "h").argv
|
|
@ -1,18 +1,22 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const newid = require("../../db/newid")
|
|
||||||
const {
|
const {
|
||||||
generateAdminPermissions,
|
generateAdminPermissions,
|
||||||
generatePowerUserPermissions,
|
generatePowerUserPermissions,
|
||||||
POWERUSER_LEVEL_ID,
|
POWERUSER_LEVEL_ID,
|
||||||
ADMIN_LEVEL_ID,
|
ADMIN_LEVEL_ID,
|
||||||
} = require("../../utilities/accessLevels")
|
} = require("../../utilities/accessLevels")
|
||||||
|
const {
|
||||||
|
generateAccessLevelID,
|
||||||
|
getAccessLevelParams,
|
||||||
|
} = require("../../db/utils")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const body = await db.query("database/by_type", {
|
const body = await db.allDocs(
|
||||||
include_docs: true,
|
getAccessLevelParams(null, {
|
||||||
key: ["accesslevel"],
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
const customAccessLevels = body.rows.map(row => row.doc)
|
const customAccessLevels = body.rows.map(row => row.doc)
|
||||||
|
|
||||||
const staticAccessLevels = [
|
const staticAccessLevels = [
|
||||||
|
@ -90,7 +94,7 @@ exports.create = async function(ctx) {
|
||||||
name: ctx.request.body.name,
|
name: ctx.request.body.name,
|
||||||
_rev: ctx.request.body._rev,
|
_rev: ctx.request.body._rev,
|
||||||
permissions: ctx.request.body.permissions || [],
|
permissions: ctx.request.body.permissions || [],
|
||||||
_id: newid(),
|
_id: generateAccessLevelID(),
|
||||||
type: "accesslevel",
|
type: "accesslevel",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ exports.fetch = async function(ctx) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
budibase: process.env.BUDIBASE_API_KEY,
|
budibase: process.env.BUDIBASE_API_KEY,
|
||||||
sendgrid: process.env.SENDGRID_API_KEY,
|
|
||||||
userId: process.env.USERID_API_KEY,
|
userId: process.env.USERID_API_KEY,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const ClientDb = require("../../db/clientDb")
|
const ClientDb = require("../../db/clientDb")
|
||||||
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
|
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
|
||||||
const newid = require("../../db/newid")
|
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const instanceController = require("./instance")
|
const instanceController = require("./instance")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("path")
|
||||||
|
@ -12,17 +11,18 @@ const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
const fs = require("fs-extra")
|
const fs = require("fs-extra")
|
||||||
const { promisify } = require("util")
|
const { promisify } = require("util")
|
||||||
const chmodr = require("chmodr")
|
const chmodr = require("chmodr")
|
||||||
|
const { generateAppID, getAppParams } = require("../../db/utils")
|
||||||
const {
|
const {
|
||||||
downloadExtractComponentLibraries,
|
downloadExtractComponentLibraries,
|
||||||
} = require("../../utilities/createAppPackage")
|
} = require("../../utilities/createAppPackage")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
||||||
const body = await db.query("client/by_type", {
|
const body = await db.allDocs(
|
||||||
include_docs: true,
|
getAppParams(null, {
|
||||||
key: ["app"],
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
ctx.body = body.rows.map(row => row.doc)
|
ctx.body = body.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ exports.create = async function(ctx) {
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
ctx.throw(400, "ClientId not suplied")
|
ctx.throw(400, "ClientId not suplied")
|
||||||
}
|
}
|
||||||
const appId = newid()
|
const appId = generateAppID()
|
||||||
// insert an appId -> clientId lookup
|
// insert an appId -> clientId lookup
|
||||||
const masterDb = new CouchDB("client_app_lookup")
|
const masterDb = new CouchDB("client_app_lookup")
|
||||||
|
|
||||||
|
@ -66,6 +66,7 @@ exports.create = async function(ctx) {
|
||||||
userInstanceMap: {},
|
userInstanceMap: {},
|
||||||
componentLibraries: ["@budibase/standard-components"],
|
componentLibraries: ["@budibase/standard-components"],
|
||||||
name: ctx.request.body.name,
|
name: ctx.request.body.name,
|
||||||
|
template: ctx.request.body.template,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rev } = await db.put(newApplication)
|
const { rev } = await db.put(newApplication)
|
||||||
|
@ -75,9 +76,13 @@ exports.create = async function(ctx) {
|
||||||
appId: newApplication._id,
|
appId: newApplication._id,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
body: { name: `dev-${clientId}` },
|
body: {
|
||||||
|
name: `dev-${clientId}`,
|
||||||
|
template: ctx.request.body.template,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
await instanceController.create(createInstCtx)
|
await instanceController.create(createInstCtx)
|
||||||
newApplication.instances.push(createInstCtx.body)
|
newApplication.instances.push(createInstCtx.body)
|
||||||
|
|
||||||
|
@ -154,6 +159,19 @@ const createEmptyAppPackage = async (ctx, app) => {
|
||||||
name: npmFriendlyAppName(app.name),
|
name: npmFriendlyAppName(app.name),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// if this app is being created from a template,
|
||||||
|
// copy the frontend page definition files from
|
||||||
|
// the template directory.
|
||||||
|
if (app.template) {
|
||||||
|
const templatePageDefinitions = join(
|
||||||
|
appsFolder,
|
||||||
|
"templates",
|
||||||
|
app.template.key,
|
||||||
|
"pages"
|
||||||
|
)
|
||||||
|
await copy(templatePageDefinitions, join(appsFolder, app._id, "pages"))
|
||||||
|
}
|
||||||
|
|
||||||
const mainJson = await updateJsonFile(
|
const mainJson = await updateJsonFile(
|
||||||
join(appsFolder, app._id, "pages", "main", "page.json"),
|
join(appsFolder, app._id, "pages", "main", "page.json"),
|
||||||
app
|
app
|
||||||
|
|
|
@ -2,6 +2,7 @@ const jwt = require("jsonwebtoken")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const ClientDb = require("../../db/clientDb")
|
const ClientDb = require("../../db/clientDb")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
|
const { generateUserID } = require("../../db/utils")
|
||||||
|
|
||||||
exports.authenticate = async ctx => {
|
exports.authenticate = async ctx => {
|
||||||
if (!ctx.user.appId) ctx.throw(400, "No appId")
|
if (!ctx.user.appId) ctx.throw(400, "No appId")
|
||||||
|
@ -35,7 +36,7 @@ exports.authenticate = async ctx => {
|
||||||
|
|
||||||
let dbUser
|
let dbUser
|
||||||
try {
|
try {
|
||||||
dbUser = await instanceDb.get(`user_${username}`)
|
dbUser = await instanceDb.get(generateUserID(username))
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// do not want to throw a 404 - as this could be
|
// do not want to throw a 404 - as this could be
|
||||||
// used to dtermine valid usernames
|
// used to dtermine valid usernames
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const newid = require("../../db/newid")
|
|
||||||
const actions = require("../../automations/actions")
|
const actions = require("../../automations/actions")
|
||||||
const logic = require("../../automations/logic")
|
const logic = require("../../automations/logic")
|
||||||
const triggers = require("../../automations/triggers")
|
const triggers = require("../../automations/triggers")
|
||||||
|
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* *
|
* *
|
||||||
|
@ -34,7 +34,7 @@ exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
let automation = ctx.request.body
|
let automation = ctx.request.body
|
||||||
|
|
||||||
automation._id = newid()
|
automation._id = generateAutomationID()
|
||||||
|
|
||||||
automation.type = "automation"
|
automation.type = "automation"
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
|
@ -72,10 +72,11 @@ exports.update = async function(ctx) {
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/by_type`, {
|
const response = await db.allDocs(
|
||||||
key: ["automation"],
|
getAutomationParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
const fs = require("fs")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const client = require("../../db/clientDb")
|
const client = require("../../db/clientDb")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
|
const { downloadTemplate } = require("../../utilities/templates")
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const instanceName = ctx.request.body.name
|
const instanceName = ctx.request.body.name
|
||||||
|
const template = ctx.request.body.template
|
||||||
const { appId } = ctx.user
|
const { appId } = ctx.user
|
||||||
const appShortId = appId.substring(0, 7)
|
const appShortId = appId.substring(0, 7)
|
||||||
const instanceId = `inst_${appShortId}_${newid()}`
|
const instanceId = `inst_${appShortId}_${newid()}`
|
||||||
|
@ -18,30 +21,7 @@ exports.create = async function(ctx) {
|
||||||
clientId,
|
clientId,
|
||||||
applicationId: appId,
|
applicationId: appId,
|
||||||
},
|
},
|
||||||
views: {
|
views: {},
|
||||||
by_username: {
|
|
||||||
map: function(doc) {
|
|
||||||
if (doc.type === "user") {
|
|
||||||
emit([doc.username], doc._id)
|
|
||||||
}
|
|
||||||
}.toString(),
|
|
||||||
},
|
|
||||||
by_type: {
|
|
||||||
map: function(doc) {
|
|
||||||
emit([doc.type], doc._id)
|
|
||||||
}.toString(),
|
|
||||||
},
|
|
||||||
by_automation_trigger: {
|
|
||||||
map: function(doc) {
|
|
||||||
if (doc.type === "automation") {
|
|
||||||
const trigger = doc.definition.trigger
|
|
||||||
if (trigger) {
|
|
||||||
emit([trigger.event], trigger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add the new instance under the app clientDB
|
// Add the new instance under the app clientDB
|
||||||
|
@ -51,6 +31,16 @@ exports.create = async function(ctx) {
|
||||||
budibaseApp.instances.push(instance)
|
budibaseApp.instances.push(instance)
|
||||||
await clientDb.put(budibaseApp)
|
await clientDb.put(budibaseApp)
|
||||||
|
|
||||||
|
// replicate the template data to the instance DB
|
||||||
|
if (template) {
|
||||||
|
const templatePath = await downloadTemplate(...template.key.split("/"))
|
||||||
|
const dbDumpReadStream = fs.createReadStream(`${templatePath}/db/dump.txt`)
|
||||||
|
const { ok } = await db.load(dbDumpReadStream)
|
||||||
|
if (!ok) {
|
||||||
|
ctx.throw(500, "Error loading database dump from template.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Instance Database ${instanceName} successfully provisioned.`
|
ctx.message = `Instance Database ${instanceName} successfully provisioned.`
|
||||||
ctx.body = instance
|
ctx.body = instance
|
||||||
|
|
|
@ -1,36 +1,45 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const newid = require("../../db/newid")
|
const csvParser = require("../../utilities/csvParser")
|
||||||
|
const {
|
||||||
|
getRecordParams,
|
||||||
|
getModelParams,
|
||||||
|
generateModelID,
|
||||||
|
generateRecordID,
|
||||||
|
} = require("../../db/utils")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const body = await db.query("database/by_type", {
|
const body = await db.allDocs(
|
||||||
include_docs: true,
|
getModelParams(null, {
|
||||||
key: ["model"],
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
ctx.body = body.rows.map(row => row.doc)
|
ctx.body = body.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const model = await db.get(ctx.params.id)
|
ctx.body = await db.get(ctx.params.id)
|
||||||
ctx.body = model
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
const { dataImport, ...rest } = ctx.request.body
|
||||||
const modelToSave = {
|
const modelToSave = {
|
||||||
type: "model",
|
type: "model",
|
||||||
_id: newid(),
|
_id: generateModelID(),
|
||||||
views: {},
|
views: {},
|
||||||
...ctx.request.body,
|
...rest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// rename record fields when table column is renamed
|
// rename record fields when table column is renamed
|
||||||
const { _rename } = modelToSave
|
const { _rename } = modelToSave
|
||||||
if (_rename) {
|
if (_rename) {
|
||||||
const records = await db.query(`database/all_${modelToSave._id}`, {
|
const records = await db.allDocs(
|
||||||
include_docs: true,
|
getRecordParams(modelToSave._id, null, {
|
||||||
})
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
const docs = records.rows.map(({ doc }) => {
|
const docs = records.rows.map(({ doc }) => {
|
||||||
doc[_rename.updated] = doc[_rename.old]
|
doc[_rename.updated] = doc[_rename.old]
|
||||||
delete doc[_rename.old]
|
delete doc[_rename.old]
|
||||||
|
@ -54,7 +63,7 @@ exports.save = async function(ctx) {
|
||||||
modelToSave._rev = result.rev
|
modelToSave._rev = result.rev
|
||||||
|
|
||||||
const { schema } = ctx.request.body
|
const { schema } = ctx.request.body
|
||||||
for (let key in schema) {
|
for (let key of Object.keys(schema)) {
|
||||||
// model has a linked record
|
// model has a linked record
|
||||||
if (schema[key].type === "link") {
|
if (schema[key].type === "link") {
|
||||||
// create the link field in the other model
|
// create the link field in the other model
|
||||||
|
@ -71,18 +80,17 @@ exports.save = async function(ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const designDoc = await db.get("_design/database")
|
if (dataImport && dataImport.path) {
|
||||||
designDoc.views = {
|
// Populate the table with records imported from CSV in a bulk update
|
||||||
...designDoc.views,
|
const data = await csvParser.transform(dataImport)
|
||||||
[`all_${modelToSave._id}`]: {
|
|
||||||
map: `function(doc) {
|
for (let row of data) {
|
||||||
if (doc.modelId === "${modelToSave._id}") {
|
row._id = generateRecordID(modelToSave._id)
|
||||||
emit(doc._id);
|
row.modelId = modelToSave._id
|
||||||
}
|
}
|
||||||
}`,
|
|
||||||
},
|
await db.bulkDocs(data)
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
|
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
|
||||||
|
@ -96,16 +104,18 @@ exports.destroy = async function(ctx) {
|
||||||
|
|
||||||
await db.remove(modelToDelete)
|
await db.remove(modelToDelete)
|
||||||
|
|
||||||
const modelViewId = `all_${ctx.params.modelId}`
|
|
||||||
|
|
||||||
// Delete all records for that model
|
// Delete all records for that model
|
||||||
const records = await db.query(`database/${modelViewId}`)
|
const records = await db.allDocs(
|
||||||
|
getRecordParams(ctx.params.modelId, null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
await db.bulkDocs(
|
await db.bulkDocs(
|
||||||
records.rows.map(record => ({ _id: record.id, _deleted: true }))
|
records.rows.map(record => ({ _id: record.id, _deleted: true }))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete linked record fields in dependent models
|
// Delete linked record fields in dependent models
|
||||||
for (let key in modelToDelete.schema) {
|
for (let key of Object.keys(modelToDelete.schema)) {
|
||||||
const { type, modelId } = modelToDelete.schema[key]
|
const { type, modelId } = modelToDelete.schema[key]
|
||||||
if (type === "link") {
|
if (type === "link") {
|
||||||
const linkedModel = await db.get(modelId)
|
const linkedModel = await db.get(modelId)
|
||||||
|
@ -114,11 +124,15 @@ exports.destroy = async function(ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the "all" view
|
|
||||||
const designDoc = await db.get("_design/database")
|
|
||||||
delete designDoc.views[modelViewId]
|
|
||||||
await db.put(designDoc)
|
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Model ${ctx.params.modelId} deleted.`
|
ctx.message = `Model ${ctx.params.modelId} deleted.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.validateCSVSchema = async function(ctx) {
|
||||||
|
const { file, schema = {} } = ctx.request.body
|
||||||
|
const result = await csvParser.parse(file.path, schema)
|
||||||
|
ctx.body = {
|
||||||
|
schema: result,
|
||||||
|
path: file.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const validateJs = require("validate.js")
|
const validateJs = require("validate.js")
|
||||||
const newid = require("../../db/newid")
|
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
||||||
|
const { cloneDeep } = require("lodash")
|
||||||
|
|
||||||
|
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
||||||
|
|
||||||
function emitEvent(eventType, ctx, record) {
|
function emitEvent(eventType, ctx, record) {
|
||||||
let event = {
|
let event = {
|
||||||
|
@ -29,10 +32,12 @@ validateJs.extend(validateJs.validators.datetime, {
|
||||||
|
|
||||||
exports.patch = async function (ctx) {
|
exports.patch = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = await db.get(ctx.params.id)
|
let record = await db.get(ctx.params.id)
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
const patchfields = ctx.request.body
|
const patchfields = ctx.request.body
|
||||||
|
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
for (let key in patchfields) {
|
for (let key in patchfields) {
|
||||||
if (!model.schema[key]) continue
|
if (!model.schema[key]) continue
|
||||||
record[key] = patchfields[key]
|
record[key] = patchfields[key]
|
||||||
|
@ -64,14 +69,88 @@ exports.save = async function (ctx) {
|
||||||
if (ctx.request.body.type === 'delete') {
|
if (ctx.request.body.type === 'delete') {
|
||||||
await bulkDelete(ctx)
|
await bulkDelete(ctx)
|
||||||
} else {
|
} else {
|
||||||
await saveRecords(ctx)
|
}
|
||||||
|
exports.save = async function (ctx) {
|
||||||
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
let record = ctx.request.body
|
||||||
|
record.modelId = ctx.params.modelId
|
||||||
|
|
||||||
|
if (!record._rev && !record._id) {
|
||||||
|
record._id = generateRecordID(record.modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = await db.get(record.modelId)
|
||||||
|
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
|
const validateResult = await validate({
|
||||||
|
record,
|
||||||
|
model,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validateResult.valid) {
|
||||||
|
ctx.status = 400
|
||||||
|
ctx.body = {
|
||||||
|
status: 400,
|
||||||
|
errors: validateResult.errors,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRecord = record._rev && (await db.get(record._id))
|
||||||
|
|
||||||
|
if (existingRecord) {
|
||||||
|
const response = await db.put(record)
|
||||||
|
record._rev = response.rev
|
||||||
|
record.type = "record"
|
||||||
|
ctx.body = record
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.message = `${model.name} updated successfully.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record.type = "record"
|
||||||
|
const response = await db.post(record)
|
||||||
|
record._rev = response.rev
|
||||||
|
|
||||||
|
// create links in other tables
|
||||||
|
for (let key in record) {
|
||||||
|
if (model.schema[key] && model.schema[key].type === "link") {
|
||||||
|
const linked = await db.allDocs({
|
||||||
|
include_docs: true,
|
||||||
|
keys: record[key],
|
||||||
|
})
|
||||||
|
|
||||||
|
// add this record to the linked records in attached models
|
||||||
|
const linkedDocs = linked.rows.map(row => {
|
||||||
|
const doc = row.doc
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
[model.name]: doc[model.name]
|
||||||
|
? [...doc[model.name], record._id]
|
||||||
|
: [record._id],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.bulkDocs(linkedDocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchView = async function (ctx) {
|
exports.fetchView = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const { stats, group, field } = ctx.query
|
const { stats, group, field } = ctx.query
|
||||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
const viewName = ctx.params.viewName
|
||||||
|
|
||||||
|
// if this is a model view being looked for just transfer to that
|
||||||
|
if (viewName.indexOf(MODEL_VIEW_BEGINS_WITH) === 0) {
|
||||||
|
ctx.params.modelId = viewName.substring(4)
|
||||||
|
await exports.fetchModelRecords(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await db.query(`database/${viewName}`, {
|
||||||
include_docs: !stats,
|
include_docs: !stats,
|
||||||
group,
|
group,
|
||||||
})
|
})
|
||||||
|
@ -92,9 +171,11 @@ exports.fetchView = async function (ctx) {
|
||||||
|
|
||||||
exports.fetchModelRecords = async function (ctx) {
|
exports.fetchModelRecords = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/all_${ctx.params.modelId}`, {
|
const response = await db.allDocs(
|
||||||
include_docs: true,
|
getRecordParams(ctx.params.modelId, null, {
|
||||||
})
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +212,23 @@ exports.destroy = async function (ctx) {
|
||||||
emitEvent(`record:delete`, ctx, record)
|
emitEvent(`record:delete`, ctx, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkDelete(ctx) {
|
||||||
|
const { records } = ctx.request.body
|
||||||
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
|
||||||
|
await db.bulkDocs(
|
||||||
|
records.map(record => ({ ...record, _deleted: true }), (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
ctx.status = 500
|
||||||
|
} else {
|
||||||
|
records.forEach(record => {
|
||||||
|
emitEvent(`record:delete`, ctx, record)
|
||||||
|
})
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
exports.validate = async function (ctx) {
|
exports.validate = async function (ctx) {
|
||||||
const errors = await validate({
|
const errors = await validate({
|
||||||
instanceId: ctx.user.instanceId,
|
instanceId: ctx.user.instanceId,
|
||||||
|
@ -157,89 +255,49 @@ async function validate({ instanceId, modelId, record, model }) {
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDelete(ctx) {
|
function coerceRecordValues(rec, model) {
|
||||||
const { records } = ctx.request.body
|
const record = cloneDeep(rec)
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
for (let [key, value] of Object.entries(record)) {
|
||||||
|
const field = model.schema[key]
|
||||||
|
if (!field) continue
|
||||||
|
|
||||||
await db.bulkDocs(
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
records.map(record => ({ ...record, _deleted: true }), (err, res) => {
|
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
|
||||||
if (err) {
|
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
|
||||||
ctx.status = 500
|
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
|
||||||
} else {
|
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
|
||||||
records.forEach(record => {
|
}
|
||||||
emitEvent(`record:delete`, ctx, record)
|
}
|
||||||
})
|
return record
|
||||||
ctx.status = 200
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRecords(ctx) {
|
const TYPE_TRANSFORM_MAP = {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
string: {
|
||||||
const record = ctx.request.body
|
"": "",
|
||||||
record.modelId = ctx.params.modelId
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
if (!record._rev && !record._id) {
|
},
|
||||||
record._id = newid()
|
number: {
|
||||||
}
|
"": null,
|
||||||
|
[null]: null,
|
||||||
const model = await db.get(record.modelId)
|
[undefined]: undefined,
|
||||||
|
parse: n => parseFloat(n),
|
||||||
const validateResult = await validate({
|
},
|
||||||
record,
|
datetime: {
|
||||||
model,
|
"": null,
|
||||||
})
|
[undefined]: undefined,
|
||||||
|
[null]: null,
|
||||||
if (!validateResult.valid) {
|
},
|
||||||
ctx.status = 400
|
attachment: {
|
||||||
ctx.body = {
|
"": [],
|
||||||
status: 400,
|
[null]: [],
|
||||||
errors: validateResult.errors,
|
[undefined]: undefined,
|
||||||
}
|
},
|
||||||
return
|
boolean: {
|
||||||
}
|
"": null,
|
||||||
|
[null]: null,
|
||||||
const existingRecord = record._rev && (await db.get(record._id))
|
[undefined]: undefined,
|
||||||
|
true: true,
|
||||||
if (existingRecord) {
|
false: false,
|
||||||
const response = await db.put(record)
|
},
|
||||||
record._rev = response.rev
|
}
|
||||||
record.type = "record"
|
|
||||||
ctx.body = record
|
|
||||||
ctx.status = 200
|
|
||||||
ctx.message = `${model.name} updated successfully.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
record.type = "record"
|
|
||||||
const response = await db.post(record)
|
|
||||||
record._rev = response.rev
|
|
||||||
|
|
||||||
// create links in other tables
|
|
||||||
for (let key in record) {
|
|
||||||
if (model.schema[key] && model.schema[key].type === "link") {
|
|
||||||
const linked = await db.allDocs({
|
|
||||||
include_docs: true,
|
|
||||||
keys: record[key],
|
|
||||||
})
|
|
||||||
|
|
||||||
// add this record to the linked records in attached models
|
|
||||||
const linkedDocs = linked.rows.map(row => {
|
|
||||||
const doc = row.doc
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
[model.name]: doc[model.name]
|
|
||||||
? [...doc[model.name], record._id]
|
|
||||||
: [record._id],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await db.bulkDocs(linkedDocs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitEvent(`record:save`, ctx, record)
|
|
||||||
ctx.body = record
|
|
||||||
ctx.status = 200
|
|
||||||
ctx.message = `${model.name} created successfully`
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ const send = require("koa-send")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("path")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const fs = require("fs")
|
const fs = require("fs-extra")
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
const { prepareUploadForS3 } = require("./deploy/aws")
|
const { prepareUploadForS3 } = require("./deploy/aws")
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const {
|
||||||
|
downloadTemplate,
|
||||||
|
exportTemplateFromApp,
|
||||||
|
} = require("../../utilities/templates")
|
||||||
|
|
||||||
|
const DEFAULT_TEMPLATES_BUCKET =
|
||||||
|
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
|
||||||
|
|
||||||
|
exports.fetch = async function(ctx) {
|
||||||
|
const { type = "app" } = ctx.query
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`
|
||||||
|
)
|
||||||
|
const json = await response.json()
|
||||||
|
ctx.body = Object.values(json.templates[type])
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.downloadTemplate = async function(ctx) {
|
||||||
|
const { type, name } = ctx.params
|
||||||
|
|
||||||
|
await downloadTemplate(type, name)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
message: `template ${type}:${name} downloaded successfully.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.exportTemplateFromApp = async function(ctx) {
|
||||||
|
const { appId, instanceId } = ctx.user
|
||||||
|
const { templateName } = ctx.request.body
|
||||||
|
|
||||||
|
await exportTemplateFromApp({
|
||||||
|
appId,
|
||||||
|
instanceId,
|
||||||
|
templateName,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.body = {
|
||||||
|
message: `Created template: ${templateName}`,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const clientDb = require("../../db/clientDb")
|
const clientDb = require("../../db/clientDb")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
const getUserId = userName => `user_${userName}`
|
const { generateUserID, getUserParams } = require("../../db/utils")
|
||||||
const {
|
const {
|
||||||
POWERUSER_LEVEL_ID,
|
POWERUSER_LEVEL_ID,
|
||||||
ADMIN_LEVEL_ID,
|
ADMIN_LEVEL_ID,
|
||||||
|
@ -9,11 +9,11 @@ const {
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.user.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
const data = await database.query("database/by_type", {
|
const data = await database.allDocs(
|
||||||
include_docs: true,
|
getUserParams(null, {
|
||||||
key: ["user"],
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
ctx.body = data.rows.map(row => row.doc)
|
ctx.body = data.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ exports.create = async function(ctx) {
|
||||||
if (!accessLevel) ctx.throw(400, "Invalid Access Level")
|
if (!accessLevel) ctx.throw(400, "Invalid Access Level")
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
_id: getUserId(username),
|
_id: generateUserID(username),
|
||||||
username,
|
username,
|
||||||
password: await bcrypt.hash(password),
|
password: await bcrypt.hash(password),
|
||||||
name: name || username,
|
name: name || username,
|
||||||
|
@ -80,14 +80,14 @@ exports.update = async function(ctx) {
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.user.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
await database.destroy(getUserId(ctx.params.username))
|
await database.destroy(generateUserID(ctx.params.username))
|
||||||
ctx.message = `User ${ctx.params.username} deleted.`
|
ctx.message = `User ${ctx.params.username} deleted.`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.user.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
const user = await database.get(getUserId(ctx.params.username))
|
const user = await database.get(generateUserID(ctx.params.username))
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
|
|
@ -11,18 +11,11 @@ const controller = {
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
const response = []
|
const response = []
|
||||||
|
|
||||||
for (let name in designDoc.views) {
|
for (let name of Object.keys(designDoc.views)) {
|
||||||
if (
|
response.push({
|
||||||
!name.startsWith("all") &&
|
name,
|
||||||
name !== "by_type" &&
|
...designDoc.views[name],
|
||||||
name !== "by_username" &&
|
})
|
||||||
name !== "by_automation_trigger"
|
|
||||||
) {
|
|
||||||
response.push({
|
|
||||||
name,
|
|
||||||
...designDoc.views[name],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
|
|
|
@ -61,8 +61,11 @@ function parseFilterExpression(filters) {
|
||||||
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
const value =
|
||||||
|
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
|
||||||
|
|
||||||
expression.push(
|
expression.push(
|
||||||
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"`
|
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const {
|
||||||
automationRoutes,
|
automationRoutes,
|
||||||
accesslevelRoutes,
|
accesslevelRoutes,
|
||||||
apiKeysRoutes,
|
apiKeysRoutes,
|
||||||
|
templatesRoutes,
|
||||||
analyticsRoutes,
|
analyticsRoutes,
|
||||||
} = require("./routes")
|
} = require("./routes")
|
||||||
|
|
||||||
|
@ -90,6 +91,9 @@ router.use(automationRoutes.allowedMethods())
|
||||||
|
|
||||||
router.use(deployRoutes.routes())
|
router.use(deployRoutes.routes())
|
||||||
router.use(deployRoutes.allowedMethods())
|
router.use(deployRoutes.allowedMethods())
|
||||||
|
|
||||||
|
router.use(templatesRoutes.routes())
|
||||||
|
router.use(templatesRoutes.allowedMethods())
|
||||||
// end auth routes
|
// end auth routes
|
||||||
|
|
||||||
router.use(pageRoutes.routes())
|
router.use(pageRoutes.routes())
|
||||||
|
|
|
@ -13,6 +13,7 @@ const automationRoutes = require("./automation")
|
||||||
const accesslevelRoutes = require("./accesslevel")
|
const accesslevelRoutes = require("./accesslevel")
|
||||||
const deployRoutes = require("./deploy")
|
const deployRoutes = require("./deploy")
|
||||||
const apiKeysRoutes = require("./apikeys")
|
const apiKeysRoutes = require("./apikeys")
|
||||||
|
const templatesRoutes = require("./templates")
|
||||||
const analyticsRoutes = require("./analytics")
|
const analyticsRoutes = require("./analytics")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -31,5 +32,6 @@ module.exports = {
|
||||||
automationRoutes,
|
automationRoutes,
|
||||||
accesslevelRoutes,
|
accesslevelRoutes,
|
||||||
apiKeysRoutes,
|
apiKeysRoutes,
|
||||||
|
templatesRoutes,
|
||||||
analyticsRoutes,
|
analyticsRoutes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ router
|
||||||
modelController.find
|
modelController.find
|
||||||
)
|
)
|
||||||
.post("/api/models", authorized(BUILDER), modelController.save)
|
.post("/api/models", authorized(BUILDER), modelController.save)
|
||||||
|
.post(
|
||||||
|
"/api/models/csv/validate",
|
||||||
|
authorized(BUILDER),
|
||||||
|
modelController.validateCSVSchema
|
||||||
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/models/:modelId/:revId",
|
"/api/models/:modelId/:revId",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const controller = require("../controllers/templates")
|
||||||
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { BUILDER } = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router
|
||||||
|
.get("/api/templates", authorized(BUILDER), controller.fetch)
|
||||||
|
.get(
|
||||||
|
"/api/templates/:type/:name",
|
||||||
|
authorized(BUILDER),
|
||||||
|
controller.downloadTemplate
|
||||||
|
)
|
||||||
|
.post("/api/templates", authorized(BUILDER), controller.exportTemplateFromApp)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -10,12 +10,14 @@ const {
|
||||||
destroyDocument,
|
destroyDocument,
|
||||||
builderEndpointShouldBlockNormalUsers
|
builderEndpointShouldBlockNormalUsers
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
|
let { generateAutomationID } = require("../../../db/utils")
|
||||||
|
|
||||||
const { delay } = require("./testUtils")
|
const { delay } = require("./testUtils")
|
||||||
|
|
||||||
const MAX_RETRIES = 4
|
const MAX_RETRIES = 4
|
||||||
|
const AUTOMATION_ID = generateAutomationID()
|
||||||
const TEST_AUTOMATION = {
|
const TEST_AUTOMATION = {
|
||||||
_id: "Test Automation",
|
_id: AUTOMATION_ID,
|
||||||
name: "My Automation",
|
name: "My Automation",
|
||||||
pageId: "123123123",
|
pageId: "123123123",
|
||||||
screenId: "kasdkfldsafkl",
|
screenId: "kasdkfldsafkl",
|
||||||
|
@ -126,14 +128,14 @@ describe("/automations", () => {
|
||||||
it("should setup the automation fully", () => {
|
it("should setup the automation fully", () => {
|
||||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||||
trigger.id = "wadiawdo34"
|
trigger.id = "wadiawdo34"
|
||||||
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
let createAction = ACTION_DEFINITIONS["CREATE_RECORD"]
|
||||||
saveAction.inputs.record = {
|
createAction.inputs.record = {
|
||||||
name: "{{trigger.name}}",
|
name: "{{trigger.name}}",
|
||||||
description: "{{trigger.description}}"
|
description: "{{trigger.description}}"
|
||||||
}
|
}
|
||||||
saveAction.id = "awde444wk"
|
createAction.id = "awde444wk"
|
||||||
|
|
||||||
TEST_AUTOMATION.definition.steps.push(saveAction)
|
TEST_AUTOMATION.definition.steps.push(createAction)
|
||||||
TEST_AUTOMATION.definition.trigger = trigger
|
TEST_AUTOMATION.definition.trigger = trigger
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -206,7 +208,7 @@ describe("/automations", () => {
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
|
expect(res.body.message).toEqual(`Automation ${AUTOMATION_ID} updated successfully.`)
|
||||||
expect(res.body.automation.name).toEqual("Updated Name")
|
expect(res.body.automation.name).toEqual("Updated Name")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,13 +46,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -180,7 +180,7 @@ describe("/models", () => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe("/records", () => {
|
||||||
|
|
||||||
const createRecord = async r =>
|
const createRecord = async r =>
|
||||||
await request
|
await request
|
||||||
.post(`/api/${model._id}/records`)
|
.post(`/api/${r ? r.modelId : record.modelId}/records`)
|
||||||
.send(r || record)
|
.send(r || record)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
|
@ -152,6 +152,95 @@ describe("/records", () => {
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(404)
|
.expect(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("record values are coerced", async () => {
|
||||||
|
const str = {type:"string", constraints: { type: "string", presence: false }}
|
||||||
|
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
|
||||||
|
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
|
||||||
|
const number = {type:"number", constraints: { type: "number", presence: false }}
|
||||||
|
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
|
||||||
|
|
||||||
|
model = await createModel(request, app._id, instance._id, {
|
||||||
|
name: "TestModel2",
|
||||||
|
type: "model",
|
||||||
|
key: "name",
|
||||||
|
schema: {
|
||||||
|
name: str,
|
||||||
|
stringUndefined: str,
|
||||||
|
stringNull: str,
|
||||||
|
stringString: str,
|
||||||
|
numberEmptyString: number,
|
||||||
|
numberNull: number,
|
||||||
|
numberUndefined: number,
|
||||||
|
numberString: number,
|
||||||
|
datetimeEmptyString: datetime,
|
||||||
|
datetimeNull: datetime,
|
||||||
|
datetimeUndefined: datetime,
|
||||||
|
datetimeString: datetime,
|
||||||
|
datetimeDate: datetime,
|
||||||
|
boolNull: bool,
|
||||||
|
boolEmpty: bool,
|
||||||
|
boolUndefined: bool,
|
||||||
|
boolString: bool,
|
||||||
|
boolBool: bool,
|
||||||
|
attachmentNull : attachment,
|
||||||
|
attachmentUndefined : attachment,
|
||||||
|
attachmentEmpty : attachment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
record = {
|
||||||
|
name: "Test Record",
|
||||||
|
stringUndefined: undefined,
|
||||||
|
stringNull: null,
|
||||||
|
stringString: "i am a string",
|
||||||
|
numberEmptyString: "",
|
||||||
|
numberNull: null,
|
||||||
|
numberUndefined: undefined,
|
||||||
|
numberString: "123",
|
||||||
|
numberNumber: 123,
|
||||||
|
datetimeEmptyString: "",
|
||||||
|
datetimeNull: null,
|
||||||
|
datetimeUndefined: undefined,
|
||||||
|
datetimeString: "1984-04-20T00:00:00.000Z",
|
||||||
|
datetimeDate: new Date("1984-04-20"),
|
||||||
|
boolNull: null,
|
||||||
|
boolEmpty: "",
|
||||||
|
boolUndefined: undefined,
|
||||||
|
boolString: "true",
|
||||||
|
boolBool: true,
|
||||||
|
modelId: model._id,
|
||||||
|
attachmentNull : null,
|
||||||
|
attachmentUndefined : undefined,
|
||||||
|
attachmentEmpty : "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = (await createRecord(record)).body._id
|
||||||
|
|
||||||
|
const saved = (await loadRecord(id)).body
|
||||||
|
|
||||||
|
expect(saved.stringUndefined).toBe(undefined)
|
||||||
|
expect(saved.stringNull).toBe("")
|
||||||
|
expect(saved.stringString).toBe("i am a string")
|
||||||
|
expect(saved.numberEmptyString).toBe(null)
|
||||||
|
expect(saved.numberNull).toBe(null)
|
||||||
|
expect(saved.numberUndefined).toBe(undefined)
|
||||||
|
expect(saved.numberString).toBe(123)
|
||||||
|
expect(saved.numberNumber).toBe(123)
|
||||||
|
expect(saved.datetimeEmptyString).toBe(null)
|
||||||
|
expect(saved.datetimeNull).toBe(null)
|
||||||
|
expect(saved.datetimeUndefined).toBe(undefined)
|
||||||
|
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
|
||||||
|
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
|
||||||
|
expect(saved.boolNull).toBe(null)
|
||||||
|
expect(saved.boolEmpty).toBe(null)
|
||||||
|
expect(saved.boolUndefined).toBe(undefined)
|
||||||
|
expect(saved.boolString).toBe(true)
|
||||||
|
expect(saved.boolBool).toBe(true)
|
||||||
|
expect(saved.attachmentNull).toEqual([])
|
||||||
|
expect(saved.attachmentUndefined).toBe(undefined)
|
||||||
|
expect(saved.attachmentEmpty).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
|
|
|
@ -69,13 +69,13 @@ describe("/views", () => {
|
||||||
filters: [],
|
filters: [],
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const sendEmail = require("./steps/sendEmail")
|
const sendEmail = require("./steps/sendEmail")
|
||||||
const saveRecord = require("./steps/saveRecord")
|
const createRecord = require("./steps/createRecord")
|
||||||
const updateRecord = require("./steps/updateRecord")
|
const updateRecord = require("./steps/updateRecord")
|
||||||
const deleteRecord = require("./steps/deleteRecord")
|
const deleteRecord = require("./steps/deleteRecord")
|
||||||
const createUser = require("./steps/createUser")
|
const createUser = require("./steps/createUser")
|
||||||
|
@ -17,14 +17,14 @@ const DEFAULT_DIRECTORY = ".budibase-automations"
|
||||||
const AUTOMATION_MANIFEST = "manifest.json"
|
const AUTOMATION_MANIFEST = "manifest.json"
|
||||||
const BUILTIN_ACTIONS = {
|
const BUILTIN_ACTIONS = {
|
||||||
SEND_EMAIL: sendEmail.run,
|
SEND_EMAIL: sendEmail.run,
|
||||||
SAVE_RECORD: saveRecord.run,
|
CREATE_RECORD: createRecord.run,
|
||||||
UPDATE_RECORD: updateRecord.run,
|
UPDATE_RECORD: updateRecord.run,
|
||||||
DELETE_RECORD: deleteRecord.run,
|
DELETE_RECORD: deleteRecord.run,
|
||||||
CREATE_USER: createUser.run,
|
CREATE_USER: createUser.run,
|
||||||
}
|
}
|
||||||
const BUILTIN_DEFINITIONS = {
|
const BUILTIN_DEFINITIONS = {
|
||||||
SEND_EMAIL: sendEmail.definition,
|
SEND_EMAIL: sendEmail.definition,
|
||||||
SAVE_RECORD: saveRecord.definition,
|
CREATE_RECORD: createRecord.definition,
|
||||||
UPDATE_RECORD: updateRecord.definition,
|
UPDATE_RECORD: updateRecord.definition,
|
||||||
DELETE_RECORD: deleteRecord.definition,
|
DELETE_RECORD: deleteRecord.definition,
|
||||||
CREATE_USER: createUser.definition,
|
CREATE_USER: createUser.definition,
|
||||||
|
|
|
@ -2,12 +2,12 @@ const recordController = require("../../api/controllers/record")
|
||||||
const automationUtils = require("../automationUtils")
|
const automationUtils = require("../automationUtils")
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
name: "Save Record",
|
name: "Create Record",
|
||||||
tagline: "Save a {{inputs.enriched.model.name}} record",
|
tagline: "Create a {{inputs.enriched.model.name}} record",
|
||||||
icon: "ri-save-3-fill",
|
icon: "ri-save-3-fill",
|
||||||
description: "Save a record to your database",
|
description: "Create a record to your database",
|
||||||
type: "ACTION",
|
type: "ACTION",
|
||||||
stepId: "SAVE_RECORD",
|
stepId: "CREATE_RECORD",
|
||||||
inputs: {},
|
inputs: {},
|
||||||
schema: {
|
schema: {
|
||||||
inputs: {
|
inputs: {
|
|
@ -55,7 +55,15 @@ module.exports.definition = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function filter({ inputs }) {
|
module.exports.run = async function filter({ inputs }) {
|
||||||
const { field, condition, value } = inputs
|
let { field, condition, value } = inputs
|
||||||
|
// coerce types so that we can use them
|
||||||
|
if (!isNaN(value) && !isNaN(field)) {
|
||||||
|
value = parseFloat(value)
|
||||||
|
field = parseFloat(field)
|
||||||
|
} else if (!isNaN(Date.parse(value)) && !isNaN(Date.parse(field))) {
|
||||||
|
value = Date.parse(value)
|
||||||
|
field = Date.parse(field)
|
||||||
|
}
|
||||||
let success
|
let success
|
||||||
if (typeof field !== "object" && typeof value !== "object") {
|
if (typeof field !== "object" && typeof value !== "object") {
|
||||||
switch (condition) {
|
switch (condition) {
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
const environment = require("../../environment")
|
|
||||||
const sgMail = require("@sendgrid/mail")
|
|
||||||
sgMail.setApiKey(environment.SENDGRID_API_KEY)
|
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Send an email",
|
description: "Send an email",
|
||||||
tagline: "Send email to {{inputs.to}}",
|
tagline: "Send email to {{inputs.to}}",
|
||||||
|
@ -13,6 +9,10 @@ module.exports.definition = {
|
||||||
schema: {
|
schema: {
|
||||||
inputs: {
|
inputs: {
|
||||||
properties: {
|
properties: {
|
||||||
|
apiKey: {
|
||||||
|
type: "string",
|
||||||
|
title: "SendGrid API key",
|
||||||
|
},
|
||||||
to: {
|
to: {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "Send To",
|
title: "Send To",
|
||||||
|
@ -49,6 +49,8 @@ module.exports.definition = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function({ inputs }) {
|
module.exports.run = async function({ inputs }) {
|
||||||
|
const sgMail = require("@sendgrid/mail")
|
||||||
|
sgMail.setApiKey(inputs.apiKey)
|
||||||
const msg = {
|
const msg = {
|
||||||
to: inputs.to,
|
to: inputs.to,
|
||||||
from: inputs.from,
|
from: inputs.from,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const emitter = require("../events/index")
|
const emitter = require("../events/index")
|
||||||
const InMemoryQueue = require("./queue/inMemoryQueue")
|
const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||||
|
const { getAutomationParams } = require("../db/utils")
|
||||||
|
|
||||||
let automationQueue = new InMemoryQueue()
|
let automationQueue = new InMemoryQueue()
|
||||||
|
|
||||||
|
@ -89,15 +90,18 @@ async function queueRelevantRecordAutomations(event, eventType) {
|
||||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||||
}
|
}
|
||||||
const db = new CouchDB(event.instanceId)
|
const db = new CouchDB(event.instanceId)
|
||||||
const automationsToTrigger = await db.query(
|
let automations = await db.allDocs(
|
||||||
"database/by_automation_trigger",
|
getAutomationParams(null, { include_docs: true })
|
||||||
{
|
|
||||||
key: [eventType],
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const automations = automationsToTrigger.rows.map(wf => wf.doc)
|
// filter down to the correct event type
|
||||||
|
automations = automations.rows
|
||||||
|
.map(automation => automation.doc)
|
||||||
|
.filter(automation => {
|
||||||
|
const trigger = automation.definition.trigger
|
||||||
|
return trigger && trigger.event === eventType
|
||||||
|
})
|
||||||
|
|
||||||
for (let automation of automations) {
|
for (let automation of automations) {
|
||||||
let automationDef = automation.definition
|
let automationDef = automation.definition
|
||||||
let automationTrigger = automationDef ? automationDef.trigger : {}
|
let automationTrigger = automationDef ? automationDef.trigger : {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const PouchDB = require("pouchdb")
|
const PouchDB = require("pouchdb")
|
||||||
|
const replicationStream = require("pouchdb-replication-stream")
|
||||||
const allDbs = require("pouchdb-all-dbs")
|
const allDbs = require("pouchdb-all-dbs")
|
||||||
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
@ -6,6 +7,9 @@ const env = require("../environment")
|
||||||
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
||||||
const isInMemory = env.NODE_ENV === "jest"
|
const isInMemory = env.NODE_ENV === "jest"
|
||||||
|
|
||||||
|
PouchDB.plugin(replicationStream.plugin)
|
||||||
|
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
||||||
|
|
||||||
let POUCH_DB_DEFAULTS = {
|
let POUCH_DB_DEFAULTS = {
|
||||||
prefix: COUCH_DB_URL,
|
prefix: COUCH_DB_URL,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
const newid = require("./newid")
|
||||||
|
|
||||||
|
const DocumentTypes = {
|
||||||
|
MODEL: "model",
|
||||||
|
RECORD: "record",
|
||||||
|
USER: "user",
|
||||||
|
AUTOMATION: "automation",
|
||||||
|
LINK: "link",
|
||||||
|
APP: "app",
|
||||||
|
ACCESS_LEVEL: "accesslevel",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.DocumentTypes = DocumentTypes
|
||||||
|
|
||||||
|
const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
|
* is usually the case as most of our docs are top level e.g. models, automations, users and so on.
|
||||||
|
* More complex cases such as link docs and records which have multiple levels of IDs that their
|
||||||
|
* ID consists of need their own functions to build the allDocs parameters.
|
||||||
|
* @param {string} docType The type of document which input params are being built for, e.g. user,
|
||||||
|
* link, app, model and so on.
|
||||||
|
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
|
||||||
|
* for a singular document.
|
||||||
|
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
|
||||||
|
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||||
|
*/
|
||||||
|
function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
|
if (docId == null) {
|
||||||
|
docId = ""
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...otherProps,
|
||||||
|
startkey: `${docType}:${docId}`,
|
||||||
|
endkey: `${docType}:${docId}${UNICODE_MAX}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving models, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
exports.getModelParams = (modelId = null, otherProps = {}) => {
|
||||||
|
return getDocParams(DocumentTypes.MODEL, modelId, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new model ID.
|
||||||
|
* @returns {string} The new model ID which the model doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateModelID = () => {
|
||||||
|
return `${DocumentTypes.MODEL}:${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the DB allDocs/query params for retrieving a record.
|
||||||
|
* @param {string} modelId The model in which the records have been stored.
|
||||||
|
* @param {string|null} recordId The ID of the record which is being specifically queried for. This can be
|
||||||
|
* left null to get all the records in the model.
|
||||||
|
* @param {object} otherProps Any other properties to add to the request.
|
||||||
|
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||||
|
*/
|
||||||
|
exports.getRecordParams = (modelId, recordId = null, otherProps = {}) => {
|
||||||
|
if (modelId == null) {
|
||||||
|
throw "Cannot build params for records without a model ID"
|
||||||
|
}
|
||||||
|
const endOfKey = recordId == null ? `${modelId}:` : `${modelId}:${recordId}`
|
||||||
|
return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new record ID for the specified model.
|
||||||
|
* @param {string} modelId The model which the record is being created for.
|
||||||
|
* @returns {string} The new ID which a record doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateRecordID = modelId => {
|
||||||
|
return `${DocumentTypes.RECORD}:${modelId}:${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
exports.getUserParams = (username = null, otherProps = {}) => {
|
||||||
|
return getDocParams(DocumentTypes.USER, username, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new user ID based on the passed in username.
|
||||||
|
* @param {string} username The username which the ID is going to be built up of.
|
||||||
|
* @returns {string} The new user ID which the user doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateUserID = username => {
|
||||||
|
return `${DocumentTypes.USER}:${username}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
exports.getAutomationParams = (automationId = null, otherProps = {}) => {
|
||||||
|
return getDocParams(DocumentTypes.AUTOMATION, automationId, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new automation ID.
|
||||||
|
* @returns {string} The new automation ID which the automation doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateAutomationID = () => {
|
||||||
|
return `${DocumentTypes.AUTOMATION}:${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new link doc ID. This is currently not usable with the alldocs call,
|
||||||
|
* instead a view is built to make walking to tree easier.
|
||||||
|
* @param {string} modelId1 The ID of the linker model.
|
||||||
|
* @param {string} modelId2 The ID of the linked model.
|
||||||
|
* @param {string} recordId1 The ID of the linker record.
|
||||||
|
* @param {string} recordId2 The ID of the linked record.
|
||||||
|
* @returns {string} The new link doc ID which the automation doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateLinkID = (modelId1, modelId2, recordId1, recordId2) => {
|
||||||
|
return `${DocumentTypes.AUTOMATION}:${modelId1}:${modelId2}:${recordId1}:${recordId2}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new app ID.
|
||||||
|
* @returns {string} The new app ID which the app doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateAppID = () => {
|
||||||
|
return `${DocumentTypes.APP}:${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving apps, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
exports.getAppParams = (appId = null, otherProps = {}) => {
|
||||||
|
return getDocParams(DocumentTypes.APP, appId, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new access level ID.
|
||||||
|
* @returns {string} The new access level ID which the access level doc can be stored under.
|
||||||
|
*/
|
||||||
|
exports.generateAccessLevelID = () => {
|
||||||
|
return `${DocumentTypes.ACCESS_LEVEL}:${newid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameters for retrieving an access level, this is a utility function for the getDocParams function.
|
||||||
|
*/
|
||||||
|
exports.getAccessLevelParams = (accessLevelId = null, otherProps = {}) => {
|
||||||
|
return getDocParams(DocumentTypes.ACCESS_LEVEL, accessLevelId, otherProps)
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
const csv = require("csvtojson")
|
||||||
|
|
||||||
|
const VALIDATORS = {
|
||||||
|
string: () => true,
|
||||||
|
number: attribute => !isNaN(Number(attribute)),
|
||||||
|
datetime: attribute => !isNaN(new Date(attribute).getTime()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARSERS = {
|
||||||
|
datetime: attribute => new Date(attribute).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(path, parsers) {
|
||||||
|
const result = csv().fromFile(path)
|
||||||
|
|
||||||
|
const schema = {}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
result.on("header", headers => {
|
||||||
|
for (let header of headers) {
|
||||||
|
schema[header] = {
|
||||||
|
type: parsers[header] ? parsers[header].type : "string",
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.fromFile(path).subscribe(row => {
|
||||||
|
// For each CSV row parse all the columns that need parsed
|
||||||
|
for (let key in parsers) {
|
||||||
|
if (!schema[key] || schema[key].success) {
|
||||||
|
// get the validator for the column type
|
||||||
|
const validator = VALIDATORS[parsers[key].type]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// allow null/undefined values
|
||||||
|
schema[key].success = !row[key] || validator(row[key])
|
||||||
|
} catch (err) {
|
||||||
|
schema[key].success = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.on("done", error => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(schema)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transform({ schema, path }) {
|
||||||
|
const colParser = {}
|
||||||
|
|
||||||
|
for (let key in schema) {
|
||||||
|
colParser[key] = PARSERS[schema[key].type] || schema[key].type
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await csv({ colParser }).fromFile(path)
|
||||||
|
return json
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error transforming CSV to JSON for data import`, err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parse,
|
||||||
|
transform,
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
const path = require("path")
|
||||||
|
const fs = require("fs-extra")
|
||||||
|
const os = require("os")
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const stream = require("stream")
|
||||||
|
const tar = require("tar-fs")
|
||||||
|
const zlib = require("zlib")
|
||||||
|
const { promisify } = require("util")
|
||||||
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
|
const { budibaseAppsDir } = require("./budibaseDir")
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
|
||||||
|
const DEFAULT_TEMPLATES_BUCKET =
|
||||||
|
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
|
||||||
|
|
||||||
|
exports.downloadTemplate = async function(type, name) {
|
||||||
|
const templateUrl = `https://${DEFAULT_TEMPLATES_BUCKET}/templates/${type}/${name}.tar.gz`
|
||||||
|
const response = await fetch(templateUrl)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Error downloading template ${type}:${name}: ${response.statusText}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream the response, unzip and extract
|
||||||
|
await streamPipeline(
|
||||||
|
response.body,
|
||||||
|
zlib.Unzip(),
|
||||||
|
tar.extract(path.join(budibaseAppsDir(), "templates", type))
|
||||||
|
)
|
||||||
|
|
||||||
|
return path.join(budibaseAppsDir(), "templates", type, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.exportTemplateFromApp = async function({
|
||||||
|
appId,
|
||||||
|
templateName,
|
||||||
|
instanceId,
|
||||||
|
}) {
|
||||||
|
// Copy frontend files
|
||||||
|
const appToExport = path.join(os.homedir(), ".budibase", appId, "pages")
|
||||||
|
const templatesDir = path.join(os.homedir(), ".budibase", "templates")
|
||||||
|
fs.ensureDirSync(templatesDir)
|
||||||
|
|
||||||
|
const templateOutputPath = path.join(templatesDir, templateName)
|
||||||
|
fs.copySync(appToExport, `${templateOutputPath}/pages`)
|
||||||
|
|
||||||
|
fs.ensureDirSync(path.join(templateOutputPath, "db"))
|
||||||
|
const writeStream = fs.createWriteStream(`${templateOutputPath}/db/dump.txt`)
|
||||||
|
|
||||||
|
// perform couch dump
|
||||||
|
const instanceDb = new CouchDB(instanceId)
|
||||||
|
|
||||||
|
await instanceDb.dump(writeStream)
|
||||||
|
return templateOutputPath
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CSV Parser transformation transforms a CSV file into JSON 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"Address": "5 Sesame Street",
|
||||||
|
"Age": 4324,
|
||||||
|
"Name": "Bert",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"Address": "1 World Trade Center",
|
||||||
|
"Age": 34,
|
||||||
|
"Name": "Ernie",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"Address": "44 Second Avenue",
|
||||||
|
"Age": 23423,
|
||||||
|
"Name": "Big Bird",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
|
@ -0,0 +1,108 @@
|
||||||
|
const csvParser = require("../csvParser");
|
||||||
|
|
||||||
|
const CSV_PATH = __dirname + "/test.csv";
|
||||||
|
|
||||||
|
const SCHEMAS = {
|
||||||
|
VALID: {
|
||||||
|
Age: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
INVALID: {
|
||||||
|
Address: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IGNORE: {
|
||||||
|
Address: {
|
||||||
|
type: "omit",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
type: "omit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BROKEN: {
|
||||||
|
Address: {
|
||||||
|
type: "datetime",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("CSV Parser", () => {
|
||||||
|
describe("parsing", () => {
|
||||||
|
it("returns status and types for a valid CSV transformation", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.parse(CSV_PATH, SCHEMAS.VALID)
|
||||||
|
).toEqual({
|
||||||
|
Address: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
success: true,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns status and types for an invalid CSV transformation", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.parse(CSV_PATH, SCHEMAS.INVALID)
|
||||||
|
).toEqual({
|
||||||
|
Address: {
|
||||||
|
success: false,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
success: true,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transformation", () => {
|
||||||
|
it("transforms a CSV file into JSON", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.transform({
|
||||||
|
schema: SCHEMAS.VALID,
|
||||||
|
path: CSV_PATH,
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transforms a CSV file into JSON ignoring certain fields", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.transform({
|
||||||
|
schema: SCHEMAS.IGNORE,
|
||||||
|
path: CSV_PATH,
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
Name: "Bert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ernie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Big Bird"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error on invalid schema", async () => {
|
||||||
|
await expect(csvParser.transform({ schema: SCHEMAS.BROKEN, path: CSV_PATH })).rejects.toThrow()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
"Name","Age","Address"
|
||||||
|
"Bert","4324","5 Sesame Street"
|
||||||
|
"Ernie","34","1 World Trade Center"
|
||||||
|
"Big Bird","23423","44 Second Avenue"
|
|
|
@ -1057,7 +1057,7 @@ bluebird-lst@^1.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
bluebird "^3.5.5"
|
bluebird "^3.5.5"
|
||||||
|
|
||||||
bluebird@^3.5.5:
|
bluebird@^3.5.1, bluebird@^3.5.5:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
|
||||||
|
@ -1596,6 +1596,15 @@ cssstyle@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssom "0.3.x"
|
cssom "0.3.x"
|
||||||
|
|
||||||
|
csvtojson@^2.0.10:
|
||||||
|
version "2.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574"
|
||||||
|
integrity sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==
|
||||||
|
dependencies:
|
||||||
|
bluebird "^3.5.1"
|
||||||
|
lodash "^4.17.3"
|
||||||
|
strip-bom "^2.0.0"
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -3303,6 +3312,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
|
|
||||||
|
is-utf8@^0.2.0:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
|
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
||||||
|
|
||||||
is-windows@^1.0.2:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
|
@ -4180,6 +4194,13 @@ lie@3.0.4:
|
||||||
inline-process-browser "^1.0.0"
|
inline-process-browser "^1.0.0"
|
||||||
unreachable-branch-transform "^0.3.0"
|
unreachable-branch-transform "^0.3.0"
|
||||||
|
|
||||||
|
lie@3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||||
|
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
|
||||||
|
dependencies:
|
||||||
|
immediate "~3.0.5"
|
||||||
|
|
||||||
lines-and-columns@^1.1.6:
|
lines-and-columns@^1.1.6:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||||
|
@ -4242,6 +4263,11 @@ lodash.once@^4.0.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
|
|
||||||
|
lodash.pick@^4.0.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
||||||
|
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
|
||||||
|
|
||||||
lodash.sortby@^4.7.0:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
|
@ -4250,6 +4276,11 @@ lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
|
||||||
version "4.17.19"
|
version "4.17.19"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||||
|
|
||||||
|
lodash@^4.17.3:
|
||||||
|
version "4.17.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|
||||||
loose-envify@^1.0.0:
|
loose-envify@^1.0.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
@ -4504,6 +4535,16 @@ natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|
||||||
|
ndjson@^1.4.3:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-1.5.0.tgz#ae603b36b134bcec347b452422b0bf98d5832ec8"
|
||||||
|
integrity sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg=
|
||||||
|
dependencies:
|
||||||
|
json-stringify-safe "^5.0.1"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
split2 "^2.1.0"
|
||||||
|
through2 "^2.0.3"
|
||||||
|
|
||||||
negotiator@0.6.2:
|
negotiator@0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||||
|
@ -4974,6 +5015,14 @@ posix-character-classes@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||||
|
|
||||||
|
pouch-stream@^0.4.0:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouch-stream/-/pouch-stream-0.4.1.tgz#0c6d8475c9307677627991a2f079b301c3b89bdd"
|
||||||
|
integrity sha1-DG2EdckwdndieZGi8HmzAcO4m90=
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.1"
|
||||||
|
readable-stream "^1.0.27-1"
|
||||||
|
|
||||||
pouchdb-adapter-leveldb-core@7.2.1:
|
pouchdb-adapter-leveldb-core@7.2.1:
|
||||||
version "7.2.1"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.1.tgz#71bf2a05755689e2b05e78e796003a18ebf65a69"
|
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.1.tgz#71bf2a05755689e2b05e78e796003a18ebf65a69"
|
||||||
|
@ -5061,6 +5110,26 @@ pouchdb-promise@5.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
lie "3.0.4"
|
lie "3.0.4"
|
||||||
|
|
||||||
|
pouchdb-promise@^6.0.4:
|
||||||
|
version "6.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3"
|
||||||
|
integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw==
|
||||||
|
dependencies:
|
||||||
|
lie "3.1.1"
|
||||||
|
|
||||||
|
pouchdb-replication-stream@^1.2.9:
|
||||||
|
version "1.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.9.tgz#aa4fa5d8f52df4825392f18e07c7e11acffc650a"
|
||||||
|
integrity sha1-qk+l2PUt9IJTkvGOB8fhGs/8ZQo=
|
||||||
|
dependencies:
|
||||||
|
argsarray "0.0.1"
|
||||||
|
inherits "^2.0.3"
|
||||||
|
lodash.pick "^4.0.0"
|
||||||
|
ndjson "^1.4.3"
|
||||||
|
pouch-stream "^0.4.0"
|
||||||
|
pouchdb-promise "^6.0.4"
|
||||||
|
through2 "^2.0.0"
|
||||||
|
|
||||||
pouchdb-utils@7.2.1:
|
pouchdb-utils@7.2.1:
|
||||||
version "7.2.1"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.1.tgz#5dec1c53c8ecba717e5762311e9a1def2d4ebf9c"
|
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.1.tgz#5dec1c53c8ecba717e5762311e9a1def2d4ebf9c"
|
||||||
|
@ -5321,7 +5390,17 @@ readable-stream@1.0.33:
|
||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5:
|
readable-stream@^1.0.27-1:
|
||||||
|
version "1.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||||
|
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
||||||
|
dependencies:
|
||||||
|
core-util-is "~1.0.0"
|
||||||
|
inherits "~2.0.1"
|
||||||
|
isarray "0.0.1"
|
||||||
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
|
readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5877,6 +5956,13 @@ split-string@^3.0.1, split-string@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
extend-shallow "^3.0.0"
|
extend-shallow "^3.0.0"
|
||||||
|
|
||||||
|
split2@^2.1.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
||||||
|
integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==
|
||||||
|
dependencies:
|
||||||
|
through2 "^2.0.2"
|
||||||
|
|
||||||
split2@^3.1.1:
|
split2@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
|
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
|
||||||
|
@ -6051,6 +6137,13 @@ strip-ansi@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^5.0.0"
|
ansi-regex "^5.0.0"
|
||||||
|
|
||||||
|
strip-bom@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||||
|
integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
|
||||||
|
dependencies:
|
||||||
|
is-utf8 "^0.2.0"
|
||||||
|
|
||||||
strip-bom@^3.0.0:
|
strip-bom@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||||
|
@ -6234,6 +6327,14 @@ through2@^0.6.2, through2@^0.6.5:
|
||||||
readable-stream ">=1.0.33-1 <1.1.0-0"
|
readable-stream ">=1.0.33-1 <1.1.0-0"
|
||||||
xtend ">=4.0.0 <4.1.0-0"
|
xtend ">=4.0.0 <4.1.0-0"
|
||||||
|
|
||||||
|
through2@^2.0.0, through2@^2.0.2, through2@^2.0.3:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||||
|
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||||
|
dependencies:
|
||||||
|
readable-stream "~2.3.6"
|
||||||
|
xtend "~4.0.1"
|
||||||
|
|
||||||
through@^2.3.6, through@^2.3.8, through@~2.3.4:
|
through@^2.3.6, through@^2.3.8, through@~2.3.4:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
|
@ -6724,7 +6825,7 @@ xmlbuilder@~9.0.1:
|
||||||
version "9.0.7"
|
version "9.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||||
|
|
||||||
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0:
|
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,7 +13,7 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@budibase/client": "^0.1.22",
|
"@budibase/client": "^0.1.25",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"rollup": "^1.11.0",
|
"rollup": "^1.11.0",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"version": "0.1.22",
|
"version": "0.1.25",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
# Budibase is in Beta
|
||||||
|
|
||||||
|
Budibase is currently beta software. Until our official launch, we cannot ensure backwards compatibility for your budibase applications between versions. Issues may arise when trying to edit apps created with old versions of the budibase builder.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
# What is Budibase?
|
# What is Budibase?
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue