Merge branch 'develop' into api-tests-user-management

This commit is contained in:
Pedro Silva 2022-12-14 21:51:39 +00:00
commit e431533b5d
73 changed files with 1571 additions and 3571 deletions

2
.gitignore vendored
View File

@ -106,3 +106,5 @@ stats.html
*.tsbuildinfo
budibase-component
budibase-datasource
*.iml

View File

@ -8,7 +8,7 @@
"editor.defaultFormatter": "vscode.json-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"debug.javascript.terminalOptions": {
"skipFiles": [
@ -16,4 +16,7 @@
"<node_internals>/**"
]
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
}

View File

@ -1,5 +1,5 @@
{
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/types": "2.1.46-alpha.1",
"@budibase/types": "2.1.46-alpha.11",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",

View File

@ -0,0 +1 @@
export * from "./src/plugin"

View File

@ -1,9 +1,13 @@
function isTest() {
return (
process.env.NODE_ENV === "jest" ||
process.env.NODE_ENV === "cypress" ||
process.env.JEST_WORKER_ID != null
)
return isCypress() || isJest()
}
function isJest() {
return !!(process.env.NODE_ENV === "jest" || process.env.JEST_WORKER_ID)
}
function isCypress() {
return process.env.NODE_ENV === "cypress"
}
function isDev() {
@ -27,6 +31,7 @@ const DefaultBucketName = {
const environment = {
isTest,
isJest,
isDev,
JS_BCRYPT: process.env.JS_BCRYPT,
JWT_SECRET: process.env.JWT_SECRET,
@ -66,7 +71,6 @@ const environment = {
PLUGIN_BUCKET_NAME:
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
USE_COUCH: process.env.USE_COUCH || true,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase",
LOG_LEVEL: process.env.LOG_LEVEL,

View File

@ -117,3 +117,7 @@ jest.spyOn(events.view, "filterDeleted")
jest.spyOn(events.view, "calculationCreated")
jest.spyOn(events.view, "calculationUpdated")
jest.spyOn(events.view, "calculationDeleted")
jest.spyOn(events.plugin, "init")
jest.spyOn(events.plugin, "imported")
jest.spyOn(events.plugin, "deleted")

View File

@ -2,4 +2,5 @@ import "./posthog"
import "./events"
export * as accounts from "./accounts"
export * as date from "./date"
export * as licenses from "./licenses"
export { default as fetch } from "./fetch"

View File

@ -0,0 +1,83 @@
import { Feature, License, Quotas } from "@budibase/types"
import _ from "lodash"
let CLOUD_FREE_LICENSE: License
let UNLIMITED_LICENSE: License
let getCachedLicense: any
// init for the packages other than pro
export function init(proPkg: any) {
initInternal({
CLOUD_FREE_LICENSE: proPkg.constants.licenses.CLOUD_FREE_LICENSE,
UNLIMITED_LICENSE: proPkg.constants.licenses.UNLIMITED_LICENSE,
getCachedLicense: proPkg.licensing.cache.getCachedLicense,
})
}
// init for the pro package
export function initInternal(opts: {
CLOUD_FREE_LICENSE: License
UNLIMITED_LICENSE: License
getCachedLicense: any
}) {
CLOUD_FREE_LICENSE = opts.CLOUD_FREE_LICENSE
UNLIMITED_LICENSE = opts.UNLIMITED_LICENSE
getCachedLicense = opts.getCachedLicense
}
export interface UseLicenseOpts {
features?: Feature[]
quotas?: Quotas
}
// LICENSES
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
if (opts) {
if (opts.features) {
license.features.push(...opts.features)
}
if (opts.quotas) {
license.quotas = opts.quotas
}
}
getCachedLicense.mockReturnValue(license)
return license
}
export const useUnlimited = (opts?: UseLicenseOpts) => {
return useLicense(UNLIMITED_LICENSE, opts)
}
export const useCloudFree = () => {
return useLicense(CLOUD_FREE_LICENSE)
}
// FEATURES
const useFeature = (feature: Feature) => {
const license = _.cloneDeep(UNLIMITED_LICENSE)
const opts: UseLicenseOpts = {
features: [feature],
}
return useLicense(license, opts)
}
export const useBackups = () => {
return useFeature(Feature.APP_BACKUPS)
}
export const useGroups = () => {
return useFeature(Feature.USER_GROUPS)
}
// QUOTAS
export const setAutomationLogsQuota = (value: number) => {
const license = _.cloneDeep(UNLIMITED_LICENSE)
license.quotas.constant.automationLogRetentionDays.value = value
return useLicense(license)
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -37,35 +37,35 @@
"dist"
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "2.1.46-alpha.1",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",
"@spectrum-css/button": "^3.0.1",
"@spectrum-css/buttongroup": "^3.0.2",
"@spectrum-css/checkbox": "^3.0.2",
"@spectrum-css/dialog": "^3.0.1",
"@spectrum-css/divider": "^1.0.3",
"@spectrum-css/dropzone": "^3.0.2",
"@spectrum-css/fieldgroup": "^3.0.2",
"@spectrum-css/fieldlabel": "^3.0.1",
"@spectrum-css/icon": "^3.0.1",
"@spectrum-css/illustratedmessage": "^3.0.2",
"@spectrum-css/inlinealert": "^2.0.1",
"@spectrum-css/inputgroup": "^3.0.2",
"@spectrum-css/label": "^2.0.10",
"@spectrum-css/link": "^3.1.1",
"@spectrum-css/menu": "^3.0.1",
"@spectrum-css/modal": "^3.0.1",
"@spectrum-css/pagination": "^3.0.3",
"@spectrum-css/picker": "^1.0.1",
"@spectrum-css/popover": "^3.0.1",
"@spectrum-css/progressbar": "^1.0.2",
"@spectrum-css/progresscircle": "^1.0.2",
"@spectrum-css/radio": "^3.0.2",
"@spectrum-css/search": "^3.0.2",
"@spectrum-css/sidenav": "^3.0.2",
"@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/string-templates": "2.1.46-alpha.11",
"@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1",
"@spectrum-css/avatar": "3.0.2",
"@spectrum-css/button": "3.0.1",
"@spectrum-css/buttongroup": "3.0.2",
"@spectrum-css/checkbox": "3.0.2",
"@spectrum-css/dialog": "3.0.1",
"@spectrum-css/divider": "1.0.3",
"@spectrum-css/dropzone": "3.0.2",
"@spectrum-css/fieldgroup": "3.0.2",
"@spectrum-css/fieldlabel": "3.0.1",
"@spectrum-css/icon": "3.0.1",
"@spectrum-css/illustratedmessage": "3.0.2",
"@spectrum-css/inlinealert": "2.0.1",
"@spectrum-css/inputgroup": "3.0.2",
"@spectrum-css/label": "2.0.10",
"@spectrum-css/link": "3.1.1",
"@spectrum-css/menu": "3.0.1",
"@spectrum-css/modal": "3.0.1",
"@spectrum-css/pagination": "3.0.3",
"@spectrum-css/picker": "1.0.1",
"@spectrum-css/popover": "3.0.1",
"@spectrum-css/progressbar": "1.0.2",
"@spectrum-css/progresscircle": "1.0.2",
"@spectrum-css/radio": "3.0.2",
"@spectrum-css/search": "3.0.2",
"@spectrum-css/sidenav": "3.0.2",
"@spectrum-css/slider": "3.0.1",
"@spectrum-css/statuslight": "3.0.2",
"@spectrum-css/stepper": "3.0.3",

View File

@ -2,7 +2,7 @@
# yarn lockfile v1
"@adobe/spectrum-css-workflow-icons@^1.2.1":
"@adobe/spectrum-css-workflow-icons@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.2.1.tgz#7e2cb3fcfb5c8b12d7275afafbb6ec44913551b4"
integrity sha512-uVgekyBXnOVkxp+CUssjN/gefARtudZC8duEn1vm0lBQFwGRZFlDEzU1QC+aIRWCrD1Z8OgRpmBYlSZ7QS003w==

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -71,10 +71,10 @@
}
},
"dependencies": {
"@budibase/bbui": "2.1.46-alpha.1",
"@budibase/client": "2.1.46-alpha.1",
"@budibase/frontend-core": "2.1.46-alpha.1",
"@budibase/string-templates": "2.1.46-alpha.1",
"@budibase/bbui": "2.1.46-alpha.11",
"@budibase/client": "2.1.46-alpha.11",
"@budibase/frontend-core": "2.1.46-alpha.11",
"@budibase/string-templates": "2.1.46-alpha.11",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.1.46-alpha.1",
"@budibase/string-templates": "2.1.46-alpha.1",
"@budibase/types": "2.1.46-alpha.1",
"@budibase/backend-core": "2.1.46-alpha.11",
"@budibase/string-templates": "2.1.46-alpha.11",
"@budibase/types": "2.1.46-alpha.11",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "2.1.46-alpha.1",
"@budibase/frontend-core": "2.1.46-alpha.1",
"@budibase/string-templates": "2.1.46-alpha.1",
"@budibase/bbui": "2.1.46-alpha.11",
"@budibase/frontend-core": "2.1.46-alpha.11",
"@budibase/string-templates": "2.1.46-alpha.11",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.1.46-alpha.1",
"@budibase/bbui": "2.1.46-alpha.11",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View File

@ -1,3 +1,4 @@
import fs from "fs"
module FetchMock {
const fetch = jest.requireActual("node-fetch")
let failCount = 0
@ -92,6 +93,83 @@ module FetchMock {
value:
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en-GB"></html>',
})
} else if (
url === "https://api.github.com/repos/my-repo/budibase-comment-box"
) {
return Promise.resolve({
json: () => {
return {
name: "budibase-comment-box",
releases_url:
"https://api.github.com/repos/my-repo/budibase-comment-box{/id}",
}
},
})
} else if (
url === "https://api.github.com/repos/my-repo/budibase-comment-box/latest"
) {
return Promise.resolve({
json: () => {
return {
assets: [
{
content_type: "application/gzip",
browser_download_url:
"https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz",
},
],
}
},
})
} else if (
url ===
"https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz"
) {
return Promise.resolve({
body: fs.createReadStream(
"src/api/routes/tests/data/comment-box-1.0.2.tar.gz"
),
ok: true,
})
} else if (url === "https://www.npmjs.com/package/budibase-component") {
return Promise.resolve({
status: 200,
json: () => {
return {
name: "budibase-component",
"dist-tags": {
latest: "1.0.0",
},
versions: {
"1.0.0": {
dist: {
tarball:
"https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz",
},
},
},
}
},
})
} else if (
url ===
"https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz"
) {
return Promise.resolve({
body: fs.createReadStream(
"src/api/routes/tests/data/budibase-component-1.0.2.tgz"
),
ok: true,
})
} else if (
url === "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz"
) {
return Promise.resolve({
body: fs.createReadStream(
"src/api/routes/tests/data/comment-box-1.0.2.tar.gz"
),
ok: true,
})
} else if (url.includes("failonce.com")) {
failCount++
if (failCount === 1) {

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -43,11 +43,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.1.46-alpha.1",
"@budibase/client": "2.1.46-alpha.1",
"@budibase/pro": "2.1.46-alpha.0",
"@budibase/string-templates": "2.1.46-alpha.1",
"@budibase/types": "2.1.46-alpha.1",
"@budibase/backend-core": "2.1.46-alpha.11",
"@budibase/client": "2.1.46-alpha.11",
"@budibase/pro": "2.1.46-alpha.11",
"@budibase/string-templates": "2.1.46-alpha.11",
"@budibase/types": "2.1.46-alpha.11",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View File

@ -12,13 +12,11 @@ jest.mock("../../../utilities/redis", () => ({
shutdown: jest.fn(),
}))
const {
clearAllApps,
checkBuilderEndpoint,
} = require("./utilities/TestFunctions")
const setup = require("./utilities")
const { AppStatus } = require("../../../db/utils")
const { events } = require("@budibase/backend-core")
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils"
import { events } from "@budibase/backend-core"
import env from "../../../environment"
describe("/applications", () => {
let request = setup.getRequest()
@ -234,4 +232,39 @@ describe("/applications", () => {
expect(getRes.body.application.updatedAt).toBeDefined()
})
})
describe("sync", () => {
it("app should sync correctly", async () => {
const res = await request
.post(`/api/applications/${config.getAppId()}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual("App sync completed successfully.")
})
it("app should not sync if production", async () => {
const res = await request
.post(`/api/applications/app_123456/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(400)
expect(res.body.message).toEqual(
"This action cannot be performed for production apps"
)
})
it("app should not sync if sync is disabled", async () => {
env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
const res = await request
.post(`/api/applications/${config.getAppId()}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual(
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
)
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
})
})
})

View File

@ -8,10 +8,10 @@ jest.mock("@budibase/backend-core", () => {
}
})
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities")
const { events } = require("@budibase/backend-core")
import * as setup from "./utilities"
import { events } from "@budibase/backend-core"
import sdk from "../../../sdk"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
describe("/backups", () => {
let request = setup.getRequest()
let config = setup.getConfig()
@ -30,7 +30,7 @@ describe("/backups", () => {
.expect(200)
expect(res.text).toBeDefined()
expect(res.headers["content-type"]).toEqual("application/gzip")
expect(events.app.exported.mock.calls.length).toBe(1)
expect(events.app.exported).toBeCalledTimes(1)
})
it("should apply authorization to endpoint", async () => {
@ -41,4 +41,15 @@ describe("/backups", () => {
})
})
})
})
describe("calculateBackupStats", () => {
it("should be able to calculate the backup statistics", async () => {
config.createAutomation()
config.createScreen()
let res = await sdk.backups.calculateBackupStats(config.getAppId())
expect(res.automations).toEqual(1)
expect(res.datasources).toEqual(1)
expect(res.screens).toEqual(1)
})
})
})

View File

@ -0,0 +1,66 @@
import { db as dbCore } from "@budibase/backend-core"
import { AppStatus } from "../../../db/utils"
import * as setup from "./utilities"
describe("/cloud", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeEach(async () => {
await config.init()
})
afterEach(async () => {
// clear all mocks
jest.clearAllMocks()
})
describe("import", () => {
it("should be able to import apps", async () => {
// first we need to delete any existing apps on the system so it looks clean otherwise the
// import will not run
await request
.delete(
`/api/applications/${dbCore.getProdAppID(
config.getAppId()
)}?unpublish=true`
)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await request
.delete(`/api/applications/${config.getAppId()}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// get a count of apps before the import
const preImportApps = await request
.get(`/api/applications?status=${AppStatus.ALL}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// Perform the import
const res = await request
.post(`/api/cloud/import`)
.attach("importFile", "src/api/routes/tests/data/export-test.tar.gz")
.set(config.defaultHeaders())
.expect(200)
expect(res.body.message).toEqual("Apps successfully imported.")
// get a count of apps after the import
const postImportApps = await request
.get(`/api/applications?status=${AppStatus.ALL}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// There are two apps in the file that was imported so check for this
expect(postImportApps.body.length).toEqual(2)
})
})
})

View File

@ -1,16 +1,16 @@
jest.mock("pg")
import * as setup from "./utilities"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import { checkCacheForDynamicVariable } from "../../../threads/utils"
import { events } from "@budibase/backend-core"
let setup = require("./utilities")
let { basicDatasource } = setup.structures
let { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const pg = require("pg")
const { checkCacheForDynamicVariable } = require("../../../threads/utils")
const { events } = require("@budibase/backend-core")
describe("/datasources", () => {
let request = setup.getRequest()
let config = setup.getConfig()
let datasource
let datasource: any
afterAll(setup.afterAll)
@ -26,7 +26,7 @@ describe("/datasources", () => {
.post(`/api/datasources`)
.send(basicDatasource())
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.datasource.name).toEqual("Test")
@ -42,7 +42,7 @@ describe("/datasources", () => {
.put(`/api/datasources/${datasource._id}`)
.send(datasource)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.datasource.name).toEqual("Updated Test")
@ -51,25 +51,34 @@ describe("/datasources", () => {
})
describe("dynamic variables", () => {
async function preview(datasource, fields) {
async function preview(
datasource: any,
fields: { path: string; queryString: string }
) {
return config.previewQuery(request, config, datasource, fields)
}
it("should invalidate changed or removed variables", async () => {
const { datasource, query } = await config.dynamicVariableDatasource()
// preview once to cache variables
await preview(datasource, { path: "www.test.com", queryString: "test={{ variable3 }}" })
await preview(datasource, {
path: "www.test.com",
queryString: "test={{ variable3 }}",
})
// check variables in cache
let contents = await checkCacheForDynamicVariable(query._id, "variable3")
let contents = await checkCacheForDynamicVariable(
query._id,
"variable3"
)
expect(contents.rows.length).toEqual(1)
// update the datasource to remove the variables
datasource.config.dynamicVariables = []
const res = await request
.put(`/api/datasources/${datasource._id}`)
.send(datasource)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.errors).toBeUndefined()
@ -85,7 +94,7 @@ describe("/datasources", () => {
const res = await request
.get(`/api/datasources`)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
const datasources = res.body
@ -160,7 +169,7 @@ describe("/datasources", () => {
const res = await request
.get(`/api/datasources`)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toEqual(1)
@ -174,6 +183,5 @@ describe("/datasources", () => {
url: `/api/datasources/${datasource._id}/${datasource._rev}`,
})
})
})
})

View File

@ -0,0 +1,179 @@
let mockObjectStore = jest.fn().mockImplementation(() => {
return [{ name: "test.js" }]
})
let deleteFolder = jest.fn().mockImplementation()
jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core")
return {
...core,
objectStore: {
...core.objectStore,
upload: jest.fn(),
uploadDirectory: mockObjectStore,
deleteFolder: deleteFolder,
},
}
})
import { events } from "@budibase/backend-core"
import * as setup from "./utilities"
describe("/plugins", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeEach(async () => {
await config.init()
jest.clearAllMocks()
})
const createPlugin = async (status?: number) => {
return request
.post(`/api/plugin/upload`)
.attach("file", "src/api/routes/tests/data/comment-box-1.0.2.tar.gz")
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
const getPlugins = async (status?: number) => {
return request
.get(`/api/plugin`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
describe("upload", () => {
it("should be able to upload a plugin", async () => {
let res = await createPlugin()
expect(res.body).toBeDefined()
expect(res.body.plugins).toBeDefined()
expect(res.body.plugins[0]._id).toEqual("plg_comment-box")
expect(events.plugin.imported).toHaveBeenCalledTimes(1)
})
it("should not be able to create a plugin if there is an error", async () => {
mockObjectStore.mockImplementationOnce(() => {
throw new Error()
})
let res = await createPlugin(400)
expect(res.body.message).toEqual("Failed to import plugin: Error")
expect(events.plugin.imported).toHaveBeenCalledTimes(0)
})
})
describe("fetch", () => {
it("should be able to fetch plugins", async () => {
await createPlugin()
const res = await getPlugins()
expect(res.body).toBeDefined()
expect(res.body[0]._id).toEqual("plg_comment-box")
})
})
describe("destroy", () => {
it("should be able to delete a plugin", async () => {
await createPlugin()
const res = await request
.delete(`/api/plugin/plg_comment-box`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body).toBeDefined()
expect(res.body.message).toEqual("Plugin plg_comment-box deleted.")
const plugins = await getPlugins()
expect(plugins.body).toBeDefined()
expect(plugins.body.length).toEqual(0)
expect(events.plugin.deleted).toHaveBeenCalledTimes(1)
})
it("should handle an error deleting a plugin", async () => {
deleteFolder.mockImplementationOnce(() => {
throw new Error()
})
await createPlugin()
const res = await request
.delete(`/api/plugin/plg_comment-box`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(400)
expect(res.body.message).toEqual("Failed to delete plugin: Error")
expect(events.plugin.deleted).toHaveBeenCalledTimes(0)
const plugins = await getPlugins()
expect(plugins.body).toBeDefined()
expect(plugins.body.length).toEqual(1)
})
})
describe("github", () => {
const createGithubPlugin = async (status?: number, url?: string) => {
return await request
.post(`/api/plugin`)
.send({
source: "Github",
url,
githubToken: "token",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(status ? status : 200)
}
it("should be able to create a plugin from github", async () => {
const res = await createGithubPlugin(
200,
"https://github.com/my-repo/budibase-comment-box.git"
)
expect(res.body).toBeDefined()
expect(res.body.plugin).toBeDefined()
expect(res.body.plugin._id).toEqual("plg_comment-box")
})
it("should fail if the url is not from github", async () => {
const res = await createGithubPlugin(
400,
"https://notgithub.com/my-repo/budibase-comment-box"
)
expect(res.body.message).toEqual(
"Failed to import plugin: The plugin origin must be from Github"
)
})
})
describe("npm", () => {
it("should be able to create a plugin from npm", async () => {
const res = await request
.post(`/api/plugin`)
.send({
source: "NPM",
url: "https://www.npmjs.com/package/budibase-component",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body).toBeDefined()
expect(res.body.plugin._id).toEqual("plg_budibase-component")
expect(events.plugin.imported).toHaveBeenCalled()
})
})
describe("url", () => {
it("should be able to create a plugin from a URL", async () => {
const res = await request
.post(`/api/plugin`)
.send({
source: "URL",
url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body).toBeDefined()
expect(res.body.plugin._id).toEqual("plg_comment-box")
expect(events.plugin.imported).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -9,7 +9,6 @@ const route = "/test"
// there are checks which are disabled in test env,
// these checks need to be enabled for this test
describe("/routing", () => {
let request = setup.getRequest()
let config = setup.getConfig()

View File

@ -51,7 +51,7 @@ describe("/tables", () => {
table.dataImport.schema = table.schema
const res = await createTable(table)
expect(events.table.created).toBeCalledTimes(1)
expect(events.table.created).toBeCalledWith(res.body)
expect(events.table.imported).toBeCalledTimes(1)
@ -87,6 +87,12 @@ describe("/tables", () => {
it("updates all the row fields for a table when a schema key is renamed", async () => {
const testTable = await config.createTable()
await config.createView({
name: "TestView",
field: "Price",
calculation: "stats",
tableId: testTable._id,
})
const testRow = await request
.post(`/api/${testTable._id}/rows`)
@ -109,7 +115,7 @@ describe("/tables", () => {
updated: "updatedName"
},
schema: {
updatedName: {type: "string"}
updatedName: { type: "string" }
}
})
.set(config.defaultHeaders())

View File

@ -90,4 +90,90 @@ describe("/users", () => {
expect(res.body.tableId).toBeDefined()
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Must supply a 'flag' field in request body.")
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
describe("getFlags", () => {
it("should get flags for a specific user", async () => {
let flagData = { value: "test", flag: "test" }
await config.createUser()
await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send(flagData)
.expect(200)
.expect("Content-Type", /json/)
const res = await request
.get(`/api/users/flags`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body[flagData.value]).toEqual(flagData.flag)
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Must supply a 'flag' field in request body.")
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
describe("syncUser", () => {
it("should sync the user", async () => {
let user = await config.createUser()
await config.createApp('New App')
let res = await request
.post(`/api/users/metadata/sync/${user._id}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual('User synced.')
})
})
})

View File

@ -63,14 +63,14 @@ export function afterAll() {
export function getRequest() {
if (!request) {
exports.beforeAll()
beforeAll()
}
return request
}
export function getConfig() {
if (!config) {
exports.beforeAll()
beforeAll()
}
return config
}

View File

@ -5,6 +5,7 @@ import {
} from "@budibase/string-templates"
import sdk from "../sdk"
import { Row } from "@budibase/types"
import { LoopStep, LoopStepType, LoopInput } from "../definitions/automations"
/**
* When values are input to the system generally they will be of type string as this is required for template strings.
@ -123,3 +124,26 @@ export function stringSplit(value: string | string[]) {
}
return value
}
export function typecastForLooping(loopStep: LoopStep, input: LoopInput) {
if (!input || !input.binding) {
return null
}
try {
switch (loopStep.inputs.option) {
case LoopStepType.ARRAY:
if (typeof input.binding === "string") {
return JSON.parse(input.binding)
}
break
case LoopStepType.STRING:
if (Array.isArray(input.binding)) {
return input.binding.join(",")
}
break
}
} catch (err) {
throw new Error("Unable to cast to correct type")
}
return input.binding
}

View File

@ -0,0 +1,34 @@
const setup = require("./utilities")
describe("test the bash action", () => {
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
})
afterAll(setup.afterAll)
it("should be able to execute a script", async () => {
let res = await setup.runStep("EXECUTE_BASH",
inputs = {
code: "echo 'test'"
}
)
expect(res.stdout).toEqual("test\n")
expect(res.success).toEqual(true)
})
it("should handle a null value", async () => {
let res = await setup.runStep("EXECUTE_BASH",
inputs = {
code: null
}
)
expect(res.stdout).toEqual("Budibase bash automation failed: Invalid inputs")
})
})

View File

@ -0,0 +1,27 @@
const setup = require("./utilities")
const fetch = require("node-fetch")
jest.mock("node-fetch")
describe("test the outgoing webhook action", () => {
let inputs
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
inputs = {
username: "joe_bloggs",
url: "http://www.test.com",
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.discord.stepId, inputs)
expect(res.response.url).toEqual("http://www.test.com")
expect(res.response.method).toEqual("post")
expect(res.success).toEqual(true)
})
})

View File

@ -0,0 +1,49 @@
const setup = require("./utilities")
describe("test the execute query action", () => {
let datasource
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
await config.createDatasource()
query = await config.createQuery()
})
afterAll(setup.afterAll)
it("should be able to execute a query", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId,
inputs = {
query: { queryId: query._id }
}
)
expect(res.response).toEqual([{ a: 'string', b: 1 }])
expect(res.success).toEqual(true)
})
it("should handle a null query value", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId,
inputs = {
query: null
}
)
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false)
})
it("should handle an error executing a query", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId,
inputs = {
query: { queryId: "wrong_id" }
}
)
expect(res.response).toEqual('{"status":404,"name":"not_found","message":"missing","reason":"missing"}')
expect(res.success).toEqual(false)
})
})

View File

@ -0,0 +1,48 @@
const setup = require("./utilities")
describe("test the execute script action", () => {
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
})
afterAll(setup.afterAll)
it("should be able to execute a script", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId,
inputs = {
code: "return 1 + 1"
}
)
expect(res.value).toEqual(2)
expect(res.success).toEqual(true)
})
it("should handle a null value", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId,
inputs = {
code: null
}
)
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false)
})
it("should be able to handle an error gracefully", async () => {
let res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId,
inputs = {
code: "return something.map(x => x.name)"
}
)
expect(res.response).toEqual("ReferenceError: something is not defined")
expect(res.success).toEqual(false)
})
})

View File

@ -0,0 +1,71 @@
function generateResponse(to, from) {
return {
"success": true,
"response": {
"accepted": [
to
],
"envelope": {
"from": from,
"to": [
to
]
},
"message": `Email sent to ${to}.`
}
}
}
const mockFetch = jest.fn(() => ({
headers: {
raw: () => {
return { "content-type": ["application/json"] }
},
get: () => ["application/json"],
},
json: jest.fn(() => response),
status: 200,
text: jest.fn(),
}))
jest.mock("node-fetch", () => mockFetch)
const setup = require("./utilities")
describe("test the outgoing webhook action", () => {
let inputs
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
inputs = {
to: "user1@test.com",
from: "admin@test.com",
subject: "hello",
contents: "testing",
}
let resp = generateResponse(inputs.to, inputs.from)
mockFetch.mockImplementationOnce(() => ({
headers: {
raw: () => {
return { "content-type": ["application/json"] }
},
get: () => ["application/json"],
},
json: jest.fn(() => resp),
status: 200,
text: jest.fn(),
}))
const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
expect(res.response).toEqual(resp)
expect(res.success).toEqual(true)
})
})

View File

@ -0,0 +1,22 @@
const setup = require("./utilities")
describe("test the server log action", () => {
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
inputs = {
text: "log message",
}
})
afterAll(setup.afterAll)
it("should be able to log the text", async () => {
let res = await setup.runStep(setup.actions.SERVER_LOG.stepId,
inputs
)
expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`)
expect(res.success).toEqual(true)
})
})

View File

@ -31,7 +31,7 @@ export async function runInProd(fn: any) {
}
}
export async function runStep(stepId: string, inputs: any) {
export async function runStep(stepId: string, inputs: any, stepContext?: any) {
async function run() {
let step = await getAction(stepId)
expect(step).toBeDefined()
@ -39,7 +39,7 @@ export async function runStep(stepId: string, inputs: any) {
throw new Error("No step found")
}
return step({
context: {},
context: stepContext || {},
inputs,
appId: config ? config.getAppId() : null,
// don't really need an API key, mocked out usage quota, not being tested here

View File

@ -0,0 +1,27 @@
const setup = require("./utilities")
const fetch = require("node-fetch")
jest.mock("node-fetch")
describe("test the outgoing webhook action", () => {
let inputs
let config = setup.getConfig()
beforeEach(async () => {
await config.init()
inputs = {
value1: "test",
url: "http://www.test.com",
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.zapier.stepId, inputs)
expect(res.response.url).toEqual("http://www.test.com")
expect(res.response.method).toEqual("post")
expect(res.success).toEqual(true)
})
})

View File

@ -1,17 +0,0 @@
const automationUtils = require("../automationUtils")
describe("automationUtils", () => {
test("substituteLoopStep should allow multiple loop binding substitutes", () => {
expect(automationUtils.substituteLoopStep(
`{{ loop.currentItem._id }} {{ loop.currentItem._id }} {{ loop.currentItem._id }}`,
"step.2"))
.toBe(`{{ step.2.currentItem._id }} {{ step.2.currentItem._id }} {{ step.2.currentItem._id }}`)
})
test("substituteLoopStep should handle not subsituting outside of curly braces", () => {
expect(automationUtils.substituteLoopStep(
`loop {{ loop.currentItem._id }}loop loop{{ loop.currentItem._id }}loop`,
"step.2"))
.toBe(`loop {{ step.2.currentItem._id }}loop loop{{ step.2.currentItem._id }}loop`)
})
})

View File

@ -0,0 +1,65 @@
const automationUtils = require("../automationUtils")
describe("automationUtils", () => {
describe("substituteLoopStep", () => {
it("should allow multiple loop binding substitutes", () => {
expect(
automationUtils.substituteLoopStep(
`{{ loop.currentItem._id }} {{ loop.currentItem._id }} {{ loop.currentItem._id }}`,
"step.2"
)
).toBe(
`{{ step.2.currentItem._id }} {{ step.2.currentItem._id }} {{ step.2.currentItem._id }}`
)
})
it("should handle not subsituting outside of curly braces", () => {
expect(
automationUtils.substituteLoopStep(
`loop {{ loop.currentItem._id }}loop loop{{ loop.currentItem._id }}loop`,
"step.2"
)
).toBe(
`loop {{ step.2.currentItem._id }}loop loop{{ step.2.currentItem._id }}loop`
)
})
})
describe("typeCastForLooping", () => {
it("should parse to correct type", () => {
expect(
automationUtils.typecastForLooping(
{ inputs: { option: "Array" } },
{ binding: [1, 2, 3] }
)
).toEqual([1, 2, 3])
expect(
automationUtils.typecastForLooping(
{ inputs: { option: "Array" } },
{ binding: "[1, 2, 3]" }
)
).toEqual([1, 2, 3])
expect(
automationUtils.typecastForLooping(
{ inputs: { option: "String" } },
{ binding: [1, 2, 3] }
)
).toEqual("1,2,3")
})
it("should handle null values", () => {
// expect it to handle where the binding is null
expect(
automationUtils.typecastForLooping(
{ inputs: { option: "Array" } },
{ binding: null }
)
).toEqual(null)
expect(() =>
automationUtils.typecastForLooping(
{ inputs: { option: "Array" } },
{ binding: "test" }
)
).toThrow()
})
})
})

View File

@ -79,7 +79,6 @@ const environment = {
// flags
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS,
MULTI_TENANCY: process.env.MULTI_TENANCY,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,

View File

@ -1,3 +1,12 @@
import { mocks } from "@budibase/backend-core/tests"
// init the licensing mock
import * as pro from "@budibase/pro"
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
import { init as dbInit } from "../../db"
dbInit()
import env from "../../environment"

View File

@ -32,31 +32,8 @@ const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
const CRON_STEP_ID = triggerDefs.CRON.stepId
const STOPPED_STATUS = { success: true, status: AutomationStatus.STOPPED }
function typecastForLooping(loopStep: LoopStep, input: LoopInput) {
if (!input || !input.binding) {
return null
}
try {
switch (loopStep.inputs.option) {
case LoopStepType.ARRAY:
if (typeof input.binding === "string") {
return JSON.parse(input.binding)
}
break
case LoopStepType.STRING:
if (Array.isArray(input.binding)) {
return input.binding.join(",")
}
break
}
} catch (err) {
throw new Error("Unable to cast to correct type")
}
return input.binding
}
function getLoopIterations(loopStep: LoopStep, input: LoopInput) {
const binding = typecastForLooping(loopStep, input)
const binding = automationUtils.typecastForLooping(loopStep, input)
if (!loopStep || !binding) {
return 1
}
@ -289,7 +266,7 @@ class Orchestrator {
let tempOutput = { items: loopSteps, iterations: iterationCount }
try {
newInput.binding = typecastForLooping(
newInput.binding = automationUtils.typecastForLooping(
loopStep as LoopStep,
newInput
)

View File

@ -0,0 +1,57 @@
import { fixAutoColumnSubType } from "../utils"
import { AutoFieldDefaultNames, AutoFieldSubTypes } from "../../../constants"
describe("rowProcessor utility", () => {
describe("fixAutoColumnSubType", () => {
let schema = {
name: "",
type: "link",
subtype: "", // missing subtype
icon: "ri-magic-line",
autocolumn: true,
constraints: { type: "array", presence: false },
tableId: "ta_users",
fieldName: "test-Updated By",
relationshipType: "many-to-many",
sortable: false,
}
it("updates the schema with the correct subtype", async () => {
schema.name = AutoFieldDefaultNames.CREATED_BY
expect(fixAutoColumnSubType(schema).subtype).toEqual(
AutoFieldSubTypes.CREATED_BY
)
schema.subtype = ""
schema.name = AutoFieldDefaultNames.UPDATED_BY
expect(fixAutoColumnSubType(schema).subtype).toEqual(
AutoFieldSubTypes.UPDATED_BY
)
schema.subtype = ""
schema.name = AutoFieldDefaultNames.CREATED_AT
expect(fixAutoColumnSubType(schema).subtype).toEqual(
AutoFieldSubTypes.CREATED_AT
)
schema.subtype = ""
schema.name = AutoFieldDefaultNames.UPDATED_AT
expect(fixAutoColumnSubType(schema).subtype).toEqual(
AutoFieldSubTypes.UPDATED_AT
)
schema.subtype = ""
schema.name = AutoFieldDefaultNames.AUTO_ID
expect(fixAutoColumnSubType(schema).subtype).toEqual(
AutoFieldSubTypes.AUTO_ID
)
schema.subtype = ""
})
it("returns the column if subtype exists", async () => {
schema.subtype = AutoFieldSubTypes.CREATED_BY
schema.name = AutoFieldDefaultNames.CREATED_AT
expect(fixAutoColumnSubType(schema)).toEqual(schema)
})
})
})

View File

@ -0,0 +1,23 @@
import { enrichPluginURLs } from "../plugins"
const env = require("../../environment")
jest.mock("../../environment")
describe("plugins utility", () => {
let pluginsArray: any = [
{
name: "test-plugin",
},
]
it("enriches the plugins url self-hosted", async () => {
let result = enrichPluginURLs(pluginsArray)
expect(result[0].jsUrl).toEqual("/plugins/test-plugin/plugin.min.js")
})
it("enriches the plugins url cloud", async () => {
env.SELF_HOSTED = 0
let result = enrichPluginURLs(pluginsArray)
expect(result[0].jsUrl).toEqual(
"https://cdn.budi.live/test-plugin/plugin.min.js"
)
})
})

View File

@ -19,6 +19,8 @@
"node_modules",
"dist",
"src/tests",
"src/api/routes/tests/utilities",
"src/automations/tests/utilities",
"**/*.spec.ts",
"**/*.spec.js"
]

View File

@ -1273,12 +1273,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.46-alpha.0.tgz#d9063b055cb3c4edb3994a13664c6a070e701a71"
integrity sha512-ulpK2u+5+yIjIyMWhr2cre2ncdZsq5uPt1meNO+kvsetmv37ZnLtmM2gqIe1RNjYkf+mIZlUDA9QDkZowIiTUg==
"@budibase/backend-core@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.46-alpha.11.tgz#a8087f9e2cf042b09681d78c8ac3d9761e063bc2"
integrity sha512-rJC0HzwCbRBnehbhTPY/cYg7fdYPI6R7KHhU8T6Q9nbPzPH5wSbhC9+I6LqYSztT3w9DcWhTETrHOEz7L1y0DA==
dependencies:
"@budibase/types" "2.1.46-alpha.0"
"@budibase/types" "2.1.46-alpha.11"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0"
@ -1360,13 +1360,13 @@
svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0"
"@budibase/pro@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.46-alpha.0.tgz#69bf09007edc0f2be275824ba09f27422081c490"
integrity sha512-04iCZVU6BN0afei0wFITgvjRhQ46G/vI51FCBZ9ZiuyTsWBPuAhNqXVAQ/Y48+OVDBMtIvsvJ/S1kCbWUa6x3g==
"@budibase/pro@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.46-alpha.11.tgz#2fd8cc4dd71ee7be65ad921ddd4c27578a5ca7a5"
integrity sha512-88pN32BqZDTQe259HjPLprKPqvjdqRROjn+AZhzCvl2fPoybYVflzeDscRX+p2VMfIYpbIax49hQ5hJGUQotLw==
dependencies:
"@budibase/backend-core" "2.1.46-alpha.0"
"@budibase/types" "2.1.46-alpha.0"
"@budibase/backend-core" "2.1.46-alpha.11"
"@budibase/types" "2.1.46-alpha.11"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -1390,10 +1390,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.46-alpha.0.tgz#83ecd2c1a26e933fbf27ba6b7a7ef1fed82a8840"
integrity sha512-TTpyax8U0EAsaXqCLACHcfuejZ2x2isyHiM/ia8Ua6R+1rDK9kjYPsiTlJ169/Jq2hv65TzScyaqnHX+g+eJnQ==
"@budibase/types@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.46-alpha.11.tgz#4cba69a4b6c2eecf2b49a9a216d1e4792c87eb52"
integrity sha512-KqYm7CfEiTt6y0vMf8lmoPtMRCFHBRqYmztO9qMYnmlcjaCSLaiEflJ/WK1AXsujjIeK9cHZ7hPPecqqvyFOOQ==
"@bull-board/api@3.7.0":
version "3.7.0"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -19,6 +19,8 @@ if (!process.env.CI) {
}
// add pro sources if they exist
if (fs.existsSync("../../../budibase-pro")) {
config.moduleNameMapper["@budibase/pro/(.*)"] =
"<rootDir>/../../../budibase-pro/packages/pro/$1"
config.moduleNameMapper["@budibase/pro"] =
"<rootDir>/../../../budibase-pro/packages/pro/src"
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "2.1.46-alpha.1",
"version": "2.1.46-alpha.11",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "2.1.46-alpha.1",
"@budibase/pro": "2.1.46-alpha.0",
"@budibase/string-templates": "2.1.46-alpha.1",
"@budibase/types": "2.1.46-alpha.1",
"@budibase/backend-core": "2.1.46-alpha.11",
"@budibase/pro": "2.1.46-alpha.11",
"@budibase/string-templates": "2.1.46-alpha.11",
"@budibase/types": "2.1.46-alpha.11",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",

View File

@ -0,0 +1,58 @@
import { events } from "@budibase/backend-core"
import { structures, TestConfiguration, mocks } from "../../../../tests"
describe("/api/global/groups", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
beforeEach(async () => {
mocks.licenses.useGroups()
})
describe("create", () => {
it("should be able to create a new group", async () => {
const group = structures.groups.UserGroup()
await config.api.groups.saveGroup(group)
expect(events.group.created).toBeCalledTimes(1)
expect(events.group.updated).not.toBeCalled()
expect(events.group.permissionsEdited).not.toBeCalled()
})
})
describe("update", () => {
it("should be able to update a basic group", async () => {
const group = structures.groups.UserGroup()
let oldGroup = await config.api.groups.saveGroup(group)
let updatedGroup = {
...oldGroup.body,
...group,
name: "New Name",
}
await config.api.groups.saveGroup(updatedGroup)
expect(events.group.updated).toBeCalledTimes(1)
expect(events.group.permissionsEdited).not.toBeCalled()
})
describe("destroy", () => {
it("should be able to delete a basic group", async () => {
const group = structures.groups.UserGroup()
let oldGroup = await config.api.groups.saveGroup(group)
await config.api.groups.deleteGroup(
oldGroup.body._id,
oldGroup.body._rev
)
expect(events.group.deleted).toBeCalledTimes(1)
})
})
})
})

View File

@ -1,7 +1,5 @@
import { TestConfiguration } from "../../../../tests"
// TODO
describe("/api/global/license", () => {
const config = new TestConfiguration()

View File

@ -1,11 +1,47 @@
import { TestConfiguration } from "../../../../tests"
import { structures, TestConfiguration } from "../../../../tests"
import { context, db, permissions, roles } from "@budibase/backend-core"
import { Mock } from "jest-mock"
// TODO
jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core")
return {
...core,
db: {
...core.db,
},
context: {
...core.context,
getAppDB: jest.fn(),
},
}
})
const appDb = db.getDB("app_test")
const mockAppDB = context.getAppDB as Mock
mockAppDB.mockReturnValue(appDb)
async function addAppMetadata() {
await appDb.put({
_id: "app_metadata",
appId: "app_test",
name: "New App",
version: "version",
url: "url",
})
}
describe("/api/global/roles", () => {
const config = new TestConfiguration()
const role = new roles.Role(
db.generateRoleID("newRole"),
roles.BUILTIN_ROLE_IDS.BASIC,
permissions.BuiltinPermissionID.READ_ONLY
)
beforeAll(async () => {
console.debug(role)
appDb.put(role)
await addAppMetadata()
await config.beforeAll()
})
@ -18,10 +54,35 @@ describe("/api/global/roles", () => {
})
describe("GET /api/global/roles", () => {
it("retrieves roles", () => {})
it("retrieves roles", async () => {
const res = await config.api.roles.get()
expect(res.body).toBeDefined()
expect(res.body["app_test"].roles.length).toEqual(5)
expect(res.body["app_test"].roles.map((r: any) => r._id)).toContain(
role._id
)
})
})
describe("GET /api/global/roles/:appId", () => {})
describe("GET api/global/roles/:appId", () => {
it("finds a role by appId", async () => {
const res = await config.api.roles.find("app_test")
expect(res.body).toBeDefined()
expect(res.body.name).toEqual("New App")
})
})
describe("DELETE /api/global/roles/:appId", () => {})
describe("DELETE /api/global/roles/:appId", () => {
it("removes an app role", async () => {
let user = structures.users.user()
user.roles = {
app_test: "role1",
}
const userResponse = await config.createUser(user)
const res = await config.api.roles.remove("app_test")
const updatedUser = await config.api.users.getUser(userResponse._id!)
expect(updatedUser.body.roles).not.toHaveProperty("app_test")
expect(res.body.message).toEqual("App role removed from all users")
})
})
})

View File

@ -1,4 +1,17 @@
import {
addBaseTemplates,
EmailTemplates,
getTemplates,
} from "../../../../constants/templates"
import {
EmailTemplatePurpose,
TemplateMetadata,
TemplateMetadataNames,
TemplateType,
} from "../../../../constants"
import { TestConfiguration } from "../../../../tests"
import { join } from "path"
import { readStaticFile } from "../../../../../src/utilities/fileSystem"
// TODO
@ -18,18 +31,85 @@ describe("/api/global/template", () => {
})
describe("GET /api/global/template/definitions", () => {
it("retrieves definitions", () => {})
describe("retrieves definitions", () => {
it("checks description definitions", async () => {
let result = await config.api.templates.definitions()
expect(result.body.info[EmailTemplatePurpose.BASE].description).toEqual(
TemplateMetadata[TemplateType.EMAIL][0].description
)
expect(
result.body.info[EmailTemplatePurpose.PASSWORD_RECOVERY].description
).toEqual(TemplateMetadata[TemplateType.EMAIL][1].description)
expect(
result.body.info[EmailTemplatePurpose.WELCOME].description
).toEqual(TemplateMetadata[TemplateType.EMAIL][2].description)
expect(
result.body.info[EmailTemplatePurpose.INVITATION].description
).toEqual(TemplateMetadata[TemplateType.EMAIL][3].description)
expect(
result.body.info[EmailTemplatePurpose.CUSTOM].description
).toEqual(TemplateMetadata[TemplateType.EMAIL][4].description)
})
it("checks description bindings", async () => {
let result = await config.api.templates.definitions()
expect(result.body.bindings[EmailTemplatePurpose.BASE]).toEqual(
TemplateMetadata[TemplateType.EMAIL][0].bindings
)
expect(
result.body.bindings[EmailTemplatePurpose.PASSWORD_RECOVERY]
).toEqual(TemplateMetadata[TemplateType.EMAIL][1].bindings)
expect(result.body.bindings[EmailTemplatePurpose.WELCOME]).toEqual(
TemplateMetadata[TemplateType.EMAIL][2].bindings
)
expect(result.body.bindings[EmailTemplatePurpose.INVITATION]).toEqual(
TemplateMetadata[TemplateType.EMAIL][3].bindings
)
expect(result.body.bindings[EmailTemplatePurpose.CUSTOM]).toEqual(
TemplateMetadata[TemplateType.EMAIL][4].bindings
)
})
})
})
describe("POST /api/global/template", () => {})
describe("POST /api/global/template", () => {
it("adds a new template", async () => {
let purpose = "base"
let contents = "Test contents"
let updatedTemplate = {
contents: contents,
purpose: purpose,
type: "email",
}
await config.api.templates.saveTemplate(updatedTemplate)
let res = await config.api.templates.getTemplate()
let newTemplate = res.body.find((t: any) => (t.purpose = purpose))
expect(newTemplate.contents).toEqual(contents)
})
})
describe("GET /api/global/template", () => {})
describe("GET /api/global/template/:type", () => {})
describe("GET /api/global/template/:ownerId", () => {})
describe("GET /api/global/template/:id", () => {})
describe("DELETE /api/global/template/:id/:rev", () => {})
describe("GET /api/global/template", () => {
it("fetches templates", async () => {
let res = await config.api.templates.getTemplate()
expect(
res.body.find((t: any) => t.purpose === EmailTemplatePurpose.BASE)
).toBeDefined()
expect(
res.body.find((t: any) => t.purpose === EmailTemplatePurpose.CUSTOM)
).toBeDefined()
expect(
res.body.find((t: any) => t.purpose === EmailTemplatePurpose.INVITATION)
).toBeDefined()
expect(
res.body.find(
(t: any) => t.purpose === EmailTemplatePurpose.PASSWORD_RECOVERY
)
).toBeDefined()
expect(
res.body.find((t: any) => t.purpose === EmailTemplatePurpose.WELCOME)
).toBeDefined()
})
})
})

View File

@ -116,7 +116,7 @@ describe("/api/global/users", () => {
it("should ignore users existing in other tenants", async () => {
const user = await config.createUser()
jest.resetAllMocks()
jest.clearAllMocks()
await tenancy.doInTenant(TENANT_1, async () => {
const response = await config.api.users.bulkCreateUsers([user])
@ -229,7 +229,7 @@ describe("/api/global/users", () => {
it("should not be able to create user that exists in other tenant", async () => {
const user = await config.createUser()
jest.resetAllMocks()
jest.clearAllMocks()
await tenancy.doInTenant(TENANT_1, async () => {
delete user._id

View File

@ -27,6 +27,14 @@ export enum EmailTemplatePurpose {
CUSTOM = "custom",
}
export enum TemplateMetadataNames {
BASE = "Base format",
PASSWORD_RECOVERY = "Password recovery",
WELCOME = "User welcome",
INVITATION = "User invitation",
CUSTOM = "Custom",
}
export enum InternalTemplateBinding {
PLATFORM_URL = "platformUrl",
COMPANY = "company",
@ -93,7 +101,7 @@ export const TemplateBindings = {
export const TemplateMetadata = {
[TemplateType.EMAIL]: [
{
name: "Base format",
name: TemplateMetadataNames.BASE,
description:
"This is the base template, all others are based on it. The {{ body }} will be replaced with another email template.",
category: "miscellaneous",
@ -110,7 +118,7 @@ export const TemplateMetadata = {
],
},
{
name: "Password recovery",
name: TemplateMetadataNames.PASSWORD_RECOVERY,
description:
"When a user requests a password reset they will receive an email built with this template.",
category: "user management",
@ -129,7 +137,7 @@ export const TemplateMetadata = {
],
},
{
name: "User welcome",
name: TemplateMetadataNames.WELCOME,
description:
"When a new user is added they will be sent a welcome email using this template.",
category: "user management",
@ -137,7 +145,7 @@ export const TemplateMetadata = {
bindings: [],
},
{
name: "User invitation",
name: TemplateMetadataNames.INVITATION,
description:
"When inviting a user via the email on-boarding this template will be used.",
category: "user management",
@ -156,7 +164,7 @@ export const TemplateMetadata = {
],
},
{
name: "Custom",
name: TemplateMetadataNames.CUSTOM,
description:
"A custom template, this is currently used for SMTP email actions in automations.",
category: "automations",

View File

@ -1,4 +1,12 @@
import "./mocks"
import mocks from "./mocks"
// init the licensing mock
import * as pro from "@budibase/pro"
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
import * as dbConfig from "../db"
dbConfig.init()
import env from "../environment"

View File

@ -0,0 +1,26 @@
import { UserGroup } from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class GroupsAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
saveGroup = (group: UserGroup) => {
return this.request
.post(`/api/global/groups`)
.send(group)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
deleteGroup = (id: string, rev: string) => {
return this.request
.delete(`/api/global/groups/${id}/${rev}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
}

View File

@ -10,7 +10,10 @@ import { MigrationAPI } from "./migrations"
import { StatusAPI } from "./status"
import { RestoreAPI } from "./restore"
import { TenantAPI } from "./tenants"
import { GroupsAPI } from "./groups"
import { RolesAPI } from "./roles"
import { TemplatesAPI } from "./templates"
import { LicenseAPI } from "./license"
export default class API {
accounts: AccountAPI
auth: AuthAPI
@ -23,6 +26,10 @@ export default class API {
status: StatusAPI
restore: RestoreAPI
tenants: TenantAPI
groups: GroupsAPI
roles: RolesAPI
templates: TemplatesAPI
license: LicenseAPI
constructor(config: TestConfiguration) {
this.accounts = new AccountAPI(config)
@ -36,5 +43,9 @@ export default class API {
this.status = new StatusAPI(config)
this.restore = new RestoreAPI(config)
this.tenants = new TenantAPI(config)
this.groups = new GroupsAPI(config)
this.roles = new RolesAPI(config)
this.templates = new TemplatesAPI(config)
this.license = new LicenseAPI(config)
}
}

View File

@ -0,0 +1,17 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
export class LicenseAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
activate = async (licenseKey: string) => {
return this.request
.post("/api/global/license/activate")
.send({ licenseKey: licenseKey })
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
}

View File

@ -0,0 +1,32 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class RolesAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
get = (opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/roles`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(opts?.status ? opts.status : 200)
}
find = (appId: string, opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/roles/${appId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(opts?.status ? opts.status : 200)
}
remove = (appId: string, opts?: TestAPIOpts) => {
return this.request
.delete(`/api/global/roles/${appId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(opts?.status ? opts.status : 200)
}
}

View File

@ -0,0 +1,30 @@
import TestConfiguration from "../TestConfiguration"
import { TestAPI, TestAPIOpts } from "./base"
export class TemplatesAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
definitions = (opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/template/definitions`)
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
.expect(opts?.status ? opts.status : 200)
}
getTemplate = (opts?: TestAPIOpts) => {
return this.request
.get(`/api/global/template`)
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
.expect(opts?.status ? opts.status : 200)
}
saveTemplate = (data: any, opts?: TestAPIOpts) => {
return this.request
.post(`/api/global/template`)
.send(data)
.set(opts?.headers ? opts.headers : this.config.defaultHeaders())
.expect(opts?.status ? opts.status : 200)
}
}

View File

@ -1,7 +1,7 @@
const email = require("./email")
import { mocks as coreMocks } from "@budibase/backend-core/tests"
import { mocks } from "@budibase/backend-core/tests"
export = {
email,
...coreMocks,
...mocks,
}

View File

@ -4,7 +4,7 @@ export const UserGroup = () => {
color: "var(--spectrum-global-color-blue-600)",
icon: "UserGroup",
name: "New group",
roles: {},
roles: { app_uuid1: "ADMIN", app_uuid2: "POWER" },
users: [],
}
return group

View File

@ -10,7 +10,7 @@ export const user = (userProps?: any): User => {
return {
email: newEmail(),
password: "test",
roles: {},
roles: { app_test: "admin" },
...userProps,
}
}

View File

@ -25,7 +25,6 @@
"package.json"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -470,12 +470,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.46-alpha.0.tgz#d9063b055cb3c4edb3994a13664c6a070e701a71"
integrity sha512-ulpK2u+5+yIjIyMWhr2cre2ncdZsq5uPt1meNO+kvsetmv37ZnLtmM2gqIe1RNjYkf+mIZlUDA9QDkZowIiTUg==
"@budibase/backend-core@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.46-alpha.11.tgz#a8087f9e2cf042b09681d78c8ac3d9761e063bc2"
integrity sha512-rJC0HzwCbRBnehbhTPY/cYg7fdYPI6R7KHhU8T6Q9nbPzPH5wSbhC9+I6LqYSztT3w9DcWhTETrHOEz7L1y0DA==
dependencies:
"@budibase/types" "2.1.46-alpha.0"
"@budibase/types" "2.1.46-alpha.11"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0"
@ -507,22 +507,22 @@
uuid "8.3.2"
zlib "1.0.5"
"@budibase/pro@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.46-alpha.0.tgz#69bf09007edc0f2be275824ba09f27422081c490"
integrity sha512-04iCZVU6BN0afei0wFITgvjRhQ46G/vI51FCBZ9ZiuyTsWBPuAhNqXVAQ/Y48+OVDBMtIvsvJ/S1kCbWUa6x3g==
"@budibase/pro@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.46-alpha.11.tgz#2fd8cc4dd71ee7be65ad921ddd4c27578a5ca7a5"
integrity sha512-88pN32BqZDTQe259HjPLprKPqvjdqRROjn+AZhzCvl2fPoybYVflzeDscRX+p2VMfIYpbIax49hQ5hJGUQotLw==
dependencies:
"@budibase/backend-core" "2.1.46-alpha.0"
"@budibase/types" "2.1.46-alpha.0"
"@budibase/backend-core" "2.1.46-alpha.11"
"@budibase/types" "2.1.46-alpha.11"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
node-fetch "^2.6.1"
"@budibase/types@2.1.46-alpha.0":
version "2.1.46-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.46-alpha.0.tgz#83ecd2c1a26e933fbf27ba6b7a7ef1fed82a8840"
integrity sha512-TTpyax8U0EAsaXqCLACHcfuejZ2x2isyHiM/ia8Ua6R+1rDK9kjYPsiTlJ169/Jq2hv65TzScyaqnHX+g+eJnQ==
"@budibase/types@2.1.46-alpha.11":
version "2.1.46-alpha.11"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.46-alpha.11.tgz#4cba69a4b6c2eecf2b49a9a216d1e4792c87eb52"
integrity sha512-KqYm7CfEiTt6y0vMf8lmoPtMRCFHBRqYmztO9qMYnmlcjaCSLaiEflJ/WK1AXsujjIeK9cHZ7hPPecqqvyFOOQ==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"

View File

@ -1,25 +1,30 @@
echo "Linking backend-core"
cd packages/backend-core
yarn unlink
yarn link
cd -
echo "Linking string-templates"
cd packages/string-templates
cd packages/string-templates
yarn unlink
yarn link
cd -
echo "Linking types"
cd packages/types
cd packages/types
yarn unlink
yarn link
cd -
echo "Linking bbui"
cd packages/bbui
cd packages/bbui
yarn unlink
yarn link
cd -
echo "Linking frontend-core"
cd packages/frontend-core
yarn unlink
yarn link
cd -
@ -30,6 +35,7 @@ if [ -d "../budibase-pro" ]; then
cd packages/pro
echo "Linking pro"
yarn unlink
yarn link
echo "Linking backend-core to pro"