diff --git a/hosting/kubernetes/budibase/templates/app-service-deployment.yaml b/hosting/kubernetes/budibase/templates/app-service-deployment.yaml index 5d9aee2619..98fdc8dfd0 100644 --- a/hosting/kubernetes/budibase/templates/app-service-deployment.yaml +++ b/hosting/kubernetes/budibase/templates/app-service-deployment.yaml @@ -96,6 +96,10 @@ spec: value: worker-service:{{ .Values.services.worker.port }} - name: COOKIE_DOMAIN value: {{ .Values.globals.cookieDomain | quote }} + - name: ACCOUNT_PORTAL_URL + value: {{ .Values.globals.accountPortalUrl | quote }} + - name: ACCOUNT_PORTAL_API_KEY + value: {{ .Values.globals.accountPortalApiKey | quote }} image: budibase/apps imagePullPolicy: Always name: bbapps diff --git a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml index 98a921a8a6..08b40d3b6b 100644 --- a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml +++ b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml @@ -89,6 +89,8 @@ spec: value: {{ .Values.globals.selfHosted | quote }} - name: ACCOUNT_PORTAL_URL value: {{ .Values.globals.accountPortalUrl | quote }} + - name: ACCOUNT_PORTAL_API_KEY + value: {{ .Values.globals.accountPortalApiKey | quote }} - name: COOKIE_DOMAIN value: {{ .Values.globals.cookieDomain | quote }} image: budibase/worker diff --git a/hosting/kubernetes/budibase/values.yaml b/hosting/kubernetes/budibase/values.yaml index c9b2549b30..5999f9c4bc 100644 --- a/hosting/kubernetes/budibase/values.yaml +++ b/hosting/kubernetes/budibase/values.yaml @@ -90,6 +90,7 @@ globals: logLevel: info selfHosted: 1 accountPortalUrL: "" + accountPortalApiKey: "" cookieDomain: "" createSecrets: true # creates an internal API key, JWT secrets and redis password for you diff --git a/hosting/kubernetes/envoy/envoy.yaml b/hosting/kubernetes/envoy/envoy.yaml index 4bf751b3a3..25a774dc7e 100644 --- a/hosting/kubernetes/envoy/envoy.yaml +++ b/hosting/kubernetes/envoy/envoy.yaml @@ -50,6 +50,11 @@ static_resources: route: cluster: app-service + - match: { path: "/api/deploy" } + route: + timeout: 60s + cluster: app-service + # special case for when API requests are made, can just forward, not to minio - match: { prefix: "/api/" } route: diff --git a/lerna.json b/lerna.json index 016dec1b9b..1102d1e9d0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 3df577ca58..3596ec7800 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "multi:disable": "lerna run multi:disable", "selfhost:enable": "lerna run selfhost:enable", "selfhost:disable": "lerna run selfhost:disable", + "localdomain:enable": "lerna run localdomain:enable", + "localdomain:disable": "lerna run localdomain:disable", "postinstall": "husky install" } } diff --git a/packages/auth/package.json b/packages/auth/package.json index e765f7fc02..bc890882f7 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index 51bed0210e..60a2d341a8 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -12,7 +12,7 @@ const populateFromDB = async (userId, tenantId) => { const user = await getGlobalDB(tenantId).get(userId) user.budibaseAccess = true - if (!env.SELF_HOSTED) { + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) if (account) { user.account = account diff --git a/packages/auth/src/cloud/accounts.js b/packages/auth/src/cloud/accounts.js index a102df8920..a02fe60926 100644 --- a/packages/auth/src/cloud/accounts.js +++ b/packages/auth/src/cloud/accounts.js @@ -1,16 +1,18 @@ const API = require("./api") const env = require("../environment") +const { Headers } = require("../constants") const api = new API(env.ACCOUNT_PORTAL_URL) -// TODO: Authorization - exports.getAccount = async email => { const payload = { email, } const response = await api.post(`/api/accounts/search`, { body: payload, + headers: { + [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + }, }) const json = await response.json() diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index da24afc8a0..c36b469c4e 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -21,6 +21,8 @@ module.exports = { INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, MULTI_TENANCY: process.env.MULTI_TENANCY, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, + ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, + DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, isTest, diff --git a/packages/auth/src/middleware/matchers.js b/packages/auth/src/middleware/matchers.js index a555823136..3d5065c069 100644 --- a/packages/auth/src/middleware/matchers.js +++ b/packages/auth/src/middleware/matchers.js @@ -7,6 +7,7 @@ exports.buildMatcherRegex = patterns => { return patterns.map(pattern => { const isObj = typeof pattern === "object" && pattern.route const method = isObj ? pattern.method : "GET" + const strict = pattern.strict ? pattern.strict : false let route = isObj ? pattern.route : pattern const matches = route.match(PARAM_REGEX) @@ -16,13 +17,19 @@ exports.buildMatcherRegex = patterns => { route = route.replace(match, pattern) } } - return { regex: new RegExp(route), method } + return { regex: new RegExp(route), method, strict, route } }) } exports.matches = (ctx, options) => { - return options.find(({ regex, method }) => { - const urlMatch = regex.test(ctx.request.url) + return options.find(({ regex, method, strict, route }) => { + let urlMatch + if (strict) { + urlMatch = ctx.request.url === route + } else { + urlMatch = regex.test(ctx.request.url) + } + const methodMatch = method === "ALL" ? true diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index 1ace65ba40..e2ad9a9300 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -20,6 +20,10 @@ const getErrorMessage = () => { return done.mock.calls[0][2].message } +const saveUser = async (user) => { + return await db.put(user) +} + describe("third party common", () => { describe("authenticateThirdParty", () => { let thirdPartyUser @@ -36,7 +40,7 @@ describe("third party common", () => { describe("validation", () => { const testValidation = async (message) => { - await authenticateThirdParty(thirdPartyUser, false, done) + await authenticateThirdParty(thirdPartyUser, false, done, saveUser) expect(done.mock.calls.length).toBe(1) expect(getErrorMessage()).toContain(message) } @@ -78,7 +82,7 @@ describe("third party common", () => { describe("when the user doesn't exist", () => { describe("when a local account is required", () => { it("returns an error message", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) expect(done.mock.calls.length).toBe(1) expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") }) @@ -86,7 +90,7 @@ describe("third party common", () => { describe("when a local account isn't required", () => { it("creates and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, false, done) + await authenticateThirdParty(thirdPartyUser, false, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) expect(user.roles).toStrictEqual({}) @@ -123,7 +127,7 @@ describe("third party common", () => { }) it("syncs and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) @@ -139,7 +143,7 @@ describe("third party common", () => { }) it("syncs and authenticates the user", async () => { - await authenticateThirdParty(thirdPartyUser, true, done) + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) const user = expectUserIsAuthenticated() expectUserIsSynced(user, thirdPartyUser) diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index c25aa3e0b0..54a5504712 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -1,6 +1,7 @@ const env = require("../../environment") const jwt = require("jsonwebtoken") const { generateGlobalUserID } = require("../../db/utils") +const { saveUser } = require("../../utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") @@ -14,7 +15,8 @@ const fetch = require("node-fetch") exports.authenticateThirdParty = async function ( thirdPartyUser, requireLocalAccount = true, - done + done, + saveUserFn = saveUser ) { if (!thirdPartyUser.provider) { return authError(done, "third party user provider required") @@ -71,7 +73,13 @@ exports.authenticateThirdParty = async function ( dbUser = await syncUser(dbUser, thirdPartyUser) // create or sync the user - const response = await db.put(dbUser) + let response + try { + response = await saveUserFn(dbUser, getTenantId(), false, false) + } catch (err) { + return authError(done, err) + } + dbUser._rev = response.rev // authenticate diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index 9f271ad80e..87b67d464e 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -265,7 +265,7 @@ exports.downloadTarball = async (url, bucketName, path) => { const tmpPath = join(budibaseTempDir(), path) await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) - if (!env.isTest()) { + if (!env.isTest() && env.SELF_HOSTED) { await exports.uploadDirectory(bucketName, tmpPath, path) } // return the temporary path incase there is a use for it diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js index 48a24ad0bc..0ee17265ce 100644 --- a/packages/auth/src/redis/index.js +++ b/packages/auth/src/redis/index.js @@ -191,6 +191,12 @@ class RedisWrapper { } } + async getTTL(key) { + const db = this._db + const prefixedKey = addDbPrefix(db, key) + return CLIENT.ttl(prefixedKey) + } + async setExpiry(key, expirySeconds) { const db = this._db const prefixedKey = addDbPrefix(db, key) diff --git a/packages/auth/src/tenancy/deprovision.js b/packages/auth/src/tenancy/deprovision.js index b8e5bc82cf..608ca1b84a 100644 --- a/packages/auth/src/tenancy/deprovision.js +++ b/packages/auth/src/tenancy/deprovision.js @@ -19,6 +19,22 @@ const removeTenantFromInfoDB = async tenantId => { } } +exports.removeUserFromInfoDB = async dbUser => { + const infoDb = getDB(PLATFORM_INFO_DB) + const keys = [dbUser._id, dbUser.email] + const userDocs = await infoDb.allDocs({ + keys, + include_docs: true, + }) + const toDelete = userDocs.rows.map(row => { + return { + ...row.doc, + _deleted: true, + } + }) + await infoDb.bulkDocs(toDelete) +} + const removeUsersFromInfoDB = async tenantId => { try { const globalDb = getGlobalDB(tenantId) diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js index ebd573496c..67dbfd5619 100644 --- a/packages/auth/src/tenancy/tenancy.js +++ b/packages/auth/src/tenancy/tenancy.js @@ -73,7 +73,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => { await Promise.all(promises) } -exports.getGlobalDB = (tenantId = null) => { +exports.getGlobalDBName = (tenantId = null) => { // tenant ID can be set externally, for example user API where // new tenants are being created, this may be the case if (!tenantId) { @@ -81,13 +81,16 @@ exports.getGlobalDB = (tenantId = null) => { } let dbName - if (tenantId === DEFAULT_TENANT_ID) { dbName = StaticDatabases.GLOBAL.name } else { dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` } + return dbName +} +exports.getGlobalDB = (tenantId = null) => { + const dbName = exports.getGlobalDBName(tenantId) return getDB(dbName) } @@ -104,3 +107,13 @@ exports.lookupTenantId = async userId => { } return tenantId } + +// lookup, could be email or userId, either will return a doc +exports.getTenantUser = async identifier => { + const db = getDB(PLATFORM_INFO_DB) + try { + return await db.get(identifier) + } catch (err) { + return null + } +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 93b483c6be..f509a626c1 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -1,10 +1,24 @@ -const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils") +const { + DocumentTypes, + SEPARATOR, + ViewNames, + generateGlobalUserID, +} = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { createUserEmailView } = require("./db/views") -const { Headers } = require("./constants") -const { getGlobalDB } = require("./tenancy") +const { Headers, UserStatus } = require("./constants") +const { + getGlobalDB, + updateTenantId, + getTenantUser, + tryAddTenant, +} = require("./tenancy") const environment = require("./environment") +const accounts = require("./cloud/accounts") +const { hash } = require("./hashing") +const userCache = require("./cache/user") +const env = require("./environment") const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -131,3 +145,93 @@ exports.getGlobalUserByEmail = async email => { } } } + +exports.saveUser = async ( + user, + tenantId, + hashPassword = true, + requirePassword = true +) => { + if (!tenantId) { + throw "No tenancy specified." + } + // need to set the context for this request, as specified + updateTenantId(tenantId) + // specify the tenancy incase we're making a new admin user (public) + const db = getGlobalDB(tenantId) + let { email, password, _id } = user + // make sure another user isn't using the same email + let dbUser + if (email) { + // check budibase users inside the tenant + dbUser = await exports.getGlobalUserByEmail(email) + if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { + throw `Email address ${email} already in use.` + } + + // check budibase users in other tenants + if (env.MULTI_TENANCY) { + dbUser = await getTenantUser(email) + if (dbUser != null && dbUser.tenantId !== tenantId) { + throw `Email address ${email} already in use.` + } + } + + // check root account users in account portal + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const account = await accounts.getAccount(email) + if (account && account.verified && account.tenantId !== tenantId) { + throw `Email address ${email} already in use.` + } + } + } else { + dbUser = await db.get(_id) + } + + // get the password, make sure one is defined + let hashedPassword + if (password) { + hashedPassword = hashPassword ? await hash(password) : password + } else if (dbUser) { + hashedPassword = dbUser.password + } else if (requirePassword) { + throw "Password must be specified." + } + + _id = _id || generateGlobalUserID() + user = { + createdAt: Date.now(), + ...dbUser, + ...user, + _id, + password: hashedPassword, + tenantId, + } + // make sure the roles object is always present + if (!user.roles) { + user.roles = {} + } + // add the active status to a user if its not provided + if (user.status == null) { + user.status = UserStatus.ACTIVE + } + try { + const response = await db.put({ + password: hashedPassword, + ...user, + }) + await tryAddTenant(tenantId, _id, email) + await userCache.invalidateUser(response.id) + return { + _id: response.id, + _rev: response.rev, + email, + } + } catch (err) { + if (err.status === 409) { + throw "User exists already" + } else { + throw err + } + } +} diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 6924e6db16..2657593a0c 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.146-alpha.4", + "version": "0.9.154-alpha.1", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -78,7 +78,7 @@ "@spectrum-css/underlay": "^2.0.9", "@spectrum-css/vars": "^3.0.1", "dayjs": "^1.10.4", - "svelte-flatpickr": "^3.1.0", + "svelte-flatpickr": "^3.2.3", "svelte-portal": "^1.0.0" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 2516cb659d..176db9f497 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -26,6 +26,7 @@ altFormat: enableTime ? "F j Y, H:i" : "F j, Y", wrap: true, appendTo, + disableMobile: "true", } const handleChange = event => { diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index 926c3eda11..37989a291e 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -90,6 +90,7 @@ on:input={onInput} on:keyup={updateValueOnEnter} {type} + inputmode={type === "number" ? "decimal" : "text"} class="spectrum-Textfield-input" /> diff --git a/packages/bbui/yarn.lock b/packages/bbui/yarn.lock index a3b20aa862..a492c83266 100644 --- a/packages/bbui/yarn.lock +++ b/packages/bbui/yarn.lock @@ -2415,10 +2415,10 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -svelte-flatpickr@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.1.0.tgz#ad83588430dbd55196a1a258b8ba27e7f9c1ee37" - integrity sha512-zKyV+ukeVuJ8CW0Ing3T19VSekc4bPkou/5Riutt1yATrLvSsanNqcgqi7Q5IePvIoOF9GJ5OtHvn1qK9Wx9BQ== +svelte-flatpickr@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8" + integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw== dependencies: flatpickr "^4.5.2" diff --git a/packages/builder/assets/budiworld.webp b/packages/builder/assets/budiworld.webp new file mode 100644 index 0000000000..347036a797 Binary files /dev/null and b/packages/builder/assets/budiworld.webp differ diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index 24f370caf9..ccb9ae7929 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -24,9 +24,7 @@ context("Create a Table", () => { it("updates a column on the table", () => { cy.get(".title").click() cy.get(".spectrum-Table-editIcon > use").click() - cy.get("input") - .eq(1) - .type("updated", { force: true }) + cy.get("input").eq(1).type("updated", { force: true }) // Unset table display column cy.get(".spectrum-Switch-input").eq(1).click() cy.contains("Save Column").click() @@ -45,9 +43,7 @@ context("Create a Table", () => { it("deletes a row", () => { cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.contains("Delete 1 row(s)").click() - cy.get(".spectrum-Modal") - .contains("Delete") - .click() + cy.get(".spectrum-Modal").contains("Delete").click() cy.contains("RoverUpdated").should("not.exist") }) @@ -56,15 +52,18 @@ context("Create a Table", () => { cy.get(".spectrum-Table-editIcon > use").click() cy.contains("Delete").click() cy.wait(50) - cy.contains("Delete Column") - .click() + cy.contains("Delete Column").click() cy.contains("nameupdated").should("not.exist") }) it("deletes a table", () => { - cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use") - .eq(1) - .click({ force: true }) + cy.get(".nav-item") + .contains("dog") + .parents(".nav-item") + .first() + .within(() => { + cy.get(".actions .spectrum-Icon").click({ force: true }) + }) 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/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index d7d9606cd7..e82ab67c0d 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -28,11 +28,7 @@ context("Create a View", () => { const headers = Array.from($headers).map(header => header.textContent.trim() ) - expect(removeSpacing(headers)).to.deep.eq([ - "group", - "age", - "rating", - ]) + expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"]) }) }) @@ -62,7 +58,7 @@ context("Create a View", () => { 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 }) @@ -105,20 +101,20 @@ context("Create a View", () => { cy.get(".spectrum-Table-cell").then($values => { let values = Array.from($values).map(header => header.textContent.trim()) expect(values).to.deep.eq([ - "Students", - "70", - "20", - "25", - "3", - "1650", - "23.333333333333332", - "Teachers", - "85", - "36", - "49", - "2", - "3697", - "42.5", + "Students", + "70", + "20", + "25", + "3", + "1650", + "23.333333333333332", + "Teachers", + "85", + "36", + "49", + "2", + "3697", + "42.5", ]) }) }) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 0309299468..b10e7b9990 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -31,8 +31,7 @@ Cypress.Commands.add("login", () => { Cypress.Commands.add("createApp", name => { cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.wait(500) - cy.contains(/Create (new )?app/).click() - cy.wait(500) + cy.contains(/Start from scratch/).click() cy.get(".spectrum-Modal") .within(() => { cy.get("input").eq(0).type(name).should("have.value", name).blur() @@ -187,7 +186,7 @@ Cypress.Commands.add("getComponent", componentId => { .its("body") .should("not.be.null") .then(cy.wrap) - .find(`[data-component-id=${componentId}]`) + .find(`[data-id=${componentId}]`) }) Cypress.Commands.add("navigateToFrontend", () => { diff --git a/packages/builder/index.html b/packages/builder/index.html index eb58dc74d2..e3383cda39 100644 --- a/packages/builder/index.html +++ b/packages/builder/index.html @@ -4,7 +4,7 @@ Budibase - + noSpaces?.includes(invalid)) } +function replaceBetween(string, start, end, replacement) { + return string.substring(0, start) + replacement + string.substring(end) +} + /** * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ @@ -431,6 +435,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { if (typeof textWithBindings !== "string") { return textWithBindings } + // work from longest to shortest const convertFromProps = bindableProperties .map(el => el[convertFrom]) .sort((a, b) => { @@ -440,12 +445,29 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { let result = textWithBindings for (let boundValue of boundValues) { let newBoundValue = boundValue + // we use a search string, where any time we replace something we blank it out + // in the search, working from longest to shortest so always use best match first + let searchString = newBoundValue for (let from of convertFromProps) { if (shouldReplaceBinding(newBoundValue, from, convertTo)) { const binding = bindableProperties.find(el => el[convertFrom] === from) - while (newBoundValue.includes(from)) { - newBoundValue = newBoundValue.replace(from, binding[convertTo]) - } + let idx + do { + // see if any instances of this binding exist in the search string + idx = searchString.indexOf(from) + if (idx !== -1) { + let end = idx + from.length, + searchReplace = Array(binding[convertTo].length).join("*") + // blank out parts of the search string + searchString = replaceBetween(searchString, idx, end, searchReplace) + newBoundValue = replaceBetween( + newBoundValue, + idx, + end, + binding[convertTo] + ) + } + } while (idx !== -1) } } result = result.replace(boundValue, newBoundValue) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js index dcbb747e38..49928c69a9 100644 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ b/packages/builder/src/builderStore/store/automation/Automation.js @@ -17,7 +17,7 @@ export default class Automation { this.automation.testData = data } - addBlock(block) { + addBlock(block, idx) { // Make sure to add trigger if doesn't exist if (!this.hasTrigger() && block.type === "TRIGGER") { const trigger = { id: generate(), ...block } @@ -26,10 +26,7 @@ export default class Automation { } const newBlock = { id: generate(), ...block } - this.automation.definition.steps = [ - ...this.automation.definition.steps, - newBlock, - ] + this.automation.definition.steps.splice(idx, 0, newBlock) return newBlock } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 0a47970d28..16cc490bb2 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -104,9 +104,12 @@ const automationActions = store => ({ return state }) }, - addBlockToAutomation: block => { + addBlockToAutomation: (block, blockIdx) => { store.update(state => { - const newBlock = state.selectedAutomation.addBlock(cloneDeep(block)) + const newBlock = state.selectedAutomation.addBlock( + cloneDeep(block), + blockIdx + ) state.selectedBlock = newBlock return state }) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 09132f28cb..9110aa1430 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -15,7 +15,6 @@ import { database, tables, } from "stores/backend" - import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import api from "../api" import { FrontendTypes } from "constants" @@ -25,6 +24,7 @@ import { findComponentParent, findClosestMatchingComponent, findAllMatchingComponents, + findComponent, } from "../storeUtils" import { uuid } from "../uuid" import { removeBindings } from "../dataBinding" @@ -67,6 +67,14 @@ export const getFrontendStore = () => { initialise: async pkg => { const { layouts, screens, application, clientLibPath } = pkg const components = await fetchComponentLibDefinitions(application.appId) + // make sure app isn't locked + if ( + components && + components.status === 400 && + components.message?.includes("lock") + ) { + throw { ok: false, reason: "locked" } + } store.update(state => ({ ...state, libraries: application.componentLibraries, @@ -464,6 +472,24 @@ export const getFrontendStore = () => { if (!asset) { return } + + // Fetch full definition + component = findComponent(asset.props, component._id) + + // Ensure we aren't deleting the screen slot + if (component._component?.endsWith("/screenslot")) { + throw "You can't delete the screen slot" + } + + // Ensure we aren't deleting something that contains the screen slot + const screenslot = findComponentType( + component, + "@budibase/standard-components/screenslot" + ) + if (screenslot != null) { + throw "You can't delete a component that contains the screen slot" + } + const parent = findComponentParent(asset.props, component._id) if (parent) { parent._children = parent._children.filter( diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index b822973b62..acb945a96a 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -1,10 +1,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index c05a103fac..fee8afd711 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -4,9 +4,9 @@ import FlowItem from "./FlowItem.svelte" import TestDataModal from "./TestDataModal.svelte" import { flip } from "svelte/animate" - import { fade, fly } from "svelte/transition" + import { fly } from "svelte/transition" import { - Detail, + Heading, Icon, ActionButton, notifications, @@ -57,26 +57,24 @@
- {automation.name} -
- + {automation.name} +
+
- +
{ testDataModal.show() }} icon="MultipleCheck" - size="S">Run testRun test
@@ -84,16 +82,11 @@ {#each blocks as block, idx (block.id)}
- {#if idx !== blocks.length - 1} -
- -
- {/if}
{/each}
@@ -114,14 +107,6 @@
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index f077ac35d7..0c0b79c3de 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -14,7 +14,6 @@ import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import ResultsModal from "./ResultsModal.svelte" import ActionModal from "./ActionModal.svelte" - import { database } from "stores/backend" import { externalActions } from "./ExternalActions" export let onSelect @@ -29,7 +28,6 @@ $: testResult = $automationStore.selectedAutomation.testResults?.steps.filter( step => step.stepId === block.stepId ) - $: instanceId = $database._id $: isTrigger = block.type === "TRIGGER" @@ -40,6 +38,10 @@ $: blockIdx = steps.findIndex(step => step.id === block.id) $: lastStep = !isTrigger && blockIdx + 1 === steps.length + $: totalBlocks = + $automationStore.selectedAutomation?.automation?.definition?.steps.length + + 1 + // Logic for hiding / showing the add button.first we check if it has a child // then we check to see whether its inputs have been commpleted $: disableAddButton = isTrigger @@ -167,13 +169,24 @@ - +
+
+ actionModal.show()} + disabled={!hasCompletedInputs} + hoverable + name="AddCircle" + size="S" +/> +{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2} +
+{/if} diff --git a/packages/builder/src/components/automation/Shared/WebhookDisplay.svelte b/packages/builder/src/components/automation/Shared/WebhookDisplay.svelte index fc5e20f241..857640896c 100644 --- a/packages/builder/src/components/automation/Shared/WebhookDisplay.svelte +++ b/packages/builder/src/components/automation/Shared/WebhookDisplay.svelte @@ -1,22 +1,14 @@ saveDatasource()} - confirmText={integration.plus + onCancel={() => modal.show()} + confirmText={config.plus ? "Fetch tables from database" : "Save and continue to query"} cancelText="Back" @@ -62,10 +72,7 @@ - + diff --git a/packages/builder/src/components/upgrade/UpgradeModal.svelte b/packages/builder/src/components/upgrade/UpgradeModal.svelte index 570dcc06a1..7571e6d773 100644 --- a/packages/builder/src/components/upgrade/UpgradeModal.svelte +++ b/packages/builder/src/components/upgrade/UpgradeModal.svelte @@ -1,10 +1,11 @@ @@ -21,12 +22,12 @@ - Upgrade to Budibase self-hosting for free, and get SSO, unlimited apps, - and more - and it only takes a few minutes! + + Self-host budibase for free to get unlimited apps and more - and it only + takes a few minutes! + diff --git a/packages/builder/src/constants/lucene.js b/packages/builder/src/constants/lucene.js index 00da0c29bc..132790739c 100644 --- a/packages/builder/src/constants/lucene.js +++ b/packages/builder/src/constants/lucene.js @@ -44,6 +44,15 @@ export const OperatorOptions = { }, } +export const NoEmptyFilterStrings = [ + OperatorOptions.StartsWith.value, + OperatorOptions.Like.value, + OperatorOptions.Equals.value, + OperatorOptions.NotEquals.value, + OperatorOptions.Contains.value, + OperatorOptions.NotContains.value, +] + /** * Returns the valid operator options for a certain data type * @param type the data type diff --git a/packages/builder/src/helpers/lucene.js b/packages/builder/src/helpers/lucene.js index 03baa751cc..d344b462d1 100644 --- a/packages/builder/src/helpers/lucene.js +++ b/packages/builder/src/helpers/lucene.js @@ -1,3 +1,26 @@ +import { NoEmptyFilterStrings } from "../constants/lucene" + +/** + * Removes any fields that contain empty strings that would cause inconsistent + * behaviour with how backend tables are filtered (no value means no filter). + */ +function cleanupQuery(query) { + if (!query) { + return query + } + for (let filterField of NoEmptyFilterStrings) { + if (!query[filterField]) { + continue + } + for (let [key, value] of Object.entries(query[filterField])) { + if (!value || value === "") { + delete query[filterField][key] + } + } + } + return query +} + /** * Builds a lucene JSON query from the filter structure generated in the builder * @param filter the builder filter structure @@ -76,6 +99,8 @@ export const luceneQuery = (docs, query) => { if (!query) { return docs } + // make query consistent first + query = cleanupQuery(query) // Iterates over a set of filters and evaluates a fail function against a doc const match = (type, failFn) => doc => { diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 5a8e5e2fc9..39c93f0fb0 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -11,18 +11,38 @@ $: cloud = $admin.cloud $: user = $auth.user - const validateTenantId = async () => { - // set the tenant from the url in the cloud - const tenantId = window.location.host.split(".")[0] + $: useAccountPortal = cloud && !$admin.disableAccountPortal - if (!tenantId.includes("localhost:")) { - // user doesn't have permission to access this tenant - kick them out - if (user?.tenantId !== tenantId) { + const validateTenantId = async () => { + const host = window.location.host + if (host.includes("localhost:")) { + // ignore local dev + return + } + + // e.g. ['tenant', 'budibase', 'app'] vs ['budibase', 'app'] + let urlTenantId + const hostParts = host.split(".") + if (hostParts.length > 2) { + urlTenantId = hostParts[0] + } + + if (user && user.tenantId) { + // no tenant in the url - send to account portal to fix this + if (!urlTenantId) { + window.location.href = $admin.accountPortalUrl + return + } + + if (user.tenantId !== urlTenantId) { + // user should not be here - play it safe and log them out await auth.logout() await auth.setOrganisation(null) - } else { - await auth.setOrganisation(tenantId) + return } + } else { + // no user - set the org according to the url + await auth.setOrganisation(urlTenantId) } } @@ -30,7 +50,7 @@ await auth.checkAuth() await admin.init() - if (cloud && multiTenancyEnabled) { + if (useAccountPortal && multiTenancyEnabled) { await validateTenantId() } @@ -38,31 +58,35 @@ }) $: { - // We should never see the org or admin user creation screens in the cloud - if (!cloud) { - const apiReady = $admin.loaded && $auth.loaded - // if tenant is not set go to it - if (loaded && apiReady && multiTenancyEnabled && !tenantSet) { - $redirect("./auth/org") - } - // Force creation of an admin user if one doesn't exist - else if (loaded && apiReady && !hasAdminUser) { - $redirect("./admin") - } - } - } - // Redirect to log in at any time if the user isn't authenticated - $: { + const apiReady = $admin.loaded && $auth.loaded + // if tenant is not set go to it if ( + loaded && + !useAccountPortal && + apiReady && + multiTenancyEnabled && + !tenantSet + ) { + $redirect("./auth/org") + } + // Force creation of an admin user if one doesn't exist + else if (loaded && !useAccountPortal && apiReady && !hasAdminUser) { + $redirect("./admin") + } + // Redirect to log in at any time if the user isn't authenticated + else if ( loaded && (hasAdminUser || cloud) && !$auth.user && !$isActive("./auth") && - !$isActive("./invite") + !$isActive("./invite") && + !$isActive("./admin") ) { const returnUrl = encodeURIComponent(window.location.pathname) $redirect("./auth?", { returnUrl }) - } else if ($auth?.user?.forceResetPassword) { + } + // check if password reset required for user + else if ($auth.user?.forceResetPassword) { $redirect("./auth/reset") } } diff --git a/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte new file mode 100644 index 0000000000..633147e910 --- /dev/null +++ b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte @@ -0,0 +1,50 @@ + + + + Please upload the file that was exported from your Cloud environment to get + started + { + value.file = e.detail?.[0] + }} + /> + diff --git a/packages/builder/src/pages/builder/admin/_layout.svelte b/packages/builder/src/pages/builder/admin/_layout.svelte index 602b6a6de3..f03a7b8285 100644 --- a/packages/builder/src/pages/builder/admin/_layout.svelte +++ b/packages/builder/src/pages/builder/admin/_layout.svelte @@ -5,8 +5,11 @@ let loaded = false + $: cloud = $admin.cloud + $: useAccountPortal = cloud && !$admin.disableAccountPortal + onMount(() => { - if ($admin?.checklist?.adminUser.checked) { + if ($admin?.checklist?.adminUser.checked || useAccountPortal) { $redirect("../") } else { loaded = true diff --git a/packages/builder/src/pages/builder/admin/index.svelte b/packages/builder/src/pages/builder/admin/index.svelte index 4d7e39db81..f3a8d62d30 100644 --- a/packages/builder/src/pages/builder/admin/index.svelte +++ b/packages/builder/src/pages/builder/admin/index.svelte @@ -7,18 +7,22 @@ Input, Body, ActionButton, + Modal, } from "@budibase/bbui" import { goto } from "@roxi/routify" import api from "builderStore/api" import { admin, auth } from "stores/portal" import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte" + import ImportAppsModal from "./_components/ImportAppsModal.svelte" import Logo from "assets/bb-emblem.svg" let adminUser = {} let error + let modal $: tenantId = $auth.tenantId $: multiTenancyEnabled = $admin.multiTenancy + $: cloud = $admin.cloud async function save() { try { @@ -38,6 +42,9 @@ } + + +
@@ -66,6 +73,15 @@ > Change organisation + {:else if !cloud} + { + modal.show() + }} + > + Import from cloud + {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index ae21a9dbb9..603fb62d99 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -8,7 +8,7 @@ import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte" import { get } from "builderStore/api" import { auth, admin } from "stores/portal" - import { isActive, goto, layout } from "@roxi/routify" + import { isActive, goto, layout, redirect } from "@roxi/routify" import Logo from "assets/bb-emblem.svg" import { capitalise } from "helpers" import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte" @@ -34,7 +34,16 @@ const pkg = await res.json() if (res.ok) { - await store.actions.initialise(pkg) + try { + await store.actions.initialise(pkg) + // edge case, lock wasn't known to client when it re-directed, or user went directly + } catch (err) { + if (!err.ok && err.reason === "locked") { + $redirect("../../") + } else { + throw err + } + } await automationStore.actions.fetch() await roles.fetch() return pkg @@ -92,7 +101,7 @@
- {#if $admin.cloud} + {#if $admin.cloud && $auth.user.account} {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte index fbc2b401ef..583ca5e887 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte @@ -156,6 +156,8 @@ ...relateTo, through: through._id, fieldName: fromTable.primary[0], + throughFrom: relateFrom.throughTo, + throughTo: relateFrom.throughFrom, } } else { // the relateFrom.fieldName should remain the same, as it is the foreignKey in the other @@ -251,6 +253,22 @@ bind:error={errors.through} bind:value={fromRelationship.through} /> + {#if fromTable && toTable && through} + ($touched.toForeign = true)} + bind:error={errors.toForeign} + bind:value={fromRelationship.throughFrom} + /> + {/if} {:else if fromRelationship?.relationshipType && toTable} +
+
- {#each enrichedApps as app (app.appId)} + {#each filteredApps as app (app.appId)} - -
- -
- - The purpose of the Budibase builder is to help you build beautiful, - powerful applications quickly and easily. - -
+
{/if} @@ -298,10 +307,26 @@ flex-direction: row; justify-content: space-between; align-items: center; + gap: 10px; + } + + @media only screen and (max-width: 560px) { + .title { + flex-direction: column; + align-items: flex-start; + } } .select { - width: 190px; + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 10px; + } + .filter :global(.spectrum-ActionGroup) { + flex-wrap: nowrap; + } + .mobile-search { + display: none; } .appGrid { @@ -342,5 +367,11 @@ .appTable { grid-template-columns: 1fr auto; } + .desktop-search { + display: none; + } + .mobile-search { + display: block; + } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index b96cc15641..0a8555cfad 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -52,11 +52,11 @@ async function deleteUser() { const res = await users.delete(userId) - if (res.message) { + if (res.status === 200) { notifications.success(`User ${$userFetch?.data?.email} deleted.`) $goto("./") } else { - notifications.error("Failed to delete user.") + notifications.error(res?.message ? res.message : "Failed to delete user.") } } diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index 44ff63a082..ebe8294060 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -7,6 +7,7 @@ export function createAdminStore() { loaded: false, multiTenancy: false, cloud: false, + disableAccountPortal: false, accountPortalUrl: "", onboardingProgress: 0, checklist: { @@ -47,12 +48,14 @@ export function createAdminStore() { async function getEnvironment() { let multiTenancyEnabled = false let cloud = false + let disableAccountPortal = false let accountPortalUrl = "" try { const response = await api.get(`/api/system/environment`) const json = await response.json() multiTenancyEnabled = json.multiTenancy cloud = json.cloud + disableAccountPortal = json.disableAccountPortal accountPortalUrl = json.accountPortalUrl } catch (err) { // just let it stay disabled @@ -60,6 +63,7 @@ export function createAdminStore() { admin.update(store => { store.multiTenancy = multiTenancyEnabled store.cloud = cloud + store.disableAccountPortal = disableAccountPortal store.accountPortalUrl = accountPortalUrl return store }) diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 17299dc056..0535b2626d 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -55,7 +55,11 @@ export function createUsersStore() { async function del(id) { const response = await api.delete(`/api/global/users/${id}`) update(users => users.filter(user => user._id !== id)) - return await response.json() + const json = await response.json() + return { + ...json, + status: response.status, + } } async function save(data) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 2ebdb4cbad..62a90b5c6d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index 00e03c4e9a..d4ddb724f3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^0.9.146-alpha.4", + "@budibase/bbui": "^0.9.154-alpha.1", "@budibase/standard-components": "^0.9.139", - "@budibase/string-templates": "^0.9.146-alpha.4", + "@budibase/string-templates": "^0.9.154-alpha.1", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" diff --git a/packages/client/src/api/analytics.js b/packages/client/src/api/analytics.js new file mode 100644 index 0000000000..5a089eaa21 --- /dev/null +++ b/packages/client/src/api/analytics.js @@ -0,0 +1,10 @@ +import API from "./api" + +/** + * Notifies that an end user client app has been loaded. + */ +export const pingEndUser = async () => { + return await API.post({ + url: `/api/analytics/ping`, + }) +} diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index d83ede61f0..1fd5b18139 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -9,3 +9,4 @@ export * from "./routes" export * from "./queries" export * from "./app" export * from "./automations" +export * from "./analytics" diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index c9c033caa3..462daf7021 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -23,6 +23,7 @@ import SelectionIndicator from "components/preview/SelectionIndicator.svelte" import HoverIndicator from "components/preview/HoverIndicator.svelte" import CustomThemeWrapper from "./CustomThemeWrapper.svelte" + import DNDHandler from "components/preview/DNDHandler.svelte" import ErrorSVG from "builder/assets/error.svg" // Provide contexts @@ -40,6 +41,8 @@ dataLoaded = true if ($builderStore.inBuilder) { builderStore.actions.notifyLoaded() + } else { + builderStore.actions.pingEndUser() } }) @@ -104,7 +107,10 @@
{#key $screenStore.activeLayout._id} - + {/key} @@ -122,6 +128,7 @@ {#if $builderStore.inBuilder} + {/if}
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 785b989754..faf0226604 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -11,6 +11,8 @@ import Placeholder from "components/app/Placeholder.svelte" export let instance = {} + export let isLayout = false + export let isScreen = false // The enriched component settings let enrichedSettings @@ -49,11 +51,11 @@ $: children = instance._children || [] $: id = instance._id $: name = instance._instanceName - $: empty = - !children.length && - definition?.hasChildren && - definition?.showEmptyState !== false && - $builderStore.inBuilder + $: interactive = + $builderStore.inBuilder && + ($builderStore.previewType === "layout" || insideScreenslot) + $: empty = interactive && !children.length && definition?.hasChildren + $: emptyState = empty && definition?.showEmptyState !== false $: rawProps = getRawProps(instance) $: instanceKey = JSON.stringify(rawProps) $: updateComponentProps(rawProps, instanceKey, $context) @@ -61,16 +63,16 @@ $builderStore.inBuilder && $builderStore.selectedComponentId === instance._id $: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) - $: interactive = $builderStore.previewType === "layout" || insideScreenslot $: evaluateConditions(enrichedSettings?._conditions) $: componentSettings = { ...enrichedSettings, ...conditionalSettings } + $: renderKey = `${propsHash}-${emptyState}` // Update component context $: componentStore.set({ id, children: children.length, - styles: { ...instance._styles, id, empty, interactive }, - empty, + styles: { ...instance._styles, id, empty: emptyState, interactive }, + empty: emptyState, selected, name, }) @@ -169,13 +171,22 @@ conditionalSettings = result.settingUpdates visible = nextVisible } + + // Drag and drop helper tags + $: draggable = interactive && !isLayout && !isScreen + $: droppable = interactive && !isLayout && !isScreen -{#key propsHash} +{#key renderKey} {#if constructor && componentSettings && (visible || inSelectedPath)} + +
@@ -184,7 +195,7 @@ {#each children as child (child._id)} {/each} - {:else if empty} + {:else if emptyState} {/if} @@ -196,4 +207,10 @@ .component { display: contents; } + .interactive :global(*:hover) { + cursor: pointer; + } + .draggable :global(*:hover) { + cursor: grab; + } diff --git a/packages/client/src/components/Screen.svelte b/packages/client/src/components/Screen.svelte index a759119320..4878df157f 100644 --- a/packages/client/src/components/Screen.svelte +++ b/packages/client/src/components/Screen.svelte @@ -22,6 +22,6 @@ {#key screenDefinition?._id} - + {/key} diff --git a/packages/client/src/components/app/Button.svelte b/packages/client/src/components/app/Button.svelte index ed465e3e66..9900c740d2 100644 --- a/packages/client/src/components/app/Button.svelte +++ b/packages/client/src/components/app/Button.svelte @@ -31,4 +31,7 @@ .spectrum-Button--overBackground:hover { color: #555; } + .spectrum-Button::after { + display: none; + } diff --git a/packages/client/src/components/app/Container.svelte b/packages/client/src/components/app/Container.svelte index 5b2f951c8d..148179c98f 100644 --- a/packages/client/src/components/app/Container.svelte +++ b/packages/client/src/components/app/Container.svelte @@ -34,7 +34,7 @@ display: flex; max-width: 100%; } - .valid-container :global([data-type="component"] > *) { + .valid-container :global(.component > *) { max-width: 100%; } .direction-row { @@ -46,7 +46,7 @@ /* Grow containers inside a row need 0 width 0 so that they ignore content */ /* The nested selector for data-type is the wrapper around all components */ - .direction-row :global(> [data-type="component"] > .size-grow) { + .direction-row :global(> .component > .size-grow) { width: 0; } diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index 9a7ce13220..0ffbbf7ab1 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -353,7 +353,7 @@ } /* Reduce padding */ - .mobile .main { + .mobile:not(.layout--none) .main { padding: 16px; } diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte new file mode 100644 index 0000000000..8a824f6131 --- /dev/null +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -0,0 +1,240 @@ + + + + + + + diff --git a/packages/client/src/components/preview/DNDPositionIndicator.svelte b/packages/client/src/components/preview/DNDPositionIndicator.svelte new file mode 100644 index 0000000000..011b386282 --- /dev/null +++ b/packages/client/src/components/preview/DNDPositionIndicator.svelte @@ -0,0 +1,64 @@ + + +{#key renderKey} + {#if dimensions && dropInfo?.mode !== "inside"} + + {/if} +{/key} diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index 9518c6d101..1a9e6477ac 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -7,7 +7,7 @@ $: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 const onMouseOver = e => { - const element = e.target.closest("[data-type='component']") + const element = e.target.closest(".interactive.component") const newId = element?.dataset?.id if (newId !== componentId) { componentId = newId @@ -30,7 +30,7 @@
{#if text} -
+
{text}
{/if} @@ -30,6 +35,7 @@ diff --git a/packages/client/src/components/preview/IndicatorSet.svelte b/packages/client/src/components/preview/IndicatorSet.svelte index 957c7d7bef..012aa7e470 100644 --- a/packages/client/src/components/preview/IndicatorSet.svelte +++ b/packages/client/src/components/preview/IndicatorSet.svelte @@ -7,6 +7,7 @@ export let color export let transition export let zIndex + export let prefix = null let indicators = [] let interval @@ -51,6 +52,9 @@ const parents = document.getElementsByClassName(componentId) if (parents.length) { text = parents[0].dataset.name + if (prefix) { + text = `${prefix} ${text}` + } } // Batch reads to minimize reflow diff --git a/packages/client/src/components/preview/SettingsBar.svelte b/packages/client/src/components/preview/SettingsBar.svelte index 3581d4f5f8..43c77ef1e6 100644 --- a/packages/client/src/components/preview/SettingsBar.svelte +++ b/packages/client/src/components/preview/SettingsBar.svelte @@ -16,7 +16,7 @@ let measured = false $: definition = $builderStore.selectedComponentDefinition - $: showBar = definition?.showSettingsBar + $: showBar = definition?.showSettingsBar && !$builderStore.isDragging $: settings = definition?.settings?.filter(setting => setting.showInBar) ?? [] const updatePosition = () => { diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index 3f36af1d06..2740a4551c 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -1,6 +1,7 @@ import { writable, derived } from "svelte/store" import Manifest from "manifest.json" import { findComponentById, findComponentPathById } from "../utils/components" +import { pingEndUser } from "../api" const dispatchEvent = (type, data = {}) => { window.dispatchEvent( @@ -23,6 +24,7 @@ const createBuilderStore = () => { theme: null, customTheme: null, previewDevice: "desktop", + isDragging: false, } const writableStore = writable(initialState) const derivedStore = derived(writableStore, $state => { @@ -63,14 +65,28 @@ const createBuilderStore = () => { notifyLoaded: () => { dispatchEvent("preview-loaded") }, + pingEndUser: () => { + pingEndUser() + }, setSelectedPath: path => { - console.log("set to ") - console.log(path) writableStore.update(state => { state.selectedPath = path return state }) }, + moveComponent: (componentId, destinationComponentId, mode) => { + dispatchEvent("move-component", { + componentId, + destinationComponentId, + mode, + }) + }, + setDragging: dragging => { + writableStore.update(state => { + state.isDragging = dragging + return state + }) + }, } return { ...writableStore, diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index d9925af91a..6a2562fb48 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -23,10 +23,14 @@ export const styleable = (node, styles = {}) => { let applyHoverStyles let selectComponent + // Allow dragging if required + const parent = node.closest(".component") + if (parent && parent.classList.contains("draggable")) { + node.setAttribute("draggable", true) + } + // Creates event listeners and applies initial styles const setupStyles = (newStyles = {}) => { - // Use empty state styles as base styles if required, but let them, get - // overridden by any user specified styles let baseStyles = {} if (newStyles.empty) { baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)" @@ -45,7 +49,6 @@ export const styleable = (node, styles = {}) => { // Applies a style string to a DOM node const applyStyles = styleString => { node.style = styleString - node.dataset.componentId = componentId } // Applies the "normal" style definition diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index 5a388777e4..635f97b107 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -29,9 +29,9 @@ js-tokens "^4.0.0" "@budibase/bbui@^0.9.139": - version "0.9.145" - resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.145.tgz#e65425e927e9488847aaf8209ff3eb0cf00c219c" - integrity sha512-vHSi+J52U24YSJPd1cfH9ePN92kCGLxKw4naYDjavYGd568GbRPJWzerzyqhm4VQtWn8FFi47jbzAsfAhiFfLA== + version "0.9.142" + resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.142.tgz#7edbda7967c9e5dfc96e5be5231656e5aab8d0e3" + integrity sha512-m2YlqqH87T4RwqD/oGhH6twHIgvFv4oUMEhKpkgLsbxjXVLVD0OOF7WqjpDnSa4khVQaixjdkI/Jiw2qhBUSaA== dependencies: "@adobe/spectrum-css-workflow-icons" "^1.2.1" "@spectrum-css/actionbutton" "^1.0.1" diff --git a/packages/server/package.json b/packages/server/package.json index 6cead7d6a8..afe8bebf2c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "description": "Budibase Web Server", "main": "src/index.js", "repository": { @@ -27,7 +27,9 @@ "multi:enable": "node scripts/multiTenancy.js enable", "multi:disable": "node scripts/multiTenancy.js disable", "selfhost:enable": "node scripts/selfhost.js enable", - "selfhost:disable": "node scripts/selfhost.js disable" + "selfhost:disable": "node scripts/selfhost.js disable", + "localdomain:enable": "node scripts/localdomain.js enable", + "localdomain:disable": "node scripts/localdomain.js disable" }, "jest": { "preset": "ts-jest", @@ -64,9 +66,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.146-alpha.4", - "@budibase/client": "^0.9.146-alpha.4", - "@budibase/string-templates": "^0.9.146-alpha.4", + "@budibase/auth": "^0.9.154-alpha.1", + "@budibase/client": "^0.9.154-alpha.1", + "@budibase/string-templates": "^0.9.154-alpha.1", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", @@ -96,6 +98,7 @@ "koa-session": "5.12.0", "koa-static": "5.0.0", "lodash": "4.17.21", + "memorystream": "^0.3.1", "mongodb": "3.6.3", "mssql": "6.2.3", "mysql": "2.18.1", @@ -103,6 +106,7 @@ "open": "7.3.0", "pg": "8.5.1", "pino-pretty": "4.0.0", + "posthog-node": "^1.1.4", "pouchdb": "7.2.1", "pouchdb-adapter-memory": "^7.2.1", "pouchdb-all-dbs": "1.0.2", diff --git a/packages/server/scripts/dev/manage.js b/packages/server/scripts/dev/manage.js index 2557f88adf..bd91056f84 100644 --- a/packages/server/scripts/dev/manage.js +++ b/packages/server/scripts/dev/manage.js @@ -37,7 +37,7 @@ async function init() { const envFileJson = { PORT: 4001, MINIO_URL: "http://localhost:10000/", - COUCH_DB_URL: "http://@localhost:10000/db/", + COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/", REDIS_URL: "localhost:6379", WORKER_URL: "http://localhost:4002", INTERNAL_API_KEY: "budibase", @@ -48,6 +48,7 @@ async function init() { COUCH_DB_PASSWORD: "budibase", COUCH_DB_USER: "budibase", SELF_HOSTED: 1, + DISABLE_ACCOUNT_PORTAL: "", MULTI_TENANCY: "", } let envFile = "" diff --git a/packages/server/scripts/integrations/customer-categories/docker-compose.yml b/packages/server/scripts/integrations/customer-categories/docker-compose.yml new file mode 100644 index 0000000000..4dfcb0e1ad --- /dev/null +++ b/packages/server/scripts/integrations/customer-categories/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.8" +services: + db: + container_name: postgres + image: postgres + restart: always + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: root + POSTGRES_DB: main + ports: + - "5432:5432" + volumes: + #- pg_data:/var/lib/postgresql/data/ + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + + pgadmin: + container_name: pgadmin-pg + image: dpage/pgadmin4 + restart: always + environment: + PGADMIN_DEFAULT_EMAIL: root@root.com + PGADMIN_DEFAULT_PASSWORD: root + ports: + - "5050:80" + +#volumes: +# pg_data: diff --git a/packages/server/scripts/integrations/customer-categories/init.sql b/packages/server/scripts/integrations/customer-categories/init.sql new file mode 100644 index 0000000000..26868ef8f3 --- /dev/null +++ b/packages/server/scripts/integrations/customer-categories/init.sql @@ -0,0 +1,41 @@ +SELECT 'CREATE DATABASE main' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec + +CREATE TABLE categories +( + name text COLLATE pg_catalog."default", + id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + CONSTRAINT categories_pkey PRIMARY KEY (id) +); + +CREATE TABLE customers +( + id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + name text COLLATE pg_catalog."default", + email text COLLATE pg_catalog."default", + age integer, + "dateOfBirth" date, + CONSTRAINT customers_pkey PRIMARY KEY (id) +); + +CREATE TABLE customer_category +( + customer_id integer, + category_id integer, + notes text COLLATE pg_catalog."default", + id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + CONSTRAINT "Category" FOREIGN KEY (category_id) + REFERENCES public.categories (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + NOT VALID, + CONSTRAINT "Customer" FOREIGN KEY (customer_id) + REFERENCES public.customers (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + NOT VALID +); + + +INSERT INTO customers (name, email, age) VALUES ('Mike', 'mike@mike.com', 30); +INSERT INTO categories (name) VALUES ('Books'); diff --git a/packages/server/scripts/integrations/customer-categories/reset.sh b/packages/server/scripts/integrations/customer-categories/reset.sh new file mode 100755 index 0000000000..32778bd11f --- /dev/null +++ b/packages/server/scripts/integrations/customer-categories/reset.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker-compose down +docker volume prune -f diff --git a/packages/server/scripts/localdomain.js b/packages/server/scripts/localdomain.js new file mode 100644 index 0000000000..92f229f058 --- /dev/null +++ b/packages/server/scripts/localdomain.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +const updateDotEnv = require("update-dotenv") + +const arg = process.argv.slice(2)[0] + +/** + * For testing multi tenancy sub domains locally. + * + * Relies on an entry in /etc/hosts e.g: + * + * 127.0.0.1 local.com + * + * and an entry for each tenant you wish to test locally e.g: + * + * 127.0.0.1 t1.local.com + * 127.0.0.1 t2.local.com + */ +updateDotEnv({ + ACCOUNT_PORTAL_URL: + arg === "enable" ? "http://local.com:10001" : "http://localhost:10001", + COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "", +}).then(() => console.log("Updated worker!")) diff --git a/packages/server/src/api/controllers/analytics.js b/packages/server/src/api/controllers/analytics.js index eb64bc87b9..e6e5d6a748 100644 --- a/packages/server/src/api/controllers/analytics.js +++ b/packages/server/src/api/controllers/analytics.js @@ -1,7 +1,32 @@ const env = require("../../environment") +const PostHog = require("posthog-node") -exports.isEnabled = async function (ctx) { +let posthogClient + +if (env.POSTHOG_TOKEN && env.ENABLE_ANALYTICS && !env.SELF_HOSTED) { + posthogClient = new PostHog(env.POSTHOG_TOKEN) +} + +exports.isEnabled = async ctx => { ctx.body = { enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true", } } + +exports.endUserPing = async ctx => { + if (!posthogClient) { + ctx.body = { + ping: false, + } + return + } + + posthogClient.capture("budibase:end_user_ping", { + userId: ctx.user && ctx.user._id, + appId: ctx.appId, + }) + + ctx.body = { + ping: true, + } +} diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 76675116c2..6608ba0cac 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -31,7 +31,7 @@ const { getDeployedApps, removeAppFromUserRoles, } = require("../../utilities/workerRequests") -const { clientLibraryPath } = require("../../utilities") +const { clientLibraryPath, stringToReadStream } = require("../../utilities") const { getAllLocks } = require("../../utilities/redis") const { updateClientLibrary, @@ -82,7 +82,7 @@ async function getAppUrlIfNotInUse(ctx) { if (!env.SELF_HOSTED) { return url } - const deployedApps = await getDeployedApps(ctx) + const deployedApps = await getDeployedApps() if ( url && deployedApps[url] != null && @@ -114,8 +114,13 @@ async function createInstance(template) { // replicate the template data to the instance DB // this is currently very hard to test, downloading and importing template files - /* istanbul ignore next */ - if (template && template.useTemplate === "true") { + if (template && template.templateString) { + const { ok } = await db.load(stringToReadStream(template.templateString)) + if (!ok) { + throw "Error loading database dump from memory." + } + } else if (template && template.useTemplate === "true") { + /* istanbul ignore next */ const { ok } = await db.load(await getTemplateStream(template)) if (!ok) { throw "Error loading database dump from template." @@ -191,10 +196,11 @@ exports.fetchAppPackage = async function (ctx) { } exports.create = async function (ctx) { - const { useTemplate, templateKey } = ctx.request.body + const { useTemplate, templateKey, templateString } = ctx.request.body const instanceConfig = { useTemplate, key: templateKey, + templateString, } if (ctx.request.files && ctx.request.files.templateFile) { instanceConfig.file = ctx.request.files.templateFile diff --git a/packages/server/src/api/controllers/cloud.js b/packages/server/src/api/controllers/cloud.js new file mode 100644 index 0000000000..aac79bb9dd --- /dev/null +++ b/packages/server/src/api/controllers/cloud.js @@ -0,0 +1,92 @@ +const env = require("../../environment") +const { getAllApps } = require("@budibase/auth/db") +const CouchDB = require("../../db") +const { + exportDB, + sendTempFile, + readFileSync, +} = require("../../utilities/fileSystem") +const { stringToReadStream } = require("../../utilities") +const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy") +const { create } = require("./application") +const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils") + +async function createApp(appName, appImport) { + const ctx = { + request: { + body: { + templateString: appImport, + name: appName, + }, + }, + } + return create(ctx) +} + +exports.exportApps = async ctx => { + if (env.SELF_HOSTED || !env.MULTI_TENANCY) { + ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.") + } + const apps = await getAllApps(CouchDB, { all: true }) + const globalDBString = await exportDB(getGlobalDBName()) + let allDBs = { + global: globalDBString, + } + for (let app of apps) { + // only export the dev apps as they will be the latest, the user can republish the apps + // in their self hosted environment + if (isDevAppID(app._id)) { + allDBs[app.name] = await exportDB(app._id) + } + } + const filename = `cloud-export-${new Date().getTime()}.txt` + ctx.attachment(filename) + ctx.body = sendTempFile(JSON.stringify(allDBs)) +} + +async function getAllDocType(db, docType) { + const response = await db.allDocs( + getDocParams(docType, null, { + include_docs: true, + }) + ) + return response.rows.map(row => row.doc) +} + +exports.importApps = async ctx => { + if (!env.SELF_HOSTED || env.MULTI_TENANCY) { + ctx.throw(400, "Importing only allowed in self hosted environments.") + } + const apps = await getAllApps(CouchDB, { all: true }) + if ( + apps.length !== 0 || + !ctx.request.files || + !ctx.request.files.importFile + ) { + ctx.throw( + 400, + "Import file is required and environment must be fresh to import apps." + ) + } + const importFile = ctx.request.files.importFile + const importString = readFileSync(importFile.path) + const dbs = JSON.parse(importString) + const globalDbImport = dbs.global + // remove from the list of apps + delete dbs.global + const globalDb = getGlobalDB() + // load the global db first + await globalDb.load(stringToReadStream(globalDbImport)) + for (let [appName, appImport] of Object.entries(dbs)) { + await createApp(appName, appImport) + } + // once apps are created clean up the global db + let users = await getAllDocType(globalDb, DocumentTypes.USER) + for (let user of users) { + delete user.tenantId + } + await globalDb.bulkDocs(users) + ctx.body = { + message: "Apps successfully imported.", + } +} diff --git a/packages/server/src/api/controllers/deploy/index.js b/packages/server/src/api/controllers/deploy/index.js index 08f9072138..d68b2064d7 100644 --- a/packages/server/src/api/controllers/deploy/index.js +++ b/packages/server/src/api/controllers/deploy/index.js @@ -64,6 +64,7 @@ async function storeDeploymentHistory(deployment) { async function initDeployedApp(prodAppId) { const db = new CouchDB(prodAppId) + console.log("Reading automation docs") const automations = ( await db.allDocs( getAutomationParams(null, { @@ -71,12 +72,17 @@ async function initDeployedApp(prodAppId) { }) ) ).rows.map(row => row.doc) + console.log("You have " + automations.length + " automations") const promises = [] + console.log("Disabling prod crons..") await disableAllCrons(prodAppId) + console.log("Prod Cron triggers disabled..") + console.log("Enabling cron triggers for deployed app..") for (let automation of automations) { promises.push(enableCronTrigger(prodAppId, automation)) } await Promise.all(promises) + console.log("Enabled cron triggers for deployed app..") } async function deployApp(deployment) { @@ -88,13 +94,18 @@ async function deployApp(deployment) { target: productionAppId, }) + console.log("Replication object created") + await replication.replicate() + console.log("replication complete.. replacing app meta doc") const db = new CouchDB(productionAppId) const appDoc = await db.get(DocumentTypes.APP_METADATA) appDoc.appId = productionAppId appDoc.instance._id = productionAppId await db.put(appDoc) + console.log("New app doc written successfully.") + console.log("Setting up live repl between dev and prod") // Set up live sync between the live and dev instances const liveReplication = new Replication({ source: productionAppId, @@ -105,8 +116,11 @@ async function deployApp(deployment) { return doc._id !== DocumentTypes.APP_METADATA }, }) + console.log("Set up live repl between dev and prod") + console.log("Initialising deployed app") await initDeployedApp(productionAppId) + console.log("Init complete, setting deployment to successful") deployment.setStatus(DeploymentStatus.SUCCESS) await storeDeploymentHistory(deployment) } catch (err) { @@ -153,9 +167,13 @@ exports.deploymentProgress = async function (ctx) { exports.deployApp = async function (ctx) { let deployment = new Deployment(ctx.appId) + console.log("Deployment object created") deployment.setStatus(DeploymentStatus.PENDING) + console.log("Deployment object set to pending") deployment = await storeDeploymentHistory(deployment) + console.log("Stored deployment history") + console.log("Deploying app...") await deployApp(deployment) ctx.body = deployment diff --git a/packages/server/src/api/controllers/hosting.js b/packages/server/src/api/controllers/hosting.js index aa8664cd92..6b47dc8634 100644 --- a/packages/server/src/api/controllers/hosting.js +++ b/packages/server/src/api/controllers/hosting.js @@ -18,5 +18,5 @@ exports.fetchUrls = async ctx => { } exports.getDeployedApps = async ctx => { - ctx.body = await getDeployedApps(ctx) + ctx.body = await getDeployedApps() } diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 75c3e9b492..222f921018 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -178,7 +178,12 @@ module External { manyRelationships: ManyRelationship[] = [] for (let [key, field] of Object.entries(table.schema)) { // if set already, or not set just skip it - if (!row[key] || newRow[key] || field.autocolumn) { + if ((!row[key] && row[key] !== "") || newRow[key] || field.autocolumn) { + continue + } + // if its an empty string then it means return the column to null (if possible) + if (row[key] === "") { + newRow[key] = null continue } // parse floats/numbers @@ -205,9 +210,13 @@ module External { } else { // we're not inserting a doc, will be a bunch of update calls const isUpdate = !field.through - const thisKey: string = isUpdate ? "id" : linkTablePrimary + const thisKey: string = isUpdate + ? "id" + : field.throughTo || linkTablePrimary // @ts-ignore - const otherKey: string = isUpdate ? field.fieldName : tablePrimary + const otherKey: string = isUpdate + ? field.fieldName + : field.throughFrom || tablePrimary row[key].map((relationship: any) => { // we don't really support composite keys for relationships, this is why [0] is used manyRelationships.push({ @@ -328,12 +337,11 @@ module External { if (!table.primary || !linkTable.primary) { continue } - const definition = { + const definition: any = { // if no foreign key specified then use the name of the field in other table from: field.foreignKey || table.primary[0], to: field.fieldName, tableName: linkTableName, - through: undefined, // need to specify where to put this back into column: fieldName, } @@ -343,8 +351,10 @@ module External { ) definition.through = throughTableName // don't support composite keys for relationships - definition.from = table.primary[0] - definition.to = linkTable.primary[0] + definition.from = field.throughFrom || table.primary[0] + definition.to = field.throughTo || linkTable.primary[0] + definition.fromPrimary = table.primary[0] + definition.toPrimary = linkTable.primary[0] } relationships.push(definition) } @@ -369,7 +379,8 @@ module External { } const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY const tableId = isMany ? field.through : field.tableId - const fieldName = isMany ? primaryKey : field.fieldName + const manyKey = field.throughFrom || primaryKey + const fieldName = isMany ? manyKey : field.fieldName const response = await makeExternalQuery(this.appId, { endpoint: getEndpoint(tableId, DataSourceOperation.READ), filters: { diff --git a/packages/server/src/api/controllers/row/external.js b/packages/server/src/api/controllers/row/external.js index 3a96064a9f..534f2207b0 100644 --- a/packages/server/src/api/controllers/row/external.js +++ b/packages/server/src/api/controllers/row/external.js @@ -2,6 +2,7 @@ const { DataSourceOperation, SortDirection, FieldTypes, + NoEmptyFilterStrings, } = require("../../../constants") const { breakExternalTableId, @@ -11,6 +12,19 @@ const ExternalRequest = require("./ExternalRequest") const CouchDB = require("../../../db") async function handleRequest(appId, operation, tableId, opts = {}) { + // make sure the filters are cleaned up, no empty strings for equals, fuzzy or string + if (opts && opts.filters) { + for (let filterField of NoEmptyFilterStrings) { + if (!opts.filters[filterField]) { + continue + } + for (let [key, value] of Object.entries(opts.filters[filterField])) { + if (!value || value === "") { + delete opts.filters[filterField][key] + } + } + } + } return new ExternalRequest(appId, operation, tableId, opts.datasource).run( opts ) diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 02bfad85d2..ffee1dcec3 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -5,7 +5,6 @@ const { generateRowID, DocumentTypes, InternalTables, - generateMemoryViewID, } = require("../../../db/utils") const userController = require("../user") const { @@ -20,7 +19,12 @@ const { fullSearch, paginatedSearch } = require("./internalSearch") const { getGlobalUsersFromMetadata } = require("../../../utilities/global") const inMemoryViews = require("../../../db/inMemoryView") const env = require("../../../environment") -const { migrateToInMemoryView } = require("../view/utils") +const { + migrateToInMemoryView, + migrateToDesignView, + getFromDesignDoc, + getFromMemoryDoc, +} = require("../view/utils") const CALCULATION_TYPES = { SUM: "sum", @@ -74,33 +78,24 @@ async function getRawTableData(ctx, db, tableId) { } async function getView(db, viewName) { - let viewInfo - async function getFromDesignDoc() { - const designDoc = await db.get("_design/database") - viewInfo = designDoc.views[viewName] - return viewInfo - } - let migrate = false - if (env.SELF_HOSTED) { - viewInfo = await getFromDesignDoc() - } else { - try { - viewInfo = await db.get(generateMemoryViewID(viewName)) - if (viewInfo) { - viewInfo = viewInfo.view - } - } catch (err) { - // check if it can be retrieved from design doc (needs migrated) - if (err.status !== 404) { - viewInfo = null - } else { - viewInfo = await getFromDesignDoc() - migrate = !!viewInfo - } + let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc + let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc + let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView + let viewInfo, + migrate = false + try { + viewInfo = await mainGetter(db, viewName) + } catch (err) { + // check if it can be retrieved from design doc (needs migrated) + if (err.status !== 404) { + viewInfo = null + } else { + viewInfo = await secondaryGetter(db, viewName) + migrate = !!viewInfo } } if (migrate) { - await migrateToInMemoryView(db, viewName) + await migration(db, viewName) } if (!viewInfo) { throw "View does not exist." @@ -408,16 +403,32 @@ exports.fetchEnrichedRow = async ctx => { rowId, }) // look up the actual rows based on the ids - const response = await db.allDocs({ - include_docs: true, - keys: linkVals.map(linkVal => linkVal.id), - }) - // need to include the IDs in these rows for any links they may have - let linkedRows = await outputProcessing( - ctx, - table, - response.rows.map(row => row.doc) - ) + let response = ( + await db.allDocs({ + include_docs: true, + keys: linkVals.map(linkVal => linkVal.id), + }) + ).rows.map(row => row.doc) + // group responses by table + let groups = {}, + tables = {} + for (let row of response) { + const linkedTableId = row.tableId + if (groups[linkedTableId] == null) { + groups[linkedTableId] = [row] + tables[linkedTableId] = await db.get(linkedTableId) + } else { + groups[linkedTableId].push(row) + } + } + let linkedRows = [] + for (let [tableId, rows] of Object.entries(groups)) { + // need to include the IDs in these rows for any links they may have + linkedRows = linkedRows.concat( + await outputProcessing(ctx, tables[tableId], rows) + ) + } + // insert the link rows in the correct place throughout the main row for (let fieldName of Object.keys(table.schema)) { let field = table.schema[fieldName] diff --git a/packages/server/src/api/controllers/static/index.js b/packages/server/src/api/controllers/static/index.js index 36d8423219..cc463aefdb 100644 --- a/packages/server/src/api/controllers/static/index.js +++ b/packages/server/src/api/controllers/static/index.js @@ -40,7 +40,7 @@ async function prepareUpload({ s3Key, bucket, metadata, file }) { async function checkForSelfHostedURL(ctx) { // the "appId" component of the URL may actually be a specific self hosted URL let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}` - const apps = await getDeployedApps(ctx) + const apps = await getDeployedApps() if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) { return apps[possibleAppUrl].appId } else { diff --git a/packages/server/src/api/controllers/view/utils.js b/packages/server/src/api/controllers/view/utils.js index c93604177f..1f3b980882 100644 --- a/packages/server/src/api/controllers/view/utils.js +++ b/packages/server/src/api/controllers/view/utils.js @@ -107,3 +107,30 @@ exports.migrateToInMemoryView = async (db, viewName) => { await db.put(designDoc) await exports.saveView(db, null, viewName, view) } + +exports.migrateToDesignView = async (db, viewName) => { + let view = await db.get(generateMemoryViewID(viewName)) + const designDoc = await db.get("_design/database") + designDoc.views[viewName] = view.view + await db.put(designDoc) + await db.remove(view._id, view._rev) +} + +exports.getFromDesignDoc = async (db, viewName) => { + const designDoc = await db.get("_design/database") + let view = designDoc.views[viewName] + if (view == null) { + throw { status: 404, message: "Unable to get view" } + } + return view +} + +exports.getFromMemoryDoc = async (db, viewName) => { + let view = await db.get(generateMemoryViewID(viewName)) + if (view) { + view = view.view + } else { + throw { status: 404, message: "Unable to get view" } + } + return view +} diff --git a/packages/server/src/api/routes/analytics.js b/packages/server/src/api/routes/analytics.js index 781a959acb..be27e7d19b 100644 --- a/packages/server/src/api/routes/analytics.js +++ b/packages/server/src/api/routes/analytics.js @@ -3,6 +3,8 @@ const controller = require("../controllers/analytics") const router = Router() -router.get("/api/analytics", controller.isEnabled) +router + .get("/api/analytics", controller.isEnabled) + .post("/api/analytics/ping", controller.endUserPing) module.exports = router diff --git a/packages/server/src/api/routes/cloud.js b/packages/server/src/api/routes/cloud.js new file mode 100644 index 0000000000..214473f43f --- /dev/null +++ b/packages/server/src/api/routes/cloud.js @@ -0,0 +1,13 @@ +const Router = require("@koa/router") +const controller = require("../controllers/cloud") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("@budibase/auth/permissions") + +const router = Router() + +router + .get("/api/cloud/export", authorized(BUILDER), controller.exportApps) + // has to be public, only run if apps don't exist + .post("/api/cloud/import", controller.importApps) + +module.exports = router diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index 2e1353df98..29d0cd42b4 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -24,6 +24,7 @@ const hostingRoutes = require("./hosting") const backupRoutes = require("./backup") const metadataRoutes = require("./metadata") const devRoutes = require("./dev") +const cloudRoutes = require("./cloud") exports.mainRoutes = [ authRoutes, @@ -49,6 +50,7 @@ exports.mainRoutes = [ backupRoutes, metadataRoutes, devRoutes, + cloudRoutes, // these need to be handled last as they still use /api/:tableId // this could be breaking as koa may recognise other routes as this tableRoutes, diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js index d089d7775d..01284552c5 100644 --- a/packages/server/src/api/routes/tests/row.spec.js +++ b/packages/server/src/api/routes/tests/row.spec.js @@ -317,7 +317,7 @@ describe("/rows", () => { await request .get(`/api/views/derp`) .set(config.defaultHeaders()) - .expect(400) + .expect(404) }) it("should be able to run on a view", async () => { @@ -394,4 +394,4 @@ describe("/rows", () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/packages/server/src/constants/index.js b/packages/server/src/constants/index.js index bea58fd260..d19f9ff313 100644 --- a/packages/server/src/constants/index.js +++ b/packages/server/src/constants/index.js @@ -6,6 +6,29 @@ exports.JobQueues = { AUTOMATIONS: "automationQueue", } +const FilterTypes = { + STRING: "string", + FUZZY: "fuzzy", + RANGE: "range", + EQUAL: "equal", + NOT_EQUAL: "notEqual", + EMPTY: "empty", + NOT_EMPTY: "notEmpty", + CONTAINS: "contains", + NOT_CONTAINS: "notContains", + ONE_OF: "oneOf", +} + +exports.FilterTypes = FilterTypes +exports.NoEmptyFilterStrings = [ + FilterTypes.STRING, + FilterTypes.FUZZY, + FilterTypes.EQUAL, + FilterTypes.NOT_EQUAL, + FilterTypes.CONTAINS, + FilterTypes.NOT_CONTAINS, +] + exports.FieldTypes = { STRING: "string", LONGFORM: "longform", diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 3e20b30869..17b19bba49 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -110,6 +110,8 @@ function getDocParams(docType, docId = null, otherProps = {}) { } } +exports.getDocParams = getDocParams + /** * Gets parameters for retrieving tables, this is a utility function for the getDocParams function. */ diff --git a/packages/server/src/definitions/common.ts b/packages/server/src/definitions/common.ts index ec837a8101..f439fc0d28 100644 --- a/packages/server/src/definitions/common.ts +++ b/packages/server/src/definitions/common.ts @@ -15,6 +15,8 @@ export interface FieldSchema { through?: string foreignKey?: string autocolumn?: boolean + throughFrom?: string + throughTo?: string constraints?: { type?: string email?: boolean diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index 2daef8eda7..d8e767daea 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -121,6 +121,8 @@ export interface RelationshipsJson { through?: string from?: string to?: string + fromPrimary?: string + toPrimary?: string tableName: string column: string } diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index 5012808950..e7bc35b04b 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -44,6 +44,7 @@ module.exports = { NODE_ENV: process.env.NODE_ENV, JEST_WORKER_ID: process.env.JEST_WORKER_ID, BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT, + DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, // minor SALT_ROUNDS: process.env.SALT_ROUNDS, LOGGER: process.env.LOGGER, @@ -52,6 +53,7 @@ module.exports = { AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET, SENDGRID_API_KEY: process.env.SENDGRID_API_KEY, DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT, + POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, // old - to remove CLIENT_ID: process.env.CLIENT_ID, BUDIBASE_DIR: process.env.BUDIBASE_DIR, diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index c5e9bdb0bb..4130900217 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -55,6 +55,12 @@ function addFilters( query = query[fnc](key, "ilike", `${value}%`) }) } + if (filters.fuzzy) { + iterate(filters.fuzzy, (key, value) => { + const fnc = allOr ? "orWhere" : "where" + query = query[fnc](key, "ilike", `%${value}%`) + }) + } if (filters.range) { iterate(filters.range, (key, value) => { if (!value.high || !value.low) { @@ -112,14 +118,16 @@ function addRelationships( ) } else { const throughTable = relationship.through + const fromPrimary = relationship.fromPrimary + const toPrimary = relationship.toPrimary query = query // @ts-ignore .leftJoin( throughTable, - `${fromTable}.${from}`, + `${fromTable}.${fromPrimary}`, `${throughTable}.${from}` ) - .leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`) + .leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`) } } return query @@ -133,6 +141,12 @@ function buildCreate( const { endpoint, body } = json let query: KnexQuery = knex(endpoint.entityId) const parsedBody = parseBody(body) + // make sure no null values in body for creation + for (let [key, value] of Object.entries(parsedBody)) { + if (value == null) { + delete parsedBody[key] + } + } // mysql can't use returning if (opts.disableReturning) { return query.insert(parsedBody) diff --git a/packages/server/src/integrations/mongodb.ts b/packages/server/src/integrations/mongodb.ts index bbab9a0d84..71364eb783 100644 --- a/packages/server/src/integrations/mongodb.ts +++ b/packages/server/src/integrations/mongodb.ts @@ -85,10 +85,10 @@ module MongoDBModule { // which method we want to call on the collection switch (query.extra.actionTypes) { case "insertOne": { - return collection.insertOne(query.json) + return await collection.insertOne(query.json) } case "insertMany": { - return collection.insertOne(query.json).toArray() + return await collection.insertOne(query.json).toArray() } default: { throw new Error( @@ -112,19 +112,19 @@ module MongoDBModule { switch (query.extra.actionTypes) { case "find": { - return collection.find(query.json).toArray() + return await collection.find(query.json).toArray() } case "findOne": { - return collection.findOne(query.json) + return await collection.findOne(query.json) } case "findOneAndUpdate": { - return collection.findOneAndUpdate(query.json) + return await collection.findOneAndUpdate(query.json) } case "count": { - return collection.countDocuments(query.json) + return await collection.countDocuments(query.json) } case "distinct": { - return collection.distinct(query.json) + return await collection.distinct(query.json) } default: { throw new Error( @@ -148,10 +148,10 @@ module MongoDBModule { switch (query.extra.actionTypes) { case "updateOne": { - return collection.updateOne(query.json) + return await collection.updateOne(query.json) } case "updateMany": { - return collection.updateMany(query.json).toArray() + return await collection.updateMany(query.json).toArray() } default: { throw new Error( @@ -175,10 +175,10 @@ module MongoDBModule { switch (query.extra.actionTypes) { case "deleteOne": { - return collection.deleteOne(query.json) + return await collection.deleteOne(query.json) } case "deleteMany": { - return collection.deleteMany(query.json).toArray() + return await collection.deleteMany(query.json).toArray() } default: { throw new Error( diff --git a/packages/server/src/middleware/builder.js b/packages/server/src/middleware/builder.js index 240a2d1912..8ea49a3b48 100644 --- a/packages/server/src/middleware/builder.js +++ b/packages/server/src/middleware/builder.js @@ -33,7 +33,7 @@ async function checkDevAppLocks(ctx) { return } if (!(await doesUserHaveLock(appId, ctx.user))) { - ctx.throw(403, "User does not hold app lock.") + ctx.throw(400, "User does not hold app lock.") } // they do have lock, update it diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js index de54c93200..c62f0078cd 100644 --- a/packages/server/src/middleware/usageQuota.js +++ b/packages/server/src/middleware/usageQuota.js @@ -39,7 +39,6 @@ module.exports = async (ctx, next) => { return next() } - const db = new CouchDB(ctx.appId) let usage = METHOD_MAP[ctx.req.method] const property = getProperty(ctx.req.url) if (usage == null || property == null) { @@ -48,7 +47,10 @@ module.exports = async (ctx, next) => { // post request could be a save of a pre-existing entry if (ctx.request.body && ctx.request.body._id && ctx.request.body._rev) { try { - await db.get(ctx.request.body._id) + if (ctx.appId) { + const db = new CouchDB(ctx.appId) + await db.get(ctx.request.body._id) + } return next() } catch (err) { ctx.throw(404, `${ctx.request.body._id} does not exist`) diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index 172afaf609..0c2da56b58 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -19,6 +19,7 @@ const { USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX, } = require("../../db/utils") +const MemoryStream = require("memorystream") const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..") const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules") @@ -75,8 +76,9 @@ exports.getTemplateStream = async template => { if (template.file) { return fs.createReadStream(template.file.path) } else { - const tmpPath = await exports.downloadTemplate(...template.key.split("/")) - return fs.createReadStream(join(tmpPath, "db", "dump.txt")) + const [type, name] = template.key.split("/") + const tmpPath = await exports.downloadTemplate(type, name) + return fs.createReadStream(join(tmpPath, name, "db", "dump.txt")) } } @@ -111,29 +113,88 @@ exports.apiFileReturn = contents => { * to the temporary backup file (to return via API if required). */ exports.performBackup = async (appId, backupName) => { - const path = join(budibaseTempDir(), backupName) - const writeStream = fs.createWriteStream(path) - // perform couch dump - const instanceDb = new CouchDB(appId) - await instanceDb.dump(writeStream, { - // filter out anything that has a user metadata structure in its ID + return exports.exportDB(appId, { + exportName: backupName, filter: doc => !( doc._id.includes(USER_METDATA_PREFIX) || doc.includes(LINK_USER_METADATA_PREFIX) ), }) +} + +/** + * exports a DB to either file or a variable (memory). + * @param {string} dbName the DB which is to be exported. + * @param {string} exportName optional - the file name to export to, if not in memory. + * @param {function} filter optional - a filter function to clear out any un-wanted docs. + * @return Either the file stream or the variable (if no export name provided). + */ +exports.exportDB = async ( + dbName, + { exportName, filter } = { exportName: undefined, filter: undefined } +) => { + let stream, + appString = "", + path = null + if (exportName) { + path = join(budibaseTempDir(), exportName) + stream = fs.createWriteStream(path) + } else { + stream = new MemoryStream() + stream.on("data", chunk => { + appString += chunk.toString() + }) + } + // perform couch dump + const instanceDb = new CouchDB(dbName) + await instanceDb.dump(stream, { + filter, + }) + // just in memory, return the final string + if (!exportName) { + return appString + } // write the file to the object store if (env.SELF_HOSTED) { await streamUpload( ObjectStoreBuckets.BACKUPS, - join(appId, backupName), + join(dbName, exportName), fs.createReadStream(path) ) } return fs.createReadStream(path) } +/** + * Writes the provided contents to a temporary file, which can be used briefly. + * @param {string} fileContents contents which will be written to a temp file. + * @return {string} the path to the temp file. + */ +exports.storeTempFile = fileContents => { + const path = join(budibaseTempDir(), uuid()) + fs.writeFileSync(path, fileContents) + return path +} + +/** + * Utility function for getting a file read stream - a simple in memory buffered read + * stream doesn't work for pouchdb. + */ +exports.stringToFileStream = contents => { + const path = exports.storeTempFile(contents) + return fs.createReadStream(path) +} + +/** + * Creates a temp file and returns it from the API. + * @param {string} fileContents the contents to be returned in file. + */ +exports.sendTempFile = fileContents => { + const path = exports.storeTempFile(fileContents) + return fs.createReadStream(path) +} + /** * Uploads the latest client library to the object store. * @param {string} appId The ID of the app which is being created. diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index b16a687fe5..e568ba063c 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -3,6 +3,7 @@ const { OBJ_STORE_DIRECTORY } = require("../constants") const { sanitizeKey } = require("@budibase/auth/src/objectStore") const CouchDB = require("../db") const { generateMetadataID } = require("../db/utils") +const Readable = require("stream").Readable const BB_CDN = "https://cdn.budi.live" @@ -124,3 +125,12 @@ exports.escapeDangerousCharacters = string => { .replace(/[\r]/g, "\\r") .replace(/[\t]/g, "\\t") } + +exports.stringToReadStream = string => { + return new Readable({ + read() { + this.push(string) + this.push(null) + }, + }) +} diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index 07549dd8a8..55af87bc13 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -99,6 +99,7 @@ function processAutoColumn( row, opts = { reprocessing: false, noAutoRelationships: false } ) { + let noUser = !user || !user.userId let now = new Date().toISOString() // if a row doesn't have a revision then it doesn't exist yet const creating = !row._rev @@ -108,7 +109,12 @@ function processAutoColumn( } switch (schema.subtype) { case AutoFieldSubTypes.CREATED_BY: - if (creating && !opts.reprocessing && !opts.noAutoRelationships) { + if ( + creating && + !opts.reprocessing && + !opts.noAutoRelationships && + !noUser + ) { row[key] = [user.userId] } break @@ -118,7 +124,7 @@ function processAutoColumn( } break case AutoFieldSubTypes.UPDATED_BY: - if (!opts.reprocessing && !opts.noAutoRelationships) { + if (!opts.reprocessing && !opts.noAutoRelationships && !noUser) { row[key] = [user.userId] } break diff --git a/packages/server/src/utilities/usageQuota.js b/packages/server/src/utilities/usageQuota.js index 80fddb8303..1980b13ef0 100644 --- a/packages/server/src/utilities/usageQuota.js +++ b/packages/server/src/utilities/usageQuota.js @@ -58,7 +58,7 @@ exports.update = async (property, usage) => { // increment the quota quota.usageQuota[property] += usage - if (quota.usageQuota[property] >= quota.usageLimits[property]) { + if (quota.usageQuota[property] > quota.usageLimits[property]) { throw new Error( `You have exceeded your usage quota of ${quota.usageLimits[property]} ${property}.` ) diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 2ace265ca0..c07c93ba70 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -52,16 +52,17 @@ exports.sendSmtpEmail = async (to, from, subject, contents, automation) => { ) if (response.status !== 200) { - throw "Unable to send email." + const error = await response.text() + throw `Unable to send email - ${error}` } return response.json() } -exports.getDeployedApps = async ctx => { +exports.getDeployedApps = async () => { try { const response = await fetch( checkSlashesInUrl(env.WORKER_URL + `/api/apps`), - request(ctx, { + request(null, { method: "GET", }) ) diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 5c802f5004..63c4adbead 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -2815,6 +2815,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios-retry@^3.1.9: + version "3.2.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.0.tgz#eb48e72f90b177fde62329b2896aa8476cfb90ba" + integrity sha512-RK2cLMgIsAQBDhlIsJR5dOhODPigvel18XUv1dDXW+4k1FzebyfRk+C+orot6WPZOYFKSfhLwHPwVmTVOODQ5w== + dependencies: + is-retry-allowed "^1.1.0" + axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" @@ -3381,6 +3388,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + chmodr@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/chmodr/-/chmodr-1.2.0.tgz#720e96caa09b7f1cdbb01529b7d0ab6bc5e118b9" @@ -3577,6 +3589,11 @@ component-emitter@^1.2.0, component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9" + integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak= + compressible@^2.0.0: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -3732,6 +3749,11 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -5741,7 +5763,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -6880,6 +6902,11 @@ joi@17.2.1: "@hapi/pinpoint" "^2.0.0" "@hapi/topo" "^5.0.0" +join-component@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" + integrity sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU= + joycon@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" @@ -7647,6 +7674,15 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -7678,6 +7714,11 @@ memory-pager@^1.0.2: resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -7844,7 +7885,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -8634,6 +8675,20 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +posthog-node@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-1.1.4.tgz#314043b2d4e20010890f74854512447d717e71d3" + integrity sha512-XKyIzWIz6xJh8RfIpFPNFmFye/cDtfq2pcUEaLb/X5fX1KZJoiQ57OBTIrEjExIFjEU4bUrsrEA0F7FyA1sbFg== + dependencies: + axios "^0.21.1" + axios-retry "^3.1.9" + component-type "^1.2.1" + join-component "^1.1.0" + md5 "^2.3.0" + ms "^2.1.3" + remove-trailing-slash "^0.1.1" + uuid "^8.3.2" + pouch-stream@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/pouch-stream/-/pouch-stream-0.4.1.tgz#0c6d8475c9307677627991a2f079b301c3b89bdd" @@ -9309,6 +9364,11 @@ remove-trailing-separator@^1.0.1: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +remove-trailing-slash@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" + integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== + repeat-element@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" diff --git a/packages/string-templates/manifest.json b/packages/string-templates/manifest.json index 09ebeff301..748330add2 100644 --- a/packages/string-templates/manifest.json +++ b/packages/string-templates/manifest.json @@ -22,7 +22,7 @@ "array" ], "numArgs": 1, - "example": "{{ avg [1,2,3,4,5] }} -> 3", + "example": "{{ avg 1 2 3 4 5 }} -> 3", "description": "

Returns the average of all numbers in the given array.

\n" }, "ceil": { @@ -154,7 +154,7 @@ "value" ], "numArgs": 1, - "example": "{{ arrayify \"foo\" }} -> [\"foo\"]", + "example": "{{ arrayify 'foo' }} -> ['foo']", "description": "

Cast the given value to an array.

\n" }, "before": { @@ -290,7 +290,7 @@ "prop" ], "numArgs": 2, - "example": "{{pluck [{ 'name': 'Bob' }] \"name\" }} -> ['Bob']", + "example": "{{pluck [{ 'name': 'Bob' }] 'name' }} -> ['Bob']", "description": "

Map over the given object or array or objects and create an array of values from the given prop. Dot-notation may be used (as a string) to get nested properties.

\n" }, "reverse": { @@ -326,7 +326,7 @@ "props" ], "numArgs": 2, - "example": "{{ sortBy [{a: 'zzz'}, {a: 'aaa'}] \"a\" }} -> [{\"a\":\"aaa\"}, {\"a\":\"zzz\"}]", + "example": "{{ sortBy [{a: 'zzz'}, {a: 'aaa'}] 'a' }} -> [{'a':'aaa'}, {'a':'zzz'}]", "description": "

Sort an array. If an array of objects is passed, you may optionally pass a key to sort on as the second argument. You may alternatively pass a sorting function as the second argument.

\n" }, "withAfter": { @@ -471,7 +471,7 @@ "precision" ], "numArgs": 2, - "example": "{{toPrecision \"1.1234\" 2}}", + "example": "{{toPrecision '1.1234' 2}}", "description": "

Returns a string representing the Number object to the specified precision.

\n" } }, @@ -481,7 +481,7 @@ "str" ], "numArgs": 1, - "example": "{{ encodeURI \"https://myurl?Hello There\" }} -> https://myurl?Hello%20There", + "example": "{{ encodeURI 'https://myurl?Hello There' }} -> https://myurl?Hello%20There", "description": "

Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.

\n" }, "escape": { @@ -489,7 +489,7 @@ "str" ], "numArgs": 1, - "example": "{{ escape \"https://myurl?Hello+There\" }} -> https://myurl?Hello%20There", + "example": "{{ escape 'https://myurl?Hello+There' }} -> https://myurl?Hello%20There", "description": "

Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.

\n" }, "decodeURI": { @@ -497,7 +497,7 @@ "str" ], "numArgs": 1, - "example": "{{ escape \"https://myurl?Hello%20There\" }} -> https://myurl?Hello+There", + "example": "{{ escape 'https://myurl?Hello%20There' }} -> https://myurl?Hello+There", "description": "

Decode a Uniform Resource Identifier (URI) component.

\n" }, "url_encode": { @@ -516,7 +516,7 @@ "href" ], "numArgs": 2, - "example": "{{ urlResolve \"https://myurl\" \"/api/test\" }} -> https://myurl/api/test", + "example": "{{ urlResolve 'https://myurl' '/api/test' }} -> https://myurl/api/test", "description": "

Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.

\n" }, "urlParse": { @@ -524,7 +524,7 @@ "str" ], "numArgs": 1, - "example": "{{ urlParse \"https://myurl/api/test\" }}", + "example": "{{ urlParse 'https://myurl/api/test' }}", "description": "

Parses a url string into an object.

\n" }, "stripQuerystring": { @@ -532,7 +532,7 @@ "url" ], "numArgs": 1, - "example": "{{ stripQueryString \"https://myurl/api/test?foo=bar\" }} -> \"https://myurl/api/test\"", + "example": "{{ stripQueryString 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'", "description": "

Strip the query string from the given url.

\n" }, "stripProtocol": { @@ -540,7 +540,7 @@ "str" ], "numArgs": 1, - "example": "{{ stripProtocol \"https://myurl/api/test\" }} -> \"myurl/api/test\"", + "example": "{{ stripProtocol 'https://myurl/api/test' }} -> 'myurl/api/test'", "description": "

Strip protocol from a url. Useful for displaying media that may have an 'http' protocol on secure connections.

\n" } }, @@ -551,7 +551,7 @@ "suffix" ], "numArgs": 2, - "example": "{{append \"index\" \".html\"}} -> index.html", + "example": "{{append 'index' '.html'}} -> index.html", "description": "

Append the specified suffix to the given string.

\n" }, "camelcase": { @@ -559,7 +559,7 @@ "string" ], "numArgs": 1, - "example": "{{camelcase \"foo bar baz\"}} -> fooBarBaz", + "example": "{{camelcase 'foo bar baz'}} -> fooBarBaz", "description": "

camelCase the characters in the given string.

\n" }, "capitalize": { @@ -567,7 +567,7 @@ "str" ], "numArgs": 1, - "example": "{{capitalize \"foo bar baz\"}} -> Foo bar baz", + "example": "{{capitalize 'foo bar baz'}} -> Foo bar baz", "description": "

Capitalize the first word in a sentence.

\n" }, "capitalizeAll": { @@ -575,7 +575,7 @@ "str" ], "numArgs": 1, - "example": "{{ capitalizeAll \"foo bar baz\"}} -> Foo Bar Baz", + "example": "{{ capitalizeAll 'foo bar baz'}} -> Foo Bar Baz", "description": "

Capitalize all words in a string.

\n" }, "center": { @@ -584,7 +584,7 @@ "spaces" ], "numArgs": 2, - "example": "{{ center \"test\" 1}} -> \" test \"", + "example": "{{ center 'test' 1}} -> ' test '", "description": "

Center a string using non-breaking spaces

\n" }, "chop": { @@ -592,7 +592,7 @@ "string" ], "numArgs": 1, - "example": "{{ chop \" ABC \"}} -> \"ABC\"", + "example": "{{ chop ' ABC '}} -> 'ABC'", "description": "

Like trim, but removes both extraneous whitespace and non-word characters from the beginning and end of a string.

\n" }, "dashcase": { @@ -600,7 +600,7 @@ "string" ], "numArgs": 1, - "example": "{{dashcase \"a-b-c d_e\"}} -> a-b-c-d-e", + "example": "{{dashcase 'a-b-c d_e'}} -> a-b-c-d-e", "description": "

dash-case the characters in string. Replaces non-word characters and periods with hyphens.

\n" }, "dotcase": { @@ -608,7 +608,7 @@ "string" ], "numArgs": 1, - "example": "{{dotcase \"a-b-c d_e\"}} -> a.b.c.d.e", + "example": "{{dotcase 'a-b-c d_e'}} -> a.b.c.d.e", "description": "

dot.case the characters in string.

\n" }, "downcase": { @@ -616,7 +616,7 @@ "string" ], "numArgs": 1, - "example": "{{downcase \"aBcDeF\"}} -> abcdef", + "example": "{{downcase 'aBcDeF'}} -> abcdef", "description": "

Lowercase all of the characters in the given string. Alias for lowercase.

\n" }, "ellipsis": { @@ -625,7 +625,7 @@ "length" ], "numArgs": 2, - "example": "{{ellipsis \"foo bar baz\", 7}} -> foo bar…", + "example": "{{ellipsis 'foo bar baz', 7}} -> foo bar…", "description": "

Truncates a string to the specified length, and appends it with an elipsis, .

\n" }, "hyphenate": { @@ -633,7 +633,7 @@ "str" ], "numArgs": 1, - "example": "{{hyphenate \"foo bar baz qux\"}} -> foo-bar-baz-qux", + "example": "{{hyphenate 'foo bar baz qux'}} -> foo-bar-baz-qux", "description": "

Replace spaces in a string with hyphens.

\n" }, "isString": { @@ -641,7 +641,7 @@ "value" ], "numArgs": 1, - "example": "{{isString \"foo\"}} -> true", + "example": "{{isString 'foo'}} -> true", "description": "

Return true if value is a string.

\n" }, "lowercase": { @@ -649,7 +649,7 @@ "str" ], "numArgs": 1, - "example": "{{lowercase \"Foo BAR baZ\"}} -> foo bar baz", + "example": "{{lowercase 'Foo BAR baZ'}} -> foo bar baz", "description": "

Lowercase all characters in the given string.

\n" }, "occurrences": { @@ -658,7 +658,7 @@ "substring" ], "numArgs": 2, - "example": "{{occurrences \"foo bar foo bar baz\" \"foo\"}} -> 2", + "example": "{{occurrences 'foo bar foo bar baz' 'foo'}} -> 2", "description": "

Return the number of occurrences of substring within the given string.

\n" }, "pascalcase": { @@ -666,7 +666,7 @@ "string" ], "numArgs": 1, - "example": "{{pascalcase \"foo bar baz\"}} -> FooBarBaz", + "example": "{{pascalcase 'foo bar baz'}} -> FooBarBaz", "description": "

PascalCase the characters in string.

\n" }, "pathcase": { @@ -674,7 +674,7 @@ "string" ], "numArgs": 1, - "example": "{{pathcase \"a-b-c d_e\"}} -> a/b/c/d/e", + "example": "{{pathcase 'a-b-c d_e'}} -> a/b/c/d/e", "description": "

path/case the characters in string.

\n" }, "plusify": { @@ -682,7 +682,7 @@ "str" ], "numArgs": 1, - "example": "{{plusify \"foo bar baz\"}} -> foo+bar+baz", + "example": "{{plusify 'foo bar baz'}} -> foo+bar+baz", "description": "

Replace spaces in the given string with pluses.

\n" }, "prepend": { @@ -691,7 +691,7 @@ "prefix" ], "numArgs": 2, - "example": "{{prepend \"bar\" \"foo-\"}} -> foo-bar", + "example": "{{prepend 'bar' 'foo-'}} -> foo-bar", "description": "

Prepends the given string with the specified prefix.

\n" }, "raw": { @@ -708,7 +708,7 @@ "substring" ], "numArgs": 2, - "example": "{{remove \"a b a b a b\" \"a \"}} -> b b b", + "example": "{{remove 'a b a b a b' 'a '}} -> b b b", "description": "

Remove all occurrences of substring from the given str.

\n" }, "removeFirst": { @@ -717,7 +717,7 @@ "substring" ], "numArgs": 2, - "example": "{{remove \"a b a b a b\" \"a\"}} -> b a b a b", + "example": "{{remove 'a b a b a b' 'a'}} -> b a b a b", "description": "

Remove the first occurrence of substring from the given str.

\n" }, "replace": { @@ -727,7 +727,7 @@ "b" ], "numArgs": 3, - "example": "{{replace \"a b a b a b\" \"a\" \"z\"}} -> z b z b z b", + "example": "{{replace 'a b a b a b' 'a' 'z'}} -> z b z b z b", "description": "

Replace all occurrences of substring a with substring b.

\n" }, "replaceFirst": { @@ -737,7 +737,7 @@ "b" ], "numArgs": 3, - "example": "{{replace \"a b a b a b\" \"a\" \"z\"}} -> z b a b a b", + "example": "{{replace 'a b a b a b' 'a' 'z'}} -> z b a b a b", "description": "

Replace the first occurrence of substring a with substring b.

\n" }, "sentence": { @@ -745,7 +745,7 @@ "str" ], "numArgs": 1, - "example": "{{sentence \"hello world. goodbye world.\"}} -> Hello world. Goodbye world.", + "example": "{{sentence 'hello world. goodbye world.'}} -> Hello world. Goodbye world.", "description": "

Sentence case the given string

\n" }, "snakecase": { @@ -753,7 +753,7 @@ "string" ], "numArgs": 1, - "example": "{{snakecase \"a-b-c d_e\"}} -> a_b_c_d_e", + "example": "{{snakecase 'a-b-c d_e'}} -> a_b_c_d_e", "description": "

snake_case the characters in the given string.

\n" }, "split": { @@ -761,7 +761,7 @@ "string" ], "numArgs": 1, - "example": "{{split \"a,b,c\"}} -> ['a', 'b', 'c']", + "example": "{{split 'a,b,c'}} -> ['a', 'b', 'c']", "description": "

Split string by the given character.

\n" }, "startsWith": { @@ -771,7 +771,7 @@ "options" ], "numArgs": 3, - "example": "{{#startsWith \"Goodbye\" \"Hello, world!\"}} Yep {{else}} Nope {{/startsWith}} -> Nope", + "example": "{{#startsWith 'Goodbye' 'Hello, world!'}} Yep {{else}} Nope {{/startsWith}} -> Nope", "description": "

Tests whether a string begins with the given prefix.

\n" }, "titleize": { @@ -779,7 +779,7 @@ "str" ], "numArgs": 1, - "example": "{{#titleize \"this is title case\" }} -> This Is Title Case", + "example": "{{#titleize 'this is title case' }} -> This Is Title Case", "description": "

Title case the given string.

\n" }, "trim": { @@ -787,7 +787,7 @@ "string" ], "numArgs": 1, - "example": "{{trim \" ABC \" }} -> ABC", + "example": "{{trim ' ABC ' }} -> ABC", "description": "

Removes extraneous whitespace from the beginning and end of a string.

\n" }, "trimLeft": { @@ -795,7 +795,7 @@ "string" ], "numArgs": 1, - "example": "{{trimLeft \" ABC \" }} -> \"ABC \"", + "example": "{{trimLeft ' ABC ' }} -> 'ABC '", "description": "

Removes extraneous whitespace from the beginning of a string.

\n" }, "trimRight": { @@ -803,7 +803,7 @@ "string" ], "numArgs": 1, - "example": "{{trimRight \" ABC \" }} -> \" ABC \"", + "example": "{{trimRight ' ABC ' }} -> ' ABC '", "description": "

Removes extraneous whitespace from the end of a string.

\n" }, "truncate": { @@ -813,7 +813,7 @@ "suffix" ], "numArgs": 3, - "example": "{{truncate \"foo bar baz\" 7 }} -> foo bar", + "example": "{{truncate 'foo bar baz' 7 }} -> foo bar", "description": "

Truncate a string to the specified length. Also see ellipsis.

\n" }, "truncateWords": { @@ -823,7 +823,7 @@ "suffix" ], "numArgs": 3, - "example": "{{truncateWords \"foo bar baz\" 1 }} -> foo", + "example": "{{truncateWords 'foo bar baz' 1 }} -> foo", "description": "

Truncate a string to have the specified number of words. Also see truncate.

\n" }, "upcase": { @@ -831,7 +831,7 @@ "string" ], "numArgs": 1, - "example": "{{upcase \"aBcDef\"}} -> ABCDEF", + "example": "{{upcase 'aBcDef'}} -> ABCDEF", "description": "

Uppercase all of the characters in the given string. Alias for uppercase.

\n" }, "uppercase": { @@ -840,7 +840,7 @@ "options" ], "numArgs": 2, - "example": "{{uppercase \"aBcDef\"}} -> ABCDEF", + "example": "{{uppercase 'aBcDef'}} -> ABCDEF", "description": "

Uppercase all of the characters in the given string. If used as a block helper it will uppercase the entire block. This helper does not support inverse blocks.

\n" } }, @@ -863,7 +863,7 @@ "options" ], "numArgs": 4, - "example": "{{compare 10 \"<\" 5 }} -> true", + "example": "{{compare 10 '<' 5 }} -> true", "description": "

Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.

\n" }, "contains": { @@ -874,7 +874,7 @@ "options" ], "numArgs": 4, - "example": "{{#contains ['a', 'b', 'c'] \"d\"}} This will not be rendered. {{else}} This will be rendered. {{/contains}}", + "example": "{{#contains ['a', 'b', 'c'] 'd'}} This will not be rendered. {{else}} This will be rendered. {{/contains}}", "description": "

Block helper that renders the block if collection has the given value, using strict equality (===) for comparison, otherwise the inverse block is rendered (if specified). If a startIndex is specified and is negative, it is used as the offset from the end of the collection.

\n" }, "default": { @@ -883,8 +883,8 @@ "defaultValue" ], "numArgs": 2, - "example": "{{default null null \"default\"}} -> default", - "description": "

Returns the first value that is not undefined, otherwise the "default" value is returned.

\n" + "example": "{{default null null 'default'}} -> default", + "description": "

Returns the first value that is not undefined, otherwise the 'default' value is returned.

\n" }, "eq": { "args": [ @@ -894,7 +894,7 @@ ], "numArgs": 3, "example": "{{#eq 3 3}} equal{{else}} not equal{{/eq}} -> equal", - "description": "

Block helper that renders a block if a is equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare="" hash argument for the second value.

\n" + "description": "

Block helper that renders a block if a is equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

\n" }, "gt": { "args": [ @@ -904,7 +904,7 @@ ], "numArgs": 3, "example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> greater than", - "description": "

Block helper that renders a block if a is greater than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare="" hash argument for the second value.

\n" + "description": "

Block helper that renders a block if a is greater than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

\n" }, "gte": { "args": [ @@ -914,7 +914,7 @@ ], "numArgs": 3, "example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> greater than or equal", - "description": "

Block helper that renders a block if a is greater than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare="" hash argument for the second value.

\n" + "description": "

Block helper that renders a block if a is greater than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

\n" }, "has": { "args": [ @@ -923,7 +923,7 @@ "options" ], "numArgs": 3, - "example": "{{#has \"foobar\" \"foo\"}} has it{{else}} doesn't{{/has}} -> has it", + "example": "{{#has 'foobar' 'foo'}} has it{{else}} doesn't{{/has}} -> has it", "description": "

Block helper that renders a block if value has pattern. If an inverse block is specified it will be rendered when falsy.

\n" }, "isFalsey": { @@ -932,7 +932,7 @@ "options" ], "numArgs": 2, - "example": "{{isFalsey \"\" }} -> true", + "example": "{{isFalsey '' }} -> true", "description": "

Returns true if the given value is falsey. Uses the [falsey][] library for comparisons. Please see that library for more information or to report bugs with this helper.

\n" }, "isTruthy": { @@ -941,7 +941,7 @@ "options" ], "numArgs": 2, - "example": "{{isTruthy \"12\" }} -> true", + "example": "{{isTruthy '12' }} -> true", "description": "

Returns true if the given value is truthy. Uses the [falsey][] library for comparisons. Please see that library for more information or to report bugs with this helper.

\n" }, "ifEven": { @@ -999,7 +999,7 @@ ], "numArgs": 2, "example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> less than", - "description": "

Block helper that renders a block if a is less than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare="" hash argument for the second value.

\n" + "description": "

Block helper that renders a block if a is less than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

\n" }, "lte": { "args": [ @@ -1009,7 +1009,7 @@ ], "numArgs": 3, "example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> less than or equal", - "description": "

Block helper that renders a block if a is less than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare="" hash argument for the second value.

\n" + "description": "

Block helper that renders a block if a is less than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

\n" }, "neither": { "args": [ @@ -1219,4 +1219,4 @@ "description": "

Produce a humanized duration left/until given an amount of time and the type of time measurement.

\n" } } -} +} \ No newline at end of file diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 9f5a5f9d26..923ed47404 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", @@ -20,7 +20,7 @@ "manifest": "node ./scripts/gen-collection-info.js" }, "dependencies": { - "@budibase/handlebars-helpers": "^0.11.4", + "@budibase/handlebars-helpers": "^0.11.6", "dayjs": "^1.10.4", "handlebars": "^4.7.6", "handlebars-utils": "^1.0.6", diff --git a/packages/string-templates/yarn.lock b/packages/string-templates/yarn.lock index 82f99d7b31..86592f1cec 100644 --- a/packages/string-templates/yarn.lock +++ b/packages/string-templates/yarn.lock @@ -270,35 +270,31 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/handlebars-helpers@^0.11.4": - version "0.11.4" - resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.4.tgz#8acfa2ee84134f7be4b2906e710fce6d25c5d000" - integrity sha512-AJNJYlJnxZmn9QJ8tBl8nrm4YxbwHP4AR0pbiVGK+EoOylkNBlUnZ/QDL1VyqM5fTkAE/Z2IZVLKrrG3kxuWLA== +"@budibase/handlebars-helpers@^0.11.6": + version "0.11.6" + resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.6.tgz#00e924a0142aac41c07e3d104607759635eec952" + integrity sha512-FLtCWkh0cNHC0/X6Pt5Xjmp4/r4tCpv5f5sP1JcZsaSKPyE5gpNu/+fqUxgDTzVS3PVo0KE6hdRPKVWvVqwPEw== dependencies: - arr-flatten "^1.1.0" - array-sort "^0.1.4" - define-property "^1.0.0" + array-sort "^1.0.0" + define-property "^2.0.2" extend-shallow "^3.0.2" - "falsey" "^0.3.2" + "falsey" "^1.0.0" for-in "^1.0.2" - for-own "^1.0.0" get-object "^0.2.0" - get-value "^2.0.6" - handlebars "^4.0.11" + get-value "^3.0.1" + handlebars "^4.7.7" handlebars-utils "^1.0.6" - has-value "^1.0.0" + has-value "^2.0.2" helper-date "^1.0.1" helper-markdown "^1.0.0" helper-md "^0.2.2" html-tag "^2.0.0" is-even "^1.0.0" - is-glob "^4.0.0" - is-number "^4.0.0" - kind-of "^6.0.0" - logging-helpers "^1.0.0" - micromatch "^3.1.4" + is-glob "^4.0.1" + kind-of "^6.0.3" + micromatch "^3.1.5" relative "^3.0.2" - striptags "^3.1.0" + striptags "^3.1.1" to-gfm-code-block "^0.1.1" year "^0.2.1" @@ -693,130 +689,6 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-bgblack@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz#a68ba5007887701b6aafbe3fa0dadfdfa8ee3ca2" - integrity sha1-poulAHiHcBtqr74/oNrf36juPKI= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgblue@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz#67bdc04edc9b9b5278969da196dea3d75c8c3613" - integrity sha1-Z73ATtybm1J4lp2hlt6j11yMNhM= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgcyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz#58489425600bde9f5507068dd969ebfdb50fe768" - integrity sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g= - dependencies: - ansi-wrap "0.1.0" - -ansi-bggreen@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz#4e3191248529943f4321e96bf131d1c13816af49" - integrity sha1-TjGRJIUplD9DIelr8THRwTgWr0k= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgmagenta@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz#9b28432c076eaa999418672a3efbe19391c2c7a1" - integrity sha1-myhDLAduqpmUGGcqPvvhk5HCx6E= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgred@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgred/-/ansi-bgred-0.1.1.tgz#a76f92838382ba43290a6c1778424f984d6f1041" - integrity sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgwhite@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz#6504651377a58a6ececd0331994e480258e11ba8" - integrity sha1-ZQRlE3elim7OzQMxmU5IAljhG6g= - dependencies: - ansi-wrap "0.1.0" - -ansi-bgyellow@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz#c3fe2eb08cd476648029e6874d15a0b38f61d44f" - integrity sha1-w/4usIzUdmSAKeaHTRWgs49h1E8= - dependencies: - ansi-wrap "0.1.0" - -ansi-black@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-black/-/ansi-black-0.1.1.tgz#f6185e889360b2545a1ec50c0bf063fc43032453" - integrity sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM= - dependencies: - ansi-wrap "0.1.0" - -ansi-blue@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-blue/-/ansi-blue-0.1.1.tgz#15b804990e92fc9ca8c5476ce8f699777c21edbf" - integrity sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8= - dependencies: - ansi-wrap "0.1.0" - -ansi-bold@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-bold/-/ansi-bold-0.1.1.tgz#3e63950af5acc2ae2e670e6f67deb115d1a5f505" - integrity sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU= - dependencies: - ansi-wrap "0.1.0" - -ansi-colors@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-0.2.0.tgz#72c31de2a0d9a2ccd0cac30cc9823eeb2f6434b5" - integrity sha1-csMd4qDZoszQysMMyYI+6y9kNLU= - dependencies: - ansi-bgblack "^0.1.1" - ansi-bgblue "^0.1.1" - ansi-bgcyan "^0.1.1" - ansi-bggreen "^0.1.1" - ansi-bgmagenta "^0.1.1" - ansi-bgred "^0.1.1" - ansi-bgwhite "^0.1.1" - ansi-bgyellow "^0.1.1" - ansi-black "^0.1.1" - ansi-blue "^0.1.1" - ansi-bold "^0.1.1" - ansi-cyan "^0.1.1" - ansi-dim "^0.1.1" - ansi-gray "^0.1.1" - ansi-green "^0.1.1" - ansi-grey "^0.1.1" - ansi-hidden "^0.1.1" - ansi-inverse "^0.1.1" - ansi-italic "^0.1.1" - ansi-magenta "^0.1.1" - ansi-red "^0.1.1" - ansi-reset "^0.1.1" - ansi-strikethrough "^0.1.1" - ansi-underline "^0.1.1" - ansi-white "^0.1.1" - ansi-yellow "^0.1.1" - lazy-cache "^2.0.1" - -ansi-cyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" - integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= - dependencies: - ansi-wrap "0.1.0" - -ansi-dim@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-dim/-/ansi-dim-0.1.1.tgz#40de4c603aa8086d8e7a86b8ff998d5c36eefd6c" - integrity sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww= - dependencies: - ansi-wrap "0.1.0" - ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -824,81 +696,11 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= - dependencies: - ansi-wrap "0.1.0" - -ansi-green@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7" - integrity sha1-il2al55FjVfEDjNYCzc5C44Q0Pc= - dependencies: - ansi-wrap "0.1.0" - -ansi-grey@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-grey/-/ansi-grey-0.1.1.tgz#59d98b6ac2ba19f8a51798e9853fba78339a33c1" - integrity sha1-WdmLasK6GfilF5jphT+6eDOaM8E= - dependencies: - ansi-wrap "0.1.0" - -ansi-hidden@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-hidden/-/ansi-hidden-0.1.1.tgz#ed6a4c498d2bb7cbb289dbf2a8d1dcc8567fae0f" - integrity sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8= - dependencies: - ansi-wrap "0.1.0" - -ansi-inverse@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-inverse/-/ansi-inverse-0.1.1.tgz#b6af45826fe826bfb528a6c79885794355ccd269" - integrity sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk= - dependencies: - ansi-wrap "0.1.0" - -ansi-italic@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-italic/-/ansi-italic-0.1.1.tgz#104743463f625c142a036739cf85eda688986f23" - integrity sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM= - dependencies: - ansi-wrap "0.1.0" - -ansi-magenta@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-magenta/-/ansi-magenta-0.1.1.tgz#063b5ba16fb3f23e1cfda2b07c0a89de11e430ae" - integrity sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4= - dependencies: - ansi-wrap "0.1.0" - -ansi-red@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" - integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= - dependencies: - ansi-wrap "0.1.0" - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-reset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-reset/-/ansi-reset-0.1.1.tgz#e7e71292c3c7ddcd4d62ef4a6c7c05980911c3b7" - integrity sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c= - dependencies: - ansi-wrap "0.1.0" - -ansi-strikethrough@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz#d84877140b2cff07d1c93ebce69904f68885e568" - integrity sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg= - dependencies: - ansi-wrap "0.1.0" - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -913,32 +715,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-underline@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-underline/-/ansi-underline-0.1.1.tgz#dfc920f4c97b5977ea162df8ffb988308aaa71a4" - integrity sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ= - dependencies: - ansi-wrap "0.1.0" - -ansi-white@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-white/-/ansi-white-0.1.1.tgz#9c77b7c193c5ee992e6011d36ec4c921b4578944" - integrity sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ= - dependencies: - ansi-wrap "0.1.0" - -ansi-wrap@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= - -ansi-yellow@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-yellow/-/ansi-yellow-0.1.1.tgz#cb9356f2f46c732f0e3199e6102955a77da83c1d" - integrity sha1-y5NW8vRscy8OMZnmEClVp32oPB0= - dependencies: - ansi-wrap "0.1.0" - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -977,10 +753,10 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-sort@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-0.1.4.tgz#662855eaeb671b4188df4451b2f24a0753992b23" - integrity sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ== +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== dependencies: default-compare "^1.0.0" get-value "^2.0.6" @@ -1786,11 +1562,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/error-symbol/-/error-symbol-0.1.0.tgz#0a4dae37d600d15a29ba453d8ef920f1844333f6" - integrity sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y= - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1963,12 +1734,10 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -"falsey@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/falsey/-/falsey-0.3.2.tgz#b21c90c5c34660fc192bf909575db95b6880d597" - integrity sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg== - dependencies: - kind-of "^5.0.2" +"falsey@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/falsey/-/falsey-1.0.0.tgz#71bdd775c24edad9f2f5c015ce8be24400bb5d7d" + integrity sha512-zMDNZ/Ipd8MY0+346CPvhzP1AsiVyNfTOayJza4reAIWf72xbkuFUDcJNxSAsQE1b9Bu0wijKb8Ngnh/a7fI5w== fast-deep-equal@^3.1.1: version "3.1.3" @@ -2017,18 +1786,11 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -for-in@^1.0.1, for-in@^1.0.2: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - foreach@~2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -2129,6 +1891,13 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +get-value@^3.0.0, get-value@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" + integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== + dependencies: + isobject "^3.0.1" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2180,7 +1949,7 @@ handlebars-utils@^1.0.2, handlebars-utils@^1.0.4, handlebars-utils@^1.0.6: kind-of "^6.0.0" typeof-article "^0.1.1" -handlebars@^4.0.11, handlebars@^4.7.6: +handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -2233,6 +2002,14 @@ has-value@^1.0.0: has-values "^1.0.0" isobject "^3.0.0" +has-value@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-2.0.2.tgz#d0f12e8780ba8e90e66ad1a21c707fdb67c25658" + integrity sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA== + dependencies: + get-value "^3.0.0" + has-values "^2.0.1" + has-values@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" @@ -2246,6 +2023,13 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +has-values@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d" + integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w== + dependencies: + kind-of "^6.0.2" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -2389,11 +2173,6 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -info-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/info-symbol/-/info-symbol-0.1.0.tgz#27841d72867ddb4242cd612d79c10633881c6a78" - integrity sha1-J4QdcoZ920JCzWEtecEGM4gcang= - inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -2513,10 +2292,10 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -2539,11 +2318,6 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -3186,7 +2960,7 @@ kind-of@^5.0.0, kind-of@^5.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -3196,13 +2970,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lazy-cache@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" - integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= - dependencies: - set-getter "^0.1.0" - level-blobs@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/level-blobs/-/level-blobs-0.1.7.tgz#9ab9b97bb99f1edbf9f78a3433e21ed56386bdaf" @@ -3343,35 +3110,6 @@ lodash@^4.17.19, lodash@^4.17.20: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-ok@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" - integrity sha1-vqPdNqzQuKckDXhza1uXxlREozQ= - dependencies: - ansi-green "^0.1.1" - success-symbol "^0.1.0" - -log-utils@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/log-utils/-/log-utils-0.2.1.tgz#a4c217a0dd9a50515d9b920206091ab3d4e031cf" - integrity sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8= - dependencies: - ansi-colors "^0.2.0" - error-symbol "^0.1.0" - info-symbol "^0.1.0" - log-ok "^0.1.1" - success-symbol "^0.1.0" - time-stamp "^1.0.1" - warning-symbol "^0.1.0" - -logging-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/logging-helpers/-/logging-helpers-1.0.0.tgz#b5a37b32ad53eb0137c58c7898a47b175ddb7c36" - integrity sha512-qyIh2goLt1sOgQQrrIWuwkRjUx4NUcEqEGAcYqD8VOnOC6ItwkrVE8/tA4smGpjzyp4Svhc6RodDp9IO5ghpyA== - dependencies: - isobject "^3.0.0" - log-utils "^0.2.1" - lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3443,7 +3181,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^3.1.4: +micromatch@^3.1.4, micromatch@^3.1.5: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -4272,13 +4010,6 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= - dependencies: - to-object-path "^0.3.0" - set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -4551,15 +4282,10 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -striptags@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.1.1.tgz#c8c3e7fdd6fb4bb3a32a3b752e5b5e3e38093ebd" - integrity sha1-yMPn/db7S7OjKjt1LltePjgJPr0= - -success-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" - integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= +striptags@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052" + integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw== supports-color@^5.3.0: version "5.5.0" @@ -4627,11 +4353,6 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -time-stamp@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= - tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -4878,11 +4599,6 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -warning-symbol@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/warning-symbol/-/warning-symbol-0.1.0.tgz#bb31dd11b7a0f9d67ab2ed95f457b65825bbad21" - integrity sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE= - webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" diff --git a/packages/worker/package.json b/packages/worker/package.json index 98753cea86..d996413f98 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "0.9.146-alpha.4", + "version": "0.9.154-alpha.1", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -20,13 +20,15 @@ "multi:enable": "node scripts/multiTenancy.js enable", "multi:disable": "node scripts/multiTenancy.js disable", "selfhost:enable": "node scripts/selfhost.js enable", - "selfhost:disable": "node scripts/selfhost.js disable" + "selfhost:disable": "node scripts/selfhost.js disable", + "localdomain:enable": "node scripts/localdomain.js enable", + "localdomain:disable": "node scripts/localdomain.js disable" }, "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.146-alpha.4", - "@budibase/string-templates": "^0.9.146-alpha.4", + "@budibase/auth": "^0.9.154-alpha.1", + "@budibase/string-templates": "^0.9.154-alpha.1", "@koa/router": "^8.0.0", "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.811.0", diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index 3df0beb23c..e0b8c3586a 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -21,7 +21,10 @@ async function init() { COUCH_DB_PASSWORD: "budibase", // empty string is false MULTI_TENANCY: "", + DISABLE_ACCOUNT_PORTAL: "", ACCOUNT_PORTAL_URL: "http://localhost:10001", + ACCOUNT_PORTAL_API_KEY: "budibase", + PLATFORM_URL: "http://localhost:10000", } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/worker/scripts/localdomain.js b/packages/worker/scripts/localdomain.js new file mode 100644 index 0000000000..944c90506f --- /dev/null +++ b/packages/worker/scripts/localdomain.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node +const updateDotEnv = require("update-dotenv") + +const arg = process.argv.slice(2)[0] + +/** + * For testing multi tenancy sub domains locally. + * + * Relies on an entry in /etc/hosts e.g: + * + * 127.0.0.1 local.com + * + * and an entry for each tenant you wish to test locally e.g: + * + * 127.0.0.1 t1.local.com + * 127.0.0.1 t2.local.com + */ +updateDotEnv({ + ACCOUNT_PORTAL_URL: + arg === "enable" ? "http://local.com:10001" : "http://localhost:10001", + COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "", + PLATFORM_URL: + arg === "enable" ? "http://local.com:10000" : "http://localhost:10000", +}).then(() => console.log("Updated worker!")) diff --git a/packages/worker/src/api/controllers/global/auth.js b/packages/worker/src/api/controllers/global/auth.js index f3188d7777..4e5603b596 100644 --- a/packages/worker/src/api/controllers/global/auth.js +++ b/packages/worker/src/api/controllers/global/auth.js @@ -41,13 +41,19 @@ async function authInternal(ctx, user, err = null, info = null) { return ctx.throw(403, info ? info : "Unauthorized") } - // just store the user ID - ctx.cookies.set(Cookies.Auth, user.token, { + const config = { expires, path: "/", httpOnly: false, overwrite: true, - }) + } + + if (env.COOKIE_DOMAIN) { + config.domain = env.COOKIE_DOMAIN + } + + // just store the user ID + ctx.cookies.set(Cookies.Auth, user.token, config) } exports.authenticate = async (ctx, next) => { diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index 3137d6432f..38a814f465 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -1,28 +1,24 @@ const { - generateGlobalUserID, getGlobalUserParams, StaticDatabases, generateNewUsageQuotaDoc, } = require("@budibase/auth/db") -const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils -const { UserStatus, EmailTemplatePurpose } = require("../../../constants") +const { hash, getGlobalUserByEmail, saveUser } = require("@budibase/auth").utils +const { EmailTemplatePurpose } = require("../../../constants") const { checkInviteCode } = require("../../../utilities/redis") const { sendEmail } = require("../../../utilities/email") const { user: userCache } = require("@budibase/auth/cache") const { invalidateSessions } = require("@budibase/auth/sessions") -const CouchDB = require("../../../db") const accounts = require("@budibase/auth/accounts") const { getGlobalDB, getTenantId, + getTenantUser, doesTenantExist, - tryAddTenant, - updateTenantId, } = require("@budibase/auth/tenancy") +const { removeUserFromInfoDB } = require("@budibase/auth/deprovision") const env = require("../../../environment") -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name - async function allUsers() { const db = getGlobalDB() const response = await db.allDocs( @@ -33,96 +29,6 @@ async function allUsers() { return response.rows.map(row => row.doc) } -async function saveUser( - user, - tenantId, - hashPassword = true, - requirePassword = true -) { - if (!tenantId) { - throw "No tenancy specified." - } - // need to set the context for this request, as specified - updateTenantId(tenantId) - // specify the tenancy incase we're making a new admin user (public) - const db = getGlobalDB(tenantId) - let { email, password, _id } = user - // make sure another user isn't using the same email - let dbUser - if (email) { - // check budibase users inside the tenant - dbUser = await getGlobalUserByEmail(email) - if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { - throw `Email address ${email} already in use.` - } - - // check budibase users in other tenants - if (env.MULTI_TENANCY) { - dbUser = await getTenantUser(email) - if (dbUser != null && dbUser.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - - // check root account users in account portal - if (!env.SELF_HOSTED) { - const account = await accounts.getAccount(email) - if (account && account.verified && account.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - } else { - dbUser = await db.get(_id) - } - - // get the password, make sure one is defined - let hashedPassword - if (password) { - hashedPassword = hashPassword ? await hash(password) : password - } else if (dbUser) { - hashedPassword = dbUser.password - } else if (requirePassword) { - throw "Password must be specified." - } - - _id = _id || generateGlobalUserID() - user = { - createdAt: Date.now(), - ...dbUser, - ...user, - _id, - password: hashedPassword, - tenantId, - } - // make sure the roles object is always present - if (!user.roles) { - user.roles = {} - } - // add the active status to a user if its not provided - if (user.status == null) { - user.status = UserStatus.ACTIVE - } - try { - const response = await db.put({ - password: hashedPassword, - ...user, - }) - await tryAddTenant(tenantId, _id, email) - await userCache.invalidateUser(response.id) - return { - _id: response.id, - _rev: response.rev, - email, - } - } catch (err) { - if (err.status === 409) { - throw "User exists already" - } else { - throw err - } - } -} - exports.save = async ctx => { try { ctx.body = await saveUser(ctx.request.body, getTenantId()) @@ -132,7 +38,7 @@ exports.save = async ctx => { } const parseBooleanParam = param => { - if (param && param == "false") { + if (param && param === "false") { return false } else { return true @@ -160,6 +66,17 @@ exports.adminUser = async ctx => { // write usage quotas for cloud if (!env.SELF_HOSTED) { + // could be a scenario where it exists, make sure its clean + try { + const usageQuota = await db.get( + StaticDatabases.PLATFORM_INFO.docs.usageQuota + ) + if (usageQuota) { + await db.remove(usageQuota._id, usageQuota._rev) + } + } catch (err) { + // don't worry about errors + } await db.post(generateNewUsageQuotaDoc()) } @@ -193,6 +110,19 @@ exports.adminUser = async ctx => { exports.destroy = async ctx => { const db = getGlobalDB() const dbUser = await db.get(ctx.params.id) + + // root account holder can't be deleted from inside budibase + const email = dbUser.email + const account = await accounts.getAccount(email) + if (account) { + if (email === ctx.user.email) { + ctx.throw(400, 'Please visit "Account" to delete this user') + } else { + ctx.throw(400, "Account holder cannot be deleted") + } + } + + await removeUserFromInfoDB(dbUser) await db.remove(dbUser._id, dbUser._rev) await userCache.invalidateUser(dbUser._id) await invalidateSessions(dbUser._id) @@ -285,16 +215,6 @@ exports.find = async ctx => { ctx.body = user } -// lookup, could be email or userId, either will return a doc -const getTenantUser = async identifier => { - const db = new CouchDB(PLATFORM_INFO_DB) - try { - return await db.get(identifier) - } catch (err) { - return null - } -} - exports.tenantUserLookup = async ctx => { const id = ctx.params.id const user = await getTenantUser(id) diff --git a/packages/worker/src/api/controllers/system/environment.js b/packages/worker/src/api/controllers/system/environment.js index 664e950797..a4022561d4 100644 --- a/packages/worker/src/api/controllers/system/environment.js +++ b/packages/worker/src/api/controllers/system/environment.js @@ -5,5 +5,6 @@ exports.fetch = async ctx => { multiTenancy: !!env.MULTI_TENANCY, cloud: !env.SELF_HOSTED, accountPortalUrl: env.ACCOUNT_PORTAL_URL, + disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, } } diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index e3cc6efbc4..3ed6a96ac1 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -87,7 +87,7 @@ router if (ctx.publicEndpoint) { return next() } - if (!ctx.isAuthenticated || !ctx.user.budibaseAccess) { + if ((!ctx.isAuthenticated || !ctx.user.budibaseAccess) && !ctx.internal) { ctx.throw(403, "Unauthorized - no public worker access") } return next() diff --git a/packages/worker/src/api/routes/global/users.js b/packages/worker/src/api/routes/global/users.js index 1a04944a30..d2a6bece9f 100644 --- a/packages/worker/src/api/routes/global/users.js +++ b/packages/worker/src/api/routes/global/users.js @@ -3,6 +3,7 @@ const controller = require("../../controllers/global/users") const joiValidator = require("../../../middleware/joi-validator") const adminOnly = require("../../../middleware/adminOnly") const Joi = require("joi") +const cloudRestricted = require("../../../middleware/cloudRestricted") const router = Router() @@ -90,6 +91,7 @@ router ) .post( "/api/global/users/init", + cloudRestricted, buildAdminInitValidation(), controller.adminUser ) diff --git a/packages/worker/src/constants/templates/passwordRecovery.hbs b/packages/worker/src/constants/templates/passwordRecovery.hbs index 6ebe606467..128398cdb7 100644 --- a/packages/worker/src/constants/templates/passwordRecovery.hbs +++ b/packages/worker/src/constants/templates/passwordRecovery.hbs @@ -6,7 +6,7 @@
-

Hi {{ email }},

+

Hi {{#if name}}{{ name }}{{else}}{{ email }}{{/if}},

You recently requested to reset your password for your {{ company }} account in your Budibase platform. Use the button below to reset it. This password reset is only valid for the next 24 hours.

diff --git a/packages/worker/src/constants/templates/welcome.hbs b/packages/worker/src/constants/templates/welcome.hbs index b509ad5e31..18ea572bb0 100644 --- a/packages/worker/src/constants/templates/welcome.hbs +++ b/packages/worker/src/constants/templates/welcome.hbs @@ -6,7 +6,7 @@