diff --git a/packages/builder/src/components/settings/ThemeEditorDropdown.svelte b/packages/builder/src/components/settings/ThemeEditorDropdown.svelte
index 64a431fc19..99e018843e 100644
--- a/packages/builder/src/components/settings/ThemeEditorDropdown.svelte
+++ b/packages/builder/src/components/settings/ThemeEditorDropdown.svelte
@@ -43,7 +43,7 @@
.topnavitemright:hover i {
color: var(--ink);
}
-
+
.content {
padding: var(--spacing-xl);
}
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index 84c935026e..b32222ce2f 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -41,7 +41,7 @@
id,
children: children.length,
styles: { ...styles, id },
- transition
+ transition,
})
// Gets the component constructor for the specified component
diff --git a/packages/server/__mocks__/node-fetch.js b/packages/server/__mocks__/node-fetch.js
new file mode 100644
index 0000000000..1113791ec2
--- /dev/null
+++ b/packages/server/__mocks__/node-fetch.js
@@ -0,0 +1,17 @@
+const fetch = jest.requireActual("node-fetch")
+
+module.exports = async (url, opts) => {
+ // mocked data based on url
+ if (url.includes("api/apps")) {
+ return {
+ json: async () => {
+ return {
+ app1: {
+ url: "/app1",
+ },
+ }
+ },
+ }
+ }
+ return fetch(url, opts)
+}
diff --git a/packages/server/__mocks__/pg.js b/packages/server/__mocks__/pg.js
new file mode 100644
index 0000000000..2bda8afad0
--- /dev/null
+++ b/packages/server/__mocks__/pg.js
@@ -0,0 +1,21 @@
+const pg = {}
+
+// constructor
+function Client() {}
+
+Client.prototype.query = async function() {
+ return {
+ rows: [
+ {
+ a: "string",
+ b: 1,
+ },
+ ],
+ }
+}
+
+Client.prototype.connect = async function() {}
+
+pg.Client = Client
+
+module.exports = pg
diff --git a/packages/server/package.json b/packages/server/package.json
index a23c6f6e89..99763109f3 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -33,7 +33,7 @@
},
"scripts": {
"test": "jest --testPathIgnorePatterns=routes && npm run test:integration",
- "test:integration": "jest routes --runInBand --coverage",
+ "test:integration": "jest --runInBand --coverage",
"test:watch": "jest --watch",
"run:docker": "node src/index",
"dev:builder": "cross-env PORT=4001 nodemon src/index.js",
@@ -53,7 +53,11 @@
"src/**/*.js",
"!**/node_modules/**",
"!src/db/views/*.js",
- "!src/api/routes/tests"
+ "!src/api/routes/tests/**/*.js",
+ "!src/api/controllers/deploy/**/*.js",
+ "!src/api/controllers/static/templates/**/*",
+ "!src/api/controllers/static/selfhost/**/*",
+ "!src/*.js"
],
"coverageReporters": [
"lcov",
@@ -122,6 +126,7 @@
"zlib": "1.0.5"
},
"devDependencies": {
+ "@budibase/standard-components": "^0.8.5",
"@jest/test-sequencer": "^24.8.0",
"cross-env": "^7.0.3",
"electron": "10.1.3",
diff --git a/packages/server/src/api/controllers/analytics.js b/packages/server/src/api/controllers/analytics.js
index 566b4970e9..76e5eb11f1 100644
--- a/packages/server/src/api/controllers/analytics.js
+++ b/packages/server/src/api/controllers/analytics.js
@@ -1,5 +1,7 @@
const env = require("../../environment")
exports.isEnabled = async function(ctx) {
- ctx.body = JSON.stringify(env.ENABLE_ANALYTICS === "true")
+ ctx.body = {
+ enabled: env.ENABLE_ANALYTICS === "true",
+ }
}
diff --git a/packages/server/src/api/controllers/apikeys.js b/packages/server/src/api/controllers/apikeys.js
index 1cd69c54df..96754f17cc 100644
--- a/packages/server/src/api/controllers/apikeys.js
+++ b/packages/server/src/api/controllers/apikeys.js
@@ -3,20 +3,13 @@ const { join } = require("../../utilities/centralPath")
const readline = require("readline")
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const env = require("../../environment")
-const selfhost = require("../../selfhost")
const ENV_FILE_PATH = "/.env"
exports.fetch = async function(ctx) {
ctx.status = 200
- if (env.SELF_HOSTED) {
- ctx.body = {
- selfhost: await selfhost.getSelfHostAPIKey(),
- }
- } else {
- ctx.body = {
- budibase: env.BUDIBASE_API_KEY,
- userId: env.USERID_API_KEY,
- }
+ ctx.body = {
+ budibase: env.BUDIBASE_API_KEY,
+ userId: env.USERID_API_KEY,
}
}
diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js
index e899a2dca9..cba3e6455a 100644
--- a/packages/server/src/api/controllers/application.js
+++ b/packages/server/src/api/controllers/application.js
@@ -104,9 +104,10 @@ async function createInstance(template) {
await createRoutingView(appId)
// replicate the template data to the instance DB
+ // this is currently very hard to test, downloading and importing template files
+ /* istanbul ignore next */
if (template) {
let dbDumpReadStream
-
if (template.fileImportPath) {
dbDumpReadStream = fs.createReadStream(template.fileImportPath)
} else {
@@ -181,8 +182,9 @@ exports.create = async function(ctx) {
const instanceDb = new CouchDB(appId)
await instanceDb.put(newApplication)
+ const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
+ /* istanbul ignore next */
if (env.NODE_ENV !== "jest") {
- const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await downloadExtractComponentLibraries(newAppFolder)
}
diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js
index 1cc6db3185..e5c0f9a029 100644
--- a/packages/server/src/api/controllers/auth.js
+++ b/packages/server/src/api/controllers/auth.js
@@ -46,6 +46,7 @@ exports.authenticate = async ctx => {
version: app.version,
}
// if in cloud add the user api key, unless self hosted
+ /* istanbul ignore next */
if (env.CLOUD && !env.SELF_HOSTED) {
const { apiKey } = await getAPIKey(ctx.user.appId)
payload.apiKey = apiKey
@@ -70,6 +71,7 @@ exports.authenticate = async ctx => {
exports.fetchSelf = async ctx => {
const { userId, appId } = ctx.user
+ /* istanbul ignore next */
if (!userId || !appId) {
ctx.body = {}
return
diff --git a/packages/server/src/api/controllers/automation.js b/packages/server/src/api/controllers/automation.js
index 5fa618654b..df17371f92 100644
--- a/packages/server/src/api/controllers/automation.js
+++ b/packages/server/src/api/controllers/automation.js
@@ -98,6 +98,11 @@ exports.create = async function(ctx) {
let automation = ctx.request.body
automation.appId = ctx.user.appId
+ // call through to update if already exists
+ if (automation._id && automation._rev) {
+ return exports.update(ctx)
+ }
+
automation._id = generateAutomationID()
automation.type = "automation"
diff --git a/packages/server/src/api/controllers/datasource.js b/packages/server/src/api/controllers/datasource.js
index 15cea72da7..678c56a14c 100644
--- a/packages/server/src/api/controllers/datasource.js
+++ b/packages/server/src/api/controllers/datasource.js
@@ -1,5 +1,4 @@
const CouchDB = require("../../db")
-const bcrypt = require("../../utilities/bcrypt")
const {
generateDatasourceID,
getDatasourceParams,
@@ -26,35 +25,12 @@ exports.save = async function(ctx) {
...ctx.request.body,
}
- try {
- const response = await db.post(datasource)
- datasource._rev = response.rev
-
- ctx.status = 200
- ctx.message = "Datasource saved successfully."
- ctx.body = datasource
- } catch (err) {
- ctx.throw(err.status, err)
- }
-}
-
-exports.update = async function(ctx) {
- const db = new CouchDB(ctx.user.appId)
- const user = ctx.request.body
- const dbUser = await db.get(ctx.request.body._id)
- if (user.password) {
- user.password = await bcrypt.hash(user.password)
- } else {
- delete user.password
- }
- const newData = { ...dbUser, ...user }
-
- const response = await db.put(newData)
- user._rev = response.rev
+ const response = await db.post(datasource)
+ datasource._rev = response.rev
ctx.status = 200
- ctx.message = `User ${ctx.request.body.email} updated successfully.`
- ctx.body = response
+ ctx.message = "Datasource saved successfully."
+ ctx.body = datasource
}
exports.destroy = async function(ctx) {
@@ -73,6 +49,5 @@ exports.destroy = async function(ctx) {
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
- const datasource = await database.get(ctx.params.datasourceId)
- ctx.body = datasource
+ ctx.body = await database.get(ctx.params.datasourceId)
}
diff --git a/packages/server/src/api/controllers/layout.js b/packages/server/src/api/controllers/layout.js
index de4c647c9f..f270e95bec 100644
--- a/packages/server/src/api/controllers/layout.js
+++ b/packages/server/src/api/controllers/layout.js
@@ -38,6 +38,6 @@ exports.destroy = async function(ctx) {
}
await db.remove(layoutId, layoutRev)
- ctx.message = "Layout deleted successfully"
+ ctx.body = { message: "Layout deleted successfully" }
ctx.status = 200
}
diff --git a/packages/server/src/api/controllers/query.js b/packages/server/src/api/controllers/query.js
index 951fedd3d0..a2badb0d0d 100644
--- a/packages/server/src/api/controllers/query.js
+++ b/packages/server/src/api/controllers/query.js
@@ -110,7 +110,6 @@ exports.preview = async function(ctx) {
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
- return
}
const { fields, parameters, queryVerb } = ctx.request.body
@@ -140,7 +139,6 @@ exports.execute = async function(ctx) {
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
- return
}
const enrichedQuery = await enrichQueryFields(
diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js
index 5d783b7d18..bf985fe55d 100644
--- a/packages/server/src/api/controllers/row.js
+++ b/packages/server/src/api/controllers/row.js
@@ -224,6 +224,7 @@ exports.fetchView = async function(ctx) {
try {
table = await db.get(viewInfo.meta.tableId)
} catch (err) {
+ /* istanbul ignore next */
table = {
schema: {},
}
@@ -255,16 +256,24 @@ exports.fetchView = async function(ctx) {
exports.search = async function(ctx) {
const appId = ctx.user.appId
-
const db = new CouchDB(appId)
-
const {
query,
pagination: { pageSize = 10, page },
} = ctx.request.body
- query.tableId = ctx.params.tableId
+ // make all strings a starts with operation rather than pure equality
+ for (const [key, queryVal] of Object.entries(query)) {
+ if (typeof queryVal === "string") {
+ query[key] = {
+ $gt: queryVal,
+ $lt: `${queryVal}\uffff`,
+ }
+ }
+ }
+ // pure equality for table
+ query.tableId = ctx.params.tableId
const response = await db.find({
selector: query,
limit: pageSize,
@@ -324,7 +333,6 @@ exports.destroy = async function(ctx) {
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId doesn't match the row's tableId")
- return
}
await linkRows.updateLinks({
appId,
@@ -376,15 +384,6 @@ exports.fetchEnrichedRow = async function(ctx) {
const db = new CouchDB(appId)
const tableId = ctx.params.tableId
const rowId = ctx.params.rowId
- if (appId == null || tableId == null || rowId == null) {
- ctx.status = 400
- ctx.body = {
- status: 400,
- error:
- "Cannot handle request, URI params have not been successfully prepared.",
- }
- return
- }
// need table to work out where links go in row
let [table, row] = await Promise.all([
db.get(tableId),
diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js
index f56dae153e..8f9baa8172 100644
--- a/packages/server/src/api/controllers/screen.js
+++ b/packages/server/src/api/controllers/screen.js
@@ -41,6 +41,8 @@ exports.save = async ctx => {
exports.destroy = async ctx => {
const db = new CouchDB(ctx.user.appId)
await db.remove(ctx.params.screenId, ctx.params.screenRev)
- ctx.message = "Screen deleted successfully"
+ ctx.body = {
+ message: "Screen deleted successfully",
+ }
ctx.status = 200
}
diff --git a/packages/server/src/api/controllers/templates.js b/packages/server/src/api/controllers/templates.js
index 94243d3c75..c3cfa28706 100644
--- a/packages/server/src/api/controllers/templates.js
+++ b/packages/server/src/api/controllers/templates.js
@@ -24,6 +24,8 @@ exports.fetch = async function(ctx) {
}
}
+// can't currently test this, have to ignore from coverage
+/* istanbul ignore next */
exports.downloadTemplate = async function(ctx) {
const { type, name } = ctx.params
diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js
index c100f43d88..3dd28284be 100644
--- a/packages/server/src/api/controllers/user.js
+++ b/packages/server/src/api/controllers/user.js
@@ -52,7 +52,7 @@ exports.create = async function(ctx) {
const response = await db.post(user)
ctx.status = 200
ctx.message = "User created successfully."
- ctx.userId = response._id
+ ctx.userId = response.id
ctx.body = {
_rev: response.rev,
email,
@@ -70,6 +70,9 @@ exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
let dbUser
+ if (user.email && !user._id) {
+ user._id = generateUserID(user.email)
+ }
// get user incase password removed
if (user._id) {
dbUser = await db.get(user._id)
@@ -87,14 +90,15 @@ exports.update = async function(ctx) {
user._rev = response.rev
ctx.status = 200
- ctx.message = `User ${ctx.request.body.email} updated successfully.`
ctx.body = response
}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
await database.destroy(generateUserID(ctx.params.email))
- ctx.message = `User ${ctx.params.email} deleted.`
+ ctx.body = {
+ message: `User ${ctx.params.email} deleted.`,
+ }
ctx.status = 200
}
diff --git a/packages/server/src/api/controllers/webhook.js b/packages/server/src/api/controllers/webhook.js
index 7a343b1b07..5b76f86190 100644
--- a/packages/server/src/api/controllers/webhook.js
+++ b/packages/server/src/api/controllers/webhook.js
@@ -43,12 +43,10 @@ exports.save = async ctx => {
webhook._id = generateWebhookID()
}
const response = await db.put(webhook)
+ webhook._rev = response.rev
ctx.body = {
message: "Webhook created successfully",
- webhook: {
- ...webhook,
- ...response,
- },
+ webhook,
}
}
@@ -95,5 +93,7 @@ exports.trigger = async ctx => {
})
}
ctx.status = 200
- ctx.body = "Webhook trigger fired successfully"
+ ctx.body = {
+ message: "Webhook trigger fired successfully",
+ }
}
diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js
index 7b063cb522..7628fa2077 100644
--- a/packages/server/src/api/index.js
+++ b/packages/server/src/api/index.js
@@ -41,13 +41,15 @@ router.use(async (ctx, next) => {
try {
await next()
} catch (err) {
- ctx.log.error(err)
ctx.status = err.status || err.statusCode || 500
ctx.body = {
message: err.message,
status: ctx.status,
}
- console.trace(err)
+ if (env.NODE_ENV !== "jest") {
+ ctx.log.error(err)
+ console.trace(err)
+ }
}
})
diff --git a/packages/server/src/api/routes/datasource.js b/packages/server/src/api/routes/datasource.js
index 5746ce1839..ee2210704f 100644
--- a/packages/server/src/api/routes/datasource.js
+++ b/packages/server/src/api/routes/datasource.js
@@ -12,7 +12,7 @@ const router = Router()
router
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch)
.get(
- "/api/datasources/:id",
+ "/api/datasources/:datasourceId",
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
datasourceController.find
)
diff --git a/packages/server/src/api/routes/hosting.js b/packages/server/src/api/routes/hosting.js
index 44429dacbf..111968a497 100644
--- a/packages/server/src/api/routes/hosting.js
+++ b/packages/server/src/api/routes/hosting.js
@@ -11,11 +11,7 @@ router
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls)
.get("/api/hosting", authorized(BUILDER), controller.fetch)
.post("/api/hosting", authorized(BUILDER), controller.save)
- .get(
- "/api/hosting/apps",
- authorized(BUILDER),
- selfhost,
- controller.getDeployedApps
- )
+ // this isn't risky, doesn't return anything about apps other than names and URLs
+ .get("/api/hosting/apps", selfhost, controller.getDeployedApps)
module.exports = router
diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js
index 2b0c9b36fc..c812c4d3b1 100644
--- a/packages/server/src/api/routes/static.js
+++ b/packages/server/src/api/routes/static.js
@@ -8,6 +8,7 @@ const usage = require("../../middleware/usageQuota")
const router = Router()
+/* istanbul ignore next */
router.param("file", async (file, ctx, next) => {
ctx.file = file && file.includes(".") ? file : "index.html"
diff --git a/packages/server/src/api/routes/tests/apikeys.spec.js b/packages/server/src/api/routes/tests/apikeys.spec.js
new file mode 100644
index 0000000000..dbee57c8b0
--- /dev/null
+++ b/packages/server/src/api/routes/tests/apikeys.spec.js
@@ -0,0 +1,59 @@
+const setup = require("./utilities")
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
+const fs = require("fs")
+const path = require("path")
+
+describe("/api/keys", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("fetch", () => {
+ it("should allow fetching", async () => {
+ const res = await request
+ .get(`/api/keys`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body).toBeDefined()
+ })
+
+ it("should check authorization for builder", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/keys`,
+ })
+ })
+ })
+
+ describe("update", () => {
+ it("should allow updating a value", async () => {
+ fs.writeFileSync(path.join(budibaseAppsDir(), ".env"), "TEST_API_KEY=thing")
+ const res = await request
+ .put(`/api/keys/TEST`)
+ .send({
+ value: "test"
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body["TEST"]).toEqual("test")
+ expect(process.env.TEST_API_KEY).toEqual("test")
+ })
+
+ it("should check authorization for builder", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "PUT",
+ url: `/api/keys/TEST`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js
index c146cc63fb..12fcf6f8ea 100644
--- a/packages/server/src/api/routes/tests/application.spec.js
+++ b/packages/server/src/api/routes/tests/application.spec.js
@@ -58,4 +58,43 @@ describe("/applications", () => {
})
})
+ describe("fetchAppDefinition", () => {
+ it("should be able to get an apps definition", async () => {
+ const res = await request
+ .get(`/api/applications/${config.getAppId()}/definition`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ // should have empty packages
+ expect(res.body.screens.length).toEqual(2)
+ expect(res.body.layouts.length).toEqual(2)
+ })
+ })
+
+ describe("fetchAppPackage", () => {
+ it("should be able to fetch the app package", async () => {
+ const res = await request
+ .get(`/api/applications/${config.getAppId()}/appPackage`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.application).toBeDefined()
+ expect(res.body.screens.length).toEqual(2)
+ expect(res.body.layouts.length).toEqual(2)
+ })
+ })
+
+ describe("update", () => {
+ it("should be able to fetch the app package", async () => {
+ const res = await request
+ .put(`/api/applications/${config.getAppId()}`)
+ .send({
+ name: "TEST_APP"
+ })
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.rev).toBeDefined()
+ })
+ })
})
diff --git a/packages/server/src/api/routes/tests/auth.spec.js b/packages/server/src/api/routes/tests/auth.spec.js
new file mode 100644
index 0000000000..0eb0b6d851
--- /dev/null
+++ b/packages/server/src/api/routes/tests/auth.spec.js
@@ -0,0 +1,106 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+
+describe("/authenticate", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("authenticate", () => {
+ it("should be able to create a layout", async () => {
+ await config.createUser("test@test.com", "p4ssw0rd")
+ const res = await request
+ .post(`/api/authenticate`)
+ .send({
+ email: "test@test.com",
+ password: "p4ssw0rd",
+ })
+ .set(config.publicHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.token).toBeDefined()
+ expect(res.body.email).toEqual("test@test.com")
+ expect(res.body.password).toBeUndefined()
+ })
+
+ it("should error if no app specified", async () => {
+ await request
+ .post(`/api/authenticate`)
+ .expect(400)
+ })
+
+ it("should error if no email specified", async () => {
+ await request
+ .post(`/api/authenticate`)
+ .send({
+ password: "test",
+ })
+ .set(config.publicHeaders())
+ .expect(400)
+ })
+
+ it("should error if no password specified", async () => {
+ await request
+ .post(`/api/authenticate`)
+ .send({
+ email: "test",
+ })
+ .set(config.publicHeaders())
+ .expect(400)
+ })
+
+ it("should error if invalid user specified", async () => {
+ await request
+ .post(`/api/authenticate`)
+ .send({
+ email: "test",
+ password: "test",
+ })
+ .set(config.publicHeaders())
+ .expect(401)
+ })
+
+ it("should throw same error if wrong password specified", async () => {
+ await config.createUser("test@test.com", "password")
+ await request
+ .post(`/api/authenticate`)
+ .send({
+ email: "test@test.com",
+ password: "test",
+ })
+ .set(config.publicHeaders())
+ .expect(401)
+ })
+
+ it("should throw an error for inactive users", async () => {
+ await config.createUser("test@test.com", "password")
+ await config.makeUserInactive("test@test.com")
+ await request
+ .post(`/api/authenticate`)
+ .send({
+ email: "test@test.com",
+ password: "password",
+ })
+ .set(config.publicHeaders())
+ .expect(401)
+ })
+ })
+
+ describe("fetch self", () => {
+ it("should be able to delete the layout", async () => {
+ await config.createUser("test@test.com", "p4ssw0rd")
+ const headers = await config.login("test@test.com", "p4ssw0rd")
+ const res = await request
+ .get(`/api/self`)
+ .set(headers)
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.email).toEqual("test@test.com")
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js
index 0648bfefa5..9d11219506 100644
--- a/packages/server/src/api/routes/tests/automation.spec.js
+++ b/packages/server/src/api/routes/tests/automation.spec.js
@@ -73,7 +73,7 @@ describe("/automations", () => {
.expect('Content-Type', /json/)
.expect(200)
- expect(Object.keys(res.body.action).length).toEqual(Object.keys(ACTION_DEFINITIONS).length)
+ expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(Object.keys(ACTION_DEFINITIONS).length)
expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length)
expect(Object.keys(res.body.logic).length).toEqual(Object.keys(LOGIC_DEFINITIONS).length)
})
@@ -109,6 +109,35 @@ describe("/automations", () => {
automation = res.body.automation
})
+ it("should be able to create an automation with a webhook trigger", async () => {
+ const autoConfig = basicAutomation()
+ autoConfig.definition.trigger = TRIGGER_DEFINITIONS["WEBHOOK"]
+ autoConfig.definition.trigger.id = "webhook_trigger_id"
+ const res = await request
+ .post(`/api/automations`)
+ .set(config.defaultHeaders())
+ .send(autoConfig)
+ .expect('Content-Type', /json/)
+ .expect(200)
+ const originalAuto = res.body.automation
+ expect(originalAuto._id).toBeDefined()
+ expect(originalAuto._rev).toBeDefined()
+ // try removing the webhook trigger
+ const newConfig = originalAuto
+ newConfig.definition.trigger = TRIGGER_DEFINITIONS["ROW_SAVED"]
+ newConfig.definition.trigger.id = "row_saved_id"
+ const newRes = await request
+ .post(`/api/automations`)
+ .set(config.defaultHeaders())
+ .send(newConfig)
+ .expect('Content-Type', /json/)
+ .expect(200)
+ const newAuto = newRes.body.automation
+ expect(newAuto._id).toEqual(originalAuto._id)
+ expect(newAuto._rev).toBeDefined()
+ expect(newAuto._rev).not.toEqual(originalAuto._rev)
+ })
+
it("should apply authorization to endpoint", async () => {
await checkBuilderEndpoint({
config,
@@ -119,6 +148,19 @@ describe("/automations", () => {
})
})
+ describe("find", () => {
+ it("should be able to find the automation", async () => {
+ const automation = await config.createAutomation()
+ const res = await request
+ .get(`/api/automations/${automation._id}`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body._id).toEqual(automation._id)
+ expect(res.body._rev).toEqual(automation._rev)
+ })
+ })
+
describe("trigger", () => {
it("trigger the automation successfully", async () => {
let table = await config.createTable()
diff --git a/packages/server/src/api/routes/tests/backup.spec.js b/packages/server/src/api/routes/tests/backup.spec.js
new file mode 100644
index 0000000000..d603990294
--- /dev/null
+++ b/packages/server/src/api/routes/tests/backup.spec.js
@@ -0,0 +1,32 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+
+describe("/backups", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("exportAppDump", () => {
+ it("should be able to export app", async () => {
+ const res = await request
+ .get(`/api/backups/export?appId=${config.getAppId()}`)
+ .set(config.defaultHeaders())
+ .expect(200)
+ expect(res.text).toBeDefined()
+ expect(res.text.includes(`"db_name":"${config.getAppId()}"`)).toEqual(true)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/backups/export?appId=${config.getAppId()}`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/cloud.spec.js b/packages/server/src/api/routes/tests/cloud.spec.js
new file mode 100644
index 0000000000..3cb65ed819
--- /dev/null
+++ b/packages/server/src/api/routes/tests/cloud.spec.js
@@ -0,0 +1,16 @@
+const setup = require("./utilities")
+
+describe("test things in the Cloud/Self hosted", () => {
+ describe("test self hosted static page", () => {
+ it("should be able to load the static page", async () => {
+ await setup.switchToCloudForFunction(async () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ await config.init()
+ const res = await request.get(`/`).expect(200)
+ expect(res.text.includes("
Budibase self hosting️")).toEqual(true)
+ setup.afterAll()
+ })
+ })
+ })
+})
diff --git a/packages/server/src/api/routes/tests/component.spec.js b/packages/server/src/api/routes/tests/component.spec.js
new file mode 100644
index 0000000000..926efc51e3
--- /dev/null
+++ b/packages/server/src/api/routes/tests/component.spec.js
@@ -0,0 +1,49 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+const fs = require("fs")
+const { resolve, join } = require("path")
+const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
+
+describe("/component", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ function mock() {
+ const manifestFile = "manifest.json"
+ const appId = config.getAppId()
+ const libraries = ["@budibase/standard-components"]
+ for (let library of libraries) {
+ let appDirectory = resolve(budibaseAppsDir(), appId, "node_modules", library, "package")
+ fs.mkdirSync(appDirectory, { recursive: true })
+ const file = require.resolve(library).split("dist/index.js")[0] + manifestFile
+ fs.copyFileSync(file, join(appDirectory, manifestFile))
+ }
+ }
+
+ describe("fetch definitions", () => {
+ it("should be able to fetch definitions", async () => {
+ // have to "mock" the files required
+ mock()
+ const res = await request
+ .get(`/${config.getAppId()}/components/definitions`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body["@budibase/standard-components/container"]).toBeDefined()
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/${config.getAppId()}/components/definitions`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/datasource.spec.js b/packages/server/src/api/routes/tests/datasource.spec.js
index 7602b027e1..ee1a1c47f5 100644
--- a/packages/server/src/api/routes/tests/datasource.spec.js
+++ b/packages/server/src/api/routes/tests/datasource.spec.js
@@ -1,15 +1,17 @@
-let { basicDatasource } = require("./utilities/structures")
-let { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+let {basicDatasource} = require("./utilities/structures")
+let {checkBuilderEndpoint} = require("./utilities/TestFunctions")
let setup = require("./utilities")
describe("/datasources", () => {
let request = setup.getRequest()
let config = setup.getConfig()
+ let datasource
afterAll(setup.afterAll)
beforeEach(async () => {
await config.init()
+ datasource = await config.createDatasource()
})
describe("create", () => {
@@ -21,22 +23,12 @@ describe("/datasources", () => {
.expect('Content-Type', /json/)
.expect(200)
- expect(res.res.statusMessage).toEqual("Datasource saved successfully.");
- expect(res.body.name).toEqual("Test");
- })
- });
+ expect(res.res.statusMessage).toEqual("Datasource saved successfully.")
+ expect(res.body.name).toEqual("Test")
+ })
+ })
describe("fetch", () => {
- let datasource
-
- beforeEach(async () => {
- datasource = await config.createDatasource()
- });
-
- afterEach(() => {
- delete datasource._rev
- });
-
it("returns all the datasources from the server", async () => {
const res = await request
.get(`/api/datasources`)
@@ -44,36 +36,37 @@ describe("/datasources", () => {
.expect('Content-Type', /json/)
.expect(200)
- const datasources = res.body
- expect(datasources).toEqual([
- {
- "_id": datasources[0]._id,
- "_rev": datasources[0]._rev,
- ...basicDatasource()
- }
- ]);
+ const datasources = res.body
+ expect(datasources).toEqual([
+ {
+ "_id": datasources[0]._id,
+ "_rev": datasources[0]._rev,
+ ...basicDatasource()
+ }
+ ])
})
it("should apply authorization to endpoint", async () => {
- await checkBuilderEndpoint({
- config,
- method: "GET",
- url: `/api/datasources`,
- })
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/datasources`,
})
- });
+ })
+ })
+
+ describe("find", () => {
+ it("should be able to find a datasource", async () => {
+ const res = await request
+ .get(`/api/datasources/${datasource._id}`)
+ .set(config.defaultHeaders())
+ .expect(200)
+ expect(res.body._rev).toBeDefined()
+ expect(res.body._id).toEqual(datasource._id)
+ })
+ })
describe("destroy", () => {
- let datasource
-
- beforeEach(async () => {
- datasource = await config.createDatasource()
- });
-
- afterEach(() => {
- delete datasource._rev
- });
-
it("deletes queries for the datasource after deletion and returns a success message", async () => {
await config.createQuery()
@@ -87,8 +80,8 @@ describe("/datasources", () => {
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
-
- expect(res.body).toEqual([])
+
+ expect(res.body).toEqual([])
})
it("should apply authorization to endpoint", async () => {
@@ -99,5 +92,5 @@ describe("/datasources", () => {
})
})
- });
-});
+ })
+})
diff --git a/packages/server/src/api/routes/tests/hosting.spec.js b/packages/server/src/api/routes/tests/hosting.spec.js
new file mode 100644
index 0000000000..2da5b11778
--- /dev/null
+++ b/packages/server/src/api/routes/tests/hosting.spec.js
@@ -0,0 +1,130 @@
+// mock out node fetch for this
+jest.mock("node-fetch")
+
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+
+describe("/hosting", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ let app
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ app = await config.init()
+ })
+
+ describe("fetchInfo", () => {
+ it("should be able to fetch hosting information", async () => {
+ const res = await request
+ .get(`/api/hosting/info`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body).toEqual({ types: ["cloud", "self"]})
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/hosting/info`,
+ })
+ })
+ })
+
+ describe("fetchUrls", () => {
+ it("should be able to fetch current app URLs", async () => {
+ const res = await request
+ .get(`/api/hosting/urls`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.app).toEqual(`https://${config.getAppId()}.app.budi.live`)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/hosting/urls`,
+ })
+ })
+ })
+
+ describe("fetch", () => {
+ it("should be able to fetch the current hosting information", async () => {
+ const res = await request
+ .get(`/api/hosting`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body._id).toBeDefined()
+ expect(res.body.hostingUrl).toBeDefined()
+ expect(res.body.type).toEqual("cloud")
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/hosting`,
+ })
+ })
+ })
+
+ describe("save", () => {
+ it("should be able to update the hosting information", async () => {
+ const res = await request
+ .post(`/api/hosting`)
+ .send({
+ type: "self",
+ selfHostKey: "budibase",
+ hostingUrl: "localhost:10000",
+ useHttps: false,
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.ok).toEqual(true)
+ // make sure URL updated
+ const urlRes = await request
+ .get(`/api/hosting/urls`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(urlRes.body.app).toEqual(`http://localhost:10000/app`)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "POST",
+ url: `/api/hosting`,
+ })
+ })
+ })
+
+ describe("getDeployedApps", () => {
+ it("should get apps when in builder", async () => {
+ const res = await request
+ .get(`/api/hosting/apps`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.app1).toEqual({url: "/app1"})
+ })
+
+ it("should get apps when in cloud", async () => {
+ await setup.switchToCloudForFunction(async () => {
+ const res = await request
+ .get(`/api/hosting/apps`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.app1).toEqual({url: "/app1"})
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/integration.spec.js b/packages/server/src/api/routes/tests/integration.spec.js
new file mode 100644
index 0000000000..528d0d3417
--- /dev/null
+++ b/packages/server/src/api/routes/tests/integration.spec.js
@@ -0,0 +1,52 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+
+describe("/integrations", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("fetch", () => {
+ it("should be able to get all integration definitions", async () => {
+ const res = await request
+ .get(`/api/integrations`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.POSTGRES).toBeDefined()
+ expect(res.body.POSTGRES.friendlyName).toEqual("PostgreSQL")
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/integrations`,
+ })
+ })
+ })
+
+ describe("find", () => {
+ it("should be able to get postgres definition", async () => {
+ const res = await request
+ .get(`/api/integrations/POSTGRES`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.friendlyName).toEqual("PostgreSQL")
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/integrations/POSTGRES`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/layout.spec.js b/packages/server/src/api/routes/tests/layout.spec.js
new file mode 100644
index 0000000000..6b21554d71
--- /dev/null
+++ b/packages/server/src/api/routes/tests/layout.spec.js
@@ -0,0 +1,55 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+const { basicLayout } = require("./utilities/structures")
+
+describe("/layouts", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ let layout
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ layout = await config.createLayout()
+ })
+
+ describe("save", () => {
+ it("should be able to create a layout", async () => {
+ const res = await request
+ .post(`/api/layouts`)
+ .send(basicLayout())
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body._rev).toBeDefined()
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "POST",
+ url: `/api/layouts`,
+ })
+ })
+ })
+
+ describe("destroy", () => {
+ it("should be able to delete the layout", async () => {
+ const res = await request
+ .delete(`/api/layouts/${layout._id}/${layout._rev}`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.message).toBeDefined()
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "DELETE",
+ url: `/api/layouts/${layout._id}/${layout._rev}`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/misc.spec.js b/packages/server/src/api/routes/tests/misc.spec.js
new file mode 100644
index 0000000000..3d3b6047e2
--- /dev/null
+++ b/packages/server/src/api/routes/tests/misc.spec.js
@@ -0,0 +1,38 @@
+const setup = require("./utilities")
+
+describe("/analytics", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("isEnabled", () => {
+ it("check if analytics enabled", async () => {
+ const res = await request
+ .get(`/api/analytics`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(typeof res.body.enabled).toEqual("boolean")
+ })
+ })
+})
+
+describe("/health", () => {
+ it("should confirm healthy", async () => {
+ let config = setup.getConfig()
+ await config.getRequest().get("/health").expect(200)
+ })
+})
+
+describe("/version", () => {
+ it("should confirm version", async () => {
+ const config = setup.getConfig()
+ const res = await config.getRequest().get("/version").expect(200)
+ expect(res.text.split(".").length).toEqual(3)
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/permissions.spec.js b/packages/server/src/api/routes/tests/permissions.spec.js
index 93e6e29131..b24fac57c0 100644
--- a/packages/server/src/api/routes/tests/permissions.spec.js
+++ b/packages/server/src/api/routes/tests/permissions.spec.js
@@ -107,4 +107,19 @@ describe("/permission", () => {
expect(res.status).toEqual(403)
})
})
+
+ describe("fetch builtins", () => {
+ it("should be able to fetch builtin definitions", async () => {
+ const res = await request
+ .get(`/api/permission/builtin`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(Array.isArray(res.body)).toEqual(true)
+ const publicPerm = res.body.find(perm => perm._id === "public")
+ expect(publicPerm).toBeDefined()
+ expect(publicPerm.permissions).toBeDefined()
+ expect(publicPerm.name).toBeDefined()
+ })
+ })
})
diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js
index 765baa4426..aa0e5428c5 100644
--- a/packages/server/src/api/routes/tests/query.spec.js
+++ b/packages/server/src/api/routes/tests/query.spec.js
@@ -1,17 +1,32 @@
+// mock out postgres for this
+jest.mock("pg")
+
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
-const { basicQuery } = require("./utilities/structures")
+const { basicQuery, basicDatasource } = require("./utilities/structures")
const setup = require("./utilities")
describe("/queries", () => {
let request = setup.getRequest()
let config = setup.getConfig()
+ let datasource, query
afterAll(setup.afterAll)
beforeEach(async () => {
await config.init()
+ datasource = await config.createDatasource()
+ query = await config.createQuery()
})
+ async function createInvalidIntegration() {
+ const datasource = await config.createDatasource({
+ ...basicDatasource(),
+ source: "INVALID_INTEGRATION",
+ })
+ const query = await config.createQuery()
+ return { datasource, query }
+ }
+
describe("create", () => {
it("should create a new query", async () => {
const { _id } = await config.createDatasource()
@@ -35,18 +50,7 @@ describe("/queries", () => {
})
describe("fetch", () => {
- let datasource
-
- beforeEach(async () => {
- datasource = await config.createDatasource()
- })
-
- afterEach(() => {
- delete datasource._rev
- })
-
it("returns all the queries from the server", async () => {
- const query = await config.createQuery()
const res = await request
.get(`/api/queries`)
.set(config.defaultHeaders())
@@ -73,20 +77,34 @@ describe("/queries", () => {
})
})
- describe("destroy", () => {
- let datasource
-
- beforeEach(async () => {
- datasource = await config.createDatasource()
- })
-
- afterEach(() => {
- delete datasource._rev
- })
-
- it("deletes a query and returns a success message", async () => {
+ describe("find", () => {
+ it("should find a query in builder", async () => {
const query = await config.createQuery()
+ const res = await request
+ .get(`/api/queries/${query._id}`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body._id).toEqual(query._id)
+ })
+ it("should find a query in cloud", async () => {
+ await setup.switchToCloudForFunction(async () => {
+ const query = await config.createQuery()
+ const res = await request
+ .get(`/api/queries/${query._id}`)
+ .set(await config.roleHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.fields).toBeUndefined()
+ expect(res.body.parameters).toBeUndefined()
+ expect(res.body.schema).toBeUndefined()
+ })
+ })
+ })
+
+ describe("destroy", () => {
+ it("deletes a query and returns a success message", async () => {
await request
.delete(`/api/queries/${query._id}/${query._rev}`)
.set(config.defaultHeaders())
@@ -105,8 +123,74 @@ describe("/queries", () => {
await checkBuilderEndpoint({
config,
method: "DELETE",
- url: `/api/datasources/${datasource._id}/${datasource._rev}`,
+ url: `/api/queries/${config._id}/${config._rev}`,
})
})
})
+
+ describe("preview", () => {
+ it("should be able to preview the query", async () => {
+ const res = await request
+ .post(`/api/queries/preview`)
+ .send({
+ datasourceId: datasource._id,
+ parameters: {},
+ fields: {},
+ queryVerb: "read",
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ // these responses come from the mock
+ expect(res.body.schemaFields).toEqual(["a", "b"])
+ expect(res.body.rows.length).toEqual(1)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "POST",
+ url: `/api/queries/preview`,
+ })
+ })
+
+ it("should fail with invalid integration type", async () => {
+ const { datasource } = await createInvalidIntegration()
+ await request
+ .post(`/api/queries/preview`)
+ .send({
+ datasourceId: datasource._id,
+ parameters: {},
+ fields: {},
+ queryVerb: "read",
+ })
+ .set(config.defaultHeaders())
+ .expect(400)
+ })
+ })
+
+ describe("execute", () => {
+ it("should be able to execute the query", async () => {
+ const res = await request
+ .post(`/api/queries/${query._id}`)
+ .send({
+ parameters: {},
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(1)
+ })
+
+ it("should fail with invalid integration type", async () => {
+ const { query } = await createInvalidIntegration()
+ await request
+ .post(`/api/queries/${query._id}`)
+ .send({
+ parameters: {},
+ })
+ .set(config.defaultHeaders())
+ .expect(400)
+ })
+ })
})
diff --git a/packages/server/src/api/routes/tests/role.spec.js b/packages/server/src/api/routes/tests/role.spec.js
index 9c08bf289a..9bb38b295a 100644
--- a/packages/server/src/api/routes/tests/role.spec.js
+++ b/packages/server/src/api/routes/tests/role.spec.js
@@ -34,14 +34,7 @@ describe("/roles", () => {
describe("fetch", () => {
it("should list custom roles, plus 2 default roles", async () => {
- const createRes = await request
- .post(`/api/roles`)
- .send(basicRole())
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
-
- const customRole = createRes.body
+ const customRole = await config.createRole()
const res = await request
.get(`/api/roles`)
@@ -68,24 +61,31 @@ describe("/roles", () => {
BUILTIN_PERMISSION_IDS.READ_ONLY
)
})
+
+ it("should be able to get the role with a permission added", async () => {
+ const table = await config.createTable()
+ await config.addPermission(BUILTIN_ROLE_IDS.POWER, table._id)
+ const res = await request
+ .get(`/api/roles`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.length).toBeGreaterThan(0)
+ const power = res.body.find(role => role._id === BUILTIN_ROLE_IDS.POWER)
+ expect(power.permissions[table._id]).toEqual("read")
+ })
})
describe("destroy", () => {
it("should delete custom roles", async () => {
- const createRes = await request
- .post(`/api/roles`)
- .send({ name: "user", permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY })
- .set(config.defaultHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
-
- const customRole = createRes.body
-
+ const customRole = await config.createRole({
+ name: "user",
+ permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY
+ })
await request
.delete(`/api/roles/${customRole._id}/${customRole._rev}`)
.set(config.defaultHeaders())
.expect(200)
-
await request
.get(`/api/roles/${customRole._id}`)
.set(config.defaultHeaders())
diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js
new file mode 100644
index 0000000000..70d1632bf3
--- /dev/null
+++ b/packages/server/src/api/routes/tests/routing.spec.js
@@ -0,0 +1,84 @@
+const setup = require("./utilities")
+const { basicScreen } = require("./utilities/structures")
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
+
+const route = "/test"
+
+describe("/routing", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ let screen, screen2
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ screen = basicScreen()
+ screen.routing.route = route
+ screen = await config.createScreen(screen)
+ screen2 = basicScreen()
+ screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
+ screen2.routing.route = route
+ screen2 = await config.createScreen(screen2)
+ })
+
+ describe("fetch", () => {
+ it("returns the correct routing for basic user", async () => {
+ const res = await request
+ .get(`/api/routing/client`)
+ .set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.BASIC))
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.routes).toBeDefined()
+ expect(res.body.routes[route]).toEqual({
+ subpaths: {
+ [route]: {
+ screenId: screen._id,
+ roleId: screen.routing.roleId
+ }
+ }
+ })
+ })
+
+ it("returns the correct routing for power user", async () => {
+ const res = await request
+ .get(`/api/routing/client`)
+ .set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.POWER))
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.routes).toBeDefined()
+ expect(res.body.routes[route]).toEqual({
+ subpaths: {
+ [route]: {
+ screenId: screen2._id,
+ roleId: screen2.routing.roleId
+ }
+ }
+ })
+ })
+ })
+
+ describe("fetch all", () => {
+ it("should fetch all routes for builder", async () => {
+ const res = await request
+ .get(`/api/routing`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.routes).toBeDefined()
+ expect(res.body.routes[route].subpaths[route]).toBeDefined()
+ const subpath = res.body.routes[route].subpaths[route]
+ expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id)
+ expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id)
+ })
+
+ it("make sure it is a builder only endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/routing`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/row.spec.js b/packages/server/src/api/routes/tests/row.spec.js
index cc50520b77..1442e4eb75 100644
--- a/packages/server/src/api/routes/tests/row.spec.js
+++ b/packages/server/src/api/routes/tests/row.spec.js
@@ -17,15 +17,15 @@ describe("/rows", () => {
row = basicRow(table._id)
})
- const loadRow = async id =>
+ const loadRow = async (id, status = 200) =>
await request
.get(`/api/${table._id}/rows/${id}`)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
- .expect(200)
+ .expect(status)
- describe("save, load, update, delete", () => {
+ describe("save, load, update", () => {
it("returns a success message when the row is created", async () => {
const res = await request
.post(`/api/${row.tableId}/rows`)
@@ -217,38 +217,152 @@ describe("/rows", () => {
expect(savedRow.body.description).toEqual(existing.description)
expect(savedRow.body.name).toEqual("Updated Name")
-
+ })
+
+ it("should throw an error when given improper types", async () => {
+ const existing = await config.createRow()
+ await request
+ .patch(`/api/${table._id}/rows/${existing._id}`)
+ .send({
+ _id: existing._id,
+ _rev: existing._rev,
+ tableId: table._id,
+ name: 1,
+ })
+ .set(config.defaultHeaders())
+ .expect(400)
+ })
+ })
+
+ describe("destroy", () => {
+ it("should be able to delete a row", async () => {
+ const createdRow = await config.createRow(row)
+ const res = await request
+ .delete(`/api/${table._id}/rows/${createdRow._id}/${createdRow._rev}`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.ok).toEqual(true)
+ })
+
+ it("shouldn't allow deleting a row in a table which is different to the one the row was created on", async () => {
+ const createdRow = await config.createRow(row)
+ await request
+ .delete(`/api/wrong_table/rows/${createdRow._id}/${createdRow._rev}`)
+ .set(config.defaultHeaders())
+ .expect(400)
})
})
describe("validate", () => {
it("should return no errors on valid row", async () => {
- const result = await request
+ const res = await request
.post(`/api/${table._id}/rows/validate`)
.send({ name: "ivan" })
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
- expect(result.body.valid).toBe(true)
- expect(Object.keys(result.body.errors)).toEqual([])
+ expect(res.body.valid).toBe(true)
+ expect(Object.keys(res.body.errors)).toEqual([])
})
it("should errors on invalid row", async () => {
- const result = await request
+ const res = await request
.post(`/api/${table._id}/rows/validate`)
.send({ name: 1 })
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
- expect(result.body.valid).toBe(false)
- expect(Object.keys(result.body.errors)).toEqual(["name"])
+ expect(res.body.valid).toBe(false)
+ expect(Object.keys(res.body.errors)).toEqual(["name"])
})
})
- describe("enrich row unit test", () => {
+ describe("bulkDelete", () => {
+ it("should be able to delete a bulk set of rows", async () => {
+ const row1 = await config.createRow()
+ const row2 = await config.createRow()
+ const res = await request
+ .post(`/api/${table._id}/rows`)
+ .send({
+ type: "delete",
+ rows: [
+ row1,
+ row2,
+ ]
+ })
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(2)
+ await loadRow(row1._id, 404)
+ })
+ })
+
+ describe("search", () => {
+ it("should run a search on the table", async () => {
+ const row = await config.createRow()
+ // add another row that shouldn't be found
+ await config.createRow({
+ ...basicRow(),
+ name: "Other Contact",
+ })
+ const res = await request
+ .post(`/api/${table._id}/rows/search`)
+ .send({
+ query: {
+ name: "Test",
+ },
+ pagination: { pageSize: 25, page: 0 }
+ })
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(1)
+ expect(res.body[0]._id).toEqual(row._id)
+ })
+ })
+
+ describe("fetchView", () => {
+ it("should be able to fetch tables contents via 'view'", async () => {
+ const row = await config.createRow()
+ const res = await request
+ .get(`/api/views/all_${table._id}`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(1)
+ expect(res.body[0]._id).toEqual(row._id)
+ })
+
+ it("should throw an error if view doesn't exist", async () => {
+ await request
+ .get(`/api/views/derp`)
+ .set(config.defaultHeaders())
+ .expect(400)
+ })
+
+ it("should be able to run on a view", async () => {
+ const view = await config.createView()
+ const row = await config.createRow()
+ const res = await request
+ .get(`/api/views/${view._id}`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(1)
+ expect(res.body[0]._id).toEqual(row._id)
+ })
+ })
+
+ describe("user testing", () => {
+
+ })
+
+ describe("fetchEnrichedRows", () => {
it("should allow enriching some linked rows", async () => {
const table = await config.createLinkedTable()
const firstRow = await config.createRow({
@@ -262,30 +376,45 @@ describe("/rows", () => {
link: [{_id: firstRow._id}],
tableId: table._id,
})
- const enriched = await outputProcessing(config.getAppId(), table, [secondRow])
- expect(enriched[0].link.length).toBe(1)
- expect(enriched[0].link[0]._id).toBe(firstRow._id)
- expect(enriched[0].link[0].primaryDisplay).toBe("Test Contact")
+
+ // test basic enrichment
+ const resBasic = await request
+ .get(`/api/${table._id}/rows/${secondRow._id}`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(resBasic.body.link[0]._id).toBe(firstRow._id)
+ expect(resBasic.body.link[0].primaryDisplay).toBe("Test Contact")
+
+ // test full enrichment
+ const resEnriched = await request
+ .get(`/api/${table._id}/${secondRow._id}/enrich`)
+ .set(config.defaultHeaders())
+ .expect('Content-Type', /json/)
+ .expect(200)
+ expect(resEnriched.body.link.length).toBe(1)
+ expect(resEnriched.body.link[0]._id).toBe(firstRow._id)
+ expect(resEnriched.body.link[0].name).toBe("Test Contact")
+ expect(resEnriched.body.link[0].description).toBe("original description")
})
})
- it("should allow enriching attachment rows", async () => {
- const table = await config.createAttachmentTable()
- const row = await config.createRow({
- name: "test",
- description: "test",
- attachment: [{
- url: "/test/thing",
- }],
- tableId: table._id,
+ describe("attachments", () => {
+ it("should allow enriching attachment rows", async () => {
+ const table = await config.createAttachmentTable()
+ const row = await config.createRow({
+ name: "test",
+ description: "test",
+ attachment: [{
+ url: "/test/thing",
+ }],
+ tableId: table._id,
+ })
+ // the environment needs configured for this
+ await setup.switchToCloudForFunction(async () => {
+ const enriched = await outputProcessing(config.getAppId(), table, [row])
+ expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`)
+ })
})
- // the environment needs configured for this
- env.CLOUD = 1
- env.SELF_HOSTED = 1
- const enriched = await outputProcessing(config.getAppId(), table, [row])
- expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${config.getAppId()}/test/thing`)
- // remove env config
- env.CLOUD = undefined
- env.SELF_HOSTED = undefined
})
})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/screen.spec.js b/packages/server/src/api/routes/tests/screen.spec.js
new file mode 100644
index 0000000000..ae30afd29c
--- /dev/null
+++ b/packages/server/src/api/routes/tests/screen.spec.js
@@ -0,0 +1,77 @@
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const setup = require("./utilities")
+const { basicScreen } = require("./utilities/structures")
+
+describe("/screens", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ let screen
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ screen = await config.createScreen()
+ })
+
+ describe("fetch", () => {
+ it("should be able to create a layout", async () => {
+ const res = await request
+ .get(`/api/screens`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.length).toEqual(3)
+ expect(res.body.some(s => s._id === screen._id)).toEqual(true)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/screens`,
+ })
+ })
+ })
+
+ describe("save", () => {
+ it("should be able to save a screen", async () => {
+ const screenCfg = basicScreen()
+ const res = await request
+ .post(`/api/screens`)
+ .send(screenCfg)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body._rev).toBeDefined()
+ expect(res.body.name).toEqual(screenCfg.name)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "POST",
+ url: `/api/screens`,
+ })
+ })
+ })
+
+ describe("destroy", () => {
+ it("should be able to delete the screen", async () => {
+ const res = await request
+ .delete(`/api/screens/${screen._id}/${screen._rev}`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.message).toBeDefined()
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "DELETE",
+ url: `/api/screens/${screen._id}/${screen._rev}`,
+ })
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/templates.spec.js b/packages/server/src/api/routes/tests/templates.spec.js
new file mode 100644
index 0000000000..f0d26bc7db
--- /dev/null
+++ b/packages/server/src/api/routes/tests/templates.spec.js
@@ -0,0 +1,49 @@
+const setup = require("./utilities")
+const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
+const fs = require("fs")
+const { join } = require("path")
+
+describe("/templates", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ describe("fetch", () => {
+ it("should be able to fetch templates", async () => {
+ const res = await request
+ .get(`/api/templates`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ // this test is quite light right now, templates aren't heavily utilised yet
+ expect(Array.isArray(res.body)).toEqual(true)
+ })
+ })
+
+ describe("export", () => {
+ it("should be able to export the basic app", async () => {
+ const res = await request
+ .post(`/api/templates`)
+ .send({
+ templateName: "test",
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.message).toEqual("Created template: test")
+ const dir = join(
+ budibaseAppsDir(),
+ "templates",
+ "app",
+ "test",
+ "db"
+ )
+ expect(fs.existsSync(dir)).toEqual(true)
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js
index 6ec607a093..5e7ec9e9d4 100644
--- a/packages/server/src/api/routes/tests/user.spec.js
+++ b/packages/server/src/api/routes/tests/user.spec.js
@@ -42,15 +42,19 @@ describe("/users", () => {
})
describe("create", () => {
+ async function create(user, status = 200) {
+ return request
+ .post(`/api/users`)
+ .set(config.defaultHeaders())
+ .send(user)
+ .expect(status)
+ .expect("Content-Type", /json/)
+ }
+
it("returns a success message when a user is successfully created", async () => {
const body = basicUser(BUILTIN_ROLE_IDS.POWER)
body.email = "bill@budibase.com"
- const res = await request
- .post(`/api/users`)
- .set(config.defaultHeaders())
- .send(body)
- .expect(200)
- .expect("Content-Type", /json/)
+ const res = await create(body)
expect(res.res.statusMessage).toEqual("User created successfully.")
expect(res.body._id).toBeUndefined()
@@ -68,5 +72,65 @@ describe("/users", () => {
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
+
+ it("should error if no email provided", async () => {
+ const user = basicUser(BUILTIN_ROLE_IDS.POWER)
+ delete user.email
+ await create(user, 400)
+ })
+
+ it("should error if no role provided", async () => {
+ const user = basicUser(null)
+ await create(user, 400)
+ })
+
+ it("should throw error if user exists already", async () => {
+ await config.createUser("test@test.com")
+ const user = basicUser(BUILTIN_ROLE_IDS.POWER)
+ user.email = "test@test.com"
+ await create(user, 400)
+ })
+ })
+
+ describe("update", () => {
+ it("should be able to update the user", async () => {
+ const user = await config.createUser()
+ user.roleId = BUILTIN_ROLE_IDS.BASIC
+ const res = await request
+ .put(`/api/users`)
+ .set(config.defaultHeaders())
+ .send(user)
+ .expect(200)
+ .expect("Content-Type", /json/)
+ expect(res.body.ok).toEqual(true)
+ })
+ })
+
+ describe("destroy", () => {
+ it("should be able to delete the user", async () => {
+ const email = "test@test.com"
+ await config.createUser(email)
+ const res = await request
+ .delete(`/api/users/${email}`)
+ .set(config.defaultHeaders())
+ .expect(200)
+ .expect("Content-Type", /json/)
+ expect(res.body.message).toBeDefined()
+ })
+ })
+
+ describe("find", () => {
+ it("should be able to find the user", async () => {
+ const email = "test@test.com"
+ await config.createUser(email)
+ const res = await request
+ .get(`/api/users/${email}`)
+ .set(config.defaultHeaders())
+ .expect(200)
+ .expect("Content-Type", /json/)
+ expect(res.body.email).toEqual(email)
+ expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.POWER)
+ expect(res.body.tableId).toBeDefined()
+ })
})
})
diff --git a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js
index 5f6b1cc267..0ff742293d 100644
--- a/packages/server/src/api/routes/tests/utilities/TestConfiguration.js
+++ b/packages/server/src/api/routes/tests/utilities/TestConfiguration.js
@@ -8,9 +8,15 @@ const {
basicAutomation,
basicDatasource,
basicQuery,
+ basicScreen,
+ basicLayout,
+ basicWebhook,
} = require("./structures")
const controllers = require("./controllers")
const supertest = require("supertest")
+const fs = require("fs")
+const { budibaseAppsDir } = require("../../../../utilities/budibaseDir")
+const { join } = require("path")
const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password"
@@ -22,6 +28,7 @@ class TestConfiguration {
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
this.appId = null
+ this.allApps = []
}
getRequest() {
@@ -55,6 +62,13 @@ class TestConfiguration {
end() {
this.server.close()
+ const appDir = budibaseAppsDir()
+ const files = fs.readdirSync(appDir)
+ for (let file of files) {
+ if (this.allApps.some(app => file.includes(app._id))) {
+ fs.rmdirSync(join(appDir, file), { recursive: true })
+ }
+ }
}
defaultHeaders() {
@@ -83,9 +97,19 @@ class TestConfiguration {
return headers
}
+ async roleHeaders(email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN) {
+ try {
+ await this.createUser(email, PASSWORD, roleId)
+ } catch (err) {
+ // allow errors here
+ }
+ return this.login(email, PASSWORD)
+ }
+
async createApp(appName) {
this.app = await this._req({ name: appName }, null, controllers.app.create)
this.appId = this.app._id
+ this.allApps.push(this.app)
return this.app
}
@@ -208,6 +232,24 @@ class TestConfiguration {
return this._req(config, null, controllers.query.save)
}
+ async createScreen(config = null) {
+ config = config || basicScreen()
+ return this._req(config, null, controllers.screen.save)
+ }
+
+ async createWebhook(config = null) {
+ if (!this.automation) {
+ throw "Must create an automation before creating webhook."
+ }
+ config = config || basicWebhook(this.automation._id)
+ return (await this._req(config, null, controllers.webhook.save)).webhook
+ }
+
+ async createLayout(config = null) {
+ config = config || basicLayout()
+ return await this._req(config, null, controllers.layout.save)
+ }
+
async createUser(
email = EMAIL,
password = PASSWORD,
@@ -224,6 +266,24 @@ class TestConfiguration {
)
}
+ async makeUserInactive(email) {
+ const user = await this._req(
+ null,
+ {
+ email,
+ },
+ controllers.user.find
+ )
+ return this._req(
+ {
+ ...user,
+ status: "inactive",
+ },
+ null,
+ controllers.user.update
+ )
+ }
+
async login(email, password) {
if (!email || !password) {
await this.createUser()
@@ -241,6 +301,7 @@ class TestConfiguration {
return {
Accept: "application/json",
Cookie: result.headers["set-cookie"],
+ "x-budibase-app-id": this.appId,
}
}
}
diff --git a/packages/server/src/api/routes/tests/utilities/controllers.js b/packages/server/src/api/routes/tests/utilities/controllers.js
index 541495bec8..a4eb9ac9de 100644
--- a/packages/server/src/api/routes/tests/utilities/controllers.js
+++ b/packages/server/src/api/routes/tests/utilities/controllers.js
@@ -9,4 +9,7 @@ module.exports = {
automation: require("../../../controllers/automation"),
datasource: require("../../../controllers/datasource"),
query: require("../../../controllers/query"),
+ screen: require("../../../controllers/screen"),
+ webhook: require("../../../controllers/webhook"),
+ layout: require("../../../controllers/layout"),
}
diff --git a/packages/server/src/api/routes/tests/utilities/index.js b/packages/server/src/api/routes/tests/utilities/index.js
index 7e9260ce18..7126f141e2 100644
--- a/packages/server/src/api/routes/tests/utilities/index.js
+++ b/packages/server/src/api/routes/tests/utilities/index.js
@@ -1,4 +1,5 @@
const TestConfig = require("./TestConfiguration")
+const env = require("../../../../environment")
exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms))
@@ -13,6 +14,8 @@ exports.afterAll = () => {
if (config) {
config.end()
}
+ // clear app files
+
request = null
config = null
}
@@ -30,3 +33,21 @@ exports.getConfig = () => {
}
return config
}
+
+exports.switchToCloudForFunction = async func => {
+ // self hosted stops any attempts to Dynamo
+ env.CLOUD = true
+ env.SELF_HOSTED = true
+ let error
+ try {
+ await func()
+ } catch (err) {
+ error = err
+ }
+ env.CLOUD = false
+ env.SELF_HOSTED = false
+ // don't throw error until after reset
+ if (error) {
+ throw error
+ }
+}
diff --git a/packages/server/src/api/routes/tests/utilities/structures.js b/packages/server/src/api/routes/tests/utilities/structures.js
index 922228aadf..ff3a239211 100644
--- a/packages/server/src/api/routes/tests/utilities/structures.js
+++ b/packages/server/src/api/routes/tests/utilities/structures.js
@@ -2,6 +2,9 @@ const { BUILTIN_ROLE_IDS } = require("../../../../utilities/security/roles")
const {
BUILTIN_PERMISSION_IDS,
} = require("../../../../utilities/security/permissions")
+const { createHomeScreen } = require("../../../../constants/screens")
+const { EMPTY_LAYOUT } = require("../../../../constants/layouts")
+const { cloneDeep } = require("lodash/fp")
exports.basicTable = () => {
return {
@@ -85,3 +88,22 @@ exports.basicUser = role => {
roleId: role,
}
}
+
+exports.basicScreen = () => {
+ return createHomeScreen()
+}
+
+exports.basicLayout = () => {
+ return cloneDeep(EMPTY_LAYOUT)
+}
+
+exports.basicWebhook = automationId => {
+ return {
+ live: true,
+ name: "webhook",
+ action: {
+ type: "automation",
+ target: automationId,
+ },
+ }
+}
diff --git a/packages/server/src/api/routes/tests/webhook.spec.js b/packages/server/src/api/routes/tests/webhook.spec.js
new file mode 100644
index 0000000000..2bf5445a09
--- /dev/null
+++ b/packages/server/src/api/routes/tests/webhook.spec.js
@@ -0,0 +1,130 @@
+const setup = require("./utilities")
+const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
+const { basicWebhook, basicAutomation } = require("./utilities/structures")
+
+describe("/webhooks", () => {
+ let request = setup.getRequest()
+ let config = setup.getConfig()
+ let webhook
+
+ afterAll(setup.afterAll)
+
+ beforeEach(async () => {
+ await config.init()
+ const autoConfig = basicAutomation()
+ autoConfig.definition.trigger = {
+ schema: { outputs: { properties: {} } },
+ inputs: {},
+ }
+ await config.createAutomation(autoConfig)
+ webhook = await config.createWebhook()
+ })
+
+ describe("create", () => {
+ it("should create a webhook successfully", async () => {
+ const automation = await config.createAutomation()
+ const res = await request
+ .put(`/api/webhooks`)
+ .send(basicWebhook(automation._id))
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.webhook).toBeDefined()
+ expect(typeof res.body.webhook._id).toEqual("string")
+ expect(typeof res.body.webhook._rev).toEqual("string")
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "PUT",
+ url: `/api/webhooks`,
+ })
+ })
+ })
+
+ describe("fetch", () => {
+ it("returns the correct routing for basic user", async () => {
+ const res = await request
+ .get(`/api/webhooks`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(Array.isArray(res.body)).toEqual(true)
+ expect(res.body[0]._id).toEqual(webhook._id)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "GET",
+ url: `/api/webhooks`,
+ })
+ })
+ })
+
+ describe("delete", () => {
+ it("should successfully delete", async () => {
+ const res = await request
+ .delete(`/api/webhooks/${webhook._id}/${webhook._rev}`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body).toBeDefined()
+ expect(res.body.ok).toEqual(true)
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "DELETE",
+ url: `/api/webhooks/${webhook._id}/${webhook._rev}`,
+ })
+ })
+ })
+
+ describe("build schema", () => {
+ it("should allow building a schema", async () => {
+ const res = await request
+ .post(`/api/webhooks/schema/${config.getAppId()}/${webhook._id}`)
+ .send({
+ a: 1
+ })
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body).toBeDefined()
+ // fetch to see if the schema has been updated
+ const fetch = await request
+ .get(`/api/webhooks`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(fetch.body[0]).toBeDefined()
+ expect(fetch.body[0].bodySchema).toEqual({
+ properties: {
+ a: { type: "integer" }
+ },
+ type: "object",
+ })
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await checkBuilderEndpoint({
+ config,
+ method: "POST",
+ url: `/api/webhooks/schema/${config.getAppId()}/${webhook._id}`,
+ })
+ })
+ })
+
+ describe("trigger", () => {
+ it("should allow triggering from public", async () => {
+ const res = await request
+ .post(`/api/webhooks/trigger/${config.getAppId()}/${webhook._id}`)
+ .expect("Content-Type", /json/)
+ .expect(200)
+ expect(res.body.message).toBeDefined()
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js
index 1ad1d2363e..cdaab0cc5b 100644
--- a/packages/server/src/api/routes/user.js
+++ b/packages/server/src/api/routes/user.js
@@ -21,7 +21,7 @@ router
controller.find
)
.put(
- "/api/users/",
+ "/api/users",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.update
)
diff --git a/packages/server/src/app.js b/packages/server/src/app.js
index 3779890c9d..15e996cfe6 100644
--- a/packages/server/src/app.js
+++ b/packages/server/src/app.js
@@ -56,7 +56,11 @@ if (electron.app && electron.app.isPackaged) {
const server = http.createServer(app.callback())
destroyable(server)
-server.on("close", () => console.log("Server Closed"))
+server.on("close", () => {
+ if (env.NODE_ENV !== "jest") {
+ console.log("Server Closed")
+ }
+})
module.exports = server.listen(env.PORT || 0, async () => {
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js
index 659baa8f6c..32ed3f63d0 100644
--- a/packages/server/src/middleware/authenticated.js
+++ b/packages/server/src/middleware/authenticated.js
@@ -31,6 +31,7 @@ module.exports = async (ctx, next) => {
token = ctx.cookies.get(getCookieName())
authType = AuthTypes.BUILDER
}
+
if (!token && appId) {
token = ctx.cookies.get(getCookieName(appId))
authType = AuthTypes.APP
@@ -58,6 +59,7 @@ module.exports = async (ctx, next) => {
role: await getRole(appId, jwtPayload.roleId),
}
} catch (err) {
+ console.log(err)
if (authType === AuthTypes.BUILDER) {
clearCookie(ctx)
ctx.status = 200
diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js
index 7eac602f78..2a1caef2a2 100644
--- a/packages/server/src/middleware/authorized.js
+++ b/packages/server/src/middleware/authorized.js
@@ -13,7 +13,7 @@ const { AuthTypes } = require("../constants")
const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER]
-const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
+const LOCAL_PASS = new RegExp(["webhooks/trigger"].join("|"))
function hasResource(ctx) {
return ctx.resourceId != null
@@ -24,6 +24,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
return next()
}
+
if (env.CLOUD && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) {
// api key header passed by external webhook
if (await isAPIKeyValid(ctx.headers["x-api-key"])) {
@@ -37,14 +38,14 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
return next()
}
- ctx.throw(403, "API key invalid")
+ return ctx.throw(403, "API key invalid")
}
// don't expose builder endpoints in the cloud
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
if (!ctx.user) {
- ctx.throw(403, "No user info found")
+ return ctx.throw(403, "No user info found")
}
const role = ctx.user.role
@@ -52,7 +53,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
ctx.appId,
role._id
)
- const isAdmin = ADMIN_ROLES.indexOf(role._id) !== -1
+ const isAdmin = ADMIN_ROLES.includes(role._id)
const isAuthed = ctx.auth.authenticated
// this may need to change in the future, right now only admins
@@ -61,7 +62,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
if (isAdmin && isAuthed) {
return next()
} else if (permType === PermissionTypes.BUILDER) {
- ctx.throw(403, "Not Authorized")
+ return ctx.throw(403, "Not Authorized")
}
if (
diff --git a/packages/server/src/middleware/resourceId.js b/packages/server/src/middleware/resourceId.js
index 4351901dad..4216131119 100644
--- a/packages/server/src/middleware/resourceId.js
+++ b/packages/server/src/middleware/resourceId.js
@@ -36,6 +36,8 @@ class ResourceIdGetter {
}
}
+module.exports.ResourceIdGetter = ResourceIdGetter
+
module.exports.paramResource = main => {
return new ResourceIdGetter("params").mainResource(main).build()
}
diff --git a/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap b/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap
new file mode 100644
index 0000000000..1583ecb51f
--- /dev/null
+++ b/packages/server/src/middleware/tests/__snapshots__/authenticated.spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = `
+Object {
+ "apiKey": "1234",
+ "appId": "budibase:app:local",
+ "role": Role {
+ "_id": "ADMIN",
+ "inherits": "POWER",
+ "name": "Admin",
+ "permissionId": "admin",
+ },
+ "roleId": "ADMIN",
+}
+`;
+
+exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = `
+Object {
+ "apiKey": "1234",
+ "appId": "budibase:builder:local",
+ "role": Role {
+ "_id": "BUILDER",
+ "name": "Builder",
+ "permissionId": "admin",
+ },
+ "roleId": "BUILDER",
+}
+`;
diff --git a/packages/server/src/middleware/tests/authenticated.spec.js b/packages/server/src/middleware/tests/authenticated.spec.js
new file mode 100644
index 0000000000..fe7e592528
--- /dev/null
+++ b/packages/server/src/middleware/tests/authenticated.spec.js
@@ -0,0 +1,125 @@
+const { AuthTypes } = require("../../constants")
+const authenticatedMiddleware = require("../authenticated")
+const jwt = require("jsonwebtoken")
+jest.mock("jsonwebtoken")
+
+class TestConfiguration {
+ constructor(middleware) {
+ this.middleware = authenticatedMiddleware
+ this.ctx = {
+ config: {},
+ auth: {},
+ cookies: {
+ set: jest.fn(),
+ get: jest.fn()
+ },
+ headers: {},
+ params: {},
+ path: "",
+ request: {
+ headers: {}
+ },
+ throw: jest.fn()
+ }
+ this.next = jest.fn()
+ }
+
+ setHeaders(headers) {
+ this.ctx.headers = headers
+ }
+
+ executeMiddleware() {
+ return this.middleware(this.ctx, this.next)
+ }
+
+ afterEach() {
+ jest.resetAllMocks()
+ }
+}
+
+describe("Authenticated middleware", () => {
+ let config
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ })
+
+ afterEach(() => {
+ config.afterEach()
+ })
+
+ it("calls next() when on the builder path", async () => {
+ config.ctx.path = "/_builder"
+
+ await config.executeMiddleware()
+
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("sets a new cookie when the current cookie does not match the app id from context", async () => {
+ const appId = "app_123"
+ config.setHeaders({
+ "x-budibase-app-id": appId
+ })
+ config.ctx.cookies.get.mockImplementation(() => "cookieAppId")
+
+ await config.executeMiddleware()
+
+ expect(config.ctx.cookies.set).toHaveBeenCalledWith(
+ "budibase:currentapp:local",
+ appId,
+ expect.any(Object)
+ )
+
+ })
+
+ it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => {
+ config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
+ jwt.verify.mockImplementationOnce(() => ({
+ apiKey: "1234",
+ roleId: "BUILDER"
+ }))
+
+ await config.executeMiddleware()
+
+ expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER)
+ expect(config.ctx.user).toMatchSnapshot()
+ })
+
+ it("sets the correct APP auth type information when the user is not in the builder", async () => {
+ config.setHeaders({
+ "x-budibase-type": "client"
+ })
+ config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`)
+ jwt.verify.mockImplementationOnce(() => ({
+ apiKey: "1234",
+ roleId: "ADMIN"
+ }))
+
+ await config.executeMiddleware()
+
+ expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP)
+ expect(config.ctx.user).toMatchSnapshot()
+ })
+
+ it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => {
+ config.executeMiddleware()
+ expect(config.ctx.auth.authenticated).toBe(false)
+ expect(config.ctx.user.role).toEqual({
+ _id: "PUBLIC",
+ name: "Public",
+ permissionId: "public"
+ })
+ })
+
+ it("clears the cookie when there is an error authenticating in the builder", async () => {
+ config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
+ jwt.verify.mockImplementationOnce(() => {
+ throw new Error()
+ })
+
+ await config.executeMiddleware()
+
+ expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local")
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/middleware/tests/authorized.spec.js b/packages/server/src/middleware/tests/authorized.spec.js
new file mode 100644
index 0000000000..d3e5e52d2d
--- /dev/null
+++ b/packages/server/src/middleware/tests/authorized.spec.js
@@ -0,0 +1,196 @@
+const authorizedMiddleware = require("../authorized")
+const env = require("../../environment")
+const apiKey = require("../../utilities/security/apikey")
+const { AuthTypes } = require("../../constants")
+const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions")
+const { Test } = require("supertest")
+jest.mock("../../environment")
+jest.mock("../../utilities/security/apikey")
+
+class TestConfiguration {
+ constructor(role) {
+ this.middleware = authorizedMiddleware(role)
+ this.next = jest.fn()
+ this.throw = jest.fn()
+ this.ctx = {
+ headers: {},
+ request: {
+ url: ""
+ },
+ auth: {},
+ next: this.next,
+ throw: this.throw
+ }
+ }
+
+ executeMiddleware() {
+ return this.middleware(this.ctx, this.next)
+ }
+
+ setUser(user) {
+ this.ctx.user = user
+ }
+
+ setMiddlewareRequiredPermission(...perms) {
+ this.middleware = authorizedMiddleware(...perms)
+ }
+
+ setResourceId(id) {
+ this.ctx.resourceId = id
+ }
+
+ setAuthenticated(isAuthed) {
+ this.ctx.auth = { authenticated: isAuthed }
+ }
+
+ setRequestUrl(url) {
+ this.ctx.request.url = url
+ }
+
+ setCloudEnv(isCloud) {
+ env.CLOUD = isCloud
+ }
+
+ setRequestHeaders(headers) {
+ this.ctx.headers = headers
+ }
+
+ afterEach() {
+ jest.clearAllMocks()
+ }
+}
+
+
+describe("Authorization middleware", () => {
+ const next = jest.fn()
+ let config
+
+ afterEach(() => {
+ config.afterEach()
+ })
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ })
+
+ it("passes the middleware for local webhooks", async () => {
+ config.setRequestUrl("https://something/webhooks/trigger")
+ await config.executeMiddleware()
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ describe("external web hook call", () => {
+ let ctx = {}
+ let middleware
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ config.setCloudEnv(true)
+ config.setRequestHeaders({
+ "x-api-key": "abc123",
+ "x-instanceid": "instance123",
+ })
+ })
+
+ it("passes to next() if api key is valid", async () => {
+ apiKey.isAPIKeyValid.mockResolvedValueOnce(true)
+
+ await config.executeMiddleware()
+
+ expect(config.next).toHaveBeenCalled()
+ expect(config.ctx.auth).toEqual({
+ authenticated: AuthTypes.EXTERNAL,
+ apiKey: config.ctx.headers["x-api-key"],
+ })
+ expect(config.ctx.user).toEqual({
+ appId: config.ctx.headers["x-instanceid"],
+ })
+ })
+
+ it("throws if api key is invalid", async () => {
+ apiKey.isAPIKeyValid.mockResolvedValueOnce(false)
+
+ await config.executeMiddleware()
+
+ expect(config.throw).toHaveBeenCalledWith(403, "API key invalid")
+ })
+ })
+
+ describe("non-webhook call", () => {
+ let config
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ config.setCloudEnv(true)
+ config.setAuthenticated(true)
+ })
+
+ it("throws when no user data is present in context", async () => {
+ await config.executeMiddleware()
+
+ expect(config.throw).toHaveBeenCalledWith(403, "No user info found")
+ })
+
+ it("passes on to next() middleware if user is an admin", async () => {
+ config.setUser({
+ role: {
+ _id: "ADMIN",
+ }
+ })
+
+ await config.executeMiddleware()
+
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("throws if the user has only builder permissions", async () => {
+ config.setCloudEnv(false)
+ config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER)
+ config.setUser({
+ role: {
+ _id: ""
+ }
+ })
+ await config.executeMiddleware()
+
+ expect(config.throw).toHaveBeenCalledWith(403, "Not Authorized")
+ })
+
+ it("passes on to next() middleware if the user has resource permission", async () => {
+ config.setResourceId(PermissionTypes.QUERY)
+ config.setUser({
+ role: {
+ _id: ""
+ }
+ })
+ config.setMiddlewareRequiredPermission(PermissionTypes.QUERY)
+
+ await config.executeMiddleware()
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("throws if the user session is not authenticated after permission checks", async () => {
+ config.setUser({
+ role: {
+ _id: ""
+ },
+ })
+ config.setAuthenticated(false)
+
+ await config.executeMiddleware()
+ expect(config.throw).toHaveBeenCalledWith(403, "Session not authenticated")
+ })
+
+ it("throws if the user does not have base permissions to perform the operation", async () => {
+ config.setUser({
+ role: {
+ _id: ""
+ },
+ })
+ config.setMiddlewareRequiredPermission(PermissionTypes.ADMIN, PermissionLevels.BASIC)
+
+ await config.executeMiddleware()
+ expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission")
+ })
+ })
+})
diff --git a/packages/server/src/middleware/tests/resourceId.spec.js b/packages/server/src/middleware/tests/resourceId.spec.js
new file mode 100644
index 0000000000..35e6e5af50
--- /dev/null
+++ b/packages/server/src/middleware/tests/resourceId.spec.js
@@ -0,0 +1,105 @@
+const {
+ paramResource,
+ paramSubResource,
+ bodyResource,
+ bodySubResource,
+ ResourceIdGetter
+} = require("../resourceId")
+
+class TestConfiguration {
+ constructor(middleware) {
+ this.middleware = middleware
+ this.ctx = {
+ request: {},
+ }
+ this.next = jest.fn()
+ }
+
+ setParams(params) {
+ this.ctx.params = params
+ }
+
+ setBody(body) {
+ this.ctx.body = body
+ }
+
+ executeMiddleware() {
+ return this.middleware(this.ctx, this.next)
+ }
+}
+
+describe("resourceId middleware", () => {
+ it("calls next() when there is no request object to parse", () => {
+ const config = new TestConfiguration(paramResource("main"))
+
+ config.executeMiddleware()
+
+ expect(config.next).toHaveBeenCalled()
+ expect(config.ctx.resourceId).toBeUndefined()
+ })
+
+ it("generates a resourceId middleware for context query parameters", () => {
+ const config = new TestConfiguration(paramResource("main"))
+ config.setParams({
+ main: "test"
+ })
+
+ config.executeMiddleware()
+
+ expect(config.ctx.resourceId).toEqual("test")
+ })
+
+ it("generates a resourceId middleware for context query sub parameters", () => {
+ const config = new TestConfiguration(paramSubResource("main", "sub"))
+ config.setParams({
+ main: "main",
+ sub: "test"
+ })
+
+ config.executeMiddleware()
+
+ expect(config.ctx.resourceId).toEqual("main")
+ expect(config.ctx.subResourceId).toEqual("test")
+ })
+
+ it("generates a resourceId middleware for context request body", () => {
+ const config = new TestConfiguration(bodyResource("main"))
+ config.setBody({
+ main: "test"
+ })
+
+ config.executeMiddleware()
+
+ expect(config.ctx.resourceId).toEqual("test")
+ })
+
+ it("generates a resourceId middleware for context request body sub fields", () => {
+ const config = new TestConfiguration(bodySubResource("main", "sub"))
+ config.setBody({
+ main: "main",
+ sub: "test"
+ })
+
+ config.executeMiddleware()
+
+ expect(config.ctx.resourceId).toEqual("main")
+ expect(config.ctx.subResourceId).toEqual("test")
+ })
+
+ it("parses resourceIds correctly for custom middlewares", () => {
+ const middleware = new ResourceIdGetter("body")
+ .mainResource("custom")
+ .subResource("customSub")
+ .build()
+ config = new TestConfiguration(middleware)
+ config.setBody({
+ custom: "test",
+ customSub: "subtest"
+ })
+
+ config.executeMiddleware()
+
+ expect(config.ctx.resourceId).toEqual("test")
+ expect(config.ctx.subResourceId).toEqual("subtest")
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/middleware/tests/selfhost.spec.js b/packages/server/src/middleware/tests/selfhost.spec.js
new file mode 100644
index 0000000000..061da17f9c
--- /dev/null
+++ b/packages/server/src/middleware/tests/selfhost.spec.js
@@ -0,0 +1,75 @@
+const selfHostMiddleware = require("../selfhost");
+const env = require("../../environment")
+const hosting = require("../../utilities/builder/hosting");
+jest.mock("../../environment")
+jest.mock("../../utilities/builder/hosting")
+
+class TestConfiguration {
+ constructor() {
+ this.next = jest.fn()
+ this.throw = jest.fn()
+ this.middleware = selfHostMiddleware
+
+ this.ctx = {
+ next: this.next,
+ throw: this.throw
+ }
+ }
+
+ executeMiddleware() {
+ return this.middleware(this.ctx, this.next)
+ }
+
+ setCloudHosted() {
+ env.CLOUD = 1
+ env.SELF_HOSTED = 0
+ }
+
+ setSelfHosted() {
+ env.CLOUD = 0
+ env.SELF_HOSTED = 1
+ }
+
+ afterEach() {
+ jest.clearAllMocks()
+ }
+}
+
+describe("Self host middleware", () => {
+ let config
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ })
+
+ afterEach(() => {
+ config.afterEach()
+ })
+
+ it("calls next() when CLOUD and SELF_HOSTED env vars are set", async () => {
+ env.CLOUD = 1
+ env.SELF_HOSTED = 1
+
+ await config.executeMiddleware()
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("throws when hostingInfo type is cloud", async () => {
+ config.setSelfHosted()
+
+ hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.CLOUD }))
+
+ await config.executeMiddleware()
+ expect(config.throw).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.")
+ expect(config.next).not.toHaveBeenCalled()
+ })
+
+ it("calls the self hosting middleware to pass through to next() when the hostingInfo type is self", async () => {
+ config.setSelfHosted()
+
+ hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.SELF }))
+
+ await config.executeMiddleware()
+ expect(config.next).toHaveBeenCalled()
+ })
+})
diff --git a/packages/server/src/middleware/tests/usageQuota.spec.js b/packages/server/src/middleware/tests/usageQuota.spec.js
new file mode 100644
index 0000000000..c76acb47d2
--- /dev/null
+++ b/packages/server/src/middleware/tests/usageQuota.spec.js
@@ -0,0 +1,129 @@
+const usageQuotaMiddleware = require("../usageQuota")
+const usageQuota = require("../../utilities/usageQuota")
+const CouchDB = require("../../db")
+const env = require("../../environment")
+
+jest.mock("../../db");
+jest.mock("../../utilities/usageQuota")
+jest.mock("../../environment")
+
+class TestConfiguration {
+ constructor() {
+ this.throw = jest.fn()
+ this.next = jest.fn()
+ this.middleware = usageQuotaMiddleware
+ this.ctx = {
+ throw: this.throw,
+ next: this.next,
+ user: {
+ appId: "test"
+ },
+ request: {
+ body: {}
+ },
+ req: {
+ method: "POST",
+ url: "/rows"
+ }
+ }
+ }
+
+ executeMiddleware() {
+ return this.middleware(this.ctx, this.next)
+ }
+
+ cloudHosted(bool) {
+ if (bool) {
+ env.CLOUD = 1
+ this.ctx.auth = { apiKey: "test" }
+ } else {
+ env.CLOUD = 0
+ }
+ }
+
+ setMethod(method) {
+ this.ctx.req.method = method
+ }
+
+ setUrl(url) {
+ this.ctx.req.url = url
+ }
+
+ setBody(body) {
+ this.ctx.request.body = body
+ }
+
+ setFiles(files) {
+ this.ctx.request.files = { file: files }
+ }
+}
+
+describe("usageQuota middleware", () => {
+ let config
+
+ beforeEach(() => {
+ config = new TestConfiguration()
+ })
+
+ it("skips the middleware if there is no usage property or method", async () => {
+ await config.executeMiddleware()
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("passes through to next middleware if document already exists", async () => {
+ config.setBody({
+ _id: "test"
+ })
+
+ CouchDB.mockImplementationOnce(() => ({
+ get: async () => true
+ }))
+
+ await config.executeMiddleware()
+
+ expect(config.next).toHaveBeenCalled()
+ expect(config.ctx.preExisting).toBe(true)
+ })
+
+ it("throws if request has _id, but the document no longer exists", async () => {
+ config.setBody({
+ _id: "123"
+ })
+
+ CouchDB.mockImplementationOnce(() => ({
+ get: async () => {
+ throw new Error()
+ }
+ }))
+
+ await config.executeMiddleware()
+ expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`)
+ })
+
+ it("calculates and persists the correct usage quota for the relevant action", async () => {
+ config.setUrl("/rows")
+ config.cloudHosted(true)
+
+ await config.executeMiddleware()
+
+ expect(usageQuota.update).toHaveBeenCalledWith("test", "rows", 1)
+ expect(config.next).toHaveBeenCalled()
+ })
+
+ it("calculates the correct file size from a file upload call and adds it to quota", async () => {
+ config.setUrl("/upload")
+ config.cloudHosted(true)
+ config.setFiles([
+ {
+ size: 100
+ },
+ {
+ size: 10000
+ },
+ ])
+ await config.executeMiddleware()
+
+ expect(usageQuota.update).toHaveBeenCalledWith("test", "storage", 10100)
+ expect(config.next).toHaveBeenCalled()
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js
index e980afe678..1b809868be 100644
--- a/packages/server/src/middleware/usageQuota.js
+++ b/packages/server/src/middleware/usageQuota.js
@@ -43,6 +43,7 @@ module.exports = async (ctx, next) => {
return
}
}
+
// if running in builder or a self hosted cloud usage quotas should not be executed
if (!env.CLOUD || env.SELF_HOSTED) {
return next()
diff --git a/packages/server/src/utilities/security/roles.js b/packages/server/src/utilities/security/roles.js
index 79fd720078..b0cd843591 100644
--- a/packages/server/src/utilities/security/roles.js
+++ b/packages/server/src/utilities/security/roles.js
@@ -205,7 +205,8 @@ class AccessController {
tryingRoleId == null ||
tryingRoleId === "" ||
tryingRoleId === userRoleId ||
- tryingRoleId === BUILTIN_IDS.BUILDER
+ tryingRoleId === BUILTIN_IDS.BUILDER ||
+ userRoleId === BUILTIN_IDS.BUILDER
) {
return true
}
diff --git a/packages/server/src/utilities/templates.js b/packages/server/src/utilities/templates.js
index 7383e3115d..c3d89477df 100644
--- a/packages/server/src/utilities/templates.js
+++ b/packages/server/src/utilities/templates.js
@@ -31,6 +31,8 @@ exports.getLocalTemplates = function() {
return templateObj
}
+// can't really test this, downloading is just not something we should do in a behavioural test
+/* istanbul ignore next */
exports.downloadTemplate = async function(type, name) {
const dirName = join(budibaseAppsDir(), "templates", type, name)
if (env.LOCAL_TEMPLATES) {
@@ -67,8 +69,7 @@ exports.performDump = performDump
exports.exportTemplateFromApp = async function({ templateName, appId }) {
// Copy frontend files
const templatesDir = join(
- os.homedir(),
- ".budibase",
+ budibaseAppsDir(),
"templates",
"app",
templateName,
diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock
index e1d6e03634..591205393a 100644
--- a/packages/server/yarn.lock
+++ b/packages/server/yarn.lock
@@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f"
integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==
+"@adobe/spectrum-css-workflow-icons@^1.1.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.2.0.tgz#cda8bbe873ba9317160458858ae979e5393e5550"
+ integrity sha512-STSQQHvoBM0kf1JrNL3KEt88RklIctaGyGOzwUTnhtTkT1jHLaF4FgxrPDCvr1AT8VOq1nGplKUCeyZ9vdUUmA==
+
"@azure/ms-rest-azure-env@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-env/-/ms-rest-azure-env-1.1.2.tgz#8505873afd4a1227ec040894a64fdd736b4a101f"
@@ -249,12 +254,24 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
-"@budibase/client@^0.8.3":
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.8.3.tgz#944a745cc82845987cabd48e2ce3a7e58b387865"
- integrity sha512-gEOmHlqStsFTtotduRRz9bld2s/066pSwM3CWRuspsz5yycPLMhWKcA3CdfxVlNoR9y7I7IFC9+pfM5STDJAMQ==
+"@budibase/bbui@^1.58.13":
+ version "1.58.13"
+ resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.13.tgz#59df9c73def2d81c75dcbd2266c52c19db88dbd7"
+ integrity sha512-Zk6CKXdBfKsTVzA1Xs5++shdSSZLfphVpZuKVbjfzkgtuhyH7ruucexuSHEpFsxjW5rEKgKIBoRFzCK5vPvN0w==
dependencies:
- "@budibase/string-templates" "^0.8.3"
+ markdown-it "^12.0.2"
+ quill "^1.3.7"
+ sirv-cli "^0.4.6"
+ svelte-flatpickr "^2.4.0"
+ svelte-portal "^1.0.0"
+ turndown "^7.0.0"
+
+"@budibase/client@^0.8.5":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.8.5.tgz#31a6bbf8e7ff2a5ab635e8987357c310dcedf555"
+ integrity sha512-igiHyFpqbYm2EyCy0aUlBlaPibpFa5DtQow1kFBAjUW2cyZdEt84JV4Mei77NueGo7zHcr6/ByF6ycdyeBgXQw==
+ dependencies:
+ "@budibase/string-templates" "^0.8.5"
deep-equal "^2.0.1"
regexparam "^1.3.0"
shortid "^2.2.15"
@@ -292,10 +309,42 @@
to-gfm-code-block "^0.1.1"
year "^0.2.1"
-"@budibase/string-templates@^0.8.3":
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.8.3.tgz#f3b1f31ef914b926fb5285bc701e1200568dc92d"
- integrity sha512-X4Z9/1TS5PtO5sF1CDoyp8xSJhXFWIhOldTNBzPeCjAaD+c9Q8gOgcwECWugJh2d05RjiVI6gDbeirT8Q2QMig==
+"@budibase/standard-components@^0.8.5":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.8.5.tgz#4b94653110e4f20a8cb252b6421b620fd5ac31bc"
+ integrity sha512-wDEuxiu/DyPQYR2zQSt7TdPlAzdjjePitfKDzdIxm/WM7umXDSvLkA39nRzicEXikti34+waS7H96xGNuednVw==
+ dependencies:
+ "@adobe/spectrum-css-workflow-icons" "^1.1.0"
+ "@budibase/bbui" "^1.58.13"
+ "@budibase/svelte-ag-grid" "^1.0.4"
+ "@spectrum-css/actionbutton" "^1.0.0-beta.1"
+ "@spectrum-css/button" "^3.0.0-beta.6"
+ "@spectrum-css/checkbox" "^3.0.0-beta.6"
+ "@spectrum-css/fieldlabel" "^3.0.0-beta.7"
+ "@spectrum-css/icon" "^3.0.0-beta.2"
+ "@spectrum-css/inputgroup" "^3.0.0-beta.7"
+ "@spectrum-css/menu" "^3.0.0-beta.5"
+ "@spectrum-css/page" "^3.0.0-beta.0"
+ "@spectrum-css/picker" "^1.0.0-beta.3"
+ "@spectrum-css/popover" "^3.0.0-beta.6"
+ "@spectrum-css/stepper" "^3.0.0-beta.7"
+ "@spectrum-css/textfield" "^3.0.0-beta.6"
+ "@spectrum-css/vars" "^3.0.0-beta.2"
+ apexcharts "^3.22.1"
+ flatpickr "^4.6.6"
+ loadicons "^1.0.0"
+ lodash.debounce "^4.0.8"
+ markdown-it "^12.0.2"
+ quill "^1.3.7"
+ remixicon "^2.5.0"
+ svelte-apexcharts "^1.0.2"
+ svelte-flatpickr "^3.1.0"
+ turndown "^7.0.0"
+
+"@budibase/string-templates@^0.8.5":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.8.5.tgz#ad30e318f7486d4256b1165099fe2bd8004ef472"
+ integrity sha512-PcpiiDlYJFIVwtFGIRqZQtRl8wbO6yr0/+1Gca0TwR2WhyUyAs/ojO+jLIj97JWh/hE5zKaZW7d4cMOf+BDI/A==
dependencies:
"@budibase/handlebars-helpers" "^0.11.3"
dayjs "^1.10.4"
@@ -303,6 +352,13 @@
handlebars-utils "^1.0.6"
lodash "^4.17.20"
+"@budibase/svelte-ag-grid@^1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@budibase/svelte-ag-grid/-/svelte-ag-grid-1.0.4.tgz#41cceec4bde2c4aea8b9da8f610fe36055c7709f"
+ integrity sha512-JZm6qujxnZpqw7Twbegr6se4sHhyWzN0Cibrk5bVBH32hBgzD6dd33fxwrjHKkWFxjys9wRT+cqYgYVlSt9E3w==
+ dependencies:
+ ag-grid-community "^24.0.0"
+
"@cnakazawa/watch@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@@ -830,6 +886,11 @@
path-to-regexp "^1.1.1"
urijs "^1.19.0"
+"@polka/url@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31"
+ integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==
+
"@sendgrid/client@^7.1.1":
version "7.4.2"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.4.2.tgz#204a9fbb5dc05a721a5d8cd8930f57f9f8e612b1"
@@ -942,6 +1003,73 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
+"@spectrum-css/actionbutton@^1.0.0-beta.1":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.0.tgz#c2f0939f6d49de0a855f08a9466e3f27105a1747"
+ integrity sha512-klE5CGJEJXkc4DMLF8W+VPlLZ6SFr4WXI5Tc9NarOtbAc7mqhs2gWA8HpsPT717FWdxRVVt3sSuAydgKC/T0UA==
+
+"@spectrum-css/button@^3.0.0-beta.6":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.0.tgz#eebdd7a05eac9a40f297f802aca3efeb95931e83"
+ integrity sha512-CUGkuHOhqgfIRYTEceybcW1YsUN61F9BgDhqymhVd1yJFsuh1xkwnmv3IIodukgS+1e3L0JY6ifU86IWX/Dx5w==
+
+"@spectrum-css/checkbox@^3.0.0-beta.6":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.0.tgz#6ed15f433bed31a63818d154960aca044ce62182"
+ integrity sha512-FpxxftMzuWT8qq3XB4oBQgWglXuCCEGBfgX82EI9VtrJmw9j0Lm/nThMLX353p9awM4GfT3l2LNOneHbNetaRQ==
+
+"@spectrum-css/fieldlabel@^3.0.0-beta.7":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/fieldlabel/-/fieldlabel-3.0.0.tgz#01be5e5a7b024516574820962b9a656f7b2e20d4"
+ integrity sha512-dEOvDEigL9E60kQ9fT6MLyRzPKrPXAKulqDYOYpZaK2bsKrbIvsKb7NcuQynPAOE26FiuqQsp2khv5VqF4KzrA==
+
+"@spectrum-css/icon@^3.0.0-beta.2":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.0.tgz#a822c901ca049f487420053dfeaaf71c0850c848"
+ integrity sha512-0VVx34WECxe+acSZsB+zk8T+AG8YimlCfUothuqLzcUgY6MnBESHJKOEuKKihxnihEm6EJiMc2NYA7+09kPv/A==
+
+"@spectrum-css/inputgroup@^3.0.0-beta.7":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.0.tgz#c2790c2c0a4c435ca3fff9ba04f64dd2b252f980"
+ integrity sha512-dlF8LmMwTa5G6Rl4zUiNCmRv7p2v+88jINnSwZHucgKZL0/HJZBRxjF1neeSfRFrc8R6cemoVXDHRDtZFaVtXQ==
+
+"@spectrum-css/menu@^3.0.0-beta.5":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.0.tgz#78153ea60a36c87e9d815ce51dc7f84d6b9b9abd"
+ integrity sha512-E6L6s1/cwh6hn4yhUHegiJ+Su03Bpa7qP5a6nEccpYePZxPAAN2FjZBWdMOPlGtv1e70vudAsoejli9nVthC2w==
+
+"@spectrum-css/page@^3.0.0-beta.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.0.tgz#159e79fd376e2def1a7a25a8b8a8fcfa94bd79d1"
+ integrity sha512-4rNpGq99cfNSq/IOQNCiXio5gF/EEfjcSmihHBJlh7/VOB9zE84kMNW1Gux4cGEmdP14U1Zo1ZwnPIVs5ZuPgg==
+ dependencies:
+ "@spectrum-css/vars" "^3.0.0"
+
+"@spectrum-css/picker@^1.0.0-beta.3":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.0.tgz#5758a128da081becd425b8d9433b24541f12b4b3"
+ integrity sha512-aSoin2SVYl5W2R3nFp+V/Er6rAJUnwygO4E3g/tfDuImq8p5U3FKZj4sggSqfuD2U1PIwNSwX0D1RdxuGXsnUQ==
+
+"@spectrum-css/popover@^3.0.0-beta.6":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/popover/-/popover-3.0.0.tgz#ec1ab86a66cc59bd522d3de2b7febe41e2a9fe46"
+ integrity sha512-Lr2FZSJbDbDMp3bOlLtvDjOw6AwzRu3g0BbQ7NGK1l5MB06AhnqJX+TPB2iEDTfPdNyaDc5SCp55lBHP3RzHuw==
+
+"@spectrum-css/stepper@^3.0.0-beta.7":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.0.tgz#ab5af818c86f2bc5050d0caee8b0a1c75201bfaf"
+ integrity sha512-Gwvb4YLEBy/YtnFQ4aySnlve+pBrgPIm5LSq5IkeyjAKy7ZalQm9IIEkrVERHO1b+vbRZ6DW/aj2zYgzKgGMrA==
+
+"@spectrum-css/textfield@^3.0.0-beta.6":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-3.0.0.tgz#2f2d341b8d2c6f74e074b7e8df4a28307561cbbf"
+ integrity sha512-ooXiSc5TZuZCFr3wl1JB60nS9FBBkGgqsml7kAS/7bOwRTCUPH7cY80SoaabRL8Z9Clml+K1Pa7I/r+Wphb53g==
+
+"@spectrum-css/vars@^3.0.0", "@spectrum-css/vars@^3.0.0-beta.2":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.0.tgz#c3ef4c2f07bd4f0d2734e730233ca81cb18106e7"
+ integrity sha512-fNXU6qmcCbSiUoWGe/m9A8/THRHbpzwZ+iN8o/27tWIzcQIyZBZgjmV/kIMdF1dHpu5CuWik7mGV1Ex8tlzATg==
+
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
@@ -1226,6 +1354,11 @@ adal-node@^0.1.28:
xmldom ">= 0.1.x"
xpath.js "~1.1.0"
+ag-grid-community@^24.0.0:
+ version "24.1.0"
+ resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-24.1.0.tgz#1e3cab51211822e08d56f03a491b7c0deaa398e6"
+ integrity sha512-pWnWphuDcejZ8ahf6C734EpCx3XQ6dHEZWMWTlCdHNT0mZBLJ4YKCGACX+ttAEtSX2MGM3G13JncvuratUlYag==
+
agent-base@6:
version "6.0.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4"
@@ -1563,6 +1696,18 @@ anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
+apexcharts@^3.19.2, apexcharts@^3.22.1:
+ version "3.25.0"
+ resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.25.0.tgz#f3f0f9f344f997230f5c7f2918408aa072627496"
+ integrity sha512-uM7OF+jLL4ba79noYcrMwMgJW8DI+Ff28CCQoGq23g25z8nGSQEoU+u12YWlECA9gBA5tbmdaQhMxjlK+M6B9Q==
+ dependencies:
+ svg.draggable.js "^2.2.2"
+ svg.easing.js "^2.0.0"
+ svg.filter.js "^2.0.2"
+ svg.pathmorphing.js "^0.1.3"
+ svg.resize.js "^1.4.3"
+ svg.select.js "^3.0.1"
+
app-builder-bin@3.5.10:
version "3.5.10"
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.5.10.tgz#4a7f9999fccc0c435b6284ae1366bc76a17c4a7d"
@@ -1621,6 +1766,11 @@ argparse@^1.0.10, argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
args@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761"
@@ -2356,6 +2506,11 @@ clone-response@1.0.2, clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
+clone@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+ integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
co-body@^5.1.1:
version "5.2.0"
resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124"
@@ -2489,6 +2644,11 @@ configstore@^5.0.1:
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
+console-clear@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/console-clear/-/console-clear-1.1.1.tgz#995e20cbfbf14dd792b672cde387bd128d674bf7"
+ integrity sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==
+
content-disposition@^0.5.2, content-disposition@~0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@@ -2752,6 +2912,18 @@ decompress@^4.2.1:
pify "^2.3.0"
strip-dirs "^2.0.0"
+deep-equal@^1.0.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+ integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+ dependencies:
+ is-arguments "^1.0.4"
+ is-date-object "^1.0.1"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object-keys "^1.1.1"
+ regexp.prototype.flags "^1.2.0"
+
deep-equal@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9"
@@ -2944,6 +3116,11 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
+domino@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe"
+ integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==
+
dot-prop@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -3165,6 +3342,11 @@ ent@^2.2.0:
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
+entities@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+ integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+
env-paths@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
@@ -3450,6 +3632,11 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+eventemitter3@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
+ integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
+
events@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -3551,7 +3738,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
-extend@^3.0.0, extend@~3.0.2:
+extend@^3.0.0, extend@^3.0.2, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -3621,6 +3808,11 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-diff@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
+ integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==
+
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -3800,6 +3992,11 @@ flat-cache@^2.0.1:
rimraf "2.6.3"
write "1.0.3"
+flatpickr@^4.5.2, flatpickr@^4.6.6:
+ version "4.6.9"
+ resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.9.tgz#9a13383e8a6814bda5d232eae3fcdccb97dc1499"
+ integrity sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==
+
flatstr@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931"
@@ -3988,6 +4185,11 @@ get-object@^0.2.0:
is-number "^2.0.2"
isobject "^0.2.0"
+get-port@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
+ integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=
+
get-stream@3.0.0, get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4859,7 +5061,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
-is-regex@^1.1.1, is-regex@^1.1.2:
+is-regex@^1.0.4, is-regex@^1.1.1, is-regex@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
@@ -5693,7 +5895,7 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-kleur@^3.0.3:
+kleur@^3.0.0, kleur@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
@@ -6002,6 +6204,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+linkify-it@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
+ integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==
+ dependencies:
+ uc.micro "^1.0.1"
+
load-bmfont@^1.3.1, load-bmfont@^1.4.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.1.tgz#c0f5f4711a1e2ccff725a7b6078087ccfcddd3e9"
@@ -6026,6 +6235,16 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
+loadicons@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/loadicons/-/loadicons-1.0.0.tgz#79fd9b08ef2933988c94068cbd246ef3f21cbd04"
+ integrity sha512-KSywiudfuOK5sTdhNMM8hwRpMxZ5TbQlU4ZijMxUFwRW7jpxUmb9YJoLIzDn7+xuxeLzCZWBmLJS2JDjDWCpsw==
+
+local-access@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
+ integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
+
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@@ -6280,6 +6499,17 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+markdown-it@^12.0.2:
+ version "12.0.4"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33"
+ integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==
+ dependencies:
+ argparse "^2.0.1"
+ entities "~2.1.0"
+ linkify-it "^3.0.1"
+ mdurl "^1.0.1"
+ uc.micro "^1.0.5"
+
matcher@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
@@ -6287,6 +6517,11 @@ matcher@^3.0.0:
dependencies:
escape-string-regexp "^4.0.0"
+mdurl@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+ integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6396,6 +6631,11 @@ mime@^1.3.4, mime@^1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+mime@^2.3.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
+ integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
+
mime@^2.4.6:
version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
@@ -6473,6 +6713,11 @@ mri@1.1.4:
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a"
integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==
+mri@^1.1.0:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"
+ integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -6755,7 +7000,7 @@ object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
-object-is@^1.1.4:
+object-is@^1.0.1, object-is@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
@@ -6973,6 +7218,11 @@ pako@^1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+parchment@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"
+ integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -7646,6 +7896,27 @@ quick-format-unescaped@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701"
integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A==
+quill-delta@^3.6.2:
+ version "3.6.3"
+ resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
+ integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==
+ dependencies:
+ deep-equal "^1.0.1"
+ extend "^3.0.2"
+ fast-diff "1.1.2"
+
+quill@^1.3.7:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
+ integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==
+ dependencies:
+ clone "^2.1.1"
+ deep-equal "^1.0.1"
+ eventemitter3 "^2.0.3"
+ extend "^3.0.2"
+ parchment "^1.1.4"
+ quill-delta "^3.6.2"
+
raw-body@^2.2.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
@@ -7822,7 +8093,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
-regexp.prototype.flags@^1.3.0:
+regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
@@ -7869,6 +8140,11 @@ remarkable@^1.6.2, remarkable@^1.7.1:
argparse "^1.0.10"
autolinker "~0.28.0"
+remixicon@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41"
+ integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==
+
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -8055,6 +8331,13 @@ rxjs@^6.6.0:
dependencies:
tslib "^1.9.0"
+sade@^1.4.0:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/sade/-/sade-1.7.4.tgz#ea681e0c65d248d2095c90578c03ca0bb1b54691"
+ integrity sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==
+ dependencies:
+ mri "^1.1.0"
+
safe-buffer@*, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -8287,6 +8570,27 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+sirv-cli@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/sirv-cli/-/sirv-cli-0.4.6.tgz#c28ab20deb3b34637f5a60863dc350f055abca04"
+ integrity sha512-/Vj85/kBvPL+n9ibgX6FicLE8VjidC1BhlX67PYPBfbBAphzR6i0k0HtU5c2arejfU3uzq8l3SYPCwl1x7z6Ww==
+ dependencies:
+ console-clear "^1.1.0"
+ get-port "^3.2.0"
+ kleur "^3.0.0"
+ local-access "^1.0.1"
+ sade "^1.4.0"
+ sirv "^0.4.6"
+ tinydate "^1.0.0"
+
+sirv@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/sirv/-/sirv-0.4.6.tgz#185e44eb93d24009dd183b7494285c5180b81f22"
+ integrity sha512-rYpOXlNbpHiY4nVXxuDf4mXPvKz1reZGap/LkWp9TvcZ84qD/nPBjjH/6GZsgIjVMbOslnY8YYULAyP8jMn1GQ==
+ dependencies:
+ "@polka/url" "^0.5.0"
+ mime "^2.3.1"
+
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@@ -8749,6 +9053,32 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
+svelte-apexcharts@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/svelte-apexcharts/-/svelte-apexcharts-1.0.2.tgz#4e000f8b8f7c901c05658c845457dfc8314d54c1"
+ integrity sha512-6qlx4rE+XsonZ0FZudfwqOQ34Pq+3wpxgAD75zgEmGoYhYBJcwmikTuTf3o8ZBsZue9U/pAwhNy3ed1Bkq1gmA==
+ dependencies:
+ apexcharts "^3.19.2"
+
+svelte-flatpickr@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-2.4.0.tgz#190871fc3305956c8c8fd3601cd036b8ac71ef49"
+ integrity sha512-UUC5Te+b0qi4POg7VDwfGh0m5W3Hf64OwkfOTj6FEe/dYZN4cBzpQ82EuuQl0CTbbBAsMkcjJcixV1d2V6EHCQ==
+ dependencies:
+ flatpickr "^4.5.2"
+
+svelte-flatpickr@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.1.0.tgz#ad83588430dbd55196a1a258b8ba27e7f9c1ee37"
+ integrity sha512-zKyV+ukeVuJ8CW0Ing3T19VSekc4bPkou/5Riutt1yATrLvSsanNqcgqi7Q5IePvIoOF9GJ5OtHvn1qK9Wx9BQ==
+ dependencies:
+ flatpickr "^4.5.2"
+
+svelte-portal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
+ integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
+
svelte-spa-router@^3.0.5:
version "3.1.0"
resolved "https://registry.yarnpkg.com/svelte-spa-router/-/svelte-spa-router-3.1.0.tgz#a929f0def7e12c41f32bc356f91685aeadcd75bf"
@@ -8761,6 +9091,61 @@ svelte@3.30.0:
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.30.0.tgz#cbde341e96bf34f4ac73c8f14f8a014e03bfb7d6"
integrity sha512-z+hdIACb9TROGvJBQWcItMtlr4s0DBUgJss6qWrtFkOoIInkG+iAMo/FJZQFyDBQZc+dul2+TzYSi/tpTT5/Ag==
+svg.draggable.js@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
+ integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
+ dependencies:
+ svg.js "^2.0.1"
+
+svg.easing.js@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
+ integrity sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=
+ dependencies:
+ svg.js ">=2.3.x"
+
+svg.filter.js@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
+ integrity sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=
+ dependencies:
+ svg.js "^2.2.5"
+
+svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
+ integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
+
+svg.pathmorphing.js@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
+ integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
+ dependencies:
+ svg.js "^2.4.0"
+
+svg.resize.js@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
+ integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
+ dependencies:
+ svg.js "^2.6.5"
+ svg.select.js "^2.1.2"
+
+svg.select.js@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
+ integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
+ dependencies:
+ svg.js "^2.2.5"
+
+svg.select.js@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
+ integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
+ dependencies:
+ svg.js "^2.6.5"
+
symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -8940,6 +9325,11 @@ tinycolor2@^1.4.1:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+tinydate@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/tinydate/-/tinydate-1.3.0.tgz#e6ca8e5a22b51bb4ea1c3a2a4fd1352dbd4c57fb"
+ integrity sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9083,6 +9473,13 @@ tunnel@0.0.6, tunnel@^0.0.6:
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
+turndown@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.0.0.tgz#19b2a6a2d1d700387a1e07665414e4af4fec5225"
+ integrity sha512-G1FfxfR0mUNMeGjszLYl3kxtopC4O9DRRiMlMDDVHvU1jaBkGFg4qxIyjIk2aiKLHyDyZvZyu4qBO2guuYBy3Q==
+ dependencies:
+ domino "^2.1.6"
+
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@@ -9142,6 +9539,11 @@ typeof-article@^0.1.1:
dependencies:
kind-of "^3.1.0"
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+ integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
uglify-js@^3.1.4:
version "3.13.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69"
diff --git a/packages/standard-components/src/Container.svelte b/packages/standard-components/src/Container.svelte
index 8f49ba0ca9..4134db5d64 100644
--- a/packages/standard-components/src/Container.svelte
+++ b/packages/standard-components/src/Container.svelte
@@ -8,55 +8,81 @@
{#if type === 'div'}
-
+
{:else if type === 'header'}
-
+
{:else if type === 'main'}
-
+
{:else if type === 'footer'}
-