Browse Source

Merge branch 'linked-records' of github.com:Budibase/budibase into api-usage-tracking

master
mike12345567 6 years ago
parent
commit
106badc9c6
  1. 2
      lerna.json
  2. 5
      package.json
  3. 1
      packages/builder/cypress/integration/createAutomation.spec.js
  4. 2
      packages/builder/cypress/integration/createTable.spec.js
  5. 8
      packages/builder/cypress/integration/createView.spec.js
  6. 1
      packages/builder/cypress/support/commands.js
  7. 6
      packages/builder/package.json
  8. 17
      packages/builder/src/builderStore/fetchBindableProperties.js
  9. 5
      packages/builder/src/components/backend/DataTable/ModelDataTable.svelte
  10. 24
      packages/builder/src/components/backend/DataTable/RecordFieldControl.svelte
  11. 19
      packages/builder/src/components/backend/DataTable/Table.svelte
  12. 7
      packages/builder/src/components/backend/DataTable/modals/CreateEditRecordModal.svelte
  13. 8
      packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte
  14. 11
      packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte
  15. 60
      packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte
  16. 191
      packages/builder/src/components/backend/ModelNavigator/TableDataImport.svelte
  17. 27
      packages/builder/src/components/backend/ModelNavigator/popovers/CreateTablePopover.svelte
  18. 13
      packages/builder/src/components/common/LinkedRecordSelector.svelte
  19. 278
      packages/builder/src/components/common/MultiSelect.svelte
  20. 1
      packages/builder/src/components/common/Notification/NotificationDisplay.svelte
  21. 12
      packages/builder/src/components/settings/tabs/APIKeys.svelte
  22. 2
      packages/builder/src/components/start/CreateAppModal.svelte
  23. 2
      packages/builder/src/components/start/Steps/API.svelte
  24. 2
      packages/builder/src/components/start/Steps/User.svelte
  25. 2
      packages/builder/src/components/userInterface/ModelViewSelect.svelte
  26. 10
      packages/builder/src/constants/backend/index.js
  27. 12
      packages/builder/yarn.lock
  28. 4
      packages/cli/package.json
  29. 2
      packages/client/package.json
  30. BIN
      packages/server/build/Hosting Portal-2.png
  31. 6
      packages/server/package.json
  32. 7
      packages/server/src/api/controllers/apikeys.js
  33. 4
      packages/server/src/api/controllers/application.js
  34. 2
      packages/server/src/api/controllers/component.js
  35. 5
      packages/server/src/api/controllers/deploy/aws.js
  36. 5
      packages/server/src/api/controllers/instance.js
  37. 27
      packages/server/src/api/controllers/model.js
  38. 51
      packages/server/src/api/controllers/record.js
  39. 2
      packages/server/src/api/controllers/static.js
  40. 7
      packages/server/src/api/controllers/view/index.js
  41. 5
      packages/server/src/api/controllers/view/viewBuilder.js
  42. 5
      packages/server/src/api/routes/model.js
  43. 4
      packages/server/src/api/routes/tests/couchTestUtils.js
  44. 2
      packages/server/src/api/routes/tests/model.spec.js
  45. 91
      packages/server/src/api/routes/tests/record.spec.js
  46. 4
      packages/server/src/api/routes/tests/view.spec.js
  47. 8
      packages/server/src/automations/actions.js
  48. 10
      packages/server/src/automations/steps/sendEmail.js
  49. 9
      packages/server/src/db/client.js
  50. 2
      packages/server/src/electron.js
  51. 2
      packages/server/src/index.js
  52. 2
      packages/server/src/utilities/budibaseDir.js
  53. 2
      packages/server/src/utilities/builder/buildPage.js
  54. 2
      packages/server/src/utilities/builder/convertCssToFiles.js
  55. 2
      packages/server/src/utilities/builder/getPages.js
  56. 3
      packages/server/src/utilities/builder/index.js
  57. 2
      packages/server/src/utilities/builder/listScreens.js
  58. 2
      packages/server/src/utilities/builder/publicPath.js
  59. 2
      packages/server/src/utilities/createAppPackage.js
  60. 73
      packages/server/src/utilities/csvParser.js
  61. 2
      packages/server/src/utilities/initialiseBudibase.js
  62. 38
      packages/server/src/utilities/sanitisedPath.js
  63. 20
      packages/server/src/utilities/templates.js
  64. 21
      packages/server/src/utilities/tests/__snapshots__/csvParser.spec.js.snap
  65. 108
      packages/server/src/utilities/tests/csvParser.spec.js
  66. 4
      packages/server/src/utilities/tests/test.csv
  67. 116
      packages/server/yarn.lock
  68. 6
      packages/standard-components/package.json
  69. 202
      packages/standard-components/src/DataForm.svelte
  70. 29
      packages/standard-components/src/DataTable.svelte
  71. 31
      packages/standard-components/src/ErrorsBox.svelte
  72. 65
      packages/standard-components/src/LinkedRecordSelector.svelte
  73. 14
      packages/standard-components/src/RecordDetail.svelte
  74. 2
      packages/standard-components/src/attachments/AttachmentList.svelte
  75. 23
      packages/standard-components/src/fetchData.js
  76. 1
      packages/standard-components/src/helpers.js
  77. 6
      readme.md
  78. 86
      yarn.lock

2
lerna.json

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

5
package.json

@ -30,7 +30,6 @@
"test:e2e:ci": "lerna run cy:ci"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.8",
"pouchdb-replication-stream": "^1.2.9"
"@fortawesome/fontawesome": "^1.1.8"
}
}
}

1
packages/builder/cypress/integration/createAutomation.spec.js

@ -50,7 +50,6 @@ context("Create a automation", () => {
// Activate Automation
cy.get("[data-cy=activate-automation]").click()
cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible")
})

2
packages/builder/cypress/integration/createTable.spec.js

