diff --git a/packages/builder/src/builderStore/fetchBindableProperties.js b/packages/builder/src/builderStore/fetchBindableProperties.js
index 92359ae630..fbb3b1b30c 100644
--- a/packages/builder/src/builderStore/fetchBindableProperties.js
+++ b/packages/builder/src/builderStore/fetchBindableProperties.js
@@ -90,6 +90,8 @@ const contextToBindables = (models, walkResult) => context => {
runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
+ // model / view info
+ model: context.model,
})
// see ModelViewSelect.svelte for the format of context.model
diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js
index aeff3f2528..9111f06bd5 100644
--- a/packages/builder/src/builderStore/storeUtils.js
+++ b/packages/builder/src/builderStore/storeUtils.js
@@ -36,7 +36,8 @@ export const saveCurrentPreviewItem = s =>
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
- const page = s.pages[s.currentPageName]
+ const pageName = s.currentPageName || "main"
+ const page = s.pages[pageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
diff --git a/packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte b/packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte
index 3d6241f0ab..0a76ee1efa 100644
--- a/packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte
+++ b/packages/builder/src/components/backend/DataTable/popovers/FilterPopover.svelte
@@ -79,7 +79,7 @@
}
function fieldOptions(field) {
- return viewModel.schema[field].type === "string"
+ return viewModel.schema[field].type === "options"
? viewModel.schema[field].constraints.inclusion
: [true, false]
}
diff --git a/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte
index c4b62d6cec..ed369f45d3 100644
--- a/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte
+++ b/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte
@@ -41,7 +41,6 @@
.map(template => template.create())
for (let screen of screens) {
- console.log(JSON.stringify(screen))
try {
await store.createScreen(screen)
} catch (_) {
diff --git a/packages/builder/src/components/settings/tabs/APIKeys.svelte b/packages/builder/src/components/settings/tabs/APIKeys.svelte
index 8a3c9fc710..8e569596a4 100644
--- a/packages/builder/src/components/settings/tabs/APIKeys.svelte
+++ b/packages/builder/src/components/settings/tabs/APIKeys.svelte
@@ -1,6 +1,7 @@
diff --git a/packages/builder/src/components/userInterface/pagesParsing/createProps.js b/packages/builder/src/components/userInterface/pagesParsing/createProps.js
index 44d23c5c3e..b628b6e15b 100644
--- a/packages/builder/src/components/userInterface/pagesParsing/createProps.js
+++ b/packages/builder/src/components/userInterface/pagesParsing/createProps.js
@@ -1,4 +1,4 @@
-import { isString, isUndefined } from "lodash/fp"
+import { isString, isUndefined, cloneDeep } from "lodash/fp"
import { TYPE_MAP } from "./types"
import { assign } from "lodash"
import { uuid } from "builderStore/uuid"
@@ -83,13 +83,13 @@ const parsePropDef = propDef => {
if (isString(propDef)) {
if (!TYPE_MAP[propDef]) return error(`Type ${propDef} is not recognised.`)
- return TYPE_MAP[propDef].default
+ return cloneDeep(TYPE_MAP[propDef].default)
}
const type = TYPE_MAP[propDef.type]
if (!type) return error(`Type ${propDef.type} is not recognised.`)
- return propDef.default
+ return cloneDeep(propDef.default)
}
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>
diff --git a/packages/builder/src/components/userInterface/pagesParsing/types.js b/packages/builder/src/components/userInterface/pagesParsing/types.js
index b5f073e689..b8867cc8d8 100644
--- a/packages/builder/src/components/userInterface/pagesParsing/types.js
+++ b/packages/builder/src/components/userInterface/pagesParsing/types.js
@@ -10,7 +10,6 @@ export const TYPE_MAP = {
},
options: {
default: [],
- options: [],
},
event: {
default: [],
diff --git a/packages/builder/src/components/userInterface/propertyCategories.js b/packages/builder/src/components/userInterface/propertyCategories.js
index 9c622a383a..bae44df7f5 100644
--- a/packages/builder/src/components/userInterface/propertyCategories.js
+++ b/packages/builder/src/components/userInterface/propertyCategories.js
@@ -361,19 +361,18 @@ export const typography = [
label: "Font",
key: "font-family",
control: OptionSelect,
- defaultValue: "initial",
+ defaultValue: "Arial",
options: [
- "initial",
"Arial",
"Arial Black",
"Cursive",
"Courier",
"Comic Sans MS",
"Helvetica",
+ "Helvetica Neue",
"Impact",
"Inter",
"Lucida Sans Unicode",
- "Open Sans",
"Roboto",
"Roboto Mono",
"Times New Roman",
@@ -467,9 +466,9 @@ export const background = [
label: "Gradient",
key: "background-image",
control: OptionSelect,
- defaultValue: "None",
+ defaultValue: "",
options: [
- { label: "None", value: "None" },
+ { label: "Select option", value: "" },
{
label: "Warm Flame",
value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
@@ -518,9 +517,9 @@ export const background = [
},
{
label: "Image",
- key: "background-image",
+ key: "background",
control: Input,
- placeholder: "Src",
+ placeholder: "url",
},
]
@@ -665,7 +664,7 @@ export const transitions = [
control: OptionSelect,
textAlign: "center",
placeholder: "sec",
- options: ["0.2ms", "0.4ms", "0.8ms", "1s", "2s", "4s"],
+ options: ["0.4s", "0.6s", "0.8s", "1s", "2s", "4s"],
},
{
label: "Ease",
diff --git a/packages/builder/src/components/userInterface/temporaryPanelStructure.js b/packages/builder/src/components/userInterface/temporaryPanelStructure.js
index 6585a375d2..8424811b46 100644
--- a/packages/builder/src/components/userInterface/temporaryPanelStructure.js
+++ b/packages/builder/src/components/userInterface/temporaryPanelStructure.js
@@ -386,7 +386,7 @@ export default {
{
label: "destinationUrl",
key: "destinationUrl",
- control: Input,
+ control: ScreenSelect,
placeholder: "/table/_id",
},
],
@@ -435,7 +435,7 @@ export default {
{
label: "Link Url",
key: "linkUrl",
- control: Input,
+ control: ScreenSelect,
placeholder: "Link URL",
},
{
@@ -510,7 +510,7 @@ export default {
{
label: "Link Url",
key: "linkUrl",
- control: Input,
+ control: ScreenSelect,
placeholder: "Link URL",
},
{
diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js
index 55a2d2c6c7..a735e4b864 100644
--- a/packages/builder/src/constants/backend/index.js
+++ b/packages/builder/src/constants/backend/index.js
@@ -15,7 +15,7 @@ export const FIELDS = {
type: "options",
constraints: {
type: "string",
- presence: { allowEmpty: true },
+ presence: false,
inclusion: [],
},
},
@@ -67,7 +67,7 @@ export const FIELDS = {
type: "link",
constraints: {
type: "array",
- presence: { allowEmpty: true },
+ presence: false,
},
},
}
diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js
index 87a2449f76..d9277c2f3d 100644
--- a/packages/server/src/api/controllers/model.js
+++ b/packages/server/src/api/controllers/model.js
@@ -33,6 +33,7 @@ exports.save = async function(ctx) {
views: {},
...rest,
}
+ let renameDocs = []
// if the model obj had an _id then it will have been retrieved
const oldModel = ctx.preExisting
@@ -49,14 +50,11 @@ exports.save = async function(ctx) {
include_docs: true,
})
)
-
- const docs = records.rows.map(({ doc }) => {
+ renameDocs = records.rows.map(({ doc }) => {
doc[_rename.updated] = doc[_rename.old]
delete doc[_rename.old]
return doc
})
-
- await db.bulkDocs(docs)
delete modelToSave._rename
}
@@ -69,9 +67,6 @@ exports.save = async function(ctx) {
modelView.schema = modelToSave.schema
}
- const result = await db.post(modelToSave)
- modelToSave._rev = result.rev
-
// update linked records
await linkRecords.updateLinks({
instanceId,
@@ -82,6 +77,14 @@ exports.save = async function(ctx) {
oldModel: oldModel,
})
+ // don't perform any updates until relationships have been
+ // checked by the updateLinks function
+ if (renameDocs.length !== 0) {
+ await db.bulkDocs(renameDocs)
+ }
+ const result = await db.post(modelToSave)
+ modelToSave._rev = result.rev
+
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
@@ -105,11 +108,8 @@ exports.save = async function(ctx) {
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
-
const modelToDelete = await db.get(ctx.params.modelId)
- await db.remove(modelToDelete)
-
// Delete all records for that model
const records = await db.allDocs(
getRecordParams(ctx.params.modelId, null, {
@@ -117,7 +117,7 @@ exports.destroy = async function(ctx) {
})
)
await db.bulkDocs(
- records.rows.map(record => ({ _id: record.id, _deleted: true }))
+ records.rows.map(record => ({ ...record.doc, _deleted: true }))
)
// update linked records
@@ -127,6 +127,9 @@ exports.destroy = async function(ctx) {
model: modelToDelete,
})
+ // don't remove the table itself until very end
+ await db.remove(modelToDelete)
+
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:delete`, instanceId, modelToDelete)
ctx.status = 200
diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js
index f14b794210..5aaa9ab125 100644
--- a/packages/server/src/api/controllers/static.js
+++ b/packages/server/src/api/controllers/static.js
@@ -136,7 +136,7 @@ exports.performLocalFileProcessing = async function(ctx) {
}
exports.serveApp = async function(ctx) {
- const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
+ const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
// default to homedir
const appPath = resolve(
@@ -154,7 +154,7 @@ exports.serveApp = async function(ctx) {
// only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit
const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId)
- if (looksLikeAppId && !ctx.isAuthenticated) {
+ if (looksLikeAppId && !ctx.auth.authenticated) {
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
@@ -200,7 +200,7 @@ exports.serveAttachment = async function(ctx) {
exports.serveAppAsset = async function(ctx) {
// default to homedir
- const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
+ const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated"
const appPath = resolve(
budibaseAppsDir(),
diff --git a/packages/server/src/app.js b/packages/server/src/app.js
index 7560c9cfa4..4157534365 100644
--- a/packages/server/src/app.js
+++ b/packages/server/src/app.js
@@ -24,6 +24,7 @@ app.use(
)
app.context.eventEmitter = eventEmitter
+app.context.auth = {}
// api routes
app.use(api.routes())
diff --git a/packages/server/src/db/linkedRecords/LinkController.js b/packages/server/src/db/linkedRecords/LinkController.js
index b006e798c2..48e459a1cb 100644
--- a/packages/server/src/db/linkedRecords/LinkController.js
+++ b/packages/server/src/db/linkedRecords/LinkController.js
@@ -161,7 +161,7 @@ class LinkController {
})
// now add the docs to be deleted to the bulk operation
operations.push(...toDeleteDocs)
- // replace this field with a simple entry to denote there are links
+ // remove the field from this row, link doc will be added to record on way out
delete record[fieldName]
}
}
@@ -234,8 +234,16 @@ class LinkController {
for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName]
if (field.type === "link") {
+ // handle this in a separate try catch, want
+ // the put to bubble up as an error, if can't update
+ // table for some reason
+ let linkedModel
+ try {
+ linkedModel = await this._db.get(field.modelId)
+ } catch (err) {
+ continue
+ }
// create the link field in the other model
- const linkedModel = await this._db.get(field.modelId)
linkedModel.schema[field.fieldName] = {
name: field.fieldName,
type: "link",
diff --git a/packages/server/src/db/linkedRecords/index.js b/packages/server/src/db/linkedRecords/index.js
index ba93f3d2e8..09a58c1062 100644
--- a/packages/server/src/db/linkedRecords/index.js
+++ b/packages/server/src/db/linkedRecords/index.js
@@ -42,6 +42,7 @@ exports.updateLinks = async function({
model,
oldModel,
}) {
+ const baseReturnObj = record == null ? model : record
if (instanceId == null) {
throw "Cannot operate without an instance ID."
}
@@ -50,12 +51,16 @@ exports.updateLinks = async function({
arguments[0].modelId = model._id
}
let linkController = new LinkController(arguments[0])
- if (
- !(await linkController.doesModelHaveLinkedFields()) &&
- (oldModel == null ||
- !(await linkController.doesModelHaveLinkedFields(oldModel)))
- ) {
- return record
+ try {
+ if (
+ !(await linkController.doesModelHaveLinkedFields()) &&
+ (oldModel == null ||
+ !(await linkController.doesModelHaveLinkedFields(oldModel)))
+ ) {
+ return baseReturnObj
+ }
+ } catch (err) {
+ return baseReturnObj
}
switch (eventType) {
case EventType.RECORD_SAVE:
diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js
index f63d1fa4cb..56572e1170 100644
--- a/packages/server/src/db/utils.js
+++ b/packages/server/src/db/utils.js
@@ -57,21 +57,26 @@ exports.generateModelID = () => {
/**
* Gets the DB allDocs/query params for retrieving a record.
- * @param {string} modelId The model in which the records have been stored.
+ * @param {string|null} modelId The model in which the records have been stored.
* @param {string|null} recordId The ID of the record which is being specifically queried for. This can be
* left null to get all the records in the model.
* @param {object} otherProps Any other properties to add to the request.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
-exports.getRecordParams = (modelId, recordId = null, otherProps = {}) => {
+exports.getRecordParams = (
+ modelId = null,
+ recordId = null,
+ otherProps = {}
+) => {
if (modelId == null) {
- throw "Cannot build params for records without a model ID"
+ return getDocParams(DocumentTypes.RECORD, null, otherProps)
+ } else {
+ const endOfKey =
+ recordId == null
+ ? `${modelId}${SEPARATOR}`
+ : `${modelId}${SEPARATOR}${recordId}`
+ return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
}
- const endOfKey =
- recordId == null
- ? `${modelId}${SEPARATOR}`
- : `${modelId}${SEPARATOR}${recordId}`
- return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
}
/**
@@ -124,7 +129,14 @@ exports.generateAutomationID = () => {
* @returns {string} The new link doc ID which the automation doc can be stored under.
*/
exports.generateLinkID = (modelId1, modelId2, recordId1, recordId2) => {
- return `${DocumentTypes.AUTOMATION}${SEPARATOR}${modelId1}${SEPARATOR}${modelId2}${SEPARATOR}${recordId1}${SEPARATOR}${recordId2}`
+ return `${DocumentTypes.LINK}${SEPARATOR}${modelId1}${SEPARATOR}${modelId2}${SEPARATOR}${recordId1}${SEPARATOR}${recordId2}`
+}
+
+/**
+ * Gets parameters for retrieving link docs, this is a utility function for the getDocParams function.
+ */
+exports.getLinkParams = (otherProps = {}) => {
+ return getDocParams(DocumentTypes.LINK, null, otherProps)
}
/**
diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js
index 93ee66b6d4..1203ea0033 100644
--- a/packages/server/src/middleware/authenticated.js
+++ b/packages/server/src/middleware/authenticated.js
@@ -20,8 +20,10 @@ module.exports = async (ctx, next) => {
if (builderToken) {
try {
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
- ctx.apiKey = jwtPayload.apiKey
- ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID
+ ctx.auth = {
+ apiKey: jwtPayload.apiKey,
+ authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID,
+ }
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
@@ -38,14 +40,13 @@ module.exports = async (ctx, next) => {
}
if (!appToken) {
- ctx.isAuthenticated = false
+ ctx.auth.authenticated = false
await next()
return
}
try {
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
- ctx.apiKey = jwtPayload.apiKey
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(
@@ -53,7 +54,10 @@ module.exports = async (ctx, next) => {
jwtPayload.accessLevelId
),
}
- ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID
+ ctx.auth = {
+ authenticated: ctx.user.accessLevelId !== ANON_LEVEL_ID,
+ apiKey: jwtPayload.apiKey,
+ }
} catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
}
diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js
index 4cce4c4670..bd09029471 100644
--- a/packages/server/src/middleware/authorized.js
+++ b/packages/server/src/middleware/authorized.js
@@ -5,9 +5,36 @@ const {
BUILDER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
+const environment = require("../environment")
+const { apiKeyTable } = require("../db/dynamoClient")
module.exports = (permName, getItemId) => async (ctx, next) => {
- if (!ctx.isAuthenticated) {
+ if (
+ environment.CLOUD &&
+ ctx.headers["x-api-key"] &&
+ ctx.headers["x-instanceid"]
+ ) {
+ // api key header passed by external webhook
+ const apiKeyInfo = await apiKeyTable.get({
+ primary: ctx.headers["x-api-key"],
+ })
+
+ if (apiKeyInfo) {
+ ctx.auth = {
+ authenticated: true,
+ external: true,
+ apiKey: ctx.headers["x-api-key"],
+ }
+ ctx.user = {
+ instanceId: ctx.headers["x-instanceid"],
+ }
+ return next()
+ }
+
+ ctx.throw(403, "API key invalid")
+ }
+
+ if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated")
}
diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js
index e82305dc12..778f51f9d8 100644
--- a/packages/server/src/middleware/usageQuota.js
+++ b/packages/server/src/middleware/usageQuota.js
@@ -55,7 +55,7 @@ module.exports = async (ctx, next) => {
return next()
}
try {
- await usageQuota.update(ctx.apiKey, property, usage)
+ await usageQuota.update(ctx.auth.apiKey, property, usage)
return next()
} catch (err) {
ctx.throw(403, err)