Merge branch 'master' into develop

This commit is contained in:
Martin McKeaveney 2021-03-03 16:00:10 +00:00 committed by GitHub
commit 69a9e3135f
32 changed files with 3467 additions and 430 deletions

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

12
packages/cli/.eslintrc Normal file
View File

@ -0,0 +1,12 @@
{
"globals": {
"emit": true,
"key": true
},
"env": {
"node": true
},
"extends": ["eslint:recommended"],
"rules": {
}
}

6
packages/cli/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
docker-compose.yaml
envoy.yaml
hosting.properties
build/
docker-error.log

25
packages/cli/package.json Normal file
View File

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

View File

@ -0,0 +1,4 @@
exports.CommandWords = {
HOSTING: "hosting",
HELP: "help",
}

View File

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

View File

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

21
packages/cli/src/index.js Normal file
View File

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

View File

@ -0,0 +1,5 @@
const hosting = require("./hosting")
exports.getCommands = () => {
return [hosting.command]
}

View File

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

View File

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

46
packages/cli/src/utils.js Normal file
View File

@ -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}`)
}

1619
packages/cli/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,3 +8,4 @@ myapps/
public/ public/
db/dev.db/ db/dev.db/
dist dist
coverage/

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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 => {

View File

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

View File

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

View File

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

View File

@ -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"),