diff --git a/.eslintignore b/.eslintignore index 91f5433596..54824be5c7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,4 +6,5 @@ packages/server/coverage packages/server/client packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js -packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js \ No newline at end of file +packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js +packages/builder/cypress/reports \ No newline at end of file diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index d4050ab40e..a20e292923 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -72,3 +72,56 @@ jobs: env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + + - name: Tag and release Proxy service docker image + run: | + docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build:docker:proxy:release + docker tag proxy-service budibase/proxy:$RELEASE_TAG + docker push budibase/proxy:$RELEASE_TAG + env: + DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + RELEASE_TAG: k8s-release + + - name: Pull values.yaml from budibase-infra + run: | + curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ + -H 'Accept: application/vnd.github.v3.raw' \ + -o values.release.yaml \ + -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml + wc -l values.release.yaml + + - name: Deploy to Release Environment + uses: glopezep/helm@v1.7.1 + with: + release: budibase-release + namespace: budibase + chart: charts/budibase + token: ${{ github.token }} + helm: helm3 + values: | + globals: + appVersion: develop + ingress: + enabled: true + nginx: true + value-files: >- + [ + "values.release.yaml" + ] + env: + KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' + + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v4.0.0 + with: + webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} + content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." + embed-title: ${{ env.RELEASE_VERSION }} \ No newline at end of file diff --git a/.github/workflows/smoke_test.yaml b/.github/workflows/smoke_test.yaml index d5a5f0b02a..7002c8335b 100644 --- a/.github/workflows/smoke_test.yaml +++ b/.github/workflows/smoke_test.yaml @@ -33,23 +33,20 @@ jobs: with: record: true install: false + tag: nightly command: yarn test:e2e:ci:record env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - # TODO: upload recordings to s3 - # - name: Configure AWS Credentials - # uses: aws-actions/configure-aws-credentials@v1 - # with: - # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - # aws-region: eu-west-1 - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v4.0.0 + - uses: actions/upload-artifact@v3 with: - webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }} - content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.outputs.dashboardUrl }}" - embed-title: ${{ steps.cypress.outcome }} - embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }} + name: Test Reports + path: packages/builder/cypress/reports/testReport.html + - name: Cypress Discord Notify + run: yarn test:e2e:ci:notify + env: + CYPRESS_WEBHOOK_URL: ${{ secrets.BUDI_QA_WEBHOOK }} + CYPRESS_OUTCOME: ${{ steps.cypress.outcome }} + CYPRESS_DASHBOARD_URL: ${{ steps.cypress.outputs.dashboardUrl }} + GITHUB_RUN_URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID diff --git a/.gitignore b/.gitignore index 7d09f0a2ba..03d77c5477 100644 --- a/.gitignore +++ b/.gitignore @@ -97,5 +97,7 @@ hosting/proxy/.generated-nginx.prod.conf bin/ hosting/.generated* -packages/builder/cypress.env.json -stats.html +packages/builder/cypress.env.json +packages/builder/cypress/reports +stats.html + diff --git a/lerna.json b/lerna.json index e767606692..30ecfdb752 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.159-alpha.1", + "version": "1.0.185-alpha.7", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index fb6d9da990..0f6fdd01a9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "private": true, "devDependencies": { "@rollup/plugin-json": "^4.0.2", + "@types/mongodb": "3.6.3", + "@typescript-eslint/parser": "4.28.0", "babel-eslint": "^10.0.3", "eslint": "^7.28.0", "eslint-plugin-cypress": "^2.11.3", @@ -16,7 +18,6 @@ "rimraf": "^3.0.2", "rollup-plugin-replace": "^2.2.0", "svelte": "^3.38.2", - "@typescript-eslint/parser": "4.28.0", "typescript": "4.5.5" }, "scripts": { @@ -48,11 +49,13 @@ "test:e2e": "lerna run cy:test --stream", "test:e2e:ci": "lerna run cy:ci --stream", "test:e2e:ci:record": "lerna run cy:ci:record --stream", + "test:e2e:ci:notify": "lerna run cy:ci:notify", "build:specs": "lerna run specs", "build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", "build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy", "build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy", + "build:docker:proxy:release": "node scripts/proxy/generateProxyConfig release && npm run build:docker:proxy", "build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy", "build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", diff --git a/packages/backend-core/cache.js b/packages/backend-core/cache.js index 02344586a9..932fd7b901 100644 --- a/packages/backend-core/cache.js +++ b/packages/backend-core/cache.js @@ -1,4 +1,7 @@ +const generic = require("./src/cache/generic") + module.exports = { user: require("./src/cache/user"), app: require("./src/cache/appMetadata"), + ...generic, } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 3f215537b3..e2a61bcceb 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.159-alpha.1", + "version": "1.0.185-alpha.7", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", @@ -12,7 +12,8 @@ "dependencies": { "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.901.0", - "bcryptjs": "^2.4.3", + "bcrypt": "^5.0.1", + "dotenv": "^16.0.1", "emitter-listener": "^1.1.2", "ioredis": "^4.27.1", "jsonwebtoken": "^8.5.1", @@ -41,8 +42,7 @@ "devDependencies": { "ioredis-mock": "^5.5.5", "jest": "^26.6.3", - "pouchdb-adapter-memory": "^7.2.2", - "pouchdb-all-dbs": "^1.0.2" + "pouchdb-adapter-memory": "^7.2.2" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" } diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index f6d53522d5..b13cd932c6 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -29,7 +29,7 @@ passport.deserializeUser(async (user, done) => { const user = await db.get(user._id) return done(null, user) } catch (err) { - console.error("User not found", err) + console.error(`User not found`, err) return done(null, false, { message: "User not found" }) } }) diff --git a/packages/backend-core/src/cache/generic.js b/packages/backend-core/src/cache/generic.js new file mode 100644 index 0000000000..b23568f8b9 --- /dev/null +++ b/packages/backend-core/src/cache/generic.js @@ -0,0 +1,49 @@ +const redis = require("../redis/authRedis") +const env = require("../environment") +const { getTenantId } = require("../context") + +exports.CacheKeys = { + CHECKLIST: "checklist", +} + +exports.TTL = { + ONE_MINUTE: 600, + ONE_HOUR: 3600, + ONE_DAY: 86400, +} + +function generateTenantKey(key) { + const tenantId = getTenantId() + return `${key}:${tenantId}` +} + +exports.withCache = async (key, ttl, fetchFn) => { + key = generateTenantKey(key) + const client = await redis.getCacheClient() + const cachedValue = await client.get(key) + if (cachedValue) { + return cachedValue + } + + try { + const fetchedValue = await fetchFn() + + if (!env.isTest()) { + await client.store(key, fetchedValue, ttl) + } + return fetchedValue + } catch (err) { + console.error("Error fetching before cache - ", err) + throw err + } +} + +exports.bustCache = async key => { + const client = await redis.getCacheClient() + try { + await client.delete(generateTenantKey(key)) + } catch (err) { + console.error("Error busting cache - ", err) + throw err + } +} diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index 20e5e26693..3abbc84596 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -73,7 +73,7 @@ exports.isMultiTenant = () => { } // used for automations, API endpoints should always be in context already -exports.doInTenant = (tenantId, task) => { +exports.doInTenant = (tenantId, task, { forceNew } = {}) => { // the internal function is so that we can re-use an existing // context - don't want to close DB on a parent context async function internal(opts = { existing: false }) { @@ -98,7 +98,11 @@ exports.doInTenant = (tenantId, task) => { } } const using = cls.getFromContext(ContextKeys.IN_USE) - if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) { + if ( + !forceNew && + using && + cls.getFromContext(ContextKeys.TENANT_ID) === tenantId + ) { cls.setOnContext(ContextKeys.IN_USE, using + 1) return internal({ existing: true }) } else { @@ -135,7 +139,7 @@ const setAppTenantId = appId => { exports.updateTenantId(appTenantId) } -exports.doInAppContext = (appId, task) => { +exports.doInAppContext = (appId, task, { forceNew } = {}) => { if (!appId) { throw new Error("appId is required") } @@ -162,7 +166,7 @@ exports.doInAppContext = (appId, task) => { } } const using = cls.getFromContext(ContextKeys.IN_USE) - if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) { + if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) { cls.setOnContext(ContextKeys.IN_USE, using + 1) return internal({ existing: true }) } else { diff --git a/packages/backend-core/src/db/index.js b/packages/backend-core/src/db/index.js index 7d54b881b1..d179186988 100644 --- a/packages/backend-core/src/db/index.js +++ b/packages/backend-core/src/db/index.js @@ -3,13 +3,13 @@ const env = require("../environment") let PouchDB let initialised = false +const dbList = new Set() const put = dbPut => async (doc, options = {}) => { - const response = await dbPut(doc, options) // TODO: add created / updated - return response + return await dbPut(doc, options) } const checkInitialised = () => { @@ -28,6 +28,9 @@ exports.init = opts => { // in situations that using the function doWithDB does not work exports.dangerousGetDB = (dbName, opts) => { checkInitialised() + if (env.isTest()) { + dbList.add(dbName) + } const db = new PouchDB(dbName, opts) const dbPut = db.put db.put = put(dbPut) @@ -63,6 +66,9 @@ exports.doWithDB = async (dbName, cb, opts) => { } exports.allDbs = () => { + if (!env.isTest()) { + throw new Error("Cannot be used outside test environment.") + } checkInitialised() - return PouchDB.allDbs() + return [...dbList] } diff --git a/packages/backend-core/src/db/pouch.js b/packages/backend-core/src/db/pouch.js index 9c1ada8d76..76390ac644 100644 --- a/packages/backend-core/src/db/pouch.js +++ b/packages/backend-core/src/db/pouch.js @@ -92,11 +92,5 @@ exports.getPouch = (opts = {}) => { PouchDB.plugin(find) } - const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS) - if (opts.allDbs) { - const allDbs = require("pouchdb-all-dbs") - allDbs(Pouch) - } - - return Pouch + return PouchDB.defaults(POUCH_DB_DEFAULTS) } diff --git a/packages/backend-core/src/db/tests/utils.spec.js b/packages/backend-core/src/db/tests/utils.spec.js index ebef670a81..f8b9549d46 100644 --- a/packages/backend-core/src/db/tests/utils.spec.js +++ b/packages/backend-core/src/db/tests/utils.spec.js @@ -1,61 +1,194 @@ +require("../../tests/utilities/dbConfig"); const { generateAppID, getDevelopmentAppID, getProdAppID, isDevAppID, isProdAppID, + getPlatformUrl, + getScopedConfig } = require("../utils") +const tenancy = require("../../tenancy"); +const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); +const env = require("../../environment") -function getID() { - const appId = generateAppID() - const split = appId.split("_") - const uuid = split[split.length - 1] - const devAppId = `app_dev_${uuid}` - return { appId, devAppId, split, uuid } +describe("utils", () => { + describe("app ID manipulation", () => { + + function getID() { + const appId = generateAppID() + const split = appId.split("_") + const uuid = split[split.length - 1] + const devAppId = `app_dev_${uuid}` + return { appId, devAppId, split, uuid } + } + + it("should be able to generate a new app ID", () => { + expect(generateAppID().startsWith("app_")).toEqual(true) + }) + + it("should be able to convert a production app ID to development", () => { + const { appId, uuid } = getID() + expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development app ID to development", () => { + const { devAppId, uuid } = getID() + expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development ID to a production", () => { + const { devAppId, uuid } = getID() + expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) + }) + + it("should be able to convert a production ID to production", () => { + const { appId, uuid } = getID() + expect(getProdAppID(appId)).toEqual(`app_${uuid}`) + }) + + it("should be able to confirm dev app ID is development", () => { + const { devAppId } = getID() + expect(isDevAppID(devAppId)).toEqual(true) + }) + + it("should be able to confirm prod app ID is not development", () => { + const { appId } = getID() + expect(isDevAppID(appId)).toEqual(false) + }) + + it("should be able to confirm prod app ID is prod", () => { + const { appId } = getID() + expect(isProdAppID(appId)).toEqual(true) + }) + + it("should be able to confirm dev app ID is not prod", () => { + const { devAppId } = getID() + expect(isProdAppID(devAppId)).toEqual(false) + }) + }) +}) + +const DB_URL = "http://dburl.com" +const DEFAULT_URL = "http://localhost:10000" +const ENV_URL = "http://env.com" + +const setDbPlatformUrl = async () => { + const db = tenancy.getGlobalDB() + db.put({ + _id: "config_settings", + type: Configs.SETTINGS, + config: { + platformUrl: DB_URL + } + }) } -describe("app ID manipulation", () => { - it("should be able to generate a new app ID", () => { - expect(generateAppID().startsWith("app_")).toEqual(true) +const clearSettingsConfig = async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + const db = tenancy.getGlobalDB() + try { + const config = await db.get("config_settings") + await db.remove("config_settings", config._rev) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + }) +} + +describe("getPlatformUrl", () => { + describe("self host", () => { + + beforeEach(async () => { + env._set("SELF_HOST", 1) + await clearSettingsConfig() + }) + + it("gets the default url", async () => { + await tenancy.doInTenant(null, async () => { + const url = await getPlatformUrl() + expect(url).toBe(DEFAULT_URL) + }) + }) + + it("gets the platform url from the environment", async () => { + await tenancy.doInTenant(null, async () => { + env._set("PLATFORM_URL", ENV_URL) + const url = await getPlatformUrl() + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the database", async () => { + await tenancy.doInTenant(null, async () => { + await setDbPlatformUrl() + const url = await getPlatformUrl() + expect(url).toBe(DB_URL) + }) + }) }) - it("should be able to convert a production app ID to development", () => { - const { appId, uuid } = getID() - expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) - }) - it("should be able to convert a development app ID to development", () => { - const { devAppId, uuid } = getID() - expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) - }) + describe("cloud", () => { + const TENANT_AWARE_URL = "http://default.env.com" - it("should be able to convert a development ID to a production", () => { - const { devAppId, uuid } = getID() - expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) - }) + beforeEach(async () => { + env._set("SELF_HOSTED", 0) + env._set("MULTI_TENANCY", 1) + env._set("PLATFORM_URL", ENV_URL) + await clearSettingsConfig() + }) - it("should be able to convert a production ID to production", () => { - const { appId, uuid } = getID() - expect(getProdAppID(appId)).toEqual(`app_${uuid}`) - }) + it("gets the platform url from the environment without tenancy", async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + const url = await getPlatformUrl({ tenantAware: false }) + expect(url).toBe(ENV_URL) + }) + }) - it("should be able to confirm dev app ID is development", () => { - const { devAppId } = getID() - expect(isDevAppID(devAppId)).toEqual(true) - }) + it("gets the platform url from the environment with tenancy", async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) - it("should be able to confirm prod app ID is not development", () => { - const { appId } = getID() - expect(isDevAppID(appId)).toEqual(false) + it("never gets the platform url from the database", async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + await setDbPlatformUrl() + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) }) +}) - it("should be able to confirm prod app ID is prod", () => { - const { appId } = getID() - expect(isProdAppID(appId)).toEqual(true) - }) +describe("getScopedConfig", () => { + describe("settings config", () => { - it("should be able to confirm dev app ID is not prod", () => { - const { devAppId } = getID() - expect(isProdAppID(devAppId)).toEqual(false) + beforeEach(async () => { + env._set("SELF_HOSTED", 1) + env._set("PLATFORM_URL", "") + await clearSettingsConfig() + }) + + it("returns the platform url with an existing config", async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + await setDbPlatformUrl() + const db = tenancy.getGlobalDB() + const config = await getScopedConfig(db, { type: Configs.SETTINGS }) + expect(config.platformUrl).toBe(DB_URL) + }) + }) + + it("returns the platform url without an existing config", async () => { + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + const db = tenancy.getGlobalDB() + const config = await getScopedConfig(db, { type: Configs.SETTINGS }) + expect(config.platformUrl).toBe(DEFAULT_URL) + }) + }) }) -}) \ No newline at end of file +}) diff --git a/packages/backend-core/src/db/utils.js b/packages/backend-core/src/db/utils.js index 5f7bf794c2..d6eb0aa89e 100644 --- a/packages/backend-core/src/db/utils.js +++ b/packages/backend-core/src/db/utils.js @@ -9,7 +9,7 @@ const { APP_PREFIX, APP_DEV, } = require("./constants") -const { getTenantId, getGlobalDBName } = require("../tenancy") +const { getTenantId, getGlobalDBName, getGlobalDB } = require("../tenancy") const fetch = require("node-fetch") const { doWithDB, allDbs } = require("./index") const { getCouchInfo } = require("./pouch") @@ -392,9 +392,7 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { // always provide the platform URL if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { - scopedConfig.doc.config.platformUrl = await getPlatformUrl( - scopedConfig.doc.config - ) + scopedConfig.doc.config.platformUrl = await getPlatformUrl() } else { scopedConfig = { doc: { @@ -409,19 +407,30 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { return scopedConfig && scopedConfig.doc } -const getPlatformUrl = async settings => { +const getPlatformUrl = async (opts = { tenantAware: true }) => { let platformUrl = env.PLATFORM_URL || "http://localhost:10000" - if (!env.SELF_HOSTED && env.MULTI_TENANCY) { + if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { // cloud and multi tenant - add the tenant to the default platform url const tenantId = getTenantId() if (!platformUrl.includes("localhost:")) { platformUrl = platformUrl.replace("://", `://${tenantId}.`) } - } else { + } else if (env.SELF_HOSTED) { + const db = getGlobalDB() + // get the doc directly instead of with getScopedConfig to prevent loop + let settings + try { + settings = await db.get(generateConfigID({ type: Configs.SETTINGS })) + } catch (e) { + if (e.status !== 404) { + throw e + } + } + // self hosted - check for platform url override - if (settings && settings.platformUrl) { - platformUrl = settings.platformUrl + if (settings && settings.config && settings.config.platformUrl) { + platformUrl = settings.config.platformUrl } } diff --git a/packages/backend-core/src/environment.js b/packages/backend-core/src/environment.js index f628e899ad..fe56697011 100644 --- a/packages/backend-core/src/environment.js +++ b/packages/backend-core/src/environment.js @@ -10,7 +10,15 @@ function isDev() { return process.env.NODE_ENV !== "production" } +let LOADED = false +if (!LOADED && isDev() && !isTest()) { + require("dotenv").config() + LOADED = true +} + module.exports = { + isTest, + isDev, JWT_SECRET: process.env.JWT_SECRET, COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_USERNAME: process.env.COUCH_DB_USER, @@ -41,8 +49,8 @@ module.exports = { GLOBAL_CLOUD_BUCKET_NAME: process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads", USE_COUCH: process.env.USE_COUCH || true, - isTest, - isDev, + DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, + DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, _set(key, value) { process.env[key] = value module.exports[key] = value diff --git a/packages/backend-core/src/hashing.js b/packages/backend-core/src/hashing.js index 7f3af2855f..45abe2f9bd 100644 --- a/packages/backend-core/src/hashing.js +++ b/packages/backend-core/src/hashing.js @@ -1,4 +1,4 @@ -const bcrypt = require("bcryptjs") +const bcrypt = require("bcrypt") const env = require("./environment") const { v4 } = require("uuid") diff --git a/packages/backend-core/src/index.js b/packages/backend-core/src/index.js index 3868d9bffa..572b61fbeb 100644 --- a/packages/backend-core/src/index.js +++ b/packages/backend-core/src/index.js @@ -19,5 +19,6 @@ module.exports = { env: require("./environment"), accounts: require("./cloud/accounts"), tenancy: require("./tenancy"), + context: require("../context"), featureFlags: require("./featureFlags"), } diff --git a/packages/backend-core/src/middleware/passport/datasource/google.js b/packages/backend-core/src/middleware/passport/datasource/google.js index 96c7f99953..53719b8350 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.js +++ b/packages/backend-core/src/middleware/passport/datasource/google.js @@ -21,20 +21,12 @@ async function fetchGoogleCreds() { ) } -async function platformUrl() { - const db = getGlobalDB() - const publicConfig = await getScopedConfig(db, { - type: Configs.SETTINGS, - }) - return getPlatformUrl(publicConfig) -} - async function preAuth(passport, ctx, next) { // get the relevant config const googleConfig = await fetchGoogleCreds() - const platUrl = await platformUrl() + const platformUrl = await getPlatformUrl({ tenantAware: false }) - let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback` + let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const strategy = await google.strategyFactory(googleConfig, callbackUrl) if (!ctx.query.appId || !ctx.query.datasourceId) { @@ -51,9 +43,9 @@ async function preAuth(passport, ctx, next) { async function postAuth(passport, ctx, next) { // get the relevant config const config = await fetchGoogleCreds() - const platUrl = await platformUrl() + const platformUrl = await getPlatformUrl({ tenantAware: false }) - let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback` + let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const strategy = await google.strategyFactory( config, callbackUrl, diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 2149bd3e18..716ebc1755 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -30,7 +30,7 @@ exports.authenticate = async function (ctx, email, password, done) { const dbUser = await getGlobalUserByEmail(email) if (dbUser == null) { - return authError(done, "User not found") + return authError(done, `User not found: [${email}]`) } // check that the user is currently inactive, if this is the case throw invalid diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index bfe9f97dc0..c5e9fe0034 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -71,7 +71,7 @@ describe("oidc", () => { describe("authenticate", () => { afterEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks() }); // mock third party common authentication @@ -80,10 +80,10 @@ describe("oidc", () => { // mock the passport callback const mockDone = jest.fn() + const mockSaveUserFn = jest.fn() async function doAuthenticate() { const oidc = require("../oidc") - const mockSaveUserFn = jest.fn() const authenticate = await oidc.buildVerifyFn(mockSaveUserFn) await authenticate( @@ -105,11 +105,13 @@ describe("oidc", () => { expect(authenticateThirdParty).toHaveBeenCalledWith( user, false, - mockDone) + mockDone, + mockSaveUserFn, + ) } it("delegates authentication to third party common", async () => { - doTest() + await doTest() }) it("uses JWT email to get email", async () => { @@ -118,7 +120,7 @@ describe("oidc", () => { email : "mock@budibase.com" } - doTest() + await doTest() }) it("uses JWT username to get email", async () => { @@ -127,7 +129,7 @@ describe("oidc", () => { preferred_username : "mock@budibase.com" } - doTest() + await doTest() }) it("uses JWT invalid username to get email", async () => { diff --git a/packages/backend-core/src/redis/authRedis.js b/packages/backend-core/src/redis/authRedis.js index ca5c9bae37..b9f6d8d0b0 100644 --- a/packages/backend-core/src/redis/authRedis.js +++ b/packages/backend-core/src/redis/authRedis.js @@ -1,18 +1,20 @@ const Client = require("./index") const utils = require("./utils") -let userClient, sessionClient, appClient +let userClient, sessionClient, appClient, cacheClient async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() + cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() } process.on("exit", async () => { if (userClient) await userClient.finish() if (sessionClient) await sessionClient.finish() if (appClient) await appClient.finish() + if (cacheClient) await cacheClient.finish() }) module.exports = { @@ -34,4 +36,10 @@ module.exports = { } return appClient }, + getCacheClient: async () => { + if (!cacheClient) { + await init() + } + return cacheClient + }, } diff --git a/packages/backend-core/src/redis/utils.js b/packages/backend-core/src/redis/utils.js index 77f64f6593..90ea5c33f9 100644 --- a/packages/backend-core/src/redis/utils.js +++ b/packages/backend-core/src/redis/utils.js @@ -18,6 +18,7 @@ exports.Databases = { APP_METADATA: "appMetadata", QUERY_VARS: "queryVars", LICENSES: "license", + GENERIC_CACHE: "data_cache", } exports.SEPARATOR = SEPARATOR diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js index 1720eeb820..4e6899c248 100644 --- a/packages/backend-core/src/security/sessions.js +++ b/packages/backend-core/src/security/sessions.js @@ -15,29 +15,33 @@ function makeSessionID(userId, sessionId) { } async function invalidateSessions(userId, sessionIds = null) { - let sessions = [] + try { + let sessions = [] - // If no sessionIds, get all the sessions for the user - if (!sessionIds) { - sessions = await getSessionsForUser(userId) - sessions.forEach( - session => - (session.key = makeSessionID(session.userId, session.sessionId)) - ) - } else { - // use the passed array of sessionIds - sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds] - sessions = sessions.map(sessionId => ({ - key: makeSessionID(userId, sessionId), - })) - } + // If no sessionIds, get all the sessions for the user + if (!sessionIds) { + sessions = await getSessionsForUser(userId) + sessions.forEach( + session => + (session.key = makeSessionID(session.userId, session.sessionId)) + ) + } else { + // use the passed array of sessionIds + sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds] + sessions = sessions.map(sessionId => ({ + key: makeSessionID(userId, sessionId), + })) + } - const client = await redis.getSessionClient() - const promises = [] - for (let session of sessions) { - promises.push(client.delete(session.key)) + const client = await redis.getSessionClient() + const promises = [] + for (let session of sessions) { + promises.push(client.delete(session.key)) + } + await Promise.all(promises) + } catch (err) { + console.error(`Error invalidating sessions: ${err}`) } - await Promise.all(promises) } exports.createASession = async (userId, session) => { @@ -76,6 +80,7 @@ exports.getSession = async (userId, sessionId) => { return client.get(makeSessionID(userId, sessionId)) } catch (err) { // if can't get session don't error, just don't return anything + console.error(err) return null } } diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js index 5c922c42ad..e764f35803 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils.js @@ -197,11 +197,16 @@ exports.getBuildersCount = async () => { return builders.length } -exports.saveUser = async ( +const DEFAULT_SAVE_USER = { + hashPassword: true, + requirePassword: true, + bulkCreate: false, +} + +exports.internalSaveUser = async ( user, tenantId, - hashPassword = true, - requirePassword = true + { hashPassword, requirePassword, bulkCreate } = DEFAULT_SAVE_USER ) => { if (!tenantId) { throw "No tenancy specified." @@ -213,7 +218,10 @@ exports.saveUser = async ( let { email, password, _id } = user // make sure another user isn't using the same email let dbUser - if (email) { + // user can't exist in bulk creation + if (bulkCreate) { + dbUser = null + } else if (email) { // check budibase users inside the tenant dbUser = await exports.getGlobalUserByEmail(email) if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { @@ -267,11 +275,17 @@ exports.saveUser = async ( user.status = UserStatus.ACTIVE } try { - const response = await db.put({ + const putOpts = { password: hashedPassword, ...user, - }) - await tryAddTenant(tenantId, _id, email) + } + if (bulkCreate) { + return putOpts + } + const response = await db.put(putOpts) + if (env.MULTI_TENANCY) { + await tryAddTenant(tenantId, _id, email) + } await userCache.invalidateUser(response.id) return { _id: response.id, @@ -288,6 +302,19 @@ exports.saveUser = async ( }) } +// maintained for api compat, don't want to change function signature +exports.saveUser = async ( + user, + tenantId, + hashPassword = true, + requirePassword = true +) => { + return exports.internalSaveUser(user, tenantId, { + hashPassword, + requirePassword, + }) +} + /** * Logs a user out from budibase. Re-used across account portal and builder. */ diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 7dfa64810e..e1d178b32c 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -497,6 +497,21 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -623,6 +638,11 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -661,11 +681,6 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^5.2.1: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" @@ -701,11 +716,6 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -748,6 +758,19 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -800,11 +823,6 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types@0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= - async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" @@ -938,11 +956,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base62@^1.1.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" - integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== - base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -978,10 +991,13 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bcryptjs@^2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" - integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +bcrypt@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" + integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^3.1.0" bl@^4.0.3: version "4.1.0" @@ -1164,6 +1180,11 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -1245,6 +1266,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" @@ -1259,26 +1285,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.5.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commoner@^0.10.1: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -1294,6 +1300,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -1445,34 +1456,31 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + denque@^1.1.0: version "1.5.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -detective@^4.3.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" - integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== - dependencies: - acorn "^5.2.1" - defined "^1.0.0" - diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -1485,6 +1493,11 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +dotenv@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" + integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== + double-ended-queue@2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -1572,15 +1585,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es3ify@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.2.2.tgz#5dae3e650e5be3684b88066513d528d092629862" - integrity sha1-Xa4+ZQ5b42hLiAZlE9Uo0JJimGI= - dependencies: - esprima "^2.7.1" - jstransform "~11.0.0" - through "~2.3.4" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1608,26 +1612,11 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -esprima-fb@^15001.1.0-dev-harmony-fb: - version "15001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" - integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= - -esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= - esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - estraverse@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" @@ -1884,6 +1873,13 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1904,6 +1900,21 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -1945,17 +1956,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -1999,7 +1999,7 @@ googleapis@^16.0.0: google-auth-library "~0.10.0" string-template "~1.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.2.4: +graceful-fs@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== @@ -2055,6 +2055,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -2165,7 +2170,7 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.4.24, iconv-lite@^0.4.5: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -2988,17 +2993,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" -jstransform@~11.0.0: - version "11.0.3" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" - integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= - dependencies: - base62 "^1.1.0" - commoner "^0.10.1" - esprima-fb "^15001.1.0-dev-harmony-fb" - object-assign "^2.0.0" - source-map "^0.4.2" - jwa@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" @@ -3274,7 +3268,7 @@ ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0: resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -3387,7 +3381,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -"minimatch@2 || 3", minimatch@^3.0.4: +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -3399,6 +3393,21 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minipass@^3.0.0: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -3412,12 +3421,10 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== ms@2.0.0: version "2.0.0" @@ -3476,12 +3483,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-fetch@2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@2.6.7, node-fetch@^2.6.1: +node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -3525,6 +3537,13 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -3561,6 +3580,16 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" @@ -3581,10 +3610,10 @@ oauth@0.9.x, oauth@^0.9.15: resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= -object-assign@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" - integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo= +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= object-copy@^0.1.0: version "0.1.0" @@ -3918,17 +3947,6 @@ pouchdb-adapter-utils@7.2.2: pouchdb-merge "7.2.2" pouchdb-utils "7.2.2" -pouchdb-all-dbs@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pouchdb-all-dbs/-/pouchdb-all-dbs-1.1.1.tgz#85f04a39cafda52497ec49abf1c93bb5e72813f6" - integrity sha512-UUnsdmcnRSQ8MAOYSJjfTwKkQNb/6fvOfd/f7dNNivWZ2YDYVuMfgw1WQdL634yEtcXTxAENZ/EyLRdzPCB41A== - dependencies: - argsarray "0.0.1" - es3ify "^0.2.2" - inherits "~2.0.1" - pouchdb-promise "6.4.3" - tiny-queue "^0.2.0" - pouchdb-binary-utils@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-7.2.2.tgz#0690b348052c543b1e67f032f47092ca82bcb10e" @@ -4005,7 +4023,7 @@ pouchdb-merge@7.2.2: resolved "https://registry.yarnpkg.com/pouchdb-merge/-/pouchdb-merge-7.2.2.tgz#940d85a2b532d6a93a6cab4b250f5648511bcc16" integrity sha512-6yzKJfjIchBaS7Tusuk8280WJdESzFfQ0sb4jeMUNnrqs4Cx3b0DIEOYTRRD9EJDM+je7D3AZZ4AT0tFw8gb4A== -pouchdb-promise@6.4.3, pouchdb-promise@^6.0.4: +pouchdb-promise@^6.0.4: version "6.4.3" resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3" integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw== @@ -4088,11 +4106,6 @@ pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -private@^0.1.6, private@~0.1.5: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -4139,11 +4152,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -4193,7 +4201,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0: +"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -4225,16 +4233,6 @@ readline-sync@^1.4.9: resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== -recast@^0.11.17: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - redis-commands@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -4379,7 +4377,7 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -4467,6 +4465,13 @@ semver@^7.3.2: dependencies: lru-cache "^6.0.0" +semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4592,14 +4597,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0: +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -4728,7 +4726,7 @@ string-template@~1.0.0: resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y= -string-width@^4.1.0, string-width@^4.2.0: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4841,6 +4839,18 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -4879,16 +4889,6 @@ through2@^2.0.0, through2@^2.0.2, through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@~2.3.4: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tiny-queue@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" - integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY= - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -5229,6 +5229,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 8bc0329509..a0fbdd3d33 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": "1.0.159-alpha.1", + "version": "1.0.185-alpha.7", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.0.159-alpha.1", + "@budibase/string-templates": "^1.0.185-alpha.7", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 1d0d32b03c..73ba7bb642 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -58,6 +58,11 @@ if (timeOnly) { newValue = `2000-01-01T${newValue.split("T")[1]}` } + // date only, offset for timezone so always right date + else if (!enableTime) { + const offset = dates[0].getTimezoneOffset() * 60000 + newValue = new Date(dates[0].getTime() - offset).toISOString() + } dispatch("change", newValue) } diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index d739e751c9..36515acbc5 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -18,6 +18,7 @@ export let fileSizeLimit = BYTES_IN_MB * 20 export let processFiles = null export let handleFileTooLarge = null + export let handleTooManyFiles = null export let gallery = true export let error = null export let fileTags = [] @@ -71,6 +72,13 @@ handleFileTooLarge(fileSizeLimit, value) return } + + const fileCount = fileList.length + value.length + if (handleTooManyFiles && maximum && fileCount > maximum) { + handleTooManyFiles(maximum) + return + } + if (processFiles) { const processedFiles = await processFiles(fileList) const newValue = [...value, ...processedFiles] diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index 78b698eed2..6a64876a2c 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -1,6 +1,6 @@
{/if} { @@ -33,6 +34,7 @@ {placeholder} {type} {quiet} + {autofocus} on:change={onChange} on:click on:input diff --git a/packages/bbui/src/Layout/Layout.svelte b/packages/bbui/src/Layout/Layout.svelte index c66a409242..6a01d9bece 100644 --- a/packages/bbui/src/Layout/Layout.svelte +++ b/packages/bbui/src/Layout/Layout.svelte @@ -40,6 +40,10 @@ padding-left: var(--spacing-xl); padding-right: var(--spacing-xl); } + .paddingX-XXL { + padding-left: var(--spectrum-alias-grid-gutter-large); + padding-right: var(--spectrum-alias-grid-gutter-large); + } .paddingY-S { padding-top: var(--spacing-s); padding-bottom: var(--spacing-s); @@ -56,6 +60,10 @@ padding-top: var(--spacing-xl); padding-bottom: var(--spacing-xl); } + .paddingY-XXL { + padding-top: var(--spectrum-alias-grid-gutter-large); + padding-bottom: var(--spectrum-alias-grid-gutter-large); + } .gap-XXS { grid-gap: var(--spacing-xs); } diff --git a/packages/bbui/src/Layout/Page.svelte b/packages/bbui/src/Layout/Page.svelte index c12d54787b..2996bcc613 100644 --- a/packages/bbui/src/Layout/Page.svelte +++ b/packages/bbui/src/Layout/Page.svelte @@ -1,9 +1,10 @@ -
+
@@ -23,4 +24,9 @@ max-width: none; margin: 0; } + + .noPadding { + padding: 0px; + margin: 0px; + } diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json index 3e4d4c4d1b..06bf558946 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -2,10 +2,18 @@ "baseUrl": "http://localhost:4100", "video": true, "projectId": "bmbemn", + "reporter": "cypress-multi-reporters", + "reporterOptions": { + "configFile": "reporterConfig.json" + }, "env": { "PORT": "4100", "WORKER_PORT": "4200", "JWT_SECRET": "test", "HOST_IP": "" + }, + "retries": { + "runMode": 2, + "openMode": 0 } } diff --git a/packages/builder/cypress/fixtures/exported-app.txt b/packages/builder/cypress/fixtures/exported-app.txt new file mode 100644 index 0000000000..28aeaf958a --- /dev/null +++ b/packages/builder/cypress/fixtures/exported-app.txt @@ -0,0 +1,3 @@ +{"version":"1.2.9","db_type":"http","start_time":"2022-05-12T13:47:50.453Z","db_info":{"db_name":"app_dev_7cae7fa4db8745da848f91430a8211bf","purge_seq":"0-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArkQGP2kSGpHqIoiwAtOgYRA","update_seq":"11-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArgzmRPRcowJ5klmZgaZmKTR8e0xIZkuqhxrBAjEk0NzcwMsSmIQsA89QoWg","sizes":{"file":94626,"external":5665,"active":7627},"props":{},"doc_del_count":0,"doc_count":7,"disk_format_version":8,"compact_running":false,"cluster":{"q":2,"n":1,"w":1,"r":1},"instance_start_time":"0","host":"http://localhost:4005/app_dev_7cae7fa4db8745da848f91430a8211bf/","auto_compaction":false,"adapter":"http"}} +{"docs":[{"_id":"_design/database","_rev":"4-ad6d41ef604ab34da380438c1be89521","views":{"by_link":{"map":"function (doc) {\n // everything in this must remain constant as its going to Pouch, no external variables\n if (doc.type === \"link\") {\n let doc1 = doc.doc1;\n let doc2 = doc.doc2;\n // eslint-disable-next-line no-undef\n emit([doc1.tableId, doc1.rowId], {\n id: doc2.rowId,\n thisId: doc1.rowId,\n fieldName: doc1.fieldName,\n });\n // if linking to same table can't emit twice\n if (doc1.tableId !== doc2.tableId) {\n // eslint-disable-next-line no-undef\n emit([doc2.tableId, doc2.rowId], {\n id: doc1.rowId,\n thisId: doc2.rowId,\n fieldName: doc2.fieldName,\n });\n }\n }\n }"},"screen_routes":{"map":"function(doc) {\n if (doc._id.startsWith(\"screen_\")) {\n emit(doc._id, {\n id: doc._id,\n routing: doc.routing,\n })\n }\n }"}},"indexes":{"rows":{"index":"function (doc) {\n function idx(input, prev) {\n for (let key of Object.keys(input)) {\n let idxKey = prev != null ? `${prev}.${key}` : key;\n idxKey = idxKey.replace(/ /g, \"_\");\n if (Array.isArray(input[key])) {\n for (let val of input[key]) {\n if (typeof val !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, val, { store: true });\n }\n }\n }\n else if (key === \"_id\" || key === \"_rev\" || input[key] == null) {\n continue;\n }\n if (typeof input[key] === \"string\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key].toLowerCase(), { store: true });\n }\n else if (typeof input[key] !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key], { store: true });\n }\n else {\n idx(input[key], idxKey);\n }\n }\n }\n if (doc._id.startsWith(\"ro_\")) {\n // eslint-disable-next-line no-undef\n index(\"default\", doc._id);\n idx(doc);\n }\n }","analyzer":"keyword"}},"_revisions":{"start":4,"ids":["ad6d41ef604ab34da380438c1be89521","cb47f4fd824d5e096ac8b76117e6f93d","68a9a1d1b01b327676ecbaa4b1e5b8a7","0b24e44a44af45e51e562fd124ce3007"]}},{"_id":"app_metadata","_rev":"2-a4fe55378bfec0fc71e58a1364ac9965","appId":"app_dev_7cae7fa4db8745da848f91430a8211bf","type":"app","version":"1.0.155-alpha.0","componentLibraries":["@budibase/standard-components"],"name":"My app","url":"/my-app","instance":{"_id":"app_dev_7cae7fa4db8745da848f91430a8211bf"},"tenantId":"default","updatedAt":"2022-05-12T13:47:28.756Z","createdAt":"2022-05-12T13:47:26.825Z","status":"development","_revisions":{"start":2,"ids":["a4fe55378bfec0fc71e58a1364ac9965","f0d06bdb6e6ae4781eb5c4aa224002ff"]}},{"_id":"layout_private_master","_rev":"1-c02411b58d697e38763889a375d52159","componentLibraries":["@budibase/standard-components"],"title":"My app","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Navigation Layout","props":{"_id":"4f569166-a4f3-47ea-a09e-6d218c75586f","_instanceName":"Navigation Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"title":"My app","navigation":"Top","width":"Large","links":[{"text":"Home","url":"/"}]}},{"_id":"layout_public_master","_rev":"1-d6bce47046d4d0de4f19a6ff95064109","componentLibraries":["@budibase/standard-components"],"title":"My app","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Empty Layout","props":{"_id":"3723ffa1-f9e0-4c05-8013-98195c788ed6","_instanceName":"Empty Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"navigation":"None","width":"Large","links":[{"text":"Home","url":"/"}]}},{"_id":"screen_b19c454fa5ae41ab874a8860623ab37c","_rev":"1-02dddd460ad14de50dc885aa362452c6","layoutId":"layout_private_master","props":{"_id":"c87457efbc05a43f39064d9dacc9d4b67","_component":"@budibase/standard-components/container","_styles":{"normal":{},"hover":{},"active":{},"selected":{}},"_children":[],"_instanceName":"New Screen","direction":"column","hAlign":"stretch","vAlign":"top","size":"grow","gap":"M"},"routing":{"route":"/home","roleId":"BASIC","roldId":"BASIC"},"name":"screen-id"},{"_id":"ta_users","_rev":"1-30a4344f056c24cf776d5736eb3c7ed5","type":"table","views":{},"name":"Users","schema":{"email":{"type":"string","constraints":{"type":"string","email":true,"length":{"maximum":""},"presence":true},"fieldName":"email","name":"email"},"firstName":{"name":"firstName","fieldName":"firstName","type":"string","constraints":{"type":"string","presence":false}},"lastName":{"name":"lastName","fieldName":"lastName","type":"string","constraints":{"type":"string","presence":false}},"roleId":{"fieldName":"roleId","name":"roleId","type":"options","constraints":{"type":"string","presence":false,"inclusion":["ADMIN","POWER","BASIC","PUBLIC"]}},"status":{"fieldName":"status","name":"status","type":"options","constraints":{"type":"string","presence":false,"inclusion":["active","inactive"]}}},"primaryDisplay":"email"}]} +{"seq":"11-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVCJDUv3___-zMpgTWXKBAuxJiebmBkaG2DTgMSaPBUgyNACp_1DT2CGmmaUZWFqmYtOXBQAWQiha"} diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js index e67f1eb2a2..3e0ba92ba4 100644 --- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js +++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['all'], () => { context("Add Multi-Option Datatype", () => { @@ -17,19 +18,19 @@ filterTests(['all'], () => { cy.navigateToFrontend() cy.wait(500) // Add data provider - cy.get(`[data-cy="category-Data"]`).click() - cy.get(`[data-cy="component-Data Provider"]`).click() - cy.get('[data-cy="dataSource-prop-control"]').click() - cy.get(".dropdown").contains("Multi Data").click() + cy.get(interact.CATEGORY_DATA).click() + cy.get(interact.COMPONENT_DATA_PROVIDER).click() + cy.get(interact.DATASOURCE_PROP_CONTROL).click() + cy.get(interact.DROPDOWN).contains("Multi Data").click() cy.wait(500) // Add Form with schema to match table cy.addComponent("Form", "Form") - cy.get('[data-cy="dataSource-prop-control"').click() - cy.get(".dropdown").contains("Multi Data").click() + cy.get(interact.DATASOURCE_PROP_CONTROL).click() + cy.get(interact.DROPDOWN).contains("Multi Data").click() cy.wait(500) // Add multi-select picker to form cy.addComponent("Form", "Multi-select Picker").then(componentId => { - cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}") + cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}") cy.wait(1000) cy.getComponent(componentId).contains("Choose some options").click() // Check picker has 5 items @@ -40,7 +41,7 @@ filterTests(['all'], () => { } // Check items have been selected cy.getComponent(componentId) - .find(".spectrum-Picker-label") + .find(interact.SPECTRUM_Picker_LABEL) .contains("(5)") }) }) diff --git a/packages/builder/cypress/integration/addRadioButtons.spec.js b/packages/builder/cypress/integration/addRadioButtons.spec.js index 9888b56086..8f5b1a527b 100644 --- a/packages/builder/cypress/integration/addRadioButtons.spec.js +++ b/packages/builder/cypress/integration/addRadioButtons.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['all'], () => { context("Add Radio Buttons", () => { @@ -12,10 +13,10 @@ filterTests(['all'], () => { cy.addComponent("Form", "Form") cy.addComponent("Form", "Options Picker").then((componentId) => { // Provide field setting - cy.get(`[data-cy="field-prop-control"]`).type("1") + cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1") // Open dropdown and select Radio buttons - cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => { - cy.get('.spectrum-Popover').contains('Radio buttons') + cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => { + cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons') .wait(500) .click() }) @@ -28,8 +29,8 @@ filterTests(['all'], () => { }) const addRadioButtonData = (totalRadioButtons) => { - cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => { - cy.get('.spectrum-Popover').contains('Custom') + cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => { + cy.get(interact.SPECTRUM_POPOVER).contains('Custom') .wait(500) .click() }) diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index 93db43c861..db093344b4 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -268,23 +268,77 @@ filterTests(['all'], () => { cy.get(".details-section .page-action .spectrum-Button").should("be.disabled") }) - - after(() => { + + it("Should allow copying of the published application Id", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions").eq(0) + .within(() => { + cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + }) - cy.get(".deployment-top-nav svg[aria-label='Globe']") - .click({ force: true }) + cy.publishApp("sample-name") + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".appTable .name").eq(0).click() + + cy.get(".app-overview-actions-icon > .icon").click({ force : true }) + + cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { + cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true }) + }) + + cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible") + }) + + it("Should allow unpublishing of the application", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".appTable .name").eq(0).click() + + cy.get(".app-overview-actions-icon > .icon").click({ force : true }) + + cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { + cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true }) + cy.wait(500) + }) - cy.get("[data-cy='publish-popover-menu']").should("be.visible") - cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") - .click({ force : true }) - cy.get("[data-cy='unpublish-modal']").should("be.visible") .within(() => { cy.get(".confirm-wrap button").click({ force: true } )}) - cy.wait(1000) + + cy.get(".overview-tab [data-cy='app-status']").within(() => { + cy.get(".status-display").contains("Unpublished") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + }) + }) + + it("Should allow deleting of the application", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".appTable .name").eq(0).click() + + cy.get(".app-overview-actions-icon > .icon").click({ force : true }) + + cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { + cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true }) + cy.wait(500) + }) + + //The test application was renamed earlier in the spec + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get("input").type("sample name") + cy.get(".spectrum-Button--warning").click() + }) + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/builder/portal/apps') + }) + + cy.get(".appTable").should("not.exist") + + cy.get(".welcome .container h1").contains("Let's create your first app!") + }) + + after(() => { cy.deleteAllApps() }) diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index 71afd5accf..fb3c48645f 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -29,22 +29,8 @@ filterTests(['all'], () => { it("Should publish an application and correctly reflect that", () => { //Assuming the previous test was run and the unpublished app is open in edit mode. - cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) - cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") - .within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force : true }) - cy.wait(1000) - }); - - //Verify that the app url is presented correctly to the user - cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']") - .should("be.visible") - .within(() => { - let appUrl = Cypress.config().baseUrl + '/app/cypress-tests' - cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl) - cy.get(".spectrum-Button").contains("Done").click({ force: true }) - }) + cy.publishApp("cypress-tests") cy.visit(`${Cypress.config().baseUrl}/builder`) cy.wait(1000) diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index a0b9924008..ce5e2bd0c2 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -1,4 +1,5 @@ import filterTests from '../support/filterTests' +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Create an Application", () => { @@ -10,14 +11,14 @@ filterTests(['smoke', 'all'], () => { if (!(Cypress.env("TEST_ENV"))) { it("should show the new user UI/UX", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist") - cy.get(`[data-cy="import-app-btn"]`).should("exist") + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create + cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist") + cy.get(interact.CREATE_APP_BUTTON).should("exist") - cy.get(".template-category-filters").should("exist") - cy.get(".template-categories").should("exist") + cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") + cy.get(interact.TEMPLATE_CATEGORY).should("exist") - cy.get(".appTable").should("not.exist") + cy.get(interact.APP_TABLE).should("not.exist") }) } @@ -29,21 +30,21 @@ filterTests(['smoke', 'all'], () => { .its("body") .then(val => { if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) + cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Templates").click({force: true}) } }) - cy.get(".template-category-filters").should("exist") - cy.get(".template-categories").should("exist") + cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") + cy.get(interact.TEMPLATE_CATEGORY).should("exist") - cy.get(".template-category").its('length').should('be.gt', 1) - cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2) + cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1) + cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).its('length').should('be.gt', 2) - cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click() - cy.get(".template-category").should('have.length', 1) + cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(1).click() + cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).should('have.length', 1) - cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click() - cy.get(".template-category").its('length').should('be.gt', 1) + cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(0).click() + cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1) }) it("should enforce a valid url before submission", () => { @@ -51,37 +52,40 @@ filterTests(['smoke', 'all'], () => { cy.wait(500) // Start create app process. If apps already exist, click second button - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") .then(val => { if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) } }) const appName = "Cypress Tests" - cy.get(".spectrum-Modal").within(() => { + cy.get(interact.SPECTRUM_MODAL).within(() => { + + cy.get(interact.APP_NAME_INPUT).eq(0).should('have.focus') //Auto fill - cy.get("input").eq(0).type(appName).should("have.value", appName).blur() - cy.get("input").eq(1).should("have.value", "/cypress-tests") - cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled') + cy.get(interact.APP_NAME_INPUT).eq(0).clear() + cy.get(interact.APP_NAME_INPUT).eq(0).type(appName).should("have.value", appName).blur() + cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/cypress-tests") + cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled') //Empty the app url - disabled create - cy.get("input").eq(1).clear().blur() - cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled') + cy.get(interact.APP_NAME_INPUT).eq(1).clear().blur() + cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled') //Invalid url - cy.get("input").eq(1).type("/new app-url").blur() - cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled') + cy.get(interact.APP_NAME_INPUT).eq(1).type("/new app-url").blur() + cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled') //Specifically alter the url - cy.get("input").eq(1).clear() - cy.get("input").eq(1).type("another-app-name").blur() - cy.get("input").eq(1).should("have.value", "/another-app-name") - cy.get("input").eq(0).should("have.value", appName) - cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled') + cy.get(interact.APP_NAME_INPUT).eq(1).clear() + cy.get(interact.APP_NAME_INPUT).eq(1).type("another-app-name").blur() + cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/another-app-name") + cy.get(interact.APP_NAME_INPUT).eq(0).should("have.value", appName) + cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled') }) }) @@ -97,6 +101,77 @@ filterTests(['smoke', 'all'], () => { cy.deleteApp(appName) }) + it("should create the first application from scratch with a default name", () => { + cy.createApp() + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.wait(1000) + + cy.applicationInAppTable("My app") + cy.deleteApp("My app") + }) + + it("should create the first application from scratch, using the users first name as the default app name", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + + cy.updateUserInformation("Ted", "Userman") + + cy.createApp() + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.wait(1000) + + cy.applicationInAppTable("Teds app") + cy.deleteApp("Teds app") + cy.wait(2000) + + //Accomodate names that end in 'S' + cy.updateUserInformation("Chris", "Userman") + + cy.createApp() + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.wait(1000) + + cy.applicationInAppTable("Chris app") + cy.deleteApp("Chris app") + cy.wait(2000) + + cy.updateUserInformation("", "") + }) + + it("should create an application from an export", () => { + const exportedApp = 'cypress/fixtures/exported-app.txt' + + cy.importApp(exportedApp, "") + + cy.visit(`${Cypress.config().baseUrl}/builder`) + + cy.applicationInAppTable("My app") + + cy.get(".appTable .name").eq(0).click() + + cy.deleteApp("My app") + }) + + it("should create an application from an export, using the users first name as the default app name", () => { + const exportedApp = 'cypress/fixtures/exported-app.txt' + + cy.updateUserInformation("Ted", "Userman") + + cy.importApp(exportedApp, "") + + cy.visit(`${Cypress.config().baseUrl}/builder`) + + cy.applicationInAppTable("Teds app") + + cy.get(".appTable .name").eq(0).click() + + cy.deleteApp("Teds app") + + cy.updateUserInformation("", "") + }) + it("should generate the first application from a template", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) cy.wait(500) @@ -106,15 +181,15 @@ filterTests(['smoke', 'all'], () => { .its("body") .then(val => { if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) } }) - cy.get(".template-category-filters").should("exist") - cy.get(".template-categories").should("exist") + cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") + cy.get(interact.TEMPLATE_CATEGORY).should("exist") // Select template - cy.get('.template-category').eq(0).within(() => { + cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).eq(0).within(() => { const card = cy.get('.template-card').eq(0).should("exist"); const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist") cardOverlay.invoke("show") @@ -128,8 +203,8 @@ filterTests(['smoke', 'all'], () => { templateName.invoke('text') .then(templateNameText => { const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-") - cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText) - cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed) + cy.get(interact.SPECTRUM_MODAL_INPUT).eq(0).should("have.value", templateNameText) + cy.get(interact.SPECTRUM_MODAL_INPUT).eq(1).should("have.value", templateNameParsed) cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click() cy.wait(5000) diff --git a/packages/builder/cypress/integration/createUserAndRoles.spec.js b/packages/builder/cypress/integration/createUserAndRoles.spec.js index 2dbe91ce19..ac7ec1b5fd 100644 --- a/packages/builder/cypress/integration/createUserAndRoles.spec.js +++ b/packages/builder/cypress/integration/createUserAndRoles.spec.js @@ -4,6 +4,8 @@ filterTests(["smoke", "all"], () => { context("Create a User and Assign Roles", () => { before(() => { cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests") }) it("should create a user", () => { @@ -52,7 +54,7 @@ filterTests(["smoke", "all"], () => { cy.get(".spectrum-Table").contains("bbuser").click() cy.wait(1000) for (let i = 0; i < 3; i++) { - cy.get(".spectrum-Table") + cy.get(".spectrum-Table", { timeout: 3000}) .eq(1) .find(".spectrum-Table-row") .eq(0) @@ -79,6 +81,7 @@ filterTests(["smoke", "all"], () => { .contains("Update role") .click({ force: true }) }) + cy.reload() } // Confirm roles exist within Configure roles table cy.wait(2000) diff --git a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js index 9a00645ced..fbac463bfe 100644 --- a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter HR Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js b/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js index efb9e58c75..045a85d8f6 100644 --- a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js +++ b/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js @@ -1,28 +1,20 @@ import filterTests from "../../../support/filterTests" filterTests(["all"], () => { - context("Job Application Functionality", () => { + context("Job Application Tracker Template Functionality", () => { const templateName = "Job Application Tracker" const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') before(() => { cy.login() cy.deleteApp(templateName) - cy.visit(`${Cypress.config().baseUrl}/builder`, { + // Template navigation + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { onBeforeLoad(win) { cy.stub(win, 'open') } }) cy.wait(2000) - - // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) }) it("should create and publish app with Job Application Tracker template", () => { @@ -43,19 +35,10 @@ filterTests(["all"], () => { cy.get(".spectrum-Button").contains("Create app").click({ force: true }) }) - // Publish App + // Publish App & Verify it opened cy.wait(2000) // Wait for app to generate - cy.get(".toprightnav").contains("Publish").click({ force: true }) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - }) - - // Verify Published app - cy.wait(2000) // Wait for App to publish and modal to appear - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("View App").click({ force: true }) - cy.window().its('open').should('be.calledOnce') - }) + cy.publishApp(true) + cy.window().its('open').should('be.calledOnce') }) it("should add active/inactive vacancies", () => { diff --git a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js index baebeef60a..84cbc5707e 100644 --- a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter IT Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js b/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js new file mode 100644 index 0000000000..15628ab131 --- /dev/null +++ b/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js @@ -0,0 +1,72 @@ +import filterTests from "../../../support/filterTests" + +filterTests(["all"], () => { + context("IT Ticketing System Template Functionality", () => { + const templateName = "IT Ticketing System" + const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') + + before(() => { + cy.login() + cy.deleteApp(templateName) + // Template navigation + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { + onBeforeLoad(win) { + cy.stub(win, 'open') + } + }) + cy.wait(2000) + }) + + it("should create and publish app with IT Ticketing System template", () => { + // Select IT Ticketing System template + cy.get(".template-thumbnail-text") + .contains(templateName).parentsUntil(".template-grid").within(() => { + cy.get(".spectrum-Button").contains("Use template").click({ force: true }) + }) + + // Confirm URL matches template name + const appUrl = cy.get(".app-server") + appUrl.invoke('text').then(appUrlText => { + expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) + }) + + // Create App + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Button").contains("Create app").click({ force: true }) + }) + + // Publish App & Verify it opened + cy.wait(2000) // Wait for app to generate + cy.publishApp(true) + cy.window().its('open').should('be.calledOnce') + }) + + xit("should filter tickets by status", () => { + // Visit published app + cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) + cy.wait(1000) + + // Tickets section + cy.get(".links").contains("Tickets").click({ force: true }) + cy.wait(1000) + + // Filter by stage - Confirm table updates + cy.get(".spectrum-Picker").contains("Filter by status").click({ force: true }) + cy.get(".spectrum-Menu").find('li').its('length').then(len => { + for (let i = 1; i < len; i++) { + cy.get(".spectrum-Menu-item").eq(i).click() + const stage = cy.get(".spectrum-Picker-label") + stage.invoke('text').then(stageText => { + if (stageText == "In progress" || stageText == "On hold" || stageText == "Triaged") { + cy.get(".placeholder").should('contain', 'No rows found') + } + else { + cy.get(".spectrum-Table-row").should('contain', stageText) + } + cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) + }) + } + }) + }) + }) +}) diff --git a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js index 8c76a01126..2fa57b2c89 100644 --- a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Admin Panels Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js index 8fa0d61390..322a17f6c2 100644 --- a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Approval Apps Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js index 643c854168..734fb9a968 100644 --- a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js @@ -7,14 +7,8 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) - + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) + // Filter Business Apps Templates cy.get(".template-category-filters").within(() => { cy.get('[data-cy="Business Apps"]').click() diff --git a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js index d763dab9e7..dc874fcbaf 100644 --- a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Directories Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js index a8cb30da85..3206a71f6e 100644 --- a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Forms Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js index 02b8c7694e..b46bb46274 100644 --- a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Healthcare Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js index 16715bb5d6..57485aee40 100644 --- a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Legal Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js index a56cc80a67..e5d5745e4e 100644 --- a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Logistics Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js index b2809d78ee..30019c87fd 100644 --- a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Manufacturing Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js b/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js new file mode 100644 index 0000000000..9f08b36d56 --- /dev/null +++ b/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js @@ -0,0 +1,44 @@ +import filterTests from "../../../support/filterTests" + +filterTests(["all"], () => { + context("Lead Generation Form Template Functionality", () => { + const templateName = "Lead Generation Form" + const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') + + before(() => { + cy.login() + cy.deleteApp(templateName) + // Template navigation + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { + onBeforeLoad(win) { + cy.stub(win, 'open') + } + }) + cy.wait(2000) + }) + + it("should create and publish app with Lead Generation Form template", () => { + // Select Lead Generation Form template + cy.get(".template-thumbnail-text") + .contains(templateName).parentsUntil(".template-grid").within(() => { + cy.get(".spectrum-Button").contains("Use template").click({ force: true }) + }) + + // Confirm URL matches template name + const appUrl = cy.get(".app-server") + appUrl.invoke('text').then(appUrlText => { + expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) + }) + + // Create App + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Button").contains("Create app").click({ force: true }) + }) + + // Publish App & Verify it opened + cy.wait(2000) // Wait for app to generate + cy.publishApp(true) + cy.window().its('open').should('be.calledOnce') + }) + }) +}) diff --git a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js index 2c8840f321..66875e6939 100644 --- a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Marketing Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js index 2665de9e81..1a2ee1703a 100644 --- a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Operations Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js index 5e26948180..e81e12318d 100644 --- a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js @@ -7,21 +7,15 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) + + // Filter Portal Templates + cy.get(".template-category-filters").within(() => { + cy.get('[data-cy="Portal"]').click() + }) }) it("should verify the details option for Portal templates", () => { - // Filter Portal Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Portal"]').click() - }) - cy.get(".template-grid").find(".template-card").its('length') .then(len => { for (let i = 0; i < len; i++) { diff --git a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js index e8ed18a19c..1267d8bd5c 100644 --- a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js +++ b/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js @@ -7,13 +7,7 @@ filterTests(["all"], () => { cy.login() // Template navigation - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(".spectrum-Button").contains("Templates").click({force: true}) - } - }) + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) // Filter Professional Services Templates cy.get(".template-category-filters").within(() => { diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js index e19c931ed9..d10990573a 100644 --- a/packages/builder/cypress/setup.js +++ b/packages/builder/cypress/setup.js @@ -7,7 +7,6 @@ const tmpdir = path.join(require("os").tmpdir(), ".budibase") const SERVER_PORT = cypressConfig.env.PORT const WORKER_PORT = cypressConfig.env.WORKER_PORT -process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE" process.env.NODE_ENV = "cypress" process.env.ENABLE_ANALYTICS = "false" process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 29de1debc5..d50364fd54 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -44,7 +44,72 @@ Cypress.Commands.add("logOut", () => { Cypress.Commands.add("closeModal", () => { cy.get(".spectrum-Modal").within(() => { cy.get(".close-icon").click() - cy.wait(500) + cy.wait(1000) // Wait for modal to close + }) +}) + +Cypress.Commands.add("importApp", (exportFilePath, name) => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length > 0) { + cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.wait(500) + } + cy.get(`[data-cy="import-app-btn"]`).click({ force: true }) + }) + + cy.get(".spectrum-Modal").within(() => { + cy.get("input").eq(1).should("have.focus") + + cy.get(".spectrum-Dropzone").selectFile(exportFilePath, { + action: "drag-drop", + }) + + cy.get(".gallery .filename").contains("exported-app.txt") + + if (name && name != "") { + cy.get("input").eq(0).type(name).should("have.value", name).blur() + } + cy.get(".confirm-wrap button") + .should("not.be.disabled") + .click({ force: true }) + cy.wait(5000) + }) +}) + +Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { + cy.get(".user-dropdown .avatar > .icon").click({ force: true }) + + cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { + cy.get("li[data-cy='user-info']").click({ force: true }) + }) + + cy.get(".spectrum-Modal.is-open").within(() => { + cy.get("[data-cy='user-first-name']").clear() + + if (!firstName || firstName == "") { + cy.get("[data-cy='user-first-name']").invoke("val").should("be.empty") + } else { + cy.get("[data-cy='user-first-name']") + .type(firstName) + .should("have.value", firstName) + .blur() + } + + cy.get("[data-cy='user-last-name']").clear() + + if (!lastName || lastName == "") { + cy.get("[data-cy='user-last-name']").invoke("val").should("be.empty") + } else { + cy.get("[data-cy='user-last-name']") + .type(lastName) + .should("have.value", lastName) + .blur() + } + cy.get("button").contains("Update information").click({ force: true }) }) }) @@ -66,7 +131,11 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { }) cy.get(".spectrum-Modal").within(() => { - cy.get("input").eq(0).type(name).should("have.value", name).blur() + cy.get("input").eq(0).should("have.focus") + if (name && name != "") { + cy.get("input").eq(0).clear() + cy.get("input").eq(0).type(name).should("have.value", name).blur() + } cy.get(".spectrum-ButtonGroup") .contains("Create app") .click({ force: true }) @@ -229,6 +298,29 @@ Cypress.Commands.add("unlockApp", unlock_config => { }) }) +Cypress.Commands.add("publishApp", resolvedAppPath => { + //Assumes you have navigated to an application first + cy.get(".toprightnav button.spectrum-Button") + .contains("Publish") + .click({ force: true }) + + cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") + .should("be.visible") + .within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.wait(1000) + }) + + //Verify that the app url is presented correctly to the user + cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']") + .should("be.visible") + .within(() => { + let appUrl = Cypress.config().baseUrl + "/app/" + resolvedAppPath + cy.get("[data-cy='deployed-app-url'] input").should("have.value", appUrl) + cy.get(".spectrum-Button").contains("Done").click({ force: true }) + }) +}) + Cypress.Commands.add("createTestApp", () => { const appName = "Cypress Tests" cy.deleteApp(appName) @@ -242,6 +334,21 @@ Cypress.Commands.add("createTestTableWithData", () => { cy.addColumn("dog", "age", "Number") }) +Cypress.Commands.add("publishApp", (viewApp = false) => { + cy.get(".toprightnav").contains("Publish").click({ force: true }) + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + }) + cy.wait(2000) // Wait for App to publish and modal to appear + cy.get(".spectrum-Dialog-grid").within(() => { + if (viewApp) { + cy.get(".spectrum-Button").contains("View App").click({ force: true }) + } else { + cy.get(".spectrum-Button").contains("Done").click({ force: true }) + } + }) +}) + Cypress.Commands.add("createTable", (tableName, initialTable) => { if (!initialTable) { cy.navigateToDataSection() @@ -544,7 +651,12 @@ Cypress.Commands.add("createAppFromScratch", appName => { .contains("Start from scratch") .click({ force: true }) cy.get(".spectrum-Modal").within(() => { - cy.get("input").eq(0).type(appName).should("have.value", appName).blur() + cy.get("input") + .eq(0) + .clear() + .type(appName) + .should("have.value", appName) + .blur() cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.wait(10000) }) @@ -658,12 +770,14 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { .click({ force: true }) }) } else { + cy.intercept("**/tables").as("datasourceTables") cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button") .contains("Save and fetch tables") .click({ force: true }) - cy.wait(1000) }) + // Wait for tables to be fetched + cy.wait("@datasourceTables", { timeout: 60000 }) } }) diff --git a/packages/builder/cypress/support/interact.js b/packages/builder/cypress/support/interact.js new file mode 100644 index 0000000000..11794d940d --- /dev/null +++ b/packages/builder/cypress/support/interact.js @@ -0,0 +1,26 @@ +// createApp test +export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]' +export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters" +export const TEMPLATE_CATEGORY = ".template-categories" +export const APP_TABLE = ".appTable" +export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button" +export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category" +export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON = + ".template-category-filters .spectrum-ActionButton" +export const SPECTRUM_MODAL = ".spectrum-Modal" +export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data; +export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup" +export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input" + +//AddMultiOptionDatatype test +export const CATEGORY_DATA = '[data-cy="category-Data"]' +export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]' +export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]' +export const DROPDOWN = ".dropdown" +export const SPECTRUM_Picker_LABEL = ".spectrum-Picker-label" +export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]' +export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control' + +//AddRadioButtons +export const SPECTRUM_POPOVER = ".spectrum-Popover" +export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control' diff --git a/packages/builder/package.json b/packages/builder/package.json index d6aa438196..fa6cd5c10f 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.159-alpha.1", + "version": "1.0.185-alpha.7", "license": "GPL-3.0", "private": true, "scripts": { @@ -13,11 +13,13 @@ "cy:setup:ci": "node ./cypress/setup.js", "cy:open": "cypress open", "cy:run": "cypress run", - "cy:run:ci": "xvfb-run cypress run --headed --browser chrome", + "cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js", "cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record", "cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run", "cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci", - "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record", + "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report", + "cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline", + "cy:ci:notify": "node scripts/cypressResultsWebhook", "cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open", "cy:debug:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:open" }, @@ -67,10 +69,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.159-alpha.1", - "@budibase/client": "^1.0.159-alpha.1", - "@budibase/frontend-core": "^1.0.159-alpha.1", - "@budibase/string-templates": "^1.0.159-alpha.1", + "@budibase/bbui": "^1.0.185-alpha.7", + "@budibase/client": "^1.0.185-alpha.7", + "@budibase/frontend-core": "^1.0.185-alpha.7", + "@budibase/string-templates": "^1.0.185-alpha.7", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", @@ -99,9 +101,13 @@ "@testing-library/svelte": "^3.0.0", "babel-jest": "^26.6.3", "cypress": "^9.3.1", + "cypress-multi-reporters": "^1.6.0", "cypress-terminal-report": "^1.4.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", + "mochawesome": "^7.1.3", + "mochawesome-merge": "^4.2.1", + "mochawesome-report-generator": "^6.2.0", "ncp": "^2.0.0", "rimraf": "^3.0.2", "rollup": "^2.44.0", diff --git a/packages/builder/reporterConfig.json b/packages/builder/reporterConfig.json new file mode 100644 index 0000000000..2c2ef7c138 --- /dev/null +++ b/packages/builder/reporterConfig.json @@ -0,0 +1,10 @@ +{ + "reporterEnabled": "mochawesome", + "mochawesomeReporterOptions": { + "reportDir": "cypress/reports", + "quiet": true, + "overwrite": false, + "html": false, + "json": true + } +} \ No newline at end of file diff --git a/packages/builder/scripts/cypressResultsWebhook.js b/packages/builder/scripts/cypressResultsWebhook.js new file mode 100644 index 0000000000..457093e013 --- /dev/null +++ b/packages/builder/scripts/cypressResultsWebhook.js @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +const fetch = require("node-fetch") +const path = require("path") +const fs = require("fs") + +const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL +const OUTCOME = process.env.CYPRESS_OUTCOME +const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL +const GIT_SHA = process.env.GITHUB_SHA +const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL + +async function generateReport() { + // read the report file + const REPORT_PATH = path.resolve( + __dirname, + "..", + "cypress", + "reports", + "testReport.json" + ) + const report = fs.readFileSync(REPORT_PATH, "utf-8") + return JSON.parse(report) +} + +async function discordCypressResultsNotification(report) { + const { + suites, + tests, + passes, + pending, + failures, + duration, + passPercent, + skipped, + } = report.stats + + const options = { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + content: `**Nightly Tests Status**: ${OUTCOME}`, + embeds: [ + { + title: "Budi QA Bot", + description: `Nightly Tests`, + url: GITHUB_ACTIONS_RUN_URL, + color: OUTCOME === "success" ? 3066993 : 15548997, + timestamp: new Date(), + footer: { + icon_url: "http://bbui.budibase.com/budibase-logo.png", + text: "Budibase QA Bot", + }, + thumbnail: { + url: "http://bbui.budibase.com/budibase-logo.png", + }, + author: { + name: "Budibase QA Bot", + url: "https://discordapp.com", + icon_url: "http://bbui.budibase.com/budibase-logo.png", + }, + fields: [ + { + name: "Commit", + value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`, + }, + { + name: "Cypress Dashboard URL", + value: DASHBOARD_URL || "None Supplied", + }, + { + name: "Github Actions Run URL", + value: GITHUB_ACTIONS_RUN_URL || "None Supplied", + }, + { + name: "Test Suites", + value: suites, + }, + { + name: "Tests", + value: tests, + }, + { + name: "Passed", + value: passes, + }, + { + name: "Pending", + value: pending, + }, + { + name: "Skipped", + value: skipped, + }, + { + name: "Failures", + value: failures, + }, + { + name: "Duration", + value: `${duration / 1000} Seconds`, + }, + { + name: "Pass Percentage", + value: Math.floor(passPercent), + }, + ], + }, + ], + }), + } + const response = await fetch(WEBHOOK_URL, options) + + if (response.status >= 400) { + const text = await response.text() + console.error( + `Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` + ) + } +} + +async function run() { + const report = await generateReport() + await discordCypressResultsNotification(report) +} + +run() diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 9e759366d8..5a2019b5c1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -116,9 +116,7 @@ const automationActions = store => ({ }, select: automation => { store.update(state => { - let testResults = state.selectedAutomation?.testResults state.selectedAutomation = new Automation(cloneDeep(automation)) - state.selectedAutomation.testResults = testResults state.selectedBlock = null return state }) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 505a0b9aca..3e58b25ff6 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -14,7 +14,6 @@ } from "@budibase/bbui" export let automation - let testDataModal let blocks let confirmDeleteDialog @@ -41,66 +40,70 @@
-
-
-
- {automation.name} -
-
-
- -
-
+
+ {automation.name} +
+
+
+
+
+ +
+ { + testDataModal.show() + }} + icon="MultipleCheck" + size="M">Run test +
{ - testDataModal.show() + $automationStore.selectedAutomation.automation.showTestPanel = true }} - icon="MultipleCheck" - size="M">Run testTest Details
- {#each blocks as block, idx (block.id)} -
- {#if block.stepId !== "LOOP"} - - {/if} -
- {/each}
- - Are you sure you wish to delete the automation - {automation.name}? - This action cannot be undone. - - - - -
+
+ {#each blocks as block, idx (block.id)} +
+ {#if block.stepId !== "LOOP"} + + {/if} +
+ {/each} +
+ + Are you sure you wish to delete the automation + {automation.name}? + This action cannot be undone. + + + + + diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index ffd59b4e6a..fecd0fcc7e 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -51,6 +51,7 @@ $automationStore.selectedAutomation?.automation, testData ) + $automationStore.selectedAutomation.automation.showTestPanel = true } catch (error) { notifications.error("Error testing notification") } diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte new file mode 100644 index 0000000000..8c38d8a689 --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte @@ -0,0 +1,146 @@ + + +
+
+ +
Test Details
+
+
+ { + $automationStore.selectedAutomation.automation.showTestPanel = false + }} + hoverable + name="Close" + /> +
+
+ + + +
+ {#each blocks as block, idx} +
+ {#if block.stepId !== "LOOP"} + + {#if showParameters && showParameters[block.id]} + + {#if testResults?.[idx]?.outputs.iterations} +
+ +
+ +
+
+ {/if} + +
+ + +
+