diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 0380f92253..75e890abc2 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -45,9 +45,10 @@ jobs: - name: Build and Push Staging Docker Image # Only run on push - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build yarn build:docker:staging env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95bcd55dff..edfb7baa7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,11 @@ on: push: branches: - master + +env: + POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} + POSTHOG_URL: ${{ secrets.POSTHOG_URL }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} jobs: release: @@ -18,10 +23,6 @@ jobs: - run: yarn lint - run: yarn bootstrap - run: yarn build - env: - POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} - POSTHOG_URL: ${{ secrets.POSTHOG_URL }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - run: yarn test - name: Configure AWS Credentials diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index b03b433cef..7fb78c3ef3 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -6,6 +6,7 @@ services: app-service: restart: always image: budibase/apps + container_name: bbapps ports: - "${APP_PORT}:4002" environment: @@ -32,6 +33,7 @@ services: worker-service: restart: always image: budibase/worker + container_name: bbworker ports: - "${WORKER_PORT}:4003" environment: @@ -118,7 +120,7 @@ services: image: containrrr/watchtower volumes: - /var/run/docker.sock:/var/run/docker.sock - command: --debug --http-api-update budibase/apps budibase/worker + command: --debug --http-api-update bbapps bbworker environment: - WATCHTOWER_HTTP_API=true - WATCHTOWER_HTTP_API_TOKEN=budibase diff --git a/hosting/scripts/linux/release-to-docker-hub.sh b/hosting/scripts/linux/release-to-docker-hub.sh index a011ee1f13..b1a79c964c 100755 --- a/hosting/scripts/linux/release-to-docker-hub.sh +++ b/hosting/scripts/linux/release-to-docker-hub.sh @@ -3,7 +3,7 @@ tag=$1 tag=${tag:-latest} -echo "Tagging images with SHA: $GITHUB_SHA and version: $BUDIBASE_VERSION" +echo "Tagging images with SHA: $GITHUB_SHA and tag: $tag" docker tag app-service budibase/apps:$tag docker tag worker-service budibase/worker:$tag diff --git a/lerna.json b/lerna.json index cdd9b1316c..51936dfd42 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.24", + "version": "0.9.34", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 61c81ecb63..76cd42b7cd 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,6 @@ "test:e2e": "lerna run cy:test", "test:e2e:ci": "lerna run cy:ci", "build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -", - "build:docker:staging": "cd hosting/scripts/linux/ && ./release-to-docker-hub.sh staging && cd -" + "build:docker:staging": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh staging && cd -" } } diff --git a/packages/auth/package.json b/packages/auth/package.json index dd66ac16fe..88883d993b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.24", + "version": "0.9.34", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 93217277e7..100dc005c8 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -1,6 +1,5 @@ const { newid } = require("../hashing") const Replication = require("./Replication") -const { getCouch } = require("./index") const UNICODE_MAX = "\ufff0" const SEPARATOR = "_" @@ -164,14 +163,14 @@ exports.getDeployedAppID = appId => { * different users/companies apps as there is no security around it - all apps are returned. * @return {Promise} returns the app information document stored in each app database. */ -exports.getAllApps = async ({ dev, all } = {}) => { - const CouchDB = getCouch() +exports.getAllApps = async ({ CouchDB, dev, all } = {}) => { let allDbs = await CouchDB.allDbs() const appDbNames = allDbs.filter(dbName => dbName.startsWith(exports.APP_PREFIX) ) const appPromises = appDbNames.map(db => - new CouchDB(db).get(DocumentTypes.APP_METADATA) + // skip setup otherwise databases could be re-created + new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA) ) if (appPromises.length === 0) { return [] @@ -196,6 +195,21 @@ exports.getAllApps = async ({ dev, all } = {}) => { } } +exports.dbExists = async (CouchDB, dbName) => { + let exists = false + try { + const db = CouchDB(dbName, { skip_setup: true }) + // check if database exists + const info = await db.info() + if (info && !info.error) { + exists = true + } + } catch (err) { + exists = false + } + return exists +} + /** * Generates a new configuration ID. * @returns {string} The new configuration ID which the config doc can be stored under. diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index 96a0907536..f84da95e73 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -20,16 +20,10 @@ async function authenticate(token, tokenSecret, profile, done) { // use the google profile id dbUser = await db.get(userId) } catch (err) { - console.log("Google user not found. Creating..") - - // create the user const user = { _id: userId, provider: profile.provider, roles: {}, - builder: { - global: true, - }, ...profile._json, } @@ -50,12 +44,18 @@ async function authenticate(token, tokenSecret, profile, done) { user.roles = existing.roles user.builder = existing.builder user.admin = existing.admin + + const response = await db.post(user) + dbUser = user + dbUser._rev = response.rev + } else { + return done( + new Error( + "email does not yet exist. You must set up your local budibase account first." + ), + false + ) } - - const response = await db.post(user) - - dbUser = user - dbUser._rev = response.rev } // authenticate diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index a157332ae5..80875fdfee 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -159,7 +159,7 @@ exports.upload = async ({ * Similar to the upload function but can be used to send a file stream * through to the object store. */ -exports.streamUpload = async (bucketName, filename, stream) => { +exports.streamUpload = async (bucketName, filename, stream, extra = {}) => { const objectStore = exports.ObjectStore(bucketName) await exports.makeSureBucketExists(objectStore, bucketName) @@ -167,6 +167,7 @@ exports.streamUpload = async (bucketName, filename, stream) => { Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(filename), Body: stream, + ...extra, } return objectStore.upload(params).promise() } diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index d652c25b00..53e1b90d73 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -13,7 +13,6 @@ const BUILTIN_IDS = { POWER: "POWER", BASIC: "BASIC", PUBLIC: "PUBLIC", - BUILDER: "BUILDER", } // exclude internal roles like builder diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 550a4cd4c1..b2a54260ae 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.24", + "version": "0.9.34", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index 4ead9ed311..d7941b2518 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -37,3 +37,9 @@ {/each} {/if} + + diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json index 669ee5af34..0908f2c839 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -1,9 +1,9 @@ { - "baseUrl": "http://localhost:10000/builder/", + "baseUrl": "http://localhost:10001/builder/", "video": true, "projectId": "bmbemn", "env": { - "PORT": "10000", + "PORT": "10001", "JWT_SECRET": "test" } } diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index b8aab9d52e..75611eff06 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -8,31 +8,27 @@ context("Create a automation", () => { it("should create a automation", () => { cy.createTestTableWithData() - cy.contains("automate").click() - cy.get("[data-cy=new-automation]").click() - cy.get(".modal").within(() => { + cy.contains("Automate").click() + cy.get("[data-cy='new-screen'] > .spectrum-Icon").click() + cy.get(".spectrum-Dialog-grid").within(() => { cy.get("input").type("Add Row") - cy.get(".buttons") - .contains("Create") - .click() + cy.get(".spectrum-Button--cta").click() }) // Add trigger cy.contains("Trigger").click() cy.contains("Row Created").click() cy.get(".setup").within(() => { - cy.get("select") - .first() - .select("dog") + cy.get(".spectrum-Picker-label").click() + cy.contains("dog").click() }) // Create action cy.contains("Action").click() cy.contains("Create Row").click() cy.get(".setup").within(() => { - cy.get("select") - .first() - .select("dog") + cy.get(".spectrum-Picker-label").click() + cy.contains("dog").click() cy.get("input") .first() .type("goodboy") @@ -45,12 +41,11 @@ context("Create a automation", () => { cy.contains("Save Automation").click() // Activate Automation - cy.get("[data-cy=activate-automation]").click() - cy.get(".ri-stop-circle-fill.highlighted").should("be.visible") + cy.get("[aria-label=PlayCircle]").click() }) it("should add row when a new row is added", () => { - cy.contains("data").click() + cy.contains("Data").click() cy.addRow(["Rover", 15]) cy.reload() cy.contains("goodboy").should("have.text", "goodboy") diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js index b32584924d..571c6d6326 100644 --- a/packages/builder/cypress/integration/createBinding.spec.js +++ b/packages/builder/cypress/integration/createBinding.spec.js @@ -36,7 +36,9 @@ context("Create Bindings", () => { it("should add a binding with a handlebars helper", () => { cy.addComponent("Elements", "Paragraph").then(componentId => { // Cypress needs to escape curly brackets - addSettingBinding("text", "{{}{{} add 1 2 {}}{}}", false) + cy.get("[data-cy=setting-text] input") + .type("{{}{{} add 1 2 {}}{}}") + .blur() cy.getComponent(componentId).should("have.text", "3") }) }) @@ -51,6 +53,6 @@ const addSettingBinding = (setting, bindingText, clickOption = true) => { } else { cy.get("textarea").type(bindingText) } - cy.get("button").click() + cy.contains("Save").click() }) } diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index 8c63d85575..93867e0f1f 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -1,13 +1,14 @@ -context("Create Components", () => { +// TODO for now components are skipped, might not be good to keep doing this +xcontext("Create Components", () => { let headlineId before(() => { cy.login() cy.createTestApp() cy.createTable("dog") - cy.addColumn("dog", "name", "string") - cy.addColumn("dog", "age", "number") - cy.addColumn("dog", "type", "options") + cy.addColumn("dog", "name", "Text") + cy.addColumn("dog", "age", "Number") + cy.addColumn("dog", "type", "Options") cy.navigateToFrontend() }) diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index f37b445ab2..eda72ba36d 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -22,54 +22,49 @@ context("Create a Table", () => { }) it("updates a column on the table", () => { - cy.contains("header", "name") - .trigger("mouseover") - .find(".ri-pencil-line") - .click({ force: true }) - cy.get(".actions input") - .first() - .type("updated") + cy.get(".title").click() + cy.get(".spectrum-Table-editIcon > use").click() + cy.get("input") + .eq(1) + .type("updated", { force: true }) // Unset table display column - cy.contains("display column").click() + cy.get(".spectrum-Switch-input").eq(1).click() cy.contains("Save Column").click() - cy.contains("nameupdated ").should("have.text", "nameupdated ") + cy.contains("nameupdated ").should("contain", "nameupdated") }) it("edits a row", () => { cy.contains("button", "Edit").click({ force: true }) cy.wait(1000) - cy.get(".modal input").type("Updated") + cy.get(".spectrum-Modal input").type("Updated") cy.contains("Save").click() cy.contains("RoverUpdated").should("have.text", "RoverUpdated") }) it("deletes a row", () => { - cy.get(".ag-checkbox-input").check({ force: true }) + cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.contains("Delete 1 row(s)").click() - cy.get(".modal") + cy.get(".spectrum-Modal") .contains("Delete") .click() cy.contains("RoverUpdated").should("not.exist") }) it("deletes a column", () => { - cy.contains("header", "name") - .trigger("mouseover") - .find(".ri-pencil-line") - .click({ force: true }) + cy.get(".title").click() + cy.get(".spectrum-Table-editIcon > use").click() cy.contains("Delete").click() cy.wait(50) - cy.get(".buttons") - .contains("Delete") + cy.contains("Delete Column") .click() cy.contains("nameupdated").should("not.exist") }) it("deletes a table", () => { - cy.get(".ri-more-line") + cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use") .first() .click({ force: true }) - cy.get("[data-cy=delete-table]").click() + cy.get(".spectrum-Menu > :nth-child(2)").click() cy.contains("Delete Table").click() cy.contains("dog").should("not.exist") }) diff --git a/packages/builder/cypress/integration/createUser.spec.js b/packages/builder/cypress/integration/createUser.spec.js index 54baec79a5..18ae8a16a0 100644 --- a/packages/builder/cypress/integration/createUser.spec.js +++ b/packages/builder/cypress/integration/createUser.spec.js @@ -1,11 +1,10 @@ context("Create a User", () => { before(() => { cy.login() - cy.createTestApp() }) it("should create a user", () => { - cy.createUser("bbuser@test.com", "test", "ADMIN") + cy.createUser("bbuser@test.com") cy.contains("bbuser").should("be.visible") }) }) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index 6c8aa2004d..3aef927e8d 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -17,21 +17,21 @@ context("Create a View", () => { }) it("creates a view", () => { - cy.contains("Create New View").click() - cy.get(".menu-container").within(() => { + cy.contains("Create view").click() + cy.get(".modal-inner-wrapper").within(() => { cy.get("input").type("Test View") - cy.contains("Save View").click() + cy.get("button").contains("Create View").click({ force: true }) }) cy.get(".table-title h1").contains("Test View") - cy.get("[data-cy=table-header]").then($headers => { + cy.get(".title").then($headers => { expect($headers).to.have.length(3) const headers = Array.from($headers).map(header => header.textContent.trim() ) expect(removeSpacing(headers)).to.deep.eq([ - "rating Number", - "age Number", - "group Text", + "group", + "age", + "rating", ]) }) }) @@ -39,97 +39,95 @@ context("Create a View", () => { it("filters the view by age over 10", () => { cy.contains("Filter").click() cy.contains("Add Filter").click() - cy.get(".menu-container") - .find("select") - .first() - .select("age") - cy.get(".menu-container") - .find("select") - .eq(1) - .select("More Than") - cy.get(".menu-container") - .find("input") - .type(18) - cy.contains("Save").click() - cy.get("[role=rowgroup] .ag-row").get($values => { + + cy.get(".modal-inner-wrapper").within(() => { + cy.get(".spectrum-Picker-label").eq(0).click() + cy.contains("age").click({ force: true }) + + cy.get(".spectrum-Picker-label").eq(1).click() + cy.contains("More Than").click({ force: true }) + + cy.get("input").type(18) + cy.contains("Save").click() + }) + + cy.get(".spectrum-Table-row").get($values => { expect($values).to.have.length(5) }) }) it("creates a stats calculation view based on age", () => { - // Required due to responsive bug with ag grid in cypress - cy.viewport("macbook-15") - cy.contains("Calculate").click() - cy.get(".menu-container") - .find("select") - .eq(0) - .select("Statistics") - cy.wait(50) - cy.get(".menu-container") - .find("select") - .eq(1) - .select("age") - cy.contains("Save").click() + cy.get(".modal-inner-wrapper").within(() => { + cy.get(".spectrum-Picker-label").eq(0).click() + cy.contains("Statistics").click() + + cy.get(".spectrum-Picker-label").eq(1).click() + cy.contains("age").click({ force: true }) + + cy.contains("Save").click() + }) cy.wait(1000) - cy.get(".ag-center-cols-viewport").scrollTo("100%") - cy.get("[data-cy=table-header]").then($headers => { + + cy.get(".title").then($headers => { expect($headers).to.have.length(7) const headers = Array.from($headers).map(header => header.textContent.trim() ) expect(removeSpacing(headers)).to.deep.eq([ - "avg Number", - "sumsqr Number", - "count Number", - "max Number", - "min Number", - "sum Number", - "field Text", + "field", + "sum", + "min", + "max", + "count", + "sumsqr", + "avg", ]) }) - cy.get(".ag-cell").then($values => { + cy.get(".spectrum-Table-cell").then($values => { let values = Array.from($values).map(header => header.textContent.trim()) - expect(values).to.deep.eq(["31", "5347", "5", "49", "20", "155", "age"]) + expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"]) }) }) it("groups the view by group", () => { - // Required due to responsive bug with ag grid in cypress - cy.viewport("macbook-15") - - cy.contains("Group By").click() - cy.get("select").select("group") - cy.contains("Save").click() + cy.contains("Group by").click() + cy.get(".modal-inner-wrapper").within(() => { + cy.get(".spectrum-Picker-label").eq(0).click() + cy.contains("group").click() + cy.contains("Save").click() + }) cy.wait(1000) - cy.get(".ag-center-cols-viewport").scrollTo("100%") cy.contains("Students").should("be.visible") cy.contains("Teachers").should("be.visible") - cy.get(".ag-row[row-index=0]") - .find(".ag-cell") - .then($values => { - const values = Array.from($values).map(value => value.textContent) - expect(values.sort()).to.deep.eq( - [ + cy.get(".spectrum-Table-cell").then($values => { + let values = Array.from($values).map(header => header.textContent.trim()) + expect(values).to.deep.eq([ "Students", - "23.333333333333332", - "1650", - "3", - "25", - "20", "70", - ].sort() - ) - }) + "20", + "25", + "3", + "1650", + "23.333333333333332", + "Teachers", + "85", + "36", + "49", + "2", + "3697", + "42.5", + ]) + }) }) it("renames a view", () => { cy.contains(".nav-item", "Test View") - .find(".ri-more-line") + .find(".actions .icon") .click({ force: true }) - cy.get("[data-cy=edit-view]").click() - cy.get(".menu-container").within(() => { + cy.contains("Edit").click() + cy.get(".modal-inner-wrapper").within(() => { cy.get("input").type(" Updated") cy.contains("Save").click() }) @@ -139,9 +137,9 @@ context("Create a View", () => { it("deletes a view", () => { cy.contains(".nav-item", "Test View Updated") - .find(".ri-more-line") + .find(".actions .icon") .click({ force: true }) - cy.get("[data-cy=delete-view]").click() + cy.contains("Delete").click() cy.contains("Delete View").click() cy.wait(1000) cy.contains("TestView Updated").should("not.be.visible") diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js index c0d0dfa110..c55cef2afe 100644 --- a/packages/builder/cypress/setup.js +++ b/packages/builder/cypress/setup.js @@ -3,7 +3,9 @@ const path = require("path") const tmpdir = path.join(require("os").tmpdir(), ".budibase") -const WORKER_PORT = "4002" +// these run on ports we don't normally use so that they can run alongside the +// normal development system +const WORKER_PORT = "10002" const MAIN_PORT = cypressConfig.env.PORT process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE" process.env.NODE_ENV = "cypress" @@ -12,8 +14,8 @@ process.env.PORT = MAIN_PORT process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET process.env.COUCH_URL = `leveldb://${tmpdir}/.data/` process.env.SELF_HOSTED = 1 -process.env.WORKER_URL = "http://localhost:4002/" -process.env.MINIO_URL = "http://localhost:10000/" +process.env.WORKER_URL = "http://localhost:10002/" +process.env.MINIO_URL = `http://localhost:${MAIN_PORT}/` process.env.MINIO_ACCESS_KEY = "budibase" process.env.MINIO_SECRET_KEY = "budibase" process.env.COUCH_DB_USER = "budibase" diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 80d38937ac..0d87697fdf 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -6,80 +6,61 @@ // Cypress.Commands.add("login", () => { - cy.getCookie("budibase:auth").then(cookie => { - // Already logged in - if (cookie) return - - cy.visit(`localhost:${Cypress.env("PORT")}/builder`) - - // cy.get("button").then(btn => { - // const adminUserButton = "Create super admin user" - // console.log(btn.first().first()) - // if (!btn.first().contains(adminUserButton)) { - // // create admin user - // cy.get("input").first().type("test@test.com") - // cy.get('input[type="password"]').first().type("test") - // cy.get('input[type="password"]').eq(1).type("test") - // cy.contains(adminUserButton).click() - // } - - // login - cy.get("input").first().type("test@test.com") - cy.get('input[type="password"]').type("test") - cy.contains("Login").click() - // }) - }) -}) - -Cypress.Commands.add("createApp", name => { cy.visit(`localhost:${Cypress.env("PORT")}/builder`) - // wait for init API calls on visit - cy.wait(100) - cy.contains("Create New Web App").click() - cy.get("body") - .then($body => { - if ($body.find("input[name=apiKey]").length) { - // input was found, do something else here - cy.get("input[name=apiKey]").type(name).should("have.value", name) - cy.contains("Next").click() - } - }) - .then(() => { - cy.get(".spectrum-Modal") - .within(() => { - cy.get("input").eq(0).type(name).should("have.value", name).blur() - cy.contains("Next").click() - cy.get("input").eq(1).type("test@test.com").blur() - cy.get("input").eq(2).type("test").blur() - cy.contains("Submit").click() - }) - .then(() => { - cy.get("[data-cy=new-table]", { - timeout: 20000, - }).should("be.visible") - }) - }) -}) - -Cypress.Commands.add("deleteApp", name => { - cy.visit(`localhost:${Cypress.env("PORT")}/builder`) - cy.get(".apps").then($apps => { - cy.wait(1000) - if ($apps.find(`[data-cy="app-${name}"]`).length) { - cy.get(`[data-cy="app-${name}"]`).contains("Open").click() - cy.get("[data-cy=settings-icon]").click() - cy.get(".spectrum-Dialog").within(() => { - cy.contains("Danger Zone").click() - cy.get("input").type("DELETE").blur() - cy.contains("Delete Entire App").click() + cy.wait(500) + cy.url().then(url => { + if (url.includes("builder/admin")) { + // create admin user + cy.get("input").first().type("test@test.com") + cy.get('input[type="password"]').first().type("test") + cy.get('input[type="password"]').eq(1).type("test") + cy.contains("Create super admin user").click() + } + if (url.includes("builder/auth/login") || url.includes("builder/admin")) { + // login + cy.contains("Sign in to Budibase").then(() => { + cy.get("input").first().type("test@test.com") + cy.get('input[type="password"]').type("test") + cy.get("button").first().click() }) } }) }) +Cypress.Commands.add("createApp", name => { + cy.visit(`localhost:${Cypress.env("PORT")}/builder`) + cy.wait(500) + cy.contains(/Create (new )?app/).click() + cy.get(".spectrum-Modal") + .within(() => { + cy.get("input").eq(0).type(name).should("have.value", name).blur() + cy.contains("Create app").click() + }) + .then(() => { + cy.get("[data-cy=new-table]", { + timeout: 20000, + }).should("be.visible") + }) +}) + +Cypress.Commands.add("deleteApp", () => { + cy.visit(`localhost:${Cypress.env("PORT")}/builder`) + cy.wait(1000) + cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) + .its("body") + .then(val => { + console.log(val) + if (val.length > 0) { + cy.get(".hoverable > use").click() + cy.contains("Delete").click() + cy.get(".spectrum-Button--warning").click() + } + }) +}) + Cypress.Commands.add("createTestApp", () => { const appName = "Cypress Tests" - cy.deleteApp(appName) + cy.deleteApp() cy.createApp(appName, "This app is used for Cypress testing.") }) @@ -110,8 +91,10 @@ Cypress.Commands.add("addColumn", (tableName, columnName, type) => { // Unset table display column cy.contains("display column").click({ force: true }) - cy.get("select").select(type) - cy.contains("Save").click() + cy.get(".spectrum-Picker-label").click() + cy.contains(type).click() + + cy.contains("Save Column").click() }) }) @@ -125,18 +108,18 @@ Cypress.Commands.add("addRow", values => { }) }) -Cypress.Commands.add("createUser", (email, password, role) => { - // Create User +Cypress.Commands.add("createUser", email => { + // quick hacky recorded way to create a user cy.contains("Users").click() - cy.contains("Create user").click() - cy.get(".spectrum-Modal").within(() => { - cy.get("input").first().type(email).blur() - cy.get("input").eq(1).type(password).blur() - cy.get("select").first().select(role) - - // Save - cy.get(".spectrum-ButtonGroup").contains("Create User").click() - }) + cy.get(".spectrum-Button--primary").click() + cy.get(".spectrum-Picker-label").click() + cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click() + cy.get( + ":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input" + ) + .first() + .type(email, { force: true }) + cy.get(".spectrum-Button--cta").click({ force: true }) }) Cypress.Commands.add("addComponent", (category, component) => { @@ -165,17 +148,16 @@ Cypress.Commands.add("getComponent", componentId => { }) Cypress.Commands.add("navigateToFrontend", () => { - cy.contains("design").click() + cy.wait(1000) + cy.contains("Design").click() }) Cypress.Commands.add("createScreen", (screenName, route) => { - cy.get("[data-cy=new-screen]").click() + cy.get("[aria-label=AddCircle]").click() cy.get(".spectrum-Modal").within(() => { - cy.get("input").eq(0).type(screenName).blur() - if (route) { - cy.get("input").eq(1).type(route).blur() - } - cy.contains("Create Screen").click() + cy.get("input").first().type(screenName) + cy.get("input").eq(1).type(route) + cy.get(".spectrum-Button--cta").click() }) cy.get(".nav-items-container").within(() => { cy.contains(route).should("exist") diff --git a/packages/builder/cypress/support/index.js b/packages/builder/cypress/support/index.js index 03ef2249f9..acd53a1592 100644 --- a/packages/builder/cypress/support/index.js +++ b/packages/builder/cypress/support/index.js @@ -14,8 +14,8 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import "./cookies" import "./commands" +import "./cookies" // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/builder/package.json b/packages/builder/package.json index 4ea92961e5..63409a2dea 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.24", + "version": "0.9.34", "license": "AGPL-3.0", "private": true, "scripts": { @@ -15,9 +15,9 @@ "cy:run": "cypress run", "cy:open": "cypress open", "cy:run:ci": "cypress run --record --key f308590b-6070-41af-b970-794a3823d451", - "cy:test": "start-server-and-test cy:setup http://localhost:10000/builder cy:run", - "cy:ci": "start-server-and-test cy:setup http://localhost:10000/builder cy:run:ci", - "cy:debug": "start-server-and-test cy:setup http://localhost:10000/builder cy:open" + "cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run", + "cy:ci": "start-server-and-test cy:setup http://localhost:10001/builder cy:run:ci", + "cy:debug": "start-server-and-test cy:setup http://localhost:10001/builder cy:open" }, "jest": { "globals": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.24", - "@budibase/client": "^0.9.24", + "@budibase/bbui": "^0.9.34", + "@budibase/client": "^0.9.34", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.24", + "@budibase/string-templates": "^0.9.34", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 89532e3bc3..5ce8e407c1 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -136,7 +136,7 @@ const getContextBindings = (asset, componentId) => { if (!datasource) { return } - const info = getSchemaForDatasource(datasource) + const info = getSchemaForDatasource(asset, datasource) schema = info.schema readablePrefix = info.table?.name } @@ -191,7 +191,7 @@ const getContextBindings = (asset, componentId) => { */ const getUserBindings = () => { let bindings = [] - const { schema } = getSchemaForDatasource({ + const { schema } = getSchemaForDatasource(null, { type: "table", tableId: TableNames.USERS, }) @@ -244,11 +244,15 @@ const getUrlBindings = asset => { /** * Gets a schema for a datasource object. */ -export const getSchemaForDatasource = (datasource, isForm = false) => { +export const getSchemaForDatasource = (asset, datasource, isForm = false) => { let schema, table if (datasource) { const { type } = datasource - if (type === "query") { + if (type === "provider") { + const component = findComponent(asset.props, datasource.providerId) + const source = getDatasourceForProvider(asset, component) + return getSchemaForDatasource(asset, source, isForm) + } else if (type === "query") { const queries = get(queriesStores).list table = queries.find(query => query._id === datasource._id) } else { diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 02b3c20a2f..e234a1a770 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -174,7 +174,7 @@ const fieldTypeToComponentMap = { } export function makeDatasourceFormComponents(datasource) { - const { schema } = getSchemaForDatasource(datasource, true) + const { schema } = getSchemaForDatasource(null, datasource, true) let components = [] let fields = Object.keys(schema || {}) fields.forEach(field => { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 00e53a8c64..152f2e72c8 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -43,6 +43,7 @@ } let originalName = field.name + const linkEditDisabled = originalName != null let primaryDisplay = $tables.selected.primaryDisplay == null || $tables.selected.primaryDisplay === field.name @@ -99,6 +100,7 @@ tables.deleteField(field) notifications.success(`Column ${field.name} deleted.`) confirmDeleteDialog.hide() + hide() deletion = false } } @@ -197,7 +199,7 @@ + {:else if field.type === FORMULA_TYPE} - import { store, allScreens } from "builderStore" + import { store, allScreens, selectedAccessRole } from "builderStore" import { tables } from "stores/backend" import { roles } from "stores/backend" import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" @@ -14,7 +14,7 @@ let templateIndex let draftScreen let createLink = true - let roleId = "BASIC" + let roleId = $selectedAccessRole || "BASIC" $: templates = getTemplates($store, $tables.list) $: route = !route && $allScreens.length === 0 ? "*" : route diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte index 1c9ab9767c..91578dd313 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte @@ -1,5 +1,8 @@ - diff --git a/packages/server/src/api/controllers/table/index.js b/packages/server/src/api/controllers/table/index.js index 4299727135..335f87f1e5 100644 --- a/packages/server/src/api/controllers/table/index.js +++ b/packages/server/src/api/controllers/table/index.js @@ -75,8 +75,6 @@ exports.save = async function (ctx) { /* istanbul ignore next */ if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) { ctx.throw(400, "Cannot rename a linked column.") - } else if (_rename && tableToSave.primaryDisplay === _rename.old) { - ctx.throw(400, "Cannot rename the display column.") } tableToSave = await tableSaveFunctions.mid(tableToSave) diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 73ba56943a..a7dcc190ba 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -4,11 +4,8 @@ const { getUserMetadataParams, } = require("../../db/utils") const { InternalTables } = require("../../db/utils") -const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") -const { - getGlobalUsers, - addAppRoleToUser, -} = require("../../utilities/workerRequests") +const { addAppRoleToUser } = require("../../utilities/workerRequests") +const { getGlobalUsers } = require("../../utilities/global") const { getFullUser } = require("../../utilities/users") function removeGlobalProps(user) { @@ -21,7 +18,7 @@ function removeGlobalProps(user) { exports.fetchMetadata = async function (ctx) { const database = new CouchDB(ctx.appId) - const global = await getGlobalUsers(ctx, ctx.appId) + const global = await getGlobalUsers(ctx.appId) const metadata = ( await database.allDocs( getUserMetadataParams(null, { @@ -47,10 +44,6 @@ exports.fetchMetadata = async function (ctx) { exports.updateSelfMetadata = async function (ctx) { // overwrite the ID with current users ctx.request.body._id = ctx.user._id - if (ctx.user.builder && ctx.user.builder.global) { - // specific case, update self role in global user - await addAppRoleToUser(ctx, ctx.appId, BUILTIN_ROLE_IDS.ADMIN) - } // make sure no stale rev delete ctx.request.body._rev await exports.updateMetadata(ctx) diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 1ec0ba8e12..6eef718fd1 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -7,8 +7,8 @@ const { PermissionTypes, PermissionLevels, } = require("@budibase/auth/permissions") -const usage = require("../../middleware/usageQuota") const env = require("../../environment") +const { paramResource } = require("../../middleware/resourceId") const router = Router() @@ -39,9 +39,9 @@ router .get("/builder/:file*", controller.serveBuilder) .post("/api/attachments/process", authorized(BUILDER), controller.uploadFile) .post( - "/api/attachments/upload", + "/api/attachments/:tableId/upload", + paramResource("tableId"), authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), - usage, controller.uploadFile ) .get("/componentlibrary", controller.serveComponentLibrary) diff --git a/packages/server/src/api/routes/tests/misc.spec.js b/packages/server/src/api/routes/tests/misc.spec.js index f9608c0d49..04a5c62431 100644 --- a/packages/server/src/api/routes/tests/misc.spec.js +++ b/packages/server/src/api/routes/tests/misc.spec.js @@ -37,7 +37,37 @@ describe("run misc tests", () => { describe("test table utilities", () => { it("should be able to import a CSV", async () => { - const table = await config.createTable() + const table = await config.createTable({ + name: "table", + type: "table", + key: "name", + schema: { + a: { + type: "string", + constraints: { + type: "string", + }, + }, + b: { + type: "string", + constraints: { + type: "string", + }, + }, + c: { + type: "string", + constraints: { + type: "string", + }, + }, + d: { + type: "string", + constraints: { + type: "string", + }, + }, + }, + }) const dataImport = { csvString: "a,b,c,d\n1,2,3,4" } diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 622552c77f..96ec492dbc 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -26,13 +26,6 @@ describe("/routing", () => { describe("fetch", () => { it("returns the correct routing for basic user", async () => { - workerRequests.getGlobalUsers.mockImplementationOnce((ctx, appId) => { - return { - roles: { - [appId]: BUILTIN_ROLE_IDS.BASIC, - } - } - }) const res = await request .get(`/api/routing/client`) .set(await config.roleHeaders({ @@ -54,13 +47,6 @@ describe("/routing", () => { }) it("returns the correct routing for power user", async () => { - workerRequests.getGlobalUsers.mockImplementationOnce((ctx, appId) => { - return { - roles: { - [appId]: BUILTIN_ROLE_IDS.POWER, - } - } - }) const res = await request .get(`/api/routing/client`) .set(await config.roleHeaders({ diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index 20ec4a20c9..ce7d76100b 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -125,6 +125,7 @@ describe("/rows", () => { numberNull: number, numberUndefined: number, numberString: number, + numberNumber: number, datetimeEmptyString: datetime, datetimeNull: datetime, datetimeUndefined: datetime, diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 30aa711524..492ebfff5b 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -1,7 +1,6 @@ const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") const setup = require("./utilities") -const workerRequests = require("../../../utilities/workerRequests") jest.mock("../../../utilities/workerRequests", () => ({ getGlobalUsers: jest.fn(() => { @@ -25,30 +24,18 @@ describe("/users", () => { }) describe("fetch", () => { - beforeEach(() => { - workerRequests.getGlobalUsers.mockImplementationOnce(() => ([ - { - _id: "us_uuid1", - }, - { - _id: "us_uuid2", - } - ] - )) - }) - it("returns a list of users from an instance db", async () => { - await config.createUser("brenda@brenda.com", "brendas_password") - await config.createUser("pam@pam.com", "pam_password") + await config.createUser("uuidx") + await config.createUser("uuidy") const res = await request .get(`/api/users/metadata`) .set(config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) - expect(res.body.length).toBe(2) - expect(res.body.find(u => u._id === `ro_ta_users_us_uuid1`)).toBeDefined() - expect(res.body.find(u => u._id === `ro_ta_users_us_uuid2`)).toBeDefined() + expect(res.body.length).toBe(3) + expect(res.body.find(u => u._id === `ro_ta_users_us_uuidx`)).toBeDefined() + expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined() }) it("should apply authorization to endpoint", async () => { @@ -65,9 +52,6 @@ describe("/users", () => { }) describe("update", () => { - beforeEach(() => { - }) - it("should be able to update the user", async () => { const user = await config.createUser() user.roleId = BUILTIN_ROLE_IDS.BASIC @@ -94,14 +78,6 @@ describe("/users", () => { }) describe("find", () => { - beforeEach(() => { - jest.resetAllMocks() - workerRequests.getGlobalUsers.mockImplementationOnce(() => ({ - _id: "us_uuid1", - roleId: BUILTIN_ROLE_IDS.POWER, - })) - }) - it("should be able to find the user", async () => { const user = await config.createUser() const res = await request @@ -110,7 +86,7 @@ describe("/users", () => { .expect(200) .expect("Content-Type", /json/) expect(res.body._id).toEqual(user._id) - expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.POWER) + expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.ADMIN) expect(res.body.tableId).toBeDefined() }) }) diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index c49e44c949..dfd77eec7a 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -2,6 +2,7 @@ const rowController = require("../../../controllers/row") const appController = require("../../../controllers/application") const CouchDB = require("../../../../db") const { AppStatus } = require("../../../../db/utils") +const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") function Request(appId, params) { this.appId = appId @@ -77,11 +78,17 @@ exports.checkPermissionsEndpoint = async ({ .set(passHeader) .expect(200) - user = await config.createUser("fail@budibase.com", password, failRole) - const failHeader = await config.login("fail@budibase.com", password, { - roleId: failRole, - userId: user.globalId, - }) + let failHeader + if (failRole === BUILTIN_ROLE_IDS.PUBLIC) { + failHeader = config.publicHeaders() + } else { + user = await config.createUser("fail@budibase.com", password, failRole) + failHeader = await config.login("fail@budibase.com", password, { + roleId: failRole, + userId: user.globalId, + builder: false, + }) + } await exports .createRequest(config.request, method, url, body) diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js index 3bfbacccbe..458da6e023 100644 --- a/packages/server/src/api/routes/tests/view.spec.js +++ b/packages/server/src/api/routes/tests/view.spec.js @@ -1,5 +1,25 @@ const setup = require("./utilities") +function priceTable() { + return { + name: "table", + type: "table", + key: "name", + schema: { + Price: { + type: "number", + constraints: {}, + }, + Category: { + type: "string", + constraints: { + type: "string", + }, + }, + }, + } +} + describe("/views", () => { let request = setup.getRequest() let config = setup.getConfig() @@ -13,7 +33,7 @@ describe("/views", () => { describe("create", () => { beforeEach(async () => { - table = await config.createTable() + table = await config.createTable(priceTable()) }) it("returns a success message when the view is successfully created", async () => { @@ -83,7 +103,7 @@ describe("/views", () => { describe("fetch", () => { beforeEach(async () => { - table = await config.createTable() + table = await config.createTable(priceTable()) }) it("returns only custom views", async () => { @@ -105,7 +125,7 @@ describe("/views", () => { describe("query", () => { beforeEach(async () => { - table = await config.createTable() + table = await config.createTable(priceTable()) }) it("returns data for the created view", async () => { @@ -172,7 +192,7 @@ describe("/views", () => { describe("destroy", () => { it("should be able to delete a view", async () => { - const table = await config.createTable() + const table = await config.createTable(priceTable()) const view = await config.createView() const res = await request .delete(`/api/views/${view.name}`) @@ -186,7 +206,7 @@ describe("/views", () => { describe("exportView", () => { it("should be able to delete a view", async () => { - await config.createTable() + await config.createTable(priceTable()) await config.createRow() const view = await config.createView() let res = await request diff --git a/packages/server/src/db/linkedRows/index.js b/packages/server/src/db/linkedRows/index.js index 5af10e41d7..e8b215358f 100644 --- a/packages/server/src/db/linkedRows/index.js +++ b/packages/server/src/db/linkedRows/index.js @@ -11,7 +11,9 @@ const { const { flatten } = require("lodash") const CouchDB = require("../../db") const { FieldTypes } = require("../../constants") -const { getMultiIDParams } = require("../../db/utils") +const { getMultiIDParams, USER_METDATA_PREFIX } = require("../../db/utils") +const { partition } = require("lodash") +const { getGlobalUsers } = require("../../utilities/global") /** * This functionality makes sure that when rows with links are created, updated or deleted they are processed @@ -57,6 +59,31 @@ async function getLinksForRows(appId, rows) { ) } +async function getFullLinkedDocs(appId, links) { + // create DBs + const db = new CouchDB(appId) + const linkedRowIds = links.map(link => link.id) + let linked = (await db.allDocs(getMultiIDParams(linkedRowIds))).rows.map( + row => row.doc + ) + // need to handle users as specific cases + let [users, other] = partition(linked, linkRow => + linkRow._id.startsWith(USER_METDATA_PREFIX) + ) + const globalUsers = await getGlobalUsers(appId, users) + users = users.map(user => { + const globalUser = globalUsers.find( + globalUser => globalUser && user._id.includes(globalUser._id) + ) + return { + ...globalUser, + // doing user second overwrites the id and rev (always metadata) + ...user, + } + }) + return [...other, ...users] +} + /** * Update link documents for a row or table - this is to be called by the API controller when a change is occurring. * @param {string} eventType states what type of change which is occurring, means this can be expanded upon in the @@ -154,14 +181,13 @@ exports.attachFullLinkedDocs = async (appId, table, rows) => { if (linkedTableIds.length === 0) { return rows } + // create DBs const db = new CouchDB(appId) + // get all the links const links = (await getLinksForRows(appId, rows)).filter(link => rows.some(row => row._id === link.thisId) ) - const linkedRowIds = links.map(link => link.id) - const linked = (await db.allDocs(getMultiIDParams(linkedRowIds))).rows.map( - row => row.doc - ) + let linked = await getFullLinkedDocs(appId, links) const linkedTables = [] for (let row of rows) { for (let link of links.filter(link => link.thisId === row._id)) { diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 29617a7dff..be57f8b70f 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -6,17 +6,11 @@ const { APP_DEV_PREFIX, APP_PREFIX, SEPARATOR, + StaticDatabases, } = require("@budibase/auth/db") const UNICODE_MAX = "\ufff0" -const StaticDatabases = { - BUILDER: { - name: "builder-db", - baseDoc: "builder-doc", - }, -} - const AppStatus = { DEV: "development", ALL: "all", @@ -54,9 +48,18 @@ const SearchIndexes = { ROWS: "rows", } +exports.StaticDatabases = { + BUILDER: { + name: "builder-db", + baseDoc: "builder-doc", + }, + ...StaticDatabases, +} + exports.APP_PREFIX = APP_PREFIX exports.APP_DEV_PREFIX = APP_DEV_PREFIX -exports.StaticDatabases = StaticDatabases +exports.USER_METDATA_PREFIX = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` +exports.LINK_USER_METADATA_PREFIX = `${DocumentTypes.LINK}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` exports.ViewNames = ViewNames exports.InternalTables = InternalTables exports.DocumentTypes = DocumentTypes diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index ae83da8ba6..19d0afd560 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -1,9 +1,16 @@ -const { getAppId, setCookie, getCookie } = require("@budibase/auth").utils +const { + getAppId, + setCookie, + getCookie, + clearCookie, +} = require("@budibase/auth").utils const { Cookies } = require("@budibase/auth").constants const { getRole } = require("@budibase/auth/roles") const { getGlobalSelf } = require("../utilities/workerRequests") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { generateUserMetadataID } = require("../db/utils") +const { dbExists } = require("@budibase/auth/db") +const CouchDB = require("../db") module.exports = async (ctx, next) => { // try to get the appID from the request @@ -13,6 +20,15 @@ module.exports = async (ctx, next) => { if (!appCookie && !requestAppId) { return next() } + // check the app exists referenced in cookie + if (appCookie) { + const appId = appCookie.appId + const exists = await dbExists(CouchDB, appId) + if (!exists) { + clearCookie(ctx, Cookies.CurrentApp) + return next() + } + } let updateCookie = false, appId, @@ -33,7 +49,7 @@ module.exports = async (ctx, next) => { updateCookie = true appId = requestAppId // retrieving global user gets the right role - roleId = globalUser.roleId + roleId = globalUser.roleId || BUILTIN_ROLE_IDS.PUBLIC } else if (appCookie != null) { appId = appCookie.appId roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index 60e503c128..7e56b66dc9 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -101,7 +101,7 @@ class TestConfiguration { userId: GLOBAL_USER_ID, } const app = { - roleId: BUILTIN_ROLE_IDS.BUILDER, + roleId: BUILTIN_ROLE_IDS.ADMIN, appId: this.appId, } const authToken = jwt.sign(auth, env.JWT_SECRET) @@ -306,12 +306,9 @@ class TestConfiguration { return await this._req(config, null, controllers.layout.save) } - async createUser(roleId = BUILTIN_ROLE_IDS.POWER) { - const globalId = `us_${Math.random()}` - const resp = await this.globalUser( - globalId, - roleId === BUILTIN_ROLE_IDS.BUILDER - ) + async createUser(id = null) { + const globalId = !id ? `us_${Math.random()}` : `us_${id}` + const resp = await this.globalUser(globalId) return { ...resp, globalId, @@ -319,7 +316,6 @@ class TestConfiguration { } async login(email, password, { roleId, userId, builder } = {}) { - roleId = !roleId ? BUILTIN_ROLE_IDS.BUILDER : roleId userId = !userId ? `us_uuid1` : userId if (!this.request) { throw "Server has not been opened, cannot login." diff --git a/packages/server/src/tests/utilities/structures.js b/packages/server/src/tests/utilities/structures.js index e925c272ac..91996a7804 100644 --- a/packages/server/src/tests/utilities/structures.js +++ b/packages/server/src/tests/utilities/structures.js @@ -46,7 +46,6 @@ exports.basicRow = tableId => { return { name: "Test Contact", description: "original description", - status: "new", tableId: tableId, } } diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index 750414826a..ddda274ef5 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -18,6 +18,10 @@ const download = require("download") const env = require("../../environment") const { homedir } = require("os") const fetch = require("node-fetch") +const { + USER_METDATA_PREFIX, + LINK_USER_METADATA_PREFIX, +} = require("../../db/utils") const DEFAULT_AUTOMATION_BUCKET = "https://prod-budi-automations.s3-eu-west-1.amazonaws.com" @@ -117,7 +121,14 @@ exports.performBackup = async (appId, backupName) => { const writeStream = fs.createWriteStream(path) // perform couch dump const instanceDb = new CouchDB(appId) - await instanceDb.dump(writeStream, {}) + await instanceDb.dump(writeStream, { + // filter out anything that has a user metadata structure in its ID + filter: doc => + !( + doc._id.includes(USER_METDATA_PREFIX) || + doc.includes(LINK_USER_METADATA_PREFIX) + ), + }) // write the file to the object store await streamUpload( ObjectStoreBuckets.BACKUPS, diff --git a/packages/server/src/utilities/fileSystem/newApp.js b/packages/server/src/utilities/fileSystem/newApp.js index 22113757c9..735f0d523e 100644 --- a/packages/server/src/utilities/fileSystem/newApp.js +++ b/packages/server/src/utilities/fileSystem/newApp.js @@ -30,5 +30,7 @@ exports.uploadClientLibrary = async appId => { const sourcepath = require.resolve("@budibase/client") const destPath = join(appId, "budibase-client.js") - await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath)) + await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath), { + ContentType: "application/javascript", + }) } diff --git a/packages/server/src/utilities/global.js b/packages/server/src/utilities/global.js new file mode 100644 index 0000000000..eda0e61cff --- /dev/null +++ b/packages/server/src/utilities/global.js @@ -0,0 +1,64 @@ +const CouchDB = require("../db") +const { + getMultiIDParams, + getGlobalIDFromUserMetadataID, + StaticDatabases, +} = require("../db/utils") +const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") +const { getDeployedAppID } = require("@budibase/auth/db") +const { getGlobalUserParams } = require("@budibase/auth/db") + +exports.updateAppRole = (appId, user) => { + if (!user.roles) { + return user + } + if (user.builder && user.builder.global) { + user.roleId = BUILTIN_ROLE_IDS.ADMIN + } else { + // always use the deployed app + user.roleId = user.roles[getDeployedAppID(appId)] + if (!user.roleId) { + user.roleId = BUILTIN_ROLE_IDS.PUBLIC + } + } + delete user.roles + return user +} + +exports.getGlobalUser = async (appId, userId) => { + const db = CouchDB(StaticDatabases.GLOBAL.name) + let user = await db.get(getGlobalIDFromUserMetadataID(userId)) + if (user) { + delete user.password + } + return exports.updateAppRole(appId, user) +} + +exports.getGlobalUsers = async (appId = null, users = null) => { + const db = CouchDB(StaticDatabases.GLOBAL.name) + let globalUsers + if (users) { + const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id)) + globalUsers = (await db.allDocs(getMultiIDParams(globalIds))).rows.map( + row => row.doc + ) + } else { + globalUsers = ( + await db.allDocs( + getGlobalUserParams(null, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc) + } + globalUsers = globalUsers + .filter(user => user != null) + .map(user => { + delete user.password + return user + }) + if (!appId) { + return globalUsers + } + return globalUsers.map(user => exports.updateAppRole(appId, user)) +} diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js index 2267c9e986..807563d47e 100644 --- a/packages/server/src/utilities/rowProcessor.js +++ b/packages/server/src/utilities/rowProcessor.js @@ -169,9 +169,14 @@ exports.inputProcessing = (user = {}, table, row) => { let clonedRow = cloneDeep(row) // need to copy the table so it can be differenced on way out const copiedTable = cloneDeep(table) + const dontCleanseKeys = ["type", "_id", "_rev", "tableId"] for (let [key, value] of Object.entries(clonedRow)) { const field = table.schema[key] + // cleanse fields that aren't in the schema if (!field) { + if (dontCleanseKeys.indexOf(key) === -1) { + delete clonedRow[key] + } continue } clonedRow[key] = exports.coerce(value, field.type) diff --git a/packages/server/src/utilities/users.js b/packages/server/src/utilities/users.js index eb52f4b867..6144397bf1 100644 --- a/packages/server/src/utilities/users.js +++ b/packages/server/src/utilities/users.js @@ -1,13 +1,9 @@ const CouchDB = require("../db") -const { getGlobalIDFromUserMetadataID, InternalTables } = require("../db/utils") -const { getGlobalUsers } = require("../utilities/workerRequests") +const { InternalTables } = require("../db/utils") +const { getGlobalUser } = require("../utilities/global") exports.getFullUser = async (ctx, userId) => { - const global = await getGlobalUsers( - ctx, - ctx.appId, - getGlobalIDFromUserMetadataID(userId) - ) + const global = await getGlobalUser(ctx.appId, userId) let metadata try { // this will throw an error if the db doesn't exist, or there is no appId diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 99d9a1c3e2..cb06b5b8d4 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -1,27 +1,16 @@ const fetch = require("node-fetch") const env = require("../environment") const { checkSlashesInUrl } = require("./index") -const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { getDeployedAppID } = require("@budibase/auth/db") -const { getGlobalIDFromUserMetadataID } = require("../db/utils") +const { updateAppRole, getGlobalUser } = require("./global") -function getAppRole(appId, user) { - if (!user.roles) { - return user - } - // always use the deployed app - user.roleId = user.roles[getDeployedAppID(appId)] - if (!user.roleId) { - user.roleId = BUILTIN_ROLE_IDS.PUBLIC - } - delete user.roles - return user -} - -function request(ctx, request) { +function request(ctx, request, noApiKey) { if (!request.headers) { request.headers = {} } + if (!noApiKey) { + request.headers["x-budibase-api-key"] = env.INTERNAL_API_KEY + } if (request.body && Object.keys(request.body).length > 0) { request.headers["Content-Type"] = "application/json" request.body = @@ -44,9 +33,6 @@ exports.sendSmtpEmail = async (to, from, subject, contents) => { checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`), request(null, { method: "POST", - headers: { - "x-budibase-api-key": env.INTERNAL_API_KEY, - }, body: { email: to, from, @@ -86,49 +72,19 @@ exports.getDeployedApps = async ctx => { } } -exports.deleteGlobalUser = async (ctx, globalId) => { - const endpoint = `/api/admin/users/${globalId}` - const reqCfg = { method: "DELETE" } - const response = await fetch( - checkSlashesInUrl(env.WORKER_URL + endpoint), - request(ctx, reqCfg) - ) - return response.json() -} - -exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => { - const endpoint = globalId - ? `/api/admin/users/${globalId}` - : `/api/admin/users` - const reqCfg = { method: "GET" } - const response = await fetch( - checkSlashesInUrl(env.WORKER_URL + endpoint), - request(ctx, reqCfg) - ) - let users = await response.json() - if (!appId) { - return users - } - if (Array.isArray(users)) { - users = users.map(user => getAppRole(appId, user)) - } else { - users = getAppRole(appId, users) - } - return users -} - exports.getGlobalSelf = async (ctx, appId = null) => { const endpoint = `/api/admin/users/self` const response = await fetch( checkSlashesInUrl(env.WORKER_URL + endpoint), - request(ctx, { method: "GET" }) + // we don't want to use API key when getting self + request(ctx, { method: "GET" }, true) ) if (response.status !== 200) { ctx.throw(400, "Unable to get self globally.") } let json = await response.json() if (appId) { - json = getAppRole(appId, json) + json = updateAppRole(appId, json) } return json } @@ -141,8 +97,7 @@ exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => { user = await exports.getGlobalSelf(ctx) endpoint = `/api/admin/users/self` } else { - userId = getGlobalIDFromUserMetadataID(userId) - user = await exports.getGlobalUsers(ctx, appId, userId) + user = await getGlobalUser(appId, userId) body._id = userId endpoint = `/api/admin/users` } @@ -172,9 +127,6 @@ exports.removeAppFromUserRoles = async appId => { checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`), request(null, { method: "DELETE", - headers: { - "x-budibase-api-key": env.INTERNAL_API_KEY, - }, }) ) if (response.status !== 200) { diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 780418b318..192aa8d2dd 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1557,5 +1557,38 @@ "context": { "type": "schema" } + }, + "daterangepicker": { + "name": "Date Range", + "icon": "Date", + "styleable": true, + "hasChildren": false, + "info": "Your data provider will be automatically filtered to the given date range.", + "settings": [ + { + "type": "dataProvider", + "label": "Provider", + "key": "dataProvider" + }, + { + "type": "field", + "label": "Date field", + "key": "field" + }, + { + "type": "select", + "label": "Default range", + "key": "defaultValue", + "options": [ + "Last 1 day", + "Last 7 days", + "Last 30 days", + "Last 3 months", + "Last 6 months", + "Last 1 year" + ], + "defaultValue": "Last 30 days" + } + ] } } diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index 489a608cd6..809ed3e081 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -29,14 +29,15 @@ "keywords": [ "svelte" ], - "version": "0.9.24", + "version": "0.9.34", "license": "MIT", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "dependencies": { - "@budibase/bbui": "^0.9.24", + "@budibase/bbui": "^0.9.34", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "apexcharts": "^3.22.1", + "dayjs": "^1.10.5", "svelte-apexcharts": "^1.0.2", "svelte-flatpickr": "^3.1.0" } diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index e0b2ad859a..7b759b5348 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -1,6 +1,12 @@ + +
+