@ -8,7 +8,7 @@ context("Create a Table", () => {
cy.createTable("dog")
// Check if Table exists
cy.get(".title").should("have.text", "dog")
cy.get(".title span").should("have.text", "dog")
})
it("adds a new column to the table", () => {

8
packages/builder/cypress/integration/createView.spec.js

@ -50,13 +50,11 @@ context("Create a View", () => {
it("creates a stats calculation view based on age", () => {
cy.contains("Calculate").click()
// we may reinstate this - have commented this dropdown for now as there is only one option
//cy.get(".menu-container").find("select").first().select("Statistics")
cy.get(".menu-container")
.find("select")
.first()
.select("Statistics")
cy.get(".menu-container")
.find("select")
.eq(1)
.eq(0)
.select("age")
cy.contains("Save").click()
cy.get("thead th div").should($headers => {

1
packages/builder/cypress/support/commands.js

@ -68,6 +68,7 @@ Cypress.Commands.add("createTable", tableName => {
cy.contains("Create New Table").click()
cy.get(".menu-container")
.get("input")
.first()
.type(tableName)
cy.contains("Save").click()

6
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.1.23",
"version": "0.1.25",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -63,8 +63,8 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.39.0",
"@budibase/client": "^0.1.23",
"@budibase/bbui": "^1.40.1",
"@budibase/client": "^0.1.25",
"@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1",

17
packages/builder/src/builderStore/fetchBindableProperties.js

@ -37,9 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts
...(walkResult.target?._contexts
.map(contextToBindables(models, walkResult))
.flat(),
.flat() ?? []),
]
}
@ -73,6 +73,10 @@ const componentInstanceToBindable = walkResult => i => {
const contextToBindables = (models, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context)
const isModel = context.model?.isModel || typeof context.model === "string"
const modelId =
typeof context.model === "string" ? context.model : context.model.modelId
const model = models.find(model => model._id === modelId)
const newBindable = key => ({
type: "context",
@ -80,15 +84,12 @@ const contextToBindables = (models, walkResult) => context => {
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`,
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
})
// see ModelViewSelect.svelte for the format of context.model
// ... this allows us to bind to Model scheams, or View schemas
const model = models.find(m => m._id === context.model.modelId)
const schema = context.model.isModel
? model.schema
: model.views[context.model.name].schema
// ... this allows us to bind to Model schemas, or View schemas
const schema = isModel ? model.schema : model.views[context.model.name].schema
return (
Object.keys(schema)

5
packages/builder/src/components/backend/DataTable/ModelDataTable.svelte

@ -8,6 +8,7 @@
import Table from "./Table.svelte"
let data = []
let loading = false
$: title = $backendUiStore.selectedModel.name
$: schema = $backendUiStore.selectedModel.schema
@ -19,14 +20,16 @@
// Fetch records for specified model
$: {
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
loading = true
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
loading = false
})
}
}
</script>
<Table {title} {schema} {data} allowEditing={true}>
<Table {title} {schema} {data} allowEditing={true} {loading}>
<CreateColumnButton />
{#if Object.keys(schema).length > 0}
<CreateRowButton />

24
packages/builder/src/components/backend/DataTable/RecordFieldControl.svelte

@ -2,39 +2,33 @@
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
export let meta
export let value = meta.type === "boolean" ? false : ""
const type = determineInputType(meta)
const label = capitalise(meta.name)
function determineInputType(meta) {
if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox"
if (meta.type === "attachment") return "file"
if (meta.type === "options") return "select"
return "text"
}
$: type = meta.type
$: label = capitalise(meta.name)
</script>
{#if type === 'select'}
{#if type === 'options'}
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
<option value="">Choose an option</option>
{#each meta.constraints.inclusion as opt}
<option value={opt}>{opt}</option>
{/each}
</Select>
{:else if type === 'date'}
{:else if type === 'datetime'}
<DatePicker {label} bind:value />
{:else if type === 'file'}
{:else if type === 'attachment'}
<div>
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
<Dropzone bind:files={value} />
</div>
{:else if type === 'checkbox'}
{:else if type === 'boolean'}
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
{:else}
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
{/if}

19
packages/builder/src/components/backend/DataTable/Table.svelte

@ -1,6 +1,7 @@
<script>
import { goto, params } from "@sveltech/routify"
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
@ -16,6 +17,7 @@
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
import EditRowPopover from "./popovers/RowPopover.svelte"
import CalculationPopover from "./buttons/CalculateButton.svelte"
import Spinner from "components/common/Spinner.svelte"
const ITEMS_PER_PAGE = 10
@ -23,6 +25,7 @@
export let data = []
export let title
export let allowEditing = false
export let loading = false
let currentPage = 0
@ -50,7 +53,14 @@
<section>
<div class="table-controls">
<h2 class="title">{title}</h2>
<h2 class="title">
<span>{title}</span>
{#if loading}
<div transition:fade>
<Spinner size="10" />
</div>
{/if}
</h2>
<div class="popovers">
<slot />
</div>
@ -123,6 +133,13 @@
text-rendering: optimizeLegibility;
text-transform: capitalize;
margin-top: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.title > span {
margin-right: var(--spacing-xs);
}
table {

7
packages/builder/src/components/backend/DataTable/modals/CreateEditRecordModal.svelte

@ -1,7 +1,6 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import RecordFieldControl from "../RecordFieldControl.svelte"
import * as api from "../api"
import { Modal } from "components/common/Modal"
@ -44,11 +43,7 @@
<ErrorsBox {errors} />
{#each modelSchema as [key, meta]}
<div>
{#if meta.type === 'link'}
<LinkedRecordSelector bind:linkedRecords={record[key]} schema={meta} />
{:else}
<RecordFieldControl {meta} bind:value={record[key]} />
{/if}
<RecordFieldControl {meta} bind:value={record[key]} />
</div>
{/each}
</Modal>

8
packages/builder/src/components/backend/DataTable/popovers/CalculatePopover.svelte

@ -24,6 +24,7 @@
)
function saveView() {
if (!view.calculation) view.calculation = "stats"
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
onClosed()
@ -34,14 +35,15 @@
<div class="actions">
<h5>Calculate</h5>
<div class="input-group-row">
<p>The</p>
<!-- <p>The</p>
<Select secondary thin bind:value={view.calculation}>
<option value="">Choose an option</option>
{#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option>
{/each}
</Select>
<p>of</p>
<p>of</p> -->
<p>The statistics of</p>
<Select secondary thin bind:value={view.field}>
<option value="">Choose an option</option>
{#each fields as field}
@ -74,7 +76,7 @@
.input-group-row {
display: grid;
grid-template-columns: 30px 1fr 20px 1fr;
grid-template-columns: auto 1fr;
gap: var(--spacing-s);
align-items: center;
}

11
packages/builder/src/components/backend/DataTable/popovers/CreateEditColumnPopover.svelte

@ -34,6 +34,7 @@
$: modelOptions = $backendUiStore.models.filter(
model => model._id !== $backendUiStore.draftModel._id
)
$: required = !!field?.constraints?.presence
async function saveColumn() {
backendUiStore.update(state => {
@ -53,6 +54,12 @@
field.type = type
field.constraints = constraints
}
function onChangeRequired(e) {
const req = e.target.checked
field.constraints.presence = req ? { allowEmpty: false } : false
required = req
}
</script>
<div class="actions">
@ -71,8 +78,8 @@
{#if field.type !== 'link'}
<Toggle
checked={!field.constraints.presence.allowEmpty}
on:change={e => (field.constraints.presence.allowEmpty = !e.target.checked)}
checked={required}
on:change={onChangeRequired}
thin
text="Required" />
{/if}

60
packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte

@ -1,5 +1,5 @@
<script>
import { Button, Input, Select } from "@budibase/bbui"
import { Button, Input, Select, DatePicker } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import analytics from "analytics"
@ -71,11 +71,38 @@
function isMultipleChoice(field) {
return (
viewModel.schema[field].constraints &&
viewModel.schema[field].constraints.inclusion &&
viewModel.schema[field].constraints.inclusion.length
(viewModel.schema[field].constraints &&
viewModel.schema[field].constraints.inclusion &&
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>
<div class="actions">
@ -93,7 +120,11 @@
{/each}
</Select>
{/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>
{#each fields as field}
<option value={field}>{field}</option>
@ -108,12 +139,25 @@
{#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}>
<option value="">Choose an option</option>
{#each viewModel.schema[filter.key].constraints.inclusion as option}
<option value={option}>{option}</option>
{#each fieldOptions(filter.key) as option}
<option value={option}>{option.toString()}</option>
{/each}
</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}
<Input thin placeholder="Value" bind:value={filter.value} />
<Input
thin
placeholder={filter.key || fields[0]}
bind:value={filter.value} />
{/if}
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
{/each}

191
packages/builder/src/components/backend/ModelNavigator/TableDataImport.svelte

@ -0,0 +1,191 @@
<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-m) 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);
line-height: normal;
border: var(--border-transparent);
}
.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>

27
packages/builder/src/components/backend/ModelNavigator/popovers/CreateTablePopover.svelte

@ -2,23 +2,30 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Popover, Button, Icon, Input, Select } from "@budibase/bbui"
import { Popover, Button, Icon, Input, Select, Label } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
let anchor
let dropdown
let name
let dataImport
let loading
async function saveTable() {
loading = true
const model = await backendUiStore.actions.models.save({
name,
schema: {},
schema: dataImport.schema || {},
dataImport,
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
name = ""
dropdown.hide()
analytics.captureEvent("Table Created", { name })
loading = false
}
const onClosed = () => {
@ -38,9 +45,21 @@
thin
label="Table Name"
bind:value={name} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
<footer>
<Button secondary on:click={onClosed}>Cancel</Button>
<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>
</footer>
</div>
</Popover>

13
packages/builder/src/components/common/LinkedRecordSelector.svelte

@ -2,9 +2,8 @@
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Select, Label } from "@budibase/bbui"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
import MultiSelect from "components/common/MultiSelect.svelte"
export let schema
export let linkedRecords = []
@ -19,8 +18,7 @@
async function fetchRecords(linkedModelId) {
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
const result = await response.json()
return result
return await response.json()
}
function getPrettyName(record) {
@ -37,15 +35,14 @@
</Label>
{:else}
{#await promise then records}
<MultiSelect
thin
<Multiselect
secondary
bind:value={linkedRecords}
{label}
placeholder="Choose an option">
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</MultiSelect>
</Multiselect>
{/await}
{/if}

278
packages/builder/src/components/common/MultiSelect.svelte

@ -1,278 +0,0 @@
<script>
import { onMount } from "svelte"
import { fly } from "svelte/transition"
import { Label } from "@budibase/bbui"
const xPath =
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
export let value = []
export let label
let placeholder = "Type to search"
let options = []
let optionsVisible = false
let selected = {}
let first = true
let slot
onMount(() => {
const domOptions = Array.from(slot.querySelectorAll("option"))
options = domOptions.map(option => ({
value: option.value,
name: option.textContent,
}))
if (value) {
options.forEach(option => {
if (value.includes(option.value)) {
selected[option.value] = option
}
})
}
first = false
})
// Keep value up to date with selected options
$: {
if (!first) {
value = Object.values(selected).map(option => option.value)
}
}
function add(token) {
selected[token.value] = token
}
function remove(value) {
const { [value]: val, ...rest } = selected
selected = rest
}
function removeAll() {
selected = []
}
function showOptions(show) {
optionsVisible = show
}
function handleClick() {
showOptions(!optionsVisible)
}
function handleOptionMousedown(e) {
const value = e.target.dataset.value
if (value == null) {
return
}
if (selected[value]) {
remove(value)
} else {
add(options.filter(option => option.value === value)[0])
}
}
</script>
<div>
{#if label}
<Label extraSmall grey>{label}</Label>
{/if}
<div class="multiselect">
<div class="tokens-wrapper">
<div
class="tokens"
class:optionsVisible
on:click|self={handleClick}
class:empty={!value || !value.length}>
{#each Object.values(selected) as option}
<div class="token" data-id={option.value} on:click|self={handleClick}>
<span>{option.name}</span>
<div
class="token-remove"
title="Remove {option.name}"
on:click={() => remove(option.value)}>
<svg
class="icon-clear"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24">
<path d={xPath} />
</svg>
</div>
</div>
{/each}
{#if !value || !value.length}&nbsp;{/if}
</div>
</div>
<select bind:this={slot} type="multiple" class="hidden">
<slot />
</select>
{#if optionsVisible}
<div class="options-overlay" on:click|self={() => showOptions(false)} />
<ul
class="options"
transition:fly={{ duration: 200, y: 5 }}
on:mousedown|preventDefault={handleOptionMousedown}>
{#each options as option}
<li class:selected={selected[option.value]} data-value={option.value}>
{option.name}
</li>
{/each}
{#if !options.length}
<li class="no-results">No results</li>
{/if}
</ul>
{/if}
</div>
</div>
<style>
.multiselect {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.multiselect:hover {
border-bottom-color: hsl(0, 0%, 50%);
}
.tokens-wrapper {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex: 0 1 auto;
z-index: 2;
}
.tokens {
align-items: center;
display: flex;
flex-wrap: wrap;
position: relative;
width: 0;
flex: 1 1 auto;
background-color: var(--grey-2);
border-radius: var(--border-radius-m);
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs))
calc(var(--spacing-m) / 2);
border: var(--border-transparent);
}
.tokens:hover {
cursor: pointer;
}
.tokens::after {
background: none repeat scroll 0 0 transparent;
bottom: -1px;
content: "";
display: block;
height: 2px;
left: 50%;
position: absolute;
transition: width 0.3s ease 0s, left 0.3s ease 0s;
width: 0;
}
.tokens.optionsVisible {
border: var(--border-blue);
}
.tokens.empty {
padding: var(--spacing-m);
font-size: var(--font-size-xs);
user-select: none;
}
.tokens::after {
width: 100%;
left: 0;
}
.token {
font-size: var(--font-size-xs);
align-items: center;
background-color: white;
border-radius: var(--border-radius-l);
display: flex;
margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0
calc(var(--spacing-m) / 2);
max-height: 1.3rem;
padding: var(--spacing-xs) var(--spacing-s);
transition: background-color 0.3s;
white-space: nowrap;
}
.token span {
pointer-events: none;
user-select: none;
}
.token-remove {
align-items: center;
background-color: var(--grey-4);
border-radius: 50%;
color: var(--white);
display: flex;
justify-content: center;
height: 1rem;
width: 1rem;
margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs))
var(--spacing-xs);
}
.token-remove:hover {
background-color: var(--grey-5);
cursor: pointer;
}
.icon-clear path {
fill: white;
}
.options-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1;
}
.options {
z-index: 2;
left: 0;
list-style: none;
margin-block-end: 0;
margin-block-start: 0;
overflow-y: auto;
padding-inline-start: 0;
position: absolute;
top: calc(100% + 1px);
width: calc(100% - 4px);
border: var(--border-dark);
border-radius: var(--border-radius-m);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
margin-top: var(--spacing-xs);
padding: var(--spacing-s) 0;
background-color: white;
max-height: 200px;
}
li {
background-color: white;
cursor: pointer;
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-xs);
}
li.selected {
background-color: var(--blue);
color: white;
}
li:not(.selected):hover {
background-color: var(--grey-1);
}
li.no-results:hover {
background-color: white;
cursor: initial;
}
.hidden {
display: none;
}
</style>

1
packages/builder/src/components/common/Notification/NotificationDisplay.svelte

@ -51,6 +51,7 @@
flex-direction: column;
justify-content: flex-start;
align-items: center;
pointer-events: none;
}
.toast {

12
packages/builder/src/components/settings/tabs/APIKeys.svelte

@ -1,11 +1,9 @@
<script>
import { Input, Button } from "@budibase/bbui"
import { store } from "builderStore"
import { Input } from "@budibase/bbui"
import api from "builderStore/api"
import posthog from "posthog-js"
import analytics from "analytics"
let keys = { budibase: "", sendGrid: "" }
let keys = { budibase: "" }
async function updateKey([key, value]) {
if (key === "budibase") {
@ -40,12 +38,6 @@
edit
value={keys.budibase}
label="Budibase API Key" />
<Input
on:save={e => updateKey(['sendgrid', e.detail])}
thin
edit
value={keys.sendgrid}
label="Sendgrid API Key" />
</div>
<style>

2
packages/builder/src/components/start/CreateAppModal.svelte

@ -146,7 +146,7 @@
})
const appJson = await appResp.json()
analytics.captureEvent("App Created", {
name,
name: $createAppStore.values.applicationName,
appId: appJson._id,
template,
})

2
packages/builder/src/components/start/Steps/API.svelte

@ -11,7 +11,7 @@
on:input={() => (blurred.api = true)}
label="API Key"
name="apiKey"
placeholder="Enter your API Key"
placeholder="Use command-V to paste your API Key"
type="password"
error={blurred.api && validationErrors.apiKey} />
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>

2
packages/builder/src/components/start/Steps/User.svelte

@ -19,7 +19,7 @@
label="Password"
name="password"
placeholder="Password"
type="pasword"
type="password"
error={blurred.password && validationErrors.password} />
<Select label="Access Level" secondary name="accessLevelId">
<option value="ADMIN">Admin</option>

2
packages/builder/src/components/userInterface/ModelViewSelect.svelte

@ -10,7 +10,7 @@
function handleSelected(selected) {
dispatch("change", selected)
dropdown.hide()
dropdownRight.hide()
}
const models = $backendUiStore.models.map(m => ({

10
packages/builder/src/constants/backend/index.js

@ -6,7 +6,7 @@ export const FIELDS = {
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
presence: false,
},
},
OPTIONS: {
@ -25,7 +25,7 @@ export const FIELDS = {
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: true },
presence: false,
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
},
},
@ -35,7 +35,7 @@ export const FIELDS = {
type: "boolean",
constraints: {
type: "boolean",
presence: { allowEmpty: true },
presence: false,
},
},
DATETIME: {
@ -45,7 +45,7 @@ export const FIELDS = {
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
presence: false,
datetime: {
latest: "",
earliest: "",
@ -58,7 +58,7 @@ export const FIELDS = {
type: "attachment",
constraints: {
type: "array",
presence: { allowEmpty: true },
presence: false,
},
},
LINK: {

12
packages/builder/yarn.lock

@ -709,10 +709,10 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/bbui@^1.39.0":
version "1.39.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.39.0.tgz#7d3e259a60a0b4602f3d2da3452679d91a591fdd"
integrity sha512-9IL5Lw488sdYCa9mjHHdrap11VqW6wQHNcNTL8fFHaWNzummtlaUlVMScs9cunYgsR/L4NCgH0zSFdP0RnrUqw==
"@budibase/bbui@^1.40.1":
version "1.40.1"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.40.1.tgz#7ebfd52b4da822312d3395447a4f73caa41f8014"
integrity sha512-0t5Makyn5jOURKZIQPvd+8G4m6ps4GyfLUkAz5rKJSnAQSAgLiuZ+RihcEReDEJK8tnfW7h2echJTffJduQRRQ==
dependencies:
sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0"
@ -5844,10 +5844,6 @@ svelte-portal@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
svelte-simple-modal@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.4.2.tgz#2cfe26ec8c0760b89813d65dfee836399620d6b2"
svelte@^3.24.1:
version "3.25.1"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"

4
packages/cli/package.json

@ -1,6 +1,6 @@
{
"name": "budibase",
"version": "0.1.23",
"version": "0.1.25",
"description": "Budibase CLI",
"repository": "https://github.com/Budibase/Budibase",
"homepage": "https://www.budibase.com",
@ -17,7 +17,7 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/server": "^0.1.23",
"@budibase/server": "^0.1.25",
"@inquirer/password": "^0.0.6-alpha.0",
"chalk": "^2.4.2",
"dotenv": "^8.2.0",

2
packages/client/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.1.23",
"version": "0.1.25",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs",

BIN
packages/server/build/Hosting Portal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

6
packages/server/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/server",
"version": "0.1.23",
"version": "0.1.25",
"description": "Budibase Web Server",
"main": "src/electron.js",
"repository": {
@ -42,13 +42,14 @@
"author": "Michael Shanks",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/client": "^0.1.23",
"@budibase/client": "^0.1.25",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2",
"aws-sdk": "^2.767.0",
"bcryptjs": "^2.4.3",
"chmodr": "^1.2.0",
"csvtojson": "^2.0.10",
"dotenv": "^8.2.0",
"download": "^8.0.0",
"electron-is-dev": "^1.2.0",
@ -71,6 +72,7 @@
"pino-pretty": "^4.0.0",
"pouchdb": "^7.2.1",
"pouchdb-all-dbs": "^1.0.2",
"pouchdb-replication-stream": "^1.2.9",
"sharp": "^0.26.0",
"squirrelly": "^7.5.0",
"tar-fs": "^2.1.0",

7
packages/server/src/api/controllers/apikeys.js

@ -1,4 +1,5 @@
const fs = require("fs")
const { join } = require("../../utilities/sanitisedPath")
const readline = require("readline")
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const ENV_FILE_PATH = "/.env"
@ -7,7 +8,6 @@ exports.fetch = async function(ctx) {
ctx.status = 200
ctx.body = {
budibase: process.env.BUDIBASE_API_KEY,
sendgrid: process.env.SENDGRID_API_KEY,
userId: process.env.USERID_API_KEY,
}
}
@ -29,8 +29,9 @@ exports.update = async function(ctx) {
async function updateValues([key, value]) {
let newContent = ""
let keyExists = false
let envPath = join(budibaseAppsDir(), ENV_FILE_PATH)
const readInterface = readline.createInterface({
input: fs.createReadStream(`${budibaseAppsDir()}/${ENV_FILE_PATH}`),
input: fs.createReadStream(envPath),
output: process.stdout,
console: false,
})
@ -48,6 +49,6 @@ async function updateValues([key, value]) {
// Add API Key if it doesn't exist in the file at all
newContent = `${newContent}\n${key}=${value}`
}
fs.writeFileSync(`${budibaseAppsDir()}/${ENV_FILE_PATH}`, newContent)
fs.writeFileSync(envPath, newContent)
})
}

4
packages/server/src/api/controllers/application.js

@ -3,12 +3,12 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
const { copy, exists, readFile, writeFile } = require("fs-extra")
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const sqrl = require("squirrelly")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const fs = require("fs-extra")
const { join, resolve } = require("../../utilities/sanitisedPath")
const { promisify } = require("util")
const chmodr = require("chmodr")
const { generateAppID, getAppParams } = require("../../db/utils")
@ -116,7 +116,7 @@ exports.delete = async function(ctx) {
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
const app = await db.get(ctx.params.applicationId)
const result = await db.remove(app)
await fs.rmdir(`${budibaseAppsDir()}/${ctx.params.applicationId}`, {
await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), {
recursive: true,
})

2
packages/server/src/api/controllers/component.js

@ -1,6 +1,6 @@
const CouchDB = require("../../db")
const ClientDb = require("../../db/clientDb")
const { resolve, join } = require("path")
const { resolve, join } = require("../../utilities/sanitisedPath")
const {
budibaseTempDir,
budibaseAppsDir,

5
packages/server/src/api/controllers/deploy/aws.js

@ -1,4 +1,5 @@
const fs = require("fs")
const { join } = require("../../../utilities/sanitisedPath")
const AWS = require("aws-sdk")
const fetch = require("node-fetch")
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
@ -108,7 +109,7 @@ exports.uploadAppAssets = async function({
},
})
const appAssetsPath = `${budibaseAppsDir()}/${appId}/public`
const appAssetsPath = join(budibaseAppsDir(), appId, "public")
const appPages = fs.readdirSync(appAssetsPath)
@ -116,7 +117,7 @@ exports.uploadAppAssets = async function({
for (let page of appPages) {
// Upload HTML, CSS and JS for each page of the web app
walkDir(`${appAssetsPath}/${page}`, function(filePath) {
walkDir(join(appAssetsPath, page), function(filePath) {
const appAssetUpload = prepareUploadForS3({
file: {
path: filePath,

5
packages/server/src/api/controllers/instance.js

@ -3,6 +3,7 @@ const CouchDB = require("../../db")
const client = require("../../db/clientDb")
const newid = require("../../db/newid")
const { createLinkView } = require("../../db/linkedRecords")
const { join } = require("../../utilities/sanitisedPath")
const { downloadTemplate } = require("../../utilities/templates")
exports.create = async function(ctx) {
@ -39,7 +40,9 @@ exports.create = async function(ctx) {
// 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 dbDumpReadStream = fs.createReadStream(
join(templatePath, "db", "dump.txt")
)
const { ok } = await db.load(dbDumpReadStream)
if (!ok) {
ctx.throw(500, "Error loading database dump from template.")

27
packages/server/src/api/controllers/model.js

@ -1,9 +1,11 @@
const CouchDB = require("../../db")
const linkRecords = require("../../db/linkedRecords")
const csvParser = require("../../utilities/csvParser")
const {
getRecordParams,
getModelParams,
generateModelID,
generateRecordID,
} = require("../../db/utils")
exports.fetch = async function(ctx) {
@ -24,11 +26,12 @@ exports.find = async function(ctx) {
exports.save = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const { dataImport, ...rest } = ctx.request.body
const modelToSave = {
type: "model",
_id: generateModelID(),
views: {},
...ctx.request.body,
...rest,
}
// if the model obj had an _id then it will have been retrieved
@ -81,6 +84,19 @@ exports.save = async function(ctx) {
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
if (dataImport && dataImport.path) {
// Populate the table with records imported from CSV in a bulk update
const data = await csvParser.transform(dataImport)
for (let row of data) {
row._id = generateRecordID(modelToSave._id)
row.modelId = modelToSave._id
}
await db.bulkDocs(data)
}
ctx.status = 200
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
ctx.body = modelToSave
@ -116,3 +132,12 @@ exports.destroy = async function(ctx) {
ctx.status = 200
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,
}
}

51
packages/server/src/api/controllers/record.js

@ -2,6 +2,7 @@ const CouchDB = require("../../db")
const validateJs = require("validate.js")
const linkRecords = require("../../db/linkedRecords")
const { getRecordParams, generateRecordID } = require("../../db/utils")
const { cloneDeep } = require("lodash")
const MODEL_VIEW_BEGINS_WITH = "all_model:"
@ -21,6 +22,7 @@ exports.patch = async function(ctx) {
let record = await db.get(ctx.params.id)
const model = await db.get(record.modelId)
const patchfields = ctx.request.body
record = coerceRecordValues(record, model)
for (let key of Object.keys(patchfields)) {
if (!model.schema[key]) continue
@ -75,6 +77,8 @@ exports.save = async function(ctx) {
const model = await db.get(record.modelId)
record = coerceRecordValues(record, model)
const validateResult = await validate({
record,
model,
@ -285,3 +289,50 @@ exports.fetchEnrichedRecord = async function(ctx) {
ctx.body = record
ctx.status = 200
}
function coerceRecordValues(rec, model) {
const record = cloneDeep(rec)
for (let [key, value] of Object.entries(record)) {
const field = model.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return record
}
const TYPE_TRANSFORM_MAP = {
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}

2
packages/server/src/api/controllers/static.js

@ -1,5 +1,5 @@
const send = require("koa-send")
const { resolve, join } = require("path")
const { resolve, join } = require("../../utilities/sanitisedPath")
const jwt = require("jsonwebtoken")
const fetch = require("node-fetch")
const fs = require("fs-extra")

7
packages/server/src/api/controllers/view/index.js

@ -1,7 +1,7 @@
const CouchDB = require("../../../db")
const viewTemplate = require("./viewBuilder")
const fs = require("fs")
const path = require("path")
const { join } = require("../../../utilities/sanitisedPath")
const os = require("os")
const exporters = require("./exporters")
const { fetchView } = require("../record")
@ -99,7 +99,8 @@ const controller = {
const exporter = exporters[format]
const exportedFile = exporter(headers, ctx.body)
const filename = `${view.name}.${format}`
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
fs.writeFileSync(join(os.tmpdir(), filename), exportedFile)
ctx.body = {
url: `/api/views/export/download/${filename}`,
name: view.name,
@ -109,7 +110,7 @@ const controller = {
const filename = ctx.params.fileName
ctx.attachment(filename)
ctx.body = fs.createReadStream(path.join(os.tmpdir(), filename))
ctx.body = fs.createReadStream(join(os.tmpdir(), filename))
},
}

5
packages/server/src/api/controllers/view/viewBuilder.js

@ -61,8 +61,11 @@ function parseFilterExpression(filters) {
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
)
} else {
const value =
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
expression.push(
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"`
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
)
}
}

5
packages/server/src/api/routes/model.js

@ -14,6 +14,11 @@ router
modelController.find
)
.post("/api/models", authorized(BUILDER), usage, modelController.save)
.post(
"/api/models/csv/validate",
authorized(BUILDER),
modelController.validateCSVSchema
)
.delete(
"/api/models/:modelId/:revId",
authorized(BUILDER),

4
packages/server/src/api/routes/tests/couchTestUtils.js

@ -49,13 +49,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
key: "name",
schema: {
name: {
type: "text",
type: "string",
constraints: {
type: "string",
},
},
description: {
type: "text",
type: "string",
constraints: {
type: "string",
},

2
packages/server/src/api/routes/tests/model.spec.js

@ -180,7 +180,7 @@ describe("/models", () => {
key: "name",
schema: {
name: {
type: "text",
type: "string",
constraints: {
type: "string",
},

91
packages/server/src/api/routes/tests/record.spec.js

@ -38,7 +38,7 @@ describe("/records", () => {
const createRecord = async r =>
await request
.post(`/api/${model._id}/records`)
.post(`/api/${r ? r.modelId : record.modelId}/records`)
.send(r || record)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
@ -152,6 +152,95 @@ describe("/records", () => {
.expect('Content-Type', /json/)
.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", () => {

4
packages/server/src/api/routes/tests/view.spec.js

@ -69,13 +69,13 @@ describe("/views", () => {
filters: [],
schema: {
name: {
type: "text",
type: "string",
constraints: {
type: "string"
},
},
description: {
type: "text",
type: "string",
constraints: {
type: "string"
},

8
packages/server/src/automations/actions.js

@ -6,7 +6,7 @@ const createUser = require("./steps/createUser")
const environment = require("../environment")
const download = require("download")
const fetch = require("node-fetch")
const path = require("path")
const { join } = require("../utilities/sanitisedPath")
const os = require("os")
const fs = require("fs")
const Sentry = require("@sentry/node")
@ -43,7 +43,7 @@ async function downloadPackage(name, version, bundleName) {
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
AUTOMATION_DIRECTORY
)
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
return require(join(AUTOMATION_DIRECTORY, bundleName))
}
module.exports.getAction = async function(actionName) {
@ -57,7 +57,7 @@ module.exports.getAction = async function(actionName) {
const pkg = MANIFEST.packages[actionName]
const bundleName = buildBundleName(pkg.stepId, pkg.version)
try {
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
return require(join(AUTOMATION_DIRECTORY, bundleName))
} catch (err) {
return downloadPackage(pkg.stepId, pkg.version, bundleName)
}
@ -66,7 +66,7 @@ module.exports.getAction = async function(actionName) {
module.exports.init = async function() {
// set defaults
if (!AUTOMATION_DIRECTORY) {
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY)
AUTOMATION_DIRECTORY = join(os.homedir(), DEFAULT_DIRECTORY)
}
if (!AUTOMATION_BUCKET) {
AUTOMATION_BUCKET = DEFAULT_BUCKET

10
packages/server/src/automations/steps/sendEmail.js

@ -1,7 +1,3 @@
const environment = require("../../environment")
const sgMail = require("@sendgrid/mail")
sgMail.setApiKey(environment.SENDGRID_API_KEY)
module.exports.definition = {
description: "Send an email",
tagline: "Send email to {{inputs.to}}",
@ -13,6 +9,10 @@ module.exports.definition = {
schema: {
inputs: {
properties: {
apiKey: {
type: "string",
title: "SendGrid API key",
},
to: {
type: "string",
title: "Send To",
@ -49,6 +49,8 @@ module.exports.definition = {
}
module.exports.run = async function({ inputs }) {
const sgMail = require("@sendgrid/mail")
sgMail.setApiKey(inputs.apiKey)
const msg = {
to: inputs.to,
from: inputs.from,

9
packages/server/src/db/client.js

@ -2,6 +2,7 @@ const PouchDB = require("pouchdb")
const replicationStream = require("pouchdb-replication-stream")
const allDbs = require("pouchdb-all-dbs")
const { budibaseAppsDir } = require("../utilities/budibaseDir")
const { sanitise } = require("../utilities/sanitisedPath")
const env = require("../environment")
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
@ -26,4 +27,10 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
allDbs(Pouch)
module.exports = Pouch
function PouchWrapper(instance) {
Pouch.apply(this, [sanitise(instance)])
}
PouchWrapper.prototype = Object.create(Pouch.prototype)
module.exports = PouchWrapper

2
packages/server/src/electron.js

@ -1,5 +1,5 @@
const { app, BrowserWindow, shell, dialog } = require("electron")
const { join } = require("path")
const { join } = require("./utilities/sanitisedPath")
const isDev = require("electron-is-dev")
const { autoUpdater } = require("electron-updater")
const unhandled = require("electron-unhandled")

2
packages/server/src/index.js

@ -1,4 +1,4 @@
const { resolve, join } = require("path")
const { resolve, join } = require("./utilities/sanitisedPath")
const { homedir } = require("os")
const { app } = require("electron")
const fixPath = require("fix-path")

2
packages/server/src/utilities/budibaseDir.js

@ -1,4 +1,4 @@
const { join } = require("path")
const { join } = require("./sanitisedPath")
const { homedir, tmpdir } = require("os")
const env = require("../environment")

2
packages/server/src/utilities/builder/buildPage.js

@ -6,7 +6,7 @@ const {
readFile,
writeJSON,
} = require("fs-extra")
const { join, resolve } = require("path")
const { join, resolve } = require("../sanitisedPath")
const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
const publicPath = require("./publicPath")

2
packages/server/src/utilities/builder/convertCssToFiles.js

@ -1,6 +1,6 @@
const crypto = require("crypto")
const { ensureDir, emptyDir, writeFile } = require("fs-extra")
const { join } = require("path")
const { join } = require("../sanitisedPath")
module.exports.convertCssToFiles = async (publicPagePath, pkg) => {
const cssDir = join(publicPagePath, "css")

2
packages/server/src/utilities/builder/getPages.js

@ -1,5 +1,5 @@
const { readJSON, readdir } = require("fs-extra")
const { join } = require("path")
const { join } = require("../sanitisedPath")
module.exports = async appPath => {
const pages = {}

3
packages/server/src/utilities/builder/index.js

@ -8,7 +8,8 @@ const {
unlink,
rmdir,
} = require("fs-extra")
const { join, dirname, resolve } = require("path")
const { join, resolve } = require("../sanitisedPath")
const { dirname } = require("path")
const env = require("../../environment")
const buildPage = require("./buildPage")

2
packages/server/src/utilities/builder/listScreens.js

@ -1,6 +1,6 @@
const { appPackageFolder } = require("../createAppPackage")
const { readJSON, readdir, stat } = require("fs-extra")
const { join } = require("path")
const { join } = require("../sanitisedPath")
const { keyBy } = require("lodash/fp")
module.exports = async (config, appname, pagename) => {

2
packages/server/src/utilities/builder/publicPath.js

@ -1,3 +1,3 @@
const { join } = require("path")
const { join } = require("../sanitisedPath")
module.exports = (appPath, pageName) => join(appPath, "public", pageName)

2
packages/server/src/utilities/createAppPackage.js

@ -1,4 +1,4 @@
const { resolve } = require("path")
const { resolve } = require("./sanitisedPath")
const { cwd } = require("process")
const stream = require("stream")
const fetch = require("node-fetch")

73
packages/server/src/utilities/csvParser.js

@ -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,
}

2
packages/server/src/utilities/initialiseBudibase.js

@ -1,5 +1,5 @@
const { exists, readFile, writeFile, ensureDir } = require("fs-extra")
const { join, resolve } = require("path")
const { join, resolve } = require("./sanitisedPath")
const Sqrl = require("squirrelly")
const uuid = require("uuid")

38
packages/server/src/utilities/sanitisedPath.js

@ -0,0 +1,38 @@
const path = require("path")
const regex = new RegExp(/:(?![\\/])/g)
function sanitiseArgs(args) {
let sanitised = []
for (let arg of args) {
sanitised.push(arg.replace(regex, ""))
}
return sanitised
}
/**
* Exactly the same as path.join but creates a sanitised path.
* @param args Any number of string arguments to add to a path
* @returns {string} The final path ready to use
*/
exports.join = function(...args) {
return path.join(...sanitiseArgs(args))
}
/**
* Exactly the same as path.resolve but creates a sanitised path.
* @param args Any number of string arguments to add to a path
* @returns {string} The final path ready to use
*/
exports.resolve = function(...args) {
return path.resolve(...sanitiseArgs(args))
}
/**
* Sanitise a single string
* @param string input string to sanitise
* @returns {string} the final sanitised string
*/
exports.sanitise = function(string) {
return sanitiseArgs([string])[0]
}

20
packages/server/src/utilities/templates.js

@ -1,5 +1,5 @@
const path = require("path")
const fs = require("fs-extra")
const { join } = require("./sanitisedPath")
const os = require("os")
const fetch = require("node-fetch")
const stream = require("stream")
@ -27,10 +27,10 @@ exports.downloadTemplate = async function(type, name) {
await streamPipeline(
response.body,
zlib.Unzip(),
tar.extract(path.join(budibaseAppsDir(), "templates", type))
tar.extract(join(budibaseAppsDir(), "templates", type))
)
return path.join(budibaseAppsDir(), "templates", type, name)
return join(budibaseAppsDir(), "templates", type, name)
}
exports.exportTemplateFromApp = async function({
@ -39,15 +39,17 @@ exports.exportTemplateFromApp = async function({
instanceId,
}) {
// Copy frontend files
const appToExport = path.join(os.homedir(), ".budibase", appId, "pages")
const templatesDir = path.join(os.homedir(), ".budibase", "templates")
const appToExport = join(os.homedir(), ".budibase", appId, "pages")
const templatesDir = join(os.homedir(), ".budibase", "templates")
fs.ensureDirSync(templatesDir)
const templateOutputPath = path.join(templatesDir, templateName)
fs.copySync(appToExport, `${templateOutputPath}/pages`)
const templateOutputPath = join(templatesDir, templateName)
fs.copySync(appToExport, join(templateOutputPath, "pages"))
fs.ensureDirSync(path.join(templateOutputPath, "db"))
const writeStream = fs.createWriteStream(`${templateOutputPath}/db/dump.txt`)
fs.ensureDirSync(join(templateOutputPath, "db"))
const writeStream = fs.createWriteStream(
join(templateOutputPath, "db", "dump.txt")
)
// perform couch dump
const instanceDb = new CouchDB(instanceId)

21
packages/server/src/utilities/tests/__snapshots__/csvParser.spec.js.snap

@ -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",
},
]
`;

108
packages/server/src/utilities/tests/csvParser.spec.js

@ -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()
});
});
});

4
packages/server/src/utilities/tests/test.csv

@ -0,0 +1,4 @@
"Name","Age","Address"
"Bert","4324","5 Sesame Street"
"Ernie","34","1 World Trade Center"
"Big Bird","23423","44 Second Avenue"
1 Name Age Address
2 Bert 4324 5 Sesame Street
3 Ernie 34 1 World Trade Center
4 Big Bird 23423 44 Second Avenue

116
packages/server/yarn.lock

@ -172,10 +172,10 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/client@^0.1.23":
version "0.1.23"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.23.tgz#d72d2b26ff3a2d99f2b6c1b71020b1136880937d"
integrity sha512-pZdwdCq5kKLZfZYxasIHBNnqu3BFFrqJLxXMFs0K9ddCVZ0UNons59nn73nFGbeRgNVdWp6yyW71XyMQr8NOEw==
"@budibase/client@^0.1.25":
version "0.1.25"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.25.tgz#f08c4a614f9018eb0f0faa6d20bb05f7a3215c70"
integrity sha512-vZ0cqJwLYcs7MHihFnJO3qOe7qxibnB4Va1+IYNfnPc9kcxy4KvfQxCx/G/DDxP9CXfEvsguy9ymzR3RUAvBHw==
dependencies:
deep-equal "^2.0.1"
mustache "^4.0.1"
@ -1079,9 +1079,10 @@ bluebird-lst@^1.0.9:
dependencies:
bluebird "^3.5.5"
bluebird@^3.5.5:
bluebird@^3.5.1, bluebird@^3.5.5:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
boolean@^3.0.0, boolean@^3.0.1:
version "3.0.1"
@ -1618,6 +1619,15 @@ cssstyle@^1.0.0:
dependencies:
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:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -3455,6 +3465,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
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-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
@ -4347,6 +4362,13 @@ lie@3.0.4:
inline-process-browser "^1.0.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:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@ -4409,6 +4431,11 @@ lodash.once@^4.0.0:
version "4.1.1"
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:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -4417,6 +4444,11 @@ lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
version "4.17.19"
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:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -4671,6 +4703,16 @@ natural-compare@^1.4.0:
version "1.4.0"
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:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -5164,6 +5206,14 @@ posix-character-classes@^0.1.0:
version "0.1.1"
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:
version "7.2.1"
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.1.tgz#71bf2a05755689e2b05e78e796003a18ebf65a69"
@ -5251,6 +5301,26 @@ pouchdb-promise@5.4.3:
dependencies:
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:
version "7.2.1"
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.1.tgz#5dec1c53c8ecba717e5762311e9a1def2d4ebf9c"
@ -5511,7 +5581,17 @@ readable-stream@1.0.33:
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@^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"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
dependencies:
@ -6088,6 +6168,13 @@ split-string@^3.0.1, split-string@^3.0.2:
dependencies:
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
@ -6262,6 +6349,13 @@ strip-ansi@^6.0.0:
dependencies:
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@ -6445,6 +6539,14 @@ through2@^0.6.2, through2@^0.6.5:
readable-stream ">=1.0.33-1 <1.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:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@ -6968,7 +7070,7 @@ xmlbuilder@~9.0.1:
version "9.0.7"
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"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

6
packages/standard-components/package.json

@ -13,7 +13,7 @@
"dev:builder": "rollup -cw"
},
"devDependencies": {
"@budibase/client": "^0.1.23",
"@budibase/client": "^0.1.25",
"@rollup/plugin-commonjs": "^11.1.0",
"lodash": "^4.17.15",
"rollup": "^1.11.0",
@ -31,12 +31,12 @@
"keywords": [
"svelte"
],
"version": "0.1.23",
"version": "0.1.25",
"license": "MIT",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {
"@beyonk/svelte-googlemaps": "^2.2.0",
"@budibase/bbui": "^1.39.0",
"@budibase/bbui": "^1.40.1",
"@fortawesome/fontawesome-free": "^5.14.0",
"britecharts": "^2.16.1",
"d3-selection": "^1.4.2",

202
packages/standard-components/src/DataForm.svelte

@ -1,9 +1,19 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { Label, DatePicker } from "@budibase/bbui"
import {
Label,
DatePicker,
Input,
Select,
Button,
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers"
export let _bb
export let model
@ -32,16 +42,11 @@
let isNew = true
let errors = {}
$: fields = schema ? Object.keys(schema) : []
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = schema ? Object.keys(schema) : []
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
@ -82,11 +87,13 @@
saved = true
setTimeout(() => {
saved = false
}, 1000)
}, 3000)
}
if (response.status === 400) {
errors = json.errors
errors = Object.keys(json.errors)
.map(k => ({ dataPath: k, message: json.errors[k] }))
.flat()
}
})
@ -103,8 +110,7 @@
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
const json = await response.json()
record = json
record = await response.json()
})
</script>
@ -112,44 +118,52 @@
{#if title}
<h1>{title}</h1>
{/if}
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<hr />
<div class="form-content">
<ErrorsBox {errors} />
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select bind:value={record[field]}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<input class="input" type="checkbox" bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" bind:value={record[field]} />
{:else if schema[field].type === 'attachment'}
{#if schema[field].type === 'options'}
<Select
secondary
label={capitalise(schema[field].name)}
bind:value={record[field]}>
<option value="">Choose an option</option>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</Select>
{:else if schema[field].type === 'datetime'}
<DatePicker
label={capitalise(schema[field].name)}
bind:value={record[field]} />
{:else if schema[field].type === 'boolean'}
<Toggle
text={capitalise(schema[field].name)}
bind:checked={record[field]} />
{:else if schema[field].type === 'number'}
<Input
label={capitalise(schema[field].name)}
type="number"
bind:value={record[field]} />
{:else if schema[field].type === 'string'}
<Input
label={capitalise(schema[field].name)}
bind:value={record[field]} />
{:else if schema[field].type === 'attachment'}
<div>
<Label extraSmall grey>{schema[field].name}</Label>
<Dropzone bind:files={record[field]} />
{/if}
</div>
<hr />
</div>
{:else if schema[field].type === 'link'}
<LinkedRecordSelector
secondary
bind:linkedRecords={record[field]}
schema={schema[field]} />
{/if}
{/each}
<div class="button-block">
<button on:click={save} class:saved>
{#if saved}
<div in:fade>
<span class:saved>Success</span>
</div>
{:else}
<div>{buttonText || 'Submit Form'}</div>
{/if}
</button>
<div class="buttons">
<Button primary on:click={save} green={saved}>
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
</Button>
</div>
</div>
</form>
@ -162,104 +176,14 @@
}
.form-content {
margin-bottom: 20px;
margin-bottom: var(--spacing-xl);
display: grid;
gap: var(--spacing-xl);
width: 100%;
}
.input {
border-radius: 5px;
border: 1px solid #e6e6e6;
padding: 1rem;
font-size: 16px;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
hr {
border: 1px solid var(--grey-1);
margin: 20px 0px;
}
hr:nth-last-child(2) {
border: 1px solid #fff;
margin: 20px 0px;
}
.button-block {
.buttons {
display: flex;
justify-content: flex-end;
}
button {
font-size: 16px;
padding: 0.4em;
box-sizing: border-box;
border-radius: 4px;
color: white;
background-color: #393c44;
outline: none;
width: 300px;
height: 40px;
cursor: pointer;
transition: all 0.2s ease 0s;
overflow: hidden;
outline: none;
user-select: none;
white-space: nowrap;
text-align: center;
}
button.saved {
background-color: #84c991;
border: none;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
width: 32px;
height: 32px;
padding: 0;
margin: 0;
vertical-align: bottom;
position: relative;
top: -1px;
*overflow: hidden;
}
select::-ms-expand {
display: none;
}
select {
display: inline-block;
cursor: pointer;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
.error {
color: red;
font-weight: 500;
}
</style>

29
packages/standard-components/src/DataTable.svelte

@ -1,6 +1,6 @@
<script>
import { onMount } from "svelte"
import { cssVars, createClasses } from "./cssVars"
import { cssVars } from "./cssVars"
import ArrowUp from "./icons/ArrowUp.svelte"
import ArrowDown from "./icons/ArrowDown.svelte"
import fsort from "fast-sort"
@ -13,6 +13,7 @@
export let stripeColor
export let borderColor
export let datasource = {}
export let _bb
let data = []
let headers = []
@ -29,11 +30,19 @@
$: sorted = sort.direction ? fsort(data)[sort.direction](sort.column) : data
async function fetchModel(modelId) {
const FETCH_MODEL_URL = `/api/models/${modelId}`
const response = await _bb.api.get(FETCH_MODEL_URL)
const model = await response.json()
schema = model.schema
}
onMount(async () => {
if (!isEmpty(datasource)) {
data = await fetchData(datasource)
if (data) {
headers = Object.keys(data[0]).filter(shouldDisplayField)
if (data && data.length) {
await fetchModel(data[0].modelId)
headers = Object.keys(schema).filter(shouldDisplayField)
}
}
})
@ -85,11 +94,15 @@
{#each sorted as row (row._id)}
<tr>
{#each headers as header}
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
{#if Array.isArray(row[header])}
<AttachmentList files={row[header]} />
{:else if row[header]}
<td>{row[header]}</td>
{#if schema[header]}
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
{#if schema[header].type === 'attachment'}
<AttachmentList files={row[header]} />
{:else if schema[header].type === 'link'}
<td>{row[header] ? row[header].length : 0} related row(s)</td>
{:else if row[header]}
<td>{row[header]}</td>
{/if}
{/if}
{/each}
</tr>

31
packages/standard-components/src/ErrorsBox.svelte

@ -0,0 +1,31 @@
<script>
export let errors = []
$: hasErrors = errors.length > 0
</script>
{#if hasErrors}
<div class="container">
{#each errors as error}
<div class="error">{error.dataPath} {error.message}</div>
{/each}
</div>
{/if}
<style>
.container {
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
background-color: var(--red-light);
}
.error {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--red-dark);
}
.error:first-letter {
text-transform: uppercase;
}
</style>

65
packages/standard-components/src/LinkedRecordSelector.svelte

@ -0,0 +1,65 @@
<script>
import { onMount } from "svelte"
import { Select, Label, Multiselect } from "@budibase/bbui"
import api from "./api"
import { capitalise } from "./helpers"
export let schema = {}
export let linkedRecords = []
export let showLabel = true
export let secondary
let linkedModel
$: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: recordsPromise = fetchRecords(linkedModelId)
$: fetchModel(linkedModelId)
async function fetchModel() {
if (linkedModelId == null) {
return
}
const FETCH_MODEL_URL = `/api/models/${linkedModelId}`
const response = await api.get(FETCH_MODEL_URL)
linkedModel = await response.json()
}
async function fetchRecords(linkedModelId) {
if (linkedModelId == null) {
return
}
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()
}
function getPrettyName(record) {
return record[linkedModel?.primaryDisplay || "_id"]
}
</script>
{#if linkedModel != null}
{#if linkedModel.primaryDisplay == null}
{#if showLabel}
<Label extraSmall grey>{label}</Label>
{/if}
<Label small black>
Please choose a primary display column for the
<b>{linkedModel.name}</b>
table.
</Label>
{:else}
{#await recordsPromise then records}
<Multiselect
{secondary}
bind:value={linkedRecords}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/await}
{/if}
{/if}

14
packages/standard-components/src/RecordDetail.svelte

@ -8,6 +8,12 @@
let store = _bb.store
let target
async function fetchModel(id) {
const FETCH_MODEL_URL = `/api/models/${id}`
const response = await _bb.api.get(FETCH_MODEL_URL)
return await response.json()
}
async function fetchFirstRecord() {
const FETCH_RECORDS_URL = `/api/views/all_${model}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
@ -34,6 +40,14 @@
}
if (record) {
// Fetch model schema so we can check for linked records
const model = await fetchModel(record.modelId)
for (let key of Object.keys(model.schema)) {
if (model.schema[key].type === "link") {
record[key] = Array.isArray(record[key]) ? record[key].length : 0
}
}
_bb.attachChildren(target, {
hydrate: false,
context: record,

2
packages/standard-components/src/attachments/AttachmentList.svelte

@ -1,7 +1,7 @@
<script>
import { FILE_TYPES } from "./fileTypes"
export let files
export let files = []
export let height = "70"
export let width = "70"
</script>

23
packages/standard-components/src/fetchData.js

@ -4,7 +4,28 @@ export default async function fetchData(datasource) {
const { isModel, name } = datasource
if (name) {
return isModel ? await fetchModelData() : await fetchViewData()
const records = isModel ? await fetchModelData() : await fetchViewData()
// Fetch model schema so we can check for linked records
if (records && records.length) {
const model = await fetchModel(records[0].modelId)
const keys = Object.keys(model.schema)
records.forEach(record => {
for (let key of keys) {
if (model.schema[key].type === "link") {
record[key] = Array.isArray(record[key]) ? record[key].length : 0
}
}
})
}
return records
}
async function fetchModel(id) {
const FETCH_MODEL_URL = `/api/models/${id}`
const response = await api.get(FETCH_MODEL_URL)
return await response.json()
}
async function fetchModelData() {

1
packages/standard-components/src/helpers.js

@ -0,0 +1 @@
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)

6
readme.md

@ -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?

86
yarn.lock

@ -913,11 +913,6 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argsarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb"
integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs=
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -2336,11 +2331,6 @@ ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@ -2387,7 +2377,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
@ -2638,11 +2628,6 @@ is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -2805,13 +2790,6 @@ libnpmpublish@^1.1.1:
semver "^5.5.1"
ssri "^6.0.1"
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"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -2861,11 +2839,6 @@ lodash.ismatch@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
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.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@ -3204,16 +3177,6 @@ natural-compare@^1.4.0:
version "1.4.0"
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"
neo-async@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
@ -3717,34 +3680,6 @@ posix-character-classes@^0.1.0:
version "0.1.1"
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-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"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -3927,16 +3862,6 @@ read@1, read@~1.0.1:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
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"
readdir-scoped-modules@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
@ -4291,7 +4216,7 @@ split-string@^3.0.1, split-string@^3.0.2:
dependencies:
extend-shallow "^3.0.0"
split2@^2.0.0, split2@^2.1.0:
split2@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
dependencies:
@ -4396,11 +4321,6 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@ -4521,7 +4441,7 @@ text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
through2@^2.0.0, through2@^2.0.2, through2@^2.0.3:
through2@^2.0.0, through2@^2.0.2:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
dependencies:

Loading…
Cancel
Save