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 @@ + {#each urls as url} + {/each} 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)