Merge branch 'master' into develop
This commit is contained in:
commit
69a9e3135f
|
@ -35,6 +35,12 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
name: Budibase CI
|
name: Budibase CI
|
||||||
|
- uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
files: ./packages/server/coverage/clover.xml
|
||||||
|
name: codecov-umbrella
|
||||||
|
verbose: true
|
||||||
- run: yarn test:e2e:ci
|
- run: yarn test:e2e:ci
|
||||||
|
|
||||||
- name: Build and Push Staging Docker Image
|
- name: Build and Push Staging Docker Image
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
|
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
|
||||||
|
<a href="https://codecov.io/gh/Budibase/budibase">
|
||||||
|
<img src="https://codecov.io/gh/Budibase/budibase/branch/master/graph/badge.svg?token=E8W2ZFXQOH"/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
|
|
@ -82,7 +82,7 @@ const createScreen = table => {
|
||||||
theme: "alpine",
|
theme: "alpine",
|
||||||
height: "540",
|
height: "540",
|
||||||
pagination: true,
|
pagination: true,
|
||||||
detailUrl: `${table.name.toLowerCase()}/:id`,
|
detailUrl: `${rowListUrl(table)}/:id`,
|
||||||
})
|
})
|
||||||
.instanceName("Grid")
|
.instanceName("Grid")
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,17 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
import { FIELDS, AUTO_COLUMN_SUB_TYPES } from "constants/backend"
|
import {
|
||||||
|
FIELDS,
|
||||||
|
AUTO_COLUMN_SUB_TYPES,
|
||||||
|
RelationshipTypes,
|
||||||
|
} from "constants/backend"
|
||||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ValuesList from "components/common/ValuesList.svelte"
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
import DatePicker from "components/common/DatePicker.svelte"
|
import DatePicker from "components/common/DatePicker.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { truncate } from "lodash"
|
||||||
|
|
||||||
const AUTO_COL = "auto"
|
const AUTO_COL = "auto"
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
|
@ -36,16 +41,7 @@
|
||||||
$backendUiStore.selectedTable.primaryDisplay == null ||
|
$backendUiStore.selectedTable.primaryDisplay == null ||
|
||||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||||
|
|
||||||
let relationshipTypes = [
|
let table = $backendUiStore.selectedTable
|
||||||
{ text: "Many to many (N:N)", value: "many-to-many" },
|
|
||||||
{ text: "One to many (1:N)", value: "one-to-many" },
|
|
||||||
]
|
|
||||||
let types = ["Many to many (N:N)", "One to many (1:N)"]
|
|
||||||
|
|
||||||
let selectedRelationshipType =
|
|
||||||
relationshipTypes.find(type => type.value === field.relationshipType)
|
|
||||||
?.text || "Many to many (N:N)"
|
|
||||||
|
|
||||||
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let deletion
|
let deletion
|
||||||
|
@ -57,7 +53,7 @@
|
||||||
$: uneditable =
|
$: uneditable =
|
||||||
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
||||||
UNEDITABLE_USER_FIELDS.includes(field.name)
|
UNEDITABLE_USER_FIELDS.includes(field.name)
|
||||||
$: invalid = field.type === FIELDS.LINK.type && !field.tableId
|
$: invalid = field.type === LINK_TYPE && !field.tableId
|
||||||
|
|
||||||
// used to select what different options can be displayed for column type
|
// used to select what different options can be displayed for column type
|
||||||
$: canBeSearched =
|
$: canBeSearched =
|
||||||
|
@ -67,15 +63,9 @@
|
||||||
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
|
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
||||||
|
$: relationshipOptions = getRelationshipOptions(field)
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
// Set relationship type if it's
|
|
||||||
if (field.type === "link") {
|
|
||||||
field.relationshipType = relationshipTypes.find(
|
|
||||||
type => type.text === selectedRelationshipType
|
|
||||||
).value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.type === AUTO_COL) {
|
if (field.type === AUTO_COL) {
|
||||||
field = buildAutoColumn(
|
field = buildAutoColumn(
|
||||||
$backendUiStore.draftTable.name,
|
$backendUiStore.draftTable.name,
|
||||||
|
@ -110,12 +100,18 @@
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
field.type = definition.type
|
|
||||||
field.constraints = definition.constraints
|
|
||||||
// remove any extra fields that may not be related to this type
|
// remove any extra fields that may not be related to this type
|
||||||
delete field.autocolumn
|
delete field.autocolumn
|
||||||
delete field.subtype
|
delete field.subtype
|
||||||
delete field.tableId
|
delete field.tableId
|
||||||
|
delete field.relationshipType
|
||||||
|
// add in defaults and initial definition
|
||||||
|
field.type = definition.type
|
||||||
|
field.constraints = definition.constraints
|
||||||
|
// default relationships many to many
|
||||||
|
if (field.type === LINK_TYPE) {
|
||||||
|
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeRequired(e) {
|
function onChangeRequired(e) {
|
||||||
|
@ -153,6 +149,32 @@
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
deletion = false
|
deletion = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRelationshipOptions(field) {
|
||||||
|
if (!field || !field.tableId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const linkTable = tableOptions.find(table => table._id === field.tableId)
|
||||||
|
if (!linkTable) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const thisName = truncate(table.name, { length: 15 }),
|
||||||
|
linkName = truncate(linkTable.name, { length: 15 })
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: `Many ${thisName} rows has many ${linkName} rows`,
|
||||||
|
value: RelationshipTypes.MANY_TO_MANY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `One ${thisName} row has many ${linkName} rows`,
|
||||||
|
value: RelationshipTypes.ONE_TO_MANY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Many ${thisName} rows has one ${linkName} row`,
|
||||||
|
value: RelationshipTypes.MANY_TO_ONE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions" class:hidden={deletion}>
|
<div class="actions" class:hidden={deletion}>
|
||||||
|
@ -231,26 +253,32 @@
|
||||||
label="Max Value"
|
label="Max Value"
|
||||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||||
{:else if field.type === 'link'}
|
{:else if field.type === 'link'}
|
||||||
<div>
|
|
||||||
<Label grey extraSmall>Select relationship type</Label>
|
|
||||||
<div class="radio-buttons">
|
|
||||||
{#each types as type}
|
|
||||||
<Radio
|
|
||||||
disabled={originalName}
|
|
||||||
name="Relationship type"
|
|
||||||
value={type}
|
|
||||||
bind:group={selectedRelationshipType}>
|
|
||||||
<label for={type}>{type}</label>
|
|
||||||
</Radio>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select label="Table" thin secondary bind:value={field.tableId}>
|
<Select label="Table" thin secondary bind:value={field.tableId}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each tableOptions as table}
|
{#each tableOptions as table}
|
||||||
<option value={table._id}>{table.name}</option>
|
<option value={table._id}>{table.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{#if relationshipOptions && relationshipOptions.length > 0}
|
||||||
|
<div>
|
||||||
|
<Label grey extraSmall>Define the relationship</Label>
|
||||||
|
<div class="radio-buttons">
|
||||||
|
{#each relationshipOptions as { value, name }}
|
||||||
|
<Radio
|
||||||
|
disabled={originalName}
|
||||||
|
name="Relationship type"
|
||||||
|
{value}
|
||||||
|
bind:group={field.relationshipType}>
|
||||||
|
<div class="radio-button-labels">
|
||||||
|
<label for={value}>{name.split('has')[0]}</label>
|
||||||
|
<label class="rel-type-center" for={value}>has</label>
|
||||||
|
<label for={value}>{name.split('has')[1]}</label>
|
||||||
|
</div>
|
||||||
|
</Radio>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
label={`Column Name in Other Table`}
|
label={`Column Name in Other Table`}
|
||||||
thin
|
thin
|
||||||
|
@ -282,15 +310,16 @@
|
||||||
title="Confirm Deletion" />
|
title="Confirm Deletion" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
label {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
.radio-buttons {
|
.radio-buttons {
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-buttons :global(> *) {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
|
@ -307,7 +336,17 @@
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.rel-type-center {
|
||||||
display: none;
|
font-weight: 500;
|
||||||
|
color: var(--grey-6);
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 1px 3px 1px 3px;
|
||||||
|
background: var(--grey-3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button-labels {
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -49,8 +49,8 @@
|
||||||
{categories}
|
{categories}
|
||||||
{selectedCategory} />
|
{selectedCategory} />
|
||||||
|
|
||||||
{#if showDisplayName}
|
{#if definition && definition.name}
|
||||||
<div class="instance-name">{$selectedComponent._instanceName}</div>
|
<div class="instance-name">{definition.name}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="component-props-container">
|
<div class="component-props-container">
|
||||||
|
|
|
@ -162,16 +162,16 @@
|
||||||
|
|
||||||
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
|
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
|
||||||
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
||||||
Reset Fields
|
Update Form Fields
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmResetFieldsDialog}
|
bind:this={confirmResetFieldsDialog}
|
||||||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to reset this Field Group?`}
|
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`}
|
||||||
okText="Reset"
|
okText="Update"
|
||||||
onOk={resetFormFields}
|
onOk={resetFormFields}
|
||||||
title="Confirm Reset Fields" />
|
title="Confirm Form Field Update" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.settings-view-container {
|
.settings-view-container {
|
||||||
|
|
|
@ -123,3 +123,9 @@ export function isAutoColumnUserRelationship(subtype) {
|
||||||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RelationshipTypes = {
|
||||||
|
MANY_TO_MANY: "many-to-many",
|
||||||
|
ONE_TO_MANY: "one-to-many",
|
||||||
|
MANY_TO_ONE: "many-to-one",
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"emit": true,
|
||||||
|
"key": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules/
|
||||||
|
docker-compose.yaml
|
||||||
|
envoy.yaml
|
||||||
|
hosting.properties
|
||||||
|
build/
|
||||||
|
docker-error.log
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "cli",
|
||||||
|
"version": "0.7.8",
|
||||||
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"bin": "src/index.js",
|
||||||
|
"author": "Budibase",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pkg . --out-path build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"commander": "^7.1.0",
|
||||||
|
"docker-compose": "^0.23.6",
|
||||||
|
"inquirer": "^8.0.0",
|
||||||
|
"lookpath": "^1.1.0",
|
||||||
|
"pkg": "^4.4.9",
|
||||||
|
"randomstring": "^1.1.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^7.20.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
exports.CommandWords = {
|
||||||
|
HOSTING: "hosting",
|
||||||
|
HELP: "help",
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
const Command = require("../structures/Command")
|
||||||
|
const { CommandWords } = require("../constants")
|
||||||
|
const { lookpath } = require("lookpath")
|
||||||
|
const { downloadFile, logErrorToFile, success, info } = require("../utils")
|
||||||
|
const { confirmation } = require("../questions")
|
||||||
|
const fs = require("fs")
|
||||||
|
const compose = require("docker-compose")
|
||||||
|
const envFile = require("./makeEnv")
|
||||||
|
|
||||||
|
const BUDIBASE_SERVICES = ["app-service", "worker-service"]
|
||||||
|
const ERROR_FILE = "docker-error.log"
|
||||||
|
const FILE_URLS = [
|
||||||
|
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml",
|
||||||
|
"https://raw.githubusercontent.com/Budibase/budibase/master/hosting/envoy.yaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
async function downloadFiles() {
|
||||||
|
const promises = []
|
||||||
|
for (let url of FILE_URLS) {
|
||||||
|
const fileName = url.split("/").slice(-1)[0]
|
||||||
|
promises.push(downloadFile(url, `./${fileName}`))
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDockerConfigured() {
|
||||||
|
const error =
|
||||||
|
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/self-hosting/hosting-methods/docker-compose#installing-docker"
|
||||||
|
const docker = await lookpath("docker")
|
||||||
|
const compose = await lookpath("docker-compose")
|
||||||
|
if (!docker || !compose) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkInitComplete() {
|
||||||
|
if (!fs.existsSync(envFile.filePath)) {
|
||||||
|
throw "Please run the hosting --init command before any other hosting command."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleError(func) {
|
||||||
|
try {
|
||||||
|
await func()
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.err) {
|
||||||
|
logErrorToFile(ERROR_FILE, err.err)
|
||||||
|
}
|
||||||
|
throw `Failed to start - logs written to file: ${ERROR_FILE}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
const shouldContinue = await confirmation(
|
||||||
|
"This will create multiple files in current directory, should continue?"
|
||||||
|
)
|
||||||
|
if (!shouldContinue) {
|
||||||
|
console.log("Stopping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await downloadFiles()
|
||||||
|
await envFile.make()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Starting services, this may take a moment."))
|
||||||
|
const port = envFile.get("MAIN_PORT")
|
||||||
|
await handleError(async () => {
|
||||||
|
await compose.upAll({ cwd: "./", log: false })
|
||||||
|
})
|
||||||
|
console.log(
|
||||||
|
success(
|
||||||
|
`Services started, please go to http://localhost:${port} for next steps.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function status() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Budibase status"))
|
||||||
|
await handleError(async () => {
|
||||||
|
const response = await compose.ps()
|
||||||
|
console.log(response.out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
console.log(info("Stopping services, this may take a moment."))
|
||||||
|
await handleError(async () => {
|
||||||
|
await compose.stop()
|
||||||
|
})
|
||||||
|
console.log(success("Services have been stopped successfully."))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
await checkDockerConfigured()
|
||||||
|
checkInitComplete()
|
||||||
|
if (
|
||||||
|
await confirmation(
|
||||||
|
"Do you wish to update you docker-compose.yaml and envoy.yaml?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await downloadFiles()
|
||||||
|
}
|
||||||
|
await handleError(async () => {
|
||||||
|
const status = await compose.ps()
|
||||||
|
const parts = status.out.split("\n")
|
||||||
|
const isUp = parts[2] && parts[2].indexOf("Up") !== -1
|
||||||
|
if (isUp) {
|
||||||
|
console.log(info("Stopping services, this may take a moment."))
|
||||||
|
await compose.stop()
|
||||||
|
}
|
||||||
|
console.log(info("Beginning update, this may take a few minutes."))
|
||||||
|
await compose.pullMany(BUDIBASE_SERVICES, { log: true })
|
||||||
|
if (isUp) {
|
||||||
|
console.log(success("Update complete, restarting services..."))
|
||||||
|
await start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = new Command(`${CommandWords.HOSTING}`)
|
||||||
|
.addHelp("Controls self hosting on the Budibase platform.")
|
||||||
|
.addSubOption(
|
||||||
|
"--init",
|
||||||
|
"Configure a self hosted platform in current directory.",
|
||||||
|
init
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--start",
|
||||||
|
"Start the configured platform in current directory.",
|
||||||
|
start
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--status",
|
||||||
|
"Check the status of currently running services.",
|
||||||
|
status
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--stop",
|
||||||
|
"Stop the configured platform in the current directory.",
|
||||||
|
stop
|
||||||
|
)
|
||||||
|
.addSubOption(
|
||||||
|
"--update",
|
||||||
|
"Update the Budibase images to the latest version.",
|
||||||
|
update
|
||||||
|
)
|
||||||
|
|
||||||
|
exports.command = command
|
|
@ -0,0 +1,59 @@
|
||||||
|
const { string, number } = require("../questions")
|
||||||
|
const { success } = require("../utils")
|
||||||
|
const fs = require("fs")
|
||||||
|
const path = require("path")
|
||||||
|
const randomString = require("randomstring")
|
||||||
|
|
||||||
|
const FILE_PATH = path.resolve("./.env")
|
||||||
|
|
||||||
|
function getContents(port, hostingKey) {
|
||||||
|
return `
|
||||||
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
|
MAIN_PORT=${port}
|
||||||
|
|
||||||
|
# Use this password when configuring your self hosting settings
|
||||||
|
HOSTING_KEY=${hostingKey}
|
||||||
|
|
||||||
|
# This section contains all secrets pertaining to the system
|
||||||
|
JWT_SECRET=${randomString.generate()}
|
||||||
|
MINIO_ACCESS_KEY=${randomString.generate()}
|
||||||
|
MINIO_SECRET_KEY=${randomString.generate()}
|
||||||
|
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||||
|
COUCH_DB_USER=${randomString.generate()}
|
||||||
|
|
||||||
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
|
APP_PORT=4002
|
||||||
|
WORKER_PORT=4003
|
||||||
|
MINIO_PORT=4004
|
||||||
|
COUCH_DB_PORT=4005
|
||||||
|
BUDIBASE_ENVIRONMENT=PRODUCTION`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.filePath = FILE_PATH
|
||||||
|
|
||||||
|
module.exports.make = async () => {
|
||||||
|
const hostingKey = await string(
|
||||||
|
"Please input the password you'd like to use as your hosting key: "
|
||||||
|
)
|
||||||
|
const hostingPort = await number(
|
||||||
|
"Please enter the port on which you want your installation to run: ",
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
const fileContents = getContents(hostingPort, hostingKey)
|
||||||
|
fs.writeFileSync(FILE_PATH, fileContents)
|
||||||
|
console.log(
|
||||||
|
success(
|
||||||
|
"Configuration has been written successfully - please check .env file for more details."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.get = property => {
|
||||||
|
const props = fs.readFileSync(FILE_PATH, "utf8").split(property)
|
||||||
|
if (props[0].charAt(0) === "=") {
|
||||||
|
property = props[0]
|
||||||
|
} else {
|
||||||
|
property = props[1]
|
||||||
|
}
|
||||||
|
return property.split("=")[1].split("\n")[0]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
const { getCommands } = require("./options")
|
||||||
|
const { Command } = require("commander")
|
||||||
|
const { getHelpDescription } = require("./utils")
|
||||||
|
|
||||||
|
// add hosting config
|
||||||
|
async function init() {
|
||||||
|
const program = new Command()
|
||||||
|
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||||
|
.helpOption(false)
|
||||||
|
program.helpOption()
|
||||||
|
// add commands
|
||||||
|
for (let command of getCommands()) {
|
||||||
|
command.configure(program)
|
||||||
|
}
|
||||||
|
// this will stop the program if no command found
|
||||||
|
await program.parseAsync(process.argv)
|
||||||
|
}
|
||||||
|
|
||||||
|
init().catch(err => {
|
||||||
|
console.error(`Unexpected error - `, err)
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
const hosting = require("./hosting")
|
||||||
|
|
||||||
|
exports.getCommands = () => {
|
||||||
|
return [hosting.command]
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
const inquirer = require("inquirer")
|
||||||
|
|
||||||
|
exports.confirmation = async question => {
|
||||||
|
const config = {
|
||||||
|
type: "confirm",
|
||||||
|
message: question,
|
||||||
|
default: true,
|
||||||
|
name: "confirmation",
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).confirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.string = async (question, defaultString = null) => {
|
||||||
|
const config = {
|
||||||
|
type: "input",
|
||||||
|
name: "string",
|
||||||
|
message: question,
|
||||||
|
}
|
||||||
|
if (defaultString) {
|
||||||
|
config.default = defaultString
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).string
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.number = async (question, defaultNumber) => {
|
||||||
|
const config = {
|
||||||
|
type: "input",
|
||||||
|
name: "number",
|
||||||
|
message: question,
|
||||||
|
validate: value => {
|
||||||
|
let valid = !isNaN(parseFloat(value))
|
||||||
|
return valid || "Please enter a number"
|
||||||
|
},
|
||||||
|
filter: Number,
|
||||||
|
}
|
||||||
|
if (defaultNumber) {
|
||||||
|
config.default = defaultNumber
|
||||||
|
}
|
||||||
|
return (await inquirer.prompt(config)).number
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
const { getSubHelpDescription, getHelpDescription, error } = require("../utils")
|
||||||
|
|
||||||
|
class Command {
|
||||||
|
constructor(command, func = null) {
|
||||||
|
// if there are options, need to just get the command name
|
||||||
|
this.command = command
|
||||||
|
this.opts = []
|
||||||
|
this.func = func
|
||||||
|
}
|
||||||
|
|
||||||
|
addHelp(help) {
|
||||||
|
this.help = help
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubOption(command, help, func) {
|
||||||
|
this.opts.push({ command, help, func })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(program) {
|
||||||
|
const thisCmd = this
|
||||||
|
let command = program.command(thisCmd.command)
|
||||||
|
if (this.help) {
|
||||||
|
command = command.description(getHelpDescription(thisCmd.help))
|
||||||
|
}
|
||||||
|
for (let opt of thisCmd.opts) {
|
||||||
|
command = command.option(
|
||||||
|
`${opt.command}`,
|
||||||
|
getSubHelpDescription(opt.help)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
command.helpOption(
|
||||||
|
"--help",
|
||||||
|
getSubHelpDescription(`Get help with ${this.command} options`)
|
||||||
|
)
|
||||||
|
command.action(async options => {
|
||||||
|
try {
|
||||||
|
let executed = false
|
||||||
|
if (thisCmd.func) {
|
||||||
|
await thisCmd.func(options)
|
||||||
|
executed = true
|
||||||
|
}
|
||||||
|
for (let opt of thisCmd.opts) {
|
||||||
|
if (options[opt.command.replace("--", "")]) {
|
||||||
|
await opt.func(options)
|
||||||
|
executed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!executed) {
|
||||||
|
console.log(error(`Unknown ${this.command} option.`))
|
||||||
|
command.help()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(error(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Command
|
|
@ -0,0 +1,46 @@
|
||||||
|
const chalk = require("chalk")
|
||||||
|
const fs = require("fs")
|
||||||
|
const axios = require("axios")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
exports.downloadFile = async (url, filePath) => {
|
||||||
|
filePath = path.resolve(filePath)
|
||||||
|
const writer = fs.createWriteStream(filePath)
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "stream",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writer.on("finish", resolve)
|
||||||
|
writer.on("error", reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getHelpDescription = string => {
|
||||||
|
return chalk.cyan(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getSubHelpDescription = string => {
|
||||||
|
return chalk.green(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.error = error => {
|
||||||
|
return chalk.red(`Error - ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.success = success => {
|
||||||
|
return chalk.green(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.info = info => {
|
||||||
|
return chalk.cyan(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logErrorToFile = (file, error) => {
|
||||||
|
fs.writeFileSync(path.resolve(`./${file}`), `Budiase Error\n${error}`)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -8,3 +8,4 @@ myapps/
|
||||||
public/
|
public/
|
||||||
db/dev.db/
|
db/dev.db/
|
||||||
dist
|
dist
|
||||||
|
coverage/
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --testPathIgnorePatterns=routes && npm run test:integration",
|
"test": "jest --testPathIgnorePatterns=routes && npm run test:integration",
|
||||||
"test:integration": "jest routes --runInBand",
|
"test:integration": "jest routes --runInBand --coverage",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"run:docker": "node src/index",
|
"run:docker": "node src/index",
|
||||||
"dev:builder": "cross-env PORT=4001 nodemon src/index.js",
|
"dev:builder": "cross-env PORT=4001 nodemon src/index.js",
|
||||||
|
@ -44,6 +44,22 @@
|
||||||
"lint": "eslint --fix src/",
|
"lint": "eslint --fix src/",
|
||||||
"initialise": "node scripts/initialise.js"
|
"initialise": "node scripts/initialise.js"
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"setupFiles": [
|
||||||
|
"./scripts/jestSetup.js"
|
||||||
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.js",
|
||||||
|
"!**/node_modules/**",
|
||||||
|
"!src/db/views/*.js"
|
||||||
|
],
|
||||||
|
"coverageReporters": [
|
||||||
|
"lcov",
|
||||||
|
"json",
|
||||||
|
"clover"
|
||||||
|
]
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"budibase"
|
"budibase"
|
||||||
],
|
],
|
||||||
|
@ -116,11 +132,5 @@
|
||||||
"pouchdb-adapter-memory": "^7.2.1",
|
"pouchdb-adapter-memory": "^7.2.1",
|
||||||
"supertest": "^4.0.2"
|
"supertest": "^4.0.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"setupFiles": [
|
|
||||||
"./scripts/jestSetup.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
|
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,7 @@ const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/webhook")
|
const controller = require("../controllers/webhook")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
const {
|
const { BUILDER } = require("../../utilities/security/permissions")
|
||||||
BUILDER,
|
|
||||||
PermissionTypes,
|
|
||||||
PermissionLevels,
|
|
||||||
} = require("../../utilities/security/permissions")
|
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -40,10 +36,7 @@ router
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.buildSchema
|
controller.buildSchema
|
||||||
)
|
)
|
||||||
.post(
|
// this shouldn't have authorisation, right now its always public
|
||||||
"/api/webhooks/trigger/:instance/:id",
|
.post("/api/webhooks/trigger/:instance/:id", controller.trigger)
|
||||||
authorized(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
|
|
||||||
controller.trigger
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.FieldTypes = {
|
||||||
|
|
||||||
exports.RelationshipTypes = {
|
exports.RelationshipTypes = {
|
||||||
ONE_TO_MANY: "one-to-many",
|
ONE_TO_MANY: "one-to-many",
|
||||||
|
MANY_TO_ONE: "many-to-one",
|
||||||
MANY_TO_MANY: "many-to-many",
|
MANY_TO_MANY: "many-to-many",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,6 +145,27 @@ class LinkController {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two the field of this table, and the field of the linked table, this makes sure
|
||||||
|
* the state of relationship type is accurate on both.
|
||||||
|
*/
|
||||||
|
handleRelationshipType(field, linkedField) {
|
||||||
|
if (
|
||||||
|
!field.relationshipType ||
|
||||||
|
field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
|
) {
|
||||||
|
linkedField.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
|
// make sure by default all are many to many (if not specified)
|
||||||
|
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
|
} else if (field.relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
||||||
|
// Ensure that the other side of the relationship is locked to one record
|
||||||
|
linkedField.relationshipType = RelationshipTypes.ONE_TO_MANY
|
||||||
|
} else if (field.relationshipType === RelationshipTypes.ONE_TO_MANY) {
|
||||||
|
linkedField.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||||
|
}
|
||||||
|
return { field, linkedField }
|
||||||
|
}
|
||||||
|
|
||||||
// all operations here will assume that the table
|
// all operations here will assume that the table
|
||||||
// this operation is related to has linked rows
|
// this operation is related to has linked rows
|
||||||
/**
|
/**
|
||||||
|
@ -317,34 +338,32 @@ class LinkController {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const linkConfig = {
|
const fields = this.handleRelationshipType(field, {
|
||||||
name: field.fieldName,
|
name: field.fieldName,
|
||||||
type: FieldTypes.LINK,
|
type: FieldTypes.LINK,
|
||||||
// these are the props of the table that initiated the link
|
// these are the props of the table that initiated the link
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
fieldName: fieldName,
|
fieldName: fieldName,
|
||||||
}
|
})
|
||||||
|
|
||||||
|
// update table schema after checking relationship types
|
||||||
|
schema[fieldName] = fields.field
|
||||||
|
const linkedField = fields.linkedField
|
||||||
|
|
||||||
if (field.autocolumn) {
|
if (field.autocolumn) {
|
||||||
linkConfig.autocolumn = field.autocolumn
|
linkedField.autocolumn = field.autocolumn
|
||||||
}
|
|
||||||
|
|
||||||
if (field.relationshipType) {
|
|
||||||
// Ensure that the other side of the relationship is locked to one record
|
|
||||||
linkConfig.relationshipType = field.relationshipType
|
|
||||||
delete field.relationshipType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the linked table to make sure we aren't overwriting an existing column
|
// check the linked table to make sure we aren't overwriting an existing column
|
||||||
const existingSchema = linkedTable.schema[field.fieldName]
|
const existingSchema = linkedTable.schema[field.fieldName]
|
||||||
if (
|
if (
|
||||||
existingSchema != null &&
|
existingSchema != null &&
|
||||||
!this.areSchemasEqual(existingSchema, linkConfig)
|
!this.areSchemasEqual(existingSchema, linkedField)
|
||||||
) {
|
) {
|
||||||
throw new Error("Cannot overwrite existing column.")
|
throw new Error("Cannot overwrite existing column.")
|
||||||
}
|
}
|
||||||
// create the link field in the other table
|
// create the link field in the other table
|
||||||
linkedTable.schema[field.fieldName] = linkConfig
|
linkedTable.schema[field.fieldName] = linkedField
|
||||||
const response = await this._db.put(linkedTable)
|
const response = await this._db.put(linkedTable)
|
||||||
// special case for when linking back to self, make sure rev updated
|
// special case for when linking back to self, make sure rev updated
|
||||||
if (linkedTable._id === table._id) {
|
if (linkedTable._id === table._id) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../index")
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
const { ViewNames, getQueryIndex } = require("../utils")
|
const { ViewNames, getQueryIndex } = require("../utils")
|
||||||
const { FieldTypes } = require("../../constants")
|
const { FieldTypes } = require("../../constants")
|
||||||
|
const { createLinkView } = require("../views/staticViews")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only needed so that boolean parameters are being used for includeDocs
|
* Only needed so that boolean parameters are being used for includeDocs
|
||||||
|
@ -12,44 +13,7 @@ exports.IncludeDocs = {
|
||||||
EXCLUDE: false,
|
EXCLUDE: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
exports.createLinkView = createLinkView
|
||||||
* Creates the link view for the instance, this will overwrite the existing one, but this should only
|
|
||||||
* be called if it is found that the view does not exist.
|
|
||||||
* @param {string} appId The instance to which the view should be added.
|
|
||||||
* @returns {Promise<void>} The view now exists, please note that the next view of this query will actually build it,
|
|
||||||
* so it may be slow.
|
|
||||||
*/
|
|
||||||
exports.createLinkView = async appId => {
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const designDoc = await db.get("_design/database")
|
|
||||||
const view = {
|
|
||||||
map: function(doc) {
|
|
||||||
// everything in this must remain constant as its going to Pouch, no external variables
|
|
||||||
if (doc.type === "link") {
|
|
||||||
let doc1 = doc.doc1
|
|
||||||
let doc2 = doc.doc2
|
|
||||||
emit([doc1.tableId, doc1.rowId], {
|
|
||||||
id: doc2.rowId,
|
|
||||||
thisId: doc1.rowId,
|
|
||||||
fieldName: doc1.fieldName,
|
|
||||||
})
|
|
||||||
// if linking to same table can't emit twice
|
|
||||||
if (doc1.tableId !== doc2.tableId) {
|
|
||||||
emit([doc2.tableId, doc2.rowId], {
|
|
||||||
id: doc1.rowId,
|
|
||||||
thisId: doc2.rowId,
|
|
||||||
fieldName: doc2.fieldName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toString(),
|
|
||||||
}
|
|
||||||
designDoc.views = {
|
|
||||||
...designDoc.views,
|
|
||||||
[ViewNames.LINK]: view,
|
|
||||||
}
|
|
||||||
await db.put(designDoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the linking documents, not the linked documents themselves.
|
* Gets the linking documents, not the linked documents themselves.
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
const CouchDB = require("../index")
|
||||||
|
const { DocumentTypes, SEPARATOR, ViewNames } = require("../utils")
|
||||||
|
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
|
||||||
|
|
||||||
|
/**************************************************
|
||||||
|
* INFORMATION *
|
||||||
|
* This file exists purely to keep views separate *
|
||||||
|
* from the rest of the codebase, the reason *
|
||||||
|
* being that they affect coverage and any *
|
||||||
|
* functions written in this file cannot import *
|
||||||
|
* or make use of any constants/variables that *
|
||||||
|
* aren't defined as part of the map function *
|
||||||
|
* itself. *
|
||||||
|
**************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the link view for the instance, this will overwrite the existing one, but this should only
|
||||||
|
* be called if it is found that the view does not exist.
|
||||||
|
* @param {string} appId The instance to which the view should be added.
|
||||||
|
* @returns {Promise<void>} The view now exists, please note that the next view of this query will actually build it,
|
||||||
|
* so it may be slow.
|
||||||
|
*/
|
||||||
|
exports.createLinkView = async appId => {
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
const view = {
|
||||||
|
map: function(doc) {
|
||||||
|
// everything in this must remain constant as its going to Pouch, no external variables
|
||||||
|
if (doc.type === "link") {
|
||||||
|
let doc1 = doc.doc1
|
||||||
|
let doc2 = doc.doc2
|
||||||
|
emit([doc1.tableId, doc1.rowId], {
|
||||||
|
id: doc2.rowId,
|
||||||
|
thisId: doc1.rowId,
|
||||||
|
fieldName: doc1.fieldName,
|
||||||
|
})
|
||||||
|
// if linking to same table can't emit twice
|
||||||
|
if (doc1.tableId !== doc2.tableId) {
|
||||||
|
emit([doc2.tableId, doc2.rowId], {
|
||||||
|
id: doc1.rowId,
|
||||||
|
thisId: doc2.rowId,
|
||||||
|
fieldName: doc2.fieldName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toString(),
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.LINK]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createRoutingView = async appId => {
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
const view = {
|
||||||
|
// if using variables in a map function need to inject them before use
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${SCREEN_PREFIX}")) {
|
||||||
|
emit(doc._id, {
|
||||||
|
id: doc._id,
|
||||||
|
routing: doc.routing,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.ROUTING]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const { createRoutingView } = require("./routingUtils")
|
const { createRoutingView } = require("../../db/views/staticViews")
|
||||||
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
|
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
|
||||||
|
|
||||||
exports.getRoutingInfo = async appId => {
|
exports.getRoutingInfo = async appId => {
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
const CouchDB = require("../../db")
|
|
||||||
const { DocumentTypes, SEPARATOR, ViewNames } = require("../../db/utils")
|
|
||||||
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
|
|
||||||
|
|
||||||
exports.createRoutingView = async appId => {
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const designDoc = await db.get("_design/database")
|
|
||||||
const view = {
|
|
||||||
// if using variables in a map function need to inject them before use
|
|
||||||
map: `function(doc) {
|
|
||||||
if (doc._id.startsWith("${SCREEN_PREFIX}")) {
|
|
||||||
emit(doc._id, {
|
|
||||||
id: doc._id,
|
|
||||||
routing: doc.routing,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
}
|
|
||||||
designDoc.views = {
|
|
||||||
...designDoc.views,
|
|
||||||
[ViewNames.ROUTING]: view,
|
|
||||||
}
|
|
||||||
await db.put(designDoc)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,29 +6,7 @@
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"transitionable": true,
|
"transitionable": true,
|
||||||
"settings": [
|
"settings": []
|
||||||
{
|
|
||||||
"type": "select",
|
|
||||||
"key": "type",
|
|
||||||
"label": "Type",
|
|
||||||
"defaultValue": "div",
|
|
||||||
"options": [
|
|
||||||
"article",
|
|
||||||
"aside",
|
|
||||||
"details",
|
|
||||||
"div",
|
|
||||||
"figure",
|
|
||||||
"figcaption",
|
|
||||||
"footer",
|
|
||||||
"header",
|
|
||||||
"main",
|
|
||||||
"mark",
|
|
||||||
"nav",
|
|
||||||
"paragraph",
|
|
||||||
"summary"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"datagrid": {
|
"datagrid": {
|
||||||
"name": "Grid",
|
"name": "Grid",
|
||||||
|
|
|
@ -152,9 +152,7 @@
|
||||||
{#if selectedRows.length > 0}
|
{#if selectedRows.length > 0}
|
||||||
<DeleteButton text small on:click={modal.show()}>
|
<DeleteButton text small on:click={modal.show()}>
|
||||||
<Icon name="addrow" />
|
<Icon name="addrow" />
|
||||||
Delete
|
Delete {selectedRows.length} row(s)
|
||||||
{selectedRows.length}
|
|
||||||
row(s)
|
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,10 +19,6 @@ rimraf.sync(devDir)
|
||||||
fs.mkdirSync(`${devDir}/@budibase`, { recursive: true })
|
fs.mkdirSync(`${devDir}/@budibase`, { recursive: true })
|
||||||
|
|
||||||
const SYMLINK_PATHS = [
|
const SYMLINK_PATHS = [
|
||||||
{
|
|
||||||
symlink: `${devDir}/@budibase/materialdesign-components`,
|
|
||||||
destination: resolve("packages/materialdesign-components"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
symlink: `${devDir}/@budibase/standard-components`,
|
symlink: `${devDir}/@budibase/standard-components`,
|
||||||
destination: resolve("packages/standard-components"),
|
destination: resolve("packages/standard-components"),
|
||||||
|
|
Loading…
Reference in New Issue