Merge branch 'new-backend' of github.com:Budibase/budibase into new-backend

This commit is contained in:
Martin McKeaveney 2020-03-25 16:59:36 +00:00
commit 062776d94e
27 changed files with 135 additions and 76 deletions

View File

@ -13,6 +13,8 @@ import {
constructHierarchy, constructHierarchy,
templateApi, templateApi,
isIndex, isIndex,
canDeleteIndex,
canDeleteRecord
} from "../../common/core" } from "../../common/core"
export const getBackendUiStore = () => { export const getBackendUiStore = () => {
@ -183,7 +185,7 @@ export const saveCurrentNode = store => () => {
const defaultIndex = templateApi(state.hierarchy).getNewIndexTemplate( const defaultIndex = templateApi(state.hierarchy).getNewIndexTemplate(
cloned.parent() cloned.parent()
) )
defaultIndex.name = `all_${cloned.collectionName}` defaultIndex.name = `all_${cloned.name}s`
defaultIndex.allowedRecordNodeIds = [cloned.nodeId] defaultIndex.allowedRecordNodeIds = [cloned.nodeId]
} }
@ -202,14 +204,27 @@ export const deleteCurrentNode = store => () => {
? state.hierarchy.children.find(node => node !== state.currentNode) ? state.hierarchy.children.find(node => node !== state.currentNode)
: nodeToDelete.parent() : nodeToDelete.parent()
const recordOrIndexKey = hierarchyFunctions.isRecord(nodeToDelete) ? "children" : "indexes"; const isRecord = hierarchyFunctions.isRecord(nodeToDelete)
const check = isRecord
? canDeleteRecord(nodeToDelete)
: canDeleteIndex(nodeToDelete)
if (!check.canDelete) {
state.errors = check.errors.map(e => ({ error: e }))
return state
}
const recordOrIndexKey = isRecord ? "children" : "indexes"
// remove the selected record or index // remove the selected record or index
nodeToDelete.parent()[recordOrIndexKey] = remove( const newCollection = remove(
nodeToDelete.parent()[recordOrIndexKey], node => node.nodeId === nodeToDelete.nodeId,
node => node.nodeId === nodeToDelete.nodeId nodeToDelete.parent()[recordOrIndexKey]
) )
nodeToDelete.parent()[recordOrIndexKey] = newCollection
state.errors = [] state.errors = []
saveBackend(state) saveBackend(state)
return state return state

View File

@ -5,25 +5,14 @@
</script> </script>
{#if hasErrors} {#if hasErrors}
<div class="error-container"> <div uk-alert class="uk-alert-danger">
{#each errors as error} {#each errors as error}
<div class="error-row"> <div>
{error.field ? `${error.field}: ` : ''}{error.error} {error.field ? `${error.field}: ` : ''}{error.error}
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.error-container {
padding: 10px;
border-style: solid;
border-color: var(--deletion100);
border-radius: var(--borderradiusall);
background: var(--deletion75);
}
.error-row {
padding: 5px 0px;
}
</style>

View File

@ -9,6 +9,8 @@ import { find, filter, keyBy, flatten, map } from "lodash/fp"
import { generateSchema } from "../../../core/src/indexing/indexSchemaCreator" import { generateSchema } from "../../../core/src/indexing/indexSchemaCreator"
import { generate } from "shortid" import { generate } from "shortid"
export { canDeleteIndex } from "../../../core/src/templateApi/canDeleteIndex"
export { canDeleteRecord } from "../../../core/src/templateApi/canDeleteRecord"
export { userWithFullAccess } from "../../../core/src/index" export { userWithFullAccess } from "../../../core/src/index"
export const pipe = common.$ export const pipe = common.$

View File

@ -31,9 +31,6 @@
</ActionButton> </ActionButton>
</div> </div>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
</div> </div>
<style> <style>

View File

@ -6,7 +6,8 @@
import { store } from "../builderStore" import { store } from "../builderStore"
import { filter, some, map, compose } from "lodash/fp" import { filter, some, map, compose } from "lodash/fp"
import { hierarchy as hierarchyFunctions, common } from "../../../core/src" import { hierarchy as hierarchyFunctions, common } from "../../../core/src"
import ErrorsBox from "../common/ErrorsBox.svelte"
const SNIPPET_EDITORS = { const SNIPPET_EDITORS = {
MAP: "Map", MAP: "Map",
FILTER: "Filter", FILTER: "Filter",
@ -49,6 +50,9 @@
</heading> </heading>
<form class="uk-form-stacked root"> <form class="uk-form-stacked root">
<h4 class="budibase__label--big">Settings</h4> <h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<div class="uk-grid-small" uk-grid> <div class="uk-grid-small" uk-grid>
<div class="uk-width-1-2@s"> <div class="uk-width-1-2@s">
<Textbox bind:text={index.name} label="Name" /> <Textbox bind:text={index.name} label="Name" />

View File

@ -20,7 +20,7 @@
const response = await api.createUser( const response = await api.createUser(
password, password,
{ {
username, name:username,
accessLevels, accessLevels,
enabled: true, enabled: true,
temporaryAccessId: "" temporaryAccessId: ""

View File

@ -10,6 +10,7 @@
import { common, hierarchy } from "../../../core/src" import { common, hierarchy } from "../../../core/src"
import { templateApi, pipe, validate } from "../common/core" import { templateApi, pipe, validate } from "../common/core"
import ActionsHeader from "./ActionsHeader.svelte" import ActionsHeader from "./ActionsHeader.svelte"
import ErrorsBox from "../common/ErrorsBox.svelte"
let record let record
let getIndexAllowedRecords let getIndexAllowedRecords
@ -99,14 +100,15 @@
</heading> </heading>
{#if !editingField} {#if !editingField}
<h4 class="budibase__label--big">Settings</h4> <h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<form class="uk-form-stacked"> <form class="uk-form-stacked">
<Textbox label="Name" bind:text={record.name} on:change={nameChanged} />
<div class="horizontal-stack"> <div class="horizontal-stack">
{#if !record.isSingle} <Textbox label="Name" bind:text={record.name} on:change={nameChanged} />
<Textbox label="Collection Name" bind:text={record.collectionName} />
{/if}
<div> <div>
<label class="uk-form-label">Parent</label> <label class="uk-form-label">Parent</label>
<div class="uk-form-controls"> <div class="uk-form-controls">

View File

@ -59,6 +59,7 @@
font-size: 0.8rem; font-size: 0.8rem;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
background: rgba(0,0,0,0);
} }
.active { .active {

View File

@ -65,6 +65,7 @@
font-size: 0.8rem; font-size: 0.8rem;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
background: rgba(0,0,0,0);
} }
.active { .active {

View File

@ -75,6 +75,7 @@
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
color: var(--secondary60); color: var(--secondary60);
background: rgba(0,0,0,0);
} }
.switcher > .selected { .switcher > .selected {

View File

@ -70,6 +70,7 @@
font-size: 0.8rem; font-size: 0.8rem;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
background: rgba(0,0,0,0);
} }
.active { .active {

View File

@ -9,7 +9,7 @@ import {
reduce, reduce,
find, find,
} from "lodash/fp" } from "lodash/fp"
import { compileExpression, compileCode } from "../common/compileCode" import { compileCode } from "../common/compileCode"
import { $ } from "../common" import { $ } from "../common"
import { _executeAction } from "./execute" import { _executeAction } from "./execute"
import { BadRequestError, NotFoundError } from "../common/errors" import { BadRequestError, NotFoundError } from "../common/errors"
@ -49,7 +49,7 @@ const subscribeTriggers = (
const shouldRunTrigger = (trigger, eventContext) => { const shouldRunTrigger = (trigger, eventContext) => {
if (!trigger.condition) return true if (!trigger.condition) return true
const shouldRun = compileExpression(trigger.condition) const shouldRun = compileCode(trigger.condition)
return shouldRun({ context: eventContext }) return shouldRun({ context: eventContext })
} }

View File

@ -1,13 +1,25 @@
import { import {
compileExpression as cExp,
compileCode as cCode, compileCode as cCode,
} from "@nx-js/compiler-util" } from "@nx-js/compiler-util"
import { includes } from "lodash/fp"
export const compileCode = code => { export const compileCode = code => {
let func let func
let safeCode
if (includes("return ")(code)) {
safeCode = code
} else {
let trimmed = code.trim()
trimmed = trimmed.endsWith(";")
? trimmed.substring(0, trimmed.length - 1)
: trimmed
safeCode = `return (${trimmed})`
}
try { try {
func = cCode(code) func = cCode(safeCode)
} catch (e) { } catch (e) {
e.message = `Error compiling code : ${code} : ${e.message}` e.message = `Error compiling code : ${code} : ${e.message}`
throw e throw e
@ -15,16 +27,3 @@ export const compileCode = code => {
return func return func
} }
export const compileExpression = code => {
let func
try {
func = cExp(code)
} catch (e) {
e.message = `Error compiling expression : ${code} : ${e.message}`
throw e
}
return func
}

View File

@ -1,5 +1,5 @@
import { has, isNumber, isUndefined } from "lodash/fp" import { has, isNumber, isUndefined } from "lodash/fp"
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { safeKey, apiWrapper, events, isNonEmptyString } from "../common" import { safeKey, apiWrapper, events, isNonEmptyString } from "../common"
import { iterateIndex } from "../indexing/read" import { iterateIndex } from "../indexing/read"
import { import {
@ -147,7 +147,7 @@ const applyItemToAggregateResult = (indexNode, result, item) => {
const thisGroupResult = result[aggGroup.name] const thisGroupResult = result[aggGroup.name]
if (isNonEmptyString(aggGroup.condition)) { if (isNonEmptyString(aggGroup.condition)) {
if (!compileExpression(aggGroup.condition)({ record: item })) { if (!compileCode(aggGroup.condition)({ record: item })) {
continue continue
} }
} }

View File

@ -1,5 +1,5 @@
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { isUndefined, keys, cloneDeep, isFunction } from "lodash/fp" import { isUndefined, keys, cloneDeep, isFunction, includes } from "lodash/fp"
import { defineError } from "../common" import { defineError } from "../common"
export const filterEval = "FILTER_EVALUATE" export const filterEval = "FILTER_EVALUATE"
@ -16,7 +16,7 @@ const getEvaluateResult = () => ({
result: null, result: null,
}) })
export const compileFilter = index => compileExpression(index.filter) export const compileFilter = index => compileCode(index.filter)
export const compileMap = index => compileCode(index.map) export const compileMap = index => compileCode(index.map)
@ -46,6 +46,9 @@ export const mapRecord = (record, index) => {
if (isFunction(mapped[key])) { if (isFunction(mapped[key])) {
delete mapped[key] delete mapped[key]
} }
if (key === "IsNew") {
delete mapped.IsNew
}
} }
mapped.key = record.key mapped.key = record.key

View File

@ -1,4 +1,4 @@
import { compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { filter, includes, map, last } from "lodash/fp" import { filter, includes, map, last } from "lodash/fp"
import { import {
getActualKeyOfParent, getActualKeyOfParent,

View File

@ -1,5 +1,5 @@
import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp" import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp"
import { compileExpression } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import _ from "lodash" import _ from "lodash"
import { getExactNodeForKey } from "../templateApi/hierarchy" import { getExactNodeForKey } from "../templateApi/hierarchy"
import { validateFieldParse, validateTypeConstraints } from "../types" import { validateFieldParse, validateTypeConstraints } from "../types"
@ -35,7 +35,7 @@ const validateAllTypeConstraints = async (record, recordNode, context) => {
const runRecordValidationRules = (record, recordNode) => { const runRecordValidationRules = (record, recordNode) => {
const runValidationRule = rule => { const runValidationRule = rule => {
const isValid = compileExpression(rule.expressionWhenValid) const isValid = compileCode(rule.expressionWhenValid)
const expressionContext = { record, _ } const expressionContext = { record, _ }
return isValid(expressionContext) return isValid(expressionContext)
? { valid: true } ? { valid: true }

View File

@ -23,7 +23,7 @@ export const canDeleteIndex = indexNode => {
} }
return obj return obj
},[]), },[]),
map(f => `field ${f.name} on record ${f.record.name} uses this index as a reference`) map(f => `field "${f.name}" on record "${f.record.name}" uses this index as a reference`)
]) ])
const lookupIndexes = $(flatHierarchy,[ const lookupIndexes = $(flatHierarchy,[
@ -37,7 +37,7 @@ export const canDeleteIndex = indexNode => {
} }
return obj return obj
},[]), },[]),
map(f => `field ${f.name} on record ${f.record.name} uses this index as a lookup`) map(f => `field "${f.name}" on record "${f.record.name}" uses this index as a lookup`)
]) ])
const errors = [ const errors = [

View File

@ -20,15 +20,14 @@ export const canDeleteRecord = recordNode => {
]) ])
const belongsToAncestor = i => const belongsToAncestor = i =>
ancestors.includes(i.parent()) ancestors.includes(i.parent())
const errorsForNode = node => { const errorsForNode = node => {
const errorsThisNode = $(flatHierarchy, [ const errorsThisNode = $(flatHierarchy, [
filter(i => isAncestorIndex(i) filter(i => isAncestorIndex(i)
&& belongsToAncestor(i) && belongsToAncestor(i)
&& includes(node.nodeId)(i.allowedRecordNodeIds)), && includes(node.nodeId)(i.allowedRecordNodeIds)),
map(i => `index ${i.name} indexes this record. Please remove the record from allowedRecordIds, or delete the index`) map(i => `index "${i.name}" indexes this record. Please remove the record from the index, or delete the index`)
]) ])
for (let child of node.children) { for (let child of node.children) {
@ -40,5 +39,7 @@ export const canDeleteRecord = recordNode => {
return errorsThisNode return errorsThisNode
} }
return errorsForNode(recordNode) const errors = errorsForNode(recordNode)
return { errors, canDelete: errors.length === 0 }
} }

View File

@ -160,16 +160,17 @@ export const getNewRootLevel = () =>
}) })
const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => { const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => {
const nodeId = getNodeId(parent)
const node = constructNode(parent, { const node = constructNode(parent, {
name, name,
type: "record", type: "record",
fields: [], fields: [],
children: [], children: [],
validationRules: [], validationRules: [],
nodeId: getNodeId(parent), nodeId: nodeId,
indexes: [], indexes: [],
estimatedRecordCount: isRecord(parent) ? 500 : 1000000, estimatedRecordCount: isRecord(parent) ? 500 : 1000000,
collectionName: "", collectionName: (nodeId || "").toString(),
isSingle, isSingle,
}) })

View File

@ -11,7 +11,7 @@ import {
isEmpty, isEmpty,
has, has,
} from "lodash/fp" } from "lodash/fp"
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { import {
$, $,
isSomething, isSomething,
@ -73,7 +73,7 @@ const aggregateGroupRules = [
"condition does not compile", "condition does not compile",
a => a =>
isEmpty(a.condition) || isEmpty(a.condition) ||
executesWithoutException(() => compileExpression(a.condition)) executesWithoutException(() => compileCode(a.condition))
), ),
] ]
@ -196,7 +196,7 @@ const triggerRules = actions => [
t => { t => {
if (!t.condition) return true if (!t.condition) return true
try { try {
compileExpression(t.condition) compileCode(t.condition)
return true return true
} catch (_) { } catch (_) {
return false return false

View File

@ -1,5 +1,5 @@
import { flatten, map, isEmpty } from "lodash/fp" import { flatten, map, isEmpty } from "lodash/fp"
import { compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { isNonEmptyString, executesWithoutException, $ } from "../common" import { isNonEmptyString, executesWithoutException, $ } from "../common"
import { applyRuleSet, makerule } from "../common/validationCommon" import { applyRuleSet, makerule } from "../common/validationCommon"

View File

@ -2,6 +2,7 @@ import {
setupApphierarchy, setupApphierarchy,
basicAppHierarchyCreator_WithFields, basicAppHierarchyCreator_WithFields,
stubEventHandler, stubEventHandler,
basicAppHierarchyCreator_WithFields_AndIndexes,
} from "./specHelpers" } from "./specHelpers"
import { canDeleteIndex } from "../src/templateApi/canDeleteIndex" import { canDeleteIndex } from "../src/templateApi/canDeleteIndex"
import { canDeleteRecord } from "../src/templateApi/canDeleteRecord" import { canDeleteRecord } from "../src/templateApi/canDeleteRecord"
@ -49,15 +50,37 @@ describe("canDeleteIndex", () => {
describe("canDeleteRecord", () => { describe("canDeleteRecord", () => {
it("should return no errors when deletion is valid", () => { it("should return no errors when deletion is valid", async () => {
const { appHierarchy } = await setupApphierarchy( const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields basicAppHierarchyCreator_WithFields
) )
appHierarchy.root. appHierarchy.root.indexes = appHierarchy.root.indexes.filter(i => !i.allowedRecordNodeIds.includes(appHierarchy.customerRecord.nodeId))
const result = canDeleteIndex(appHierarchy.customerRecord) const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(true) expect(result.canDelete).toBe(true)
expect(result.errors).toEqual([]) expect(result.errors).toEqual([])
}) })
it("should return errors when record is referenced by hierarchal index", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(false)
expect(result.errors.some(e => e.includes("customer_index"))).toBe(true)
})
it("should return errors when record has a child which cannot be deleted", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields_AndIndexes
)
const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(false)
expect(result.errors.some(e => e.includes("Outstanding Invoices"))).toBe(true)
})
}) })

View File

@ -25,7 +25,7 @@ describe("hierarchy node creation", () => {
expect(record.validationRules).toEqual([]) expect(record.validationRules).toEqual([])
expect(record.indexes).toEqual([]) expect(record.indexes).toEqual([])
expect(record.parent()).toBe(root) expect(record.parent()).toBe(root)
expect(record.collectionName).toBe("") expect(record.collectionName).toBe(record.nodeId.toString())
expect(record.estimatedRecordCount).toBe(1000000) expect(record.estimatedRecordCount).toBe(1000000)
expect(record.isSingle).toBe(false) expect(record.isSingle).toBe(false)

View File

@ -171,6 +171,7 @@ module.exports = (config, app) => {
ctx.request.body.appDefinition, ctx.request.body.appDefinition,
ctx.request.body.accessLevels ctx.request.body.accessLevels
) )
ctx.master.deleteLatestPackageFromCache(ctx.params.appname)
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) })
.post("/_builder/api/:appname/pages/:pageName", async ctx => { .post("/_builder/api/:appname/pages/:pageName", async ctx => {

View File

@ -5,12 +5,16 @@ const { getRuntimePackageDirectory } = require("../utilities/runtimePackages")
const injectPlugins = require("./injectedPlugins") const injectPlugins = require("./injectedPlugins")
const { cwd } = require("process") const { cwd } = require("process")
const appDefinitionPath = appPath => join(appPath, "appDefinition.json")
const pluginsPath = appPath => join(appPath, "plugins.js")
const accessLevelsPath = appPath => join(appPath, "access_levels.json")
const createAppPackage = (context, appPath) => { const createAppPackage = (context, appPath) => {
const appDefModule = require(join(appPath, "appDefinition.json")) const appDefModule = require(appDefinitionPath(appPath))
const pluginsModule = require(join(appPath, "plugins.js")) const pluginsModule = require(pluginsPath(appPath))
const accessLevels = require(join(appPath, "access_levels.json")) const accessLevels = require(accessLevelsPath(appPath))
return { return {
appDefinition: appDefModule, appDefinition: appDefModule,
@ -87,3 +91,11 @@ module.exports.applictionVersionPackage = async (
await injectPlugins(pkg, context.master, appname, instanceKey) await injectPlugins(pkg, context.master, appname, instanceKey)
return pkg return pkg
} }
module.exports.deleteCachedPackage = (context, appname, versionId) => {
const appPath = applictionVersionPath(context, appname, versionId)
delete require.cache[resolve(appDefinitionPath(appPath))]
delete require.cache[resolve(pluginsPath(appPath))]
delete require.cache[resolve(accessLevelsPath(appPath))]
}

View File

@ -11,8 +11,9 @@ const {
masterAppPackage, masterAppPackage,
applictionVersionPackage, applictionVersionPackage,
applictionVersionPublicPaths, applictionVersionPublicPaths,
deleteCachedPackage,
} = require("../utilities/createAppPackage") } = require("../utilities/createAppPackage")
const { determineVersionId } = require("./runtimePackages") const { determineVersionId, LATEST_VERSIONID } = require("./runtimePackages")
const isMaster = appname => appname === "_master" const isMaster = appname => appname === "_master"
@ -345,6 +346,10 @@ module.exports = async context => {
await bb.recordApi.save(userInMaster) await bb.recordApi.save(userInMaster)
} }
const deleteLatestPackageFromCache = (appname) => {
deleteCachedPackage(context, appname, LATEST_VERSIONID)
}
const listApplications = () => values(applications) const listApplications = () => values(applications)
return { return {
@ -364,5 +369,6 @@ module.exports = async context => {
getFullAccessApiForInstanceId, getFullAccessApiForInstanceId,
getFullAccessApiForMaster, getFullAccessApiForMaster,
getApplicationWithInstances, getApplicationWithInstances,
deleteLatestPackageFromCache,
} }
} }