Merge branch 'master' into feature/sql-query-aliasing

This commit is contained in:
Michael Drury 2024-02-29 11:39:19 +00:00 committed by GitHub
commit 1ce15528b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 302 additions and 222 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.20.12", "version": "2.20.13",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID. * Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level. * @param ownerId The owner/user of the template, this could be global or a workspace level.
*/ */
export function generateTemplateID(ownerId: any) { export function generateTemplateID(ownerId: string) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
} }
@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
* Generates a new dev info document ID - this is scoped to a user. * Generates a new dev info document ID - this is scoped to a user.
* @returns The new dev info ID which info for dev (like api key) can be stored under. * @returns The new dev info ID which info for dev (like api key) can be stored under.
*/ */
export const generateDevInfoID = (userId: any) => { export const generateDevInfoID = (userId: string) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}` return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
} }

View File

@ -69,11 +69,12 @@
// brought back to the same screen. // brought back to the same screen.
const topItemNavigate = path => () => { const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path)) const activeTopNav = $layout.children.find(c => $isActive(c.path))
if (!activeTopNav) return if (activeTopNav) {
builderStore.setPreviousTopNavPath( builderStore.setPreviousTopNavPath(
activeTopNav.path, activeTopNav.path,
window.location.pathname window.location.pathname
) )
}
$goto($builderStore.previousTopNavPath[path] || path) $goto($builderStore.previousTopNavPath[path] || path)
} }

View File

@ -20,6 +20,7 @@ import {
AutomationActionStepId, AutomationActionStepId,
AutomationResults, AutomationResults,
UserCtx, UserCtx,
DeleteAutomationResponse,
} from "@budibase/types" } from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions" import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -72,7 +73,9 @@ function cleanAutomationInputs(automation: Automation) {
return automation return automation
} }
export async function create(ctx: UserCtx) { export async function create(
ctx: UserCtx<Automation, { message: string; automation: Automation }>
) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -207,7 +210,7 @@ export async function find(ctx: UserCtx) {
ctx.body = await db.get(ctx.params.id) ctx.body = await db.get(ctx.params.id)
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
const automationId = ctx.params.id const automationId = ctx.params.id
const oldAutomation = await db.get<Automation>(automationId) const oldAutomation = await db.get<Automation>(automationId)

View File

@ -1,9 +1,17 @@
import { EMPTY_LAYOUT } from "../../constants/layouts" import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils" import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core" import { events, context } from "@budibase/backend-core"
import { BBContext, Layout } from "@budibase/types" import {
BBContext,
Layout,
SaveLayoutRequest,
SaveLayoutResponse,
UserCtx,
} from "@budibase/types"
export async function save(ctx: BBContext) { export async function save(
ctx: UserCtx<SaveLayoutRequest, SaveLayoutResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
let layout = ctx.request.body let layout = ctx.request.body

View File

@ -73,7 +73,7 @@ const _import = async (ctx: UserCtx) => {
} }
export { _import as import } export { _import as import }
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx<Query, Query>) {
const db = context.getAppDB() const db = context.getAppDB()
const query: Query = ctx.request.body const query: Query = ctx.request.body

View File

@ -7,7 +7,13 @@ import {
roles, roles,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { updateAppPackage } from "./application" import { updateAppPackage } from "./application"
import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types" import {
Plugin,
ScreenProps,
BBContext,
Screen,
UserCtx,
} from "@budibase/types"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
export async function fetch(ctx: BBContext) { export async function fetch(ctx: BBContext) {
@ -31,7 +37,7 @@ export async function fetch(ctx: BBContext) {
) )
} }
export async function save(ctx: BBContext) { export async function save(ctx: UserCtx<Screen, Screen>) {
const db = context.getAppDB() const db = context.getAppDB()
let screen = ctx.request.body let screen = ctx.request.body

View File

@ -394,7 +394,7 @@ describe("/automations", () => {
it("deletes a automation by its ID", async () => { it("deletes a automation by its ID", async () => {
const automation = await config.createAutomation() const automation = await config.createAutomation()
const res = await request const res = await request
.delete(`/api/automations/${automation.id}/${automation.rev}`) .delete(`/api/automations/${automation._id}/${automation._rev}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
@ -408,7 +408,7 @@ describe("/automations", () => {
await checkBuilderEndpoint({ await checkBuilderEndpoint({
config, config,
method: "DELETE", method: "DELETE",
url: `/api/automations/${automation.id}/${automation._rev}`, url: `/api/automations/${automation._id}/${automation._rev}`,
}) })
}) })
}) })

View File

@ -44,7 +44,7 @@ describe("/backups", () => {
expect(headers["content-disposition"]).toEqual( expect(headers["content-disposition"]).toEqual(
`attachment; filename="${ `attachment; filename="${
config.getApp()!.name config.getApp().name
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"` }-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
) )
}) })

View File

@ -86,7 +86,7 @@ describe("/datasources", () => {
}) })
// check variables in cache // check variables in cache
let contents = await checkCacheForDynamicVariable( let contents = await checkCacheForDynamicVariable(
query._id, query._id!,
"variable3" "variable3"
) )
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
@ -102,7 +102,7 @@ describe("/datasources", () => {
expect(res.body.errors).toBeUndefined() expect(res.body.errors).toBeUndefined()
// check variables no longer in cache // check variables no longer in cache
contents = await checkCacheForDynamicVariable(query._id, "variable3") contents = await checkCacheForDynamicVariable(query._id!, "variable3")
expect(contents).toBe(null) expect(contents).toBe(null)
}) })
}) })

View File

@ -467,7 +467,10 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
// check its in cache // check its in cache
const contents = await checkCacheForDynamicVariable(base._id, "variable3") const contents = await checkCacheForDynamicVariable(
base._id!,
"variable3"
)
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
const responseBody = await preview(datasource, { const responseBody = await preview(datasource, {
path: "www.failonce.com", path: "www.failonce.com",
@ -490,7 +493,7 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
// check its in cache // check its in cache
let contents = await checkCacheForDynamicVariable(base._id, "variable3") let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
// delete the query // delete the query
@ -500,7 +503,7 @@ describe("/queries", () => {
.expect(200) .expect(200)
// check variables no longer in cache // check variables no longer in cache
contents = await checkCacheForDynamicVariable(base._id, "variable3") contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents).toBe(null) expect(contents).toBe(null)
}) })
}) })

View File

@ -110,7 +110,7 @@ describe.each([
config.api.row.get(tbl_Id, id, { expectStatus: status }) config.api.row.get(tbl_Id, id, { expectStatus: status })
const getRowUsage = async () => { const getRowUsage = async () => {
const { total } = await config.doInContext(null, () => const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS) quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
) )
return total return total

View File

@ -27,15 +27,17 @@ describe("/users", () => {
describe("fetch", () => { describe("fetch", () => {
it("returns a list of users from an instance db", async () => { it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" }) const id1 = `us_${utils.newid()}`
await config.createUser({ id: "uuidy" }) const id2 = `us_${utils.newid()}`
await config.createUser({ _id: id1 })
await config.createUser({ _id: id2 })
const res = await config.api.user.fetch() const res = await config.api.user.fetch()
expect(res.length).toBe(3) expect(res.length).toBe(3)
const ids = res.map(u => u._id) const ids = res.map(u => u._id)
expect(ids).toContain(`ro_ta_users_us_uuidx`) expect(ids).toContain(`ro_ta_users_${id1}`)
expect(ids).toContain(`ro_ta_users_us_uuidy`) expect(ids).toContain(`ro_ta_users_${id2}`)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
@ -54,7 +56,7 @@ describe("/users", () => {
describe("update", () => { describe("update", () => {
it("should be able to update the user", async () => { it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({ const user: UserMetadata = await config.createUser({
id: `us_update${utils.newid()}`, _id: `us_update${utils.newid()}`,
}) })
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev delete user._rev

View File

@ -4,6 +4,7 @@ import { AppStatus } from "../../../../db/utils"
import { roles, tenancy, context, db } from "@budibase/backend-core" import { roles, tenancy, context, db } from "@budibase/backend-core"
import env from "../../../../environment" import env from "../../../../environment"
import Nano from "@budibase/nano" import Nano from "@budibase/nano"
import TestConfiguration from "src/tests/utilities/TestConfiguration"
class Request { class Request {
appId: any appId: any
@ -52,10 +53,10 @@ export const clearAllApps = async (
}) })
} }
export const clearAllAutomations = async (config: any) => { export const clearAllAutomations = async (config: TestConfiguration) => {
const automations = await config.getAllAutomations() const automations = await config.getAllAutomations()
for (let auto of automations) { for (let auto of automations) {
await context.doInAppContext(config.appId, async () => { await context.doInAppContext(config.getAppId(), async () => {
await config.deleteAutomation(auto) await config.deleteAutomation(auto)
}) })
} }
@ -101,7 +102,12 @@ export const checkBuilderEndpoint = async ({
method, method,
url, url,
body, body,
}: any) => { }: {
config: TestConfiguration
method: string
url: string
body?: any
}) => {
const headers = await config.login({ const headers = await config.login({
userId: "us_fail", userId: "us_fail",
builder: false, builder: false,

View File

@ -36,7 +36,7 @@ describe("/webhooks", () => {
const automation = await config.createAutomation() const automation = await config.createAutomation()
const res = await request const res = await request
.put(`/api/webhooks`) .put(`/api/webhooks`)
.send(basicWebhook(automation._id)) .send(basicWebhook(automation._id!))
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
@ -145,7 +145,7 @@ describe("/webhooks", () => {
let automation = collectAutomation() let automation = collectAutomation()
let newAutomation = await config.createAutomation(automation) let newAutomation = await config.createAutomation(automation)
let syncWebhook = await config.createWebhook( let syncWebhook = await config.createWebhook(
basicWebhook(newAutomation._id) basicWebhook(newAutomation._id!)
) )
// replicate changes before checking webhook // replicate changes before checking webhook

View File

@ -29,6 +29,6 @@ start().catch(err => {
throw err throw err
}) })
export function getServer() { export function getServer(): Server {
return server return server
} }

View File

@ -1,9 +1,11 @@
import { Layout } from "@budibase/types"
export const BASE_LAYOUT_PROP_IDS = { export const BASE_LAYOUT_PROP_IDS = {
PRIVATE: "layout_private_master", PRIVATE: "layout_private_master",
PUBLIC: "layout_public_master", PUBLIC: "layout_public_master",
} }
export const EMPTY_LAYOUT = { export const EMPTY_LAYOUT: Layout = {
componentLibraries: ["@budibase/standard-components"], componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}", title: "{{ name }}",
favicon: "./_shared/favicon.png", favicon: "./_shared/favicon.png",

View File

@ -1,5 +1,6 @@
import { roles } from "@budibase/backend-core" import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts" import { BASE_LAYOUT_PROP_IDS } from "./layouts"
import { Screen } from "@budibase/types"
export function createHomeScreen( export function createHomeScreen(
config: { config: {
@ -9,10 +10,8 @@ export function createHomeScreen(
roleId: roles.BUILTIN_ROLE_IDS.BASIC, roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/", route: "/",
} }
) { ): Screen {
return { return {
description: "",
url: "",
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE, layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: { props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", _id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",

View File

@ -13,7 +13,7 @@ describe("syncApps", () => {
afterAll(config.end) afterAll(config.end)
it("runs successfully", async () => { it("runs successfully", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages // create the usage quota doc and mock usages
await quotas.getQuotaUsage() await quotas.getQuotaUsage()
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC) await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)

View File

@ -12,8 +12,8 @@ describe("syncCreators", () => {
afterAll(config.end) afterAll(config.end)
it("syncs creators", async () => { it("syncs creators", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
await config.createUser({ admin: true }) await config.createUser({ admin: { global: true } })
await syncCreators.run() await syncCreators.run()

View File

@ -14,7 +14,7 @@ describe("syncRows", () => {
afterAll(config.end) afterAll(config.end)
it("runs successfully", async () => { it("runs successfully", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages // create the usage quota doc and mock usages
await quotas.getQuotaUsage() await quotas.getQuotaUsage()
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC) await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)

View File

@ -12,7 +12,7 @@ describe("syncUsers", () => {
afterAll(config.end) afterAll(config.end)
it("syncs users", async () => { it("syncs users", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
await config.createUser() await config.createUser()
await syncUsers.run() await syncUsers.run()

View File

@ -40,7 +40,7 @@ describe("migrations", () => {
describe("backfill", () => { describe("backfill", () => {
it("runs app db migration", async () => { it("runs app db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(undefined, async () => {
await clearMigrations() await clearMigrations()
await config.createAutomation() await config.createAutomation()
await config.createAutomation(structures.newAutomation()) await config.createAutomation(structures.newAutomation())
@ -93,18 +93,18 @@ describe("migrations", () => {
}) })
it("runs global db migration", async () => { it("runs global db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(undefined, async () => {
await clearMigrations() await clearMigrations()
const appId = config.prodAppId const appId = config.getProdAppId()
const roles = { [appId]: "role_12345" } const roles = { [appId]: "role_12345" }
await config.createUser({ await config.createUser({
builder: false, builder: { global: false },
admin: true, admin: { global: true },
roles, roles,
}) // admin only }) // admin only
await config.createUser({ await config.createUser({
builder: false, builder: { global: false },
admin: false, admin: { global: false },
roles, roles,
}) // non admin non builder }) // non admin non builder
await config.createTable() await config.createTable()

View File

@ -43,8 +43,8 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
const user = await config.createUser({ const user = await config.createUser({
email, email,
roles, roles,
builder: builder || false, builder: { global: builder || false },
admin: false, admin: { global: false },
}) })
await context.doInContext(config.appId!, async () => { await context.doInContext(config.appId!, async () => {
await events.user.created(user) await events.user.created(user)
@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
async function removeUserRole(user: User) { async function removeUserRole(user: User) {
const final = await config.globalUser({ const final = await config.globalUser({
...user, ...user,
id: user._id, _id: user._id,
roles: {}, roles: {},
builder: false, builder: { global: false },
admin: false, admin: { global: false },
}) })
await context.doInContext(config.appId!, async () => { await context.doInContext(config.appId!, async () => {
await events.user.updated(final) await events.user.updated(final)
@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
groupUser = await config.createUser({ groupUser = await config.createUser({
email, email,
roles: {}, roles: {},
builder: false, builder: { global: false },
admin: false, admin: { global: false },
}) })
group = await config.createGroup() group = await config.createGroup()
await config.addUserToGroup(group._id!, groupUser._id!) await config.addUserToGroup(group._id!, groupUser._id!)

View File

@ -81,7 +81,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save( const response = await internalSdk.save(
table._id!, table._id!,
row, row,
config.user._id config.getUser()._id
) )
expect(response).toEqual({ expect(response).toEqual({
@ -129,7 +129,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save( const response = await internalSdk.save(
table._id!, table._id!,
row, row,
config.user._id config.getUser()._id
) )
expect(response).toEqual({ expect(response).toEqual({
@ -190,15 +190,15 @@ describe("sdk >> rows >> internal", () => {
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
for (const row of makeRows(5)) { for (const row of makeRows(5)) {
await internalSdk.save(table._id!, row, config.user._id) await internalSdk.save(table._id!, row, config.getUser()._id)
} }
await Promise.all( await Promise.all(
makeRows(10).map(row => makeRows(10).map(row =>
internalSdk.save(table._id!, row, config.user._id) internalSdk.save(table._id!, row, config.getUser()._id)
) )
) )
for (const row of makeRows(5)) { for (const row of makeRows(5)) {
await internalSdk.save(table._id!, row, config.user._id) await internalSdk.save(table._id!, row, config.getUser()._id)
} }
}) })

View File

@ -22,15 +22,18 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(1) expect(metadata).toHaveLength(1)
expect(metadata).toEqual([ expect(metadata).toEqual([
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(config.user._id), _id: db.generateUserMetadataID(config.getUser()._id!),
}), }),
]) ])
}) })
}) })
it("admin and builders users are synced", async () => { it("admin and builders users are synced", async () => {
const user1 = await config.createUser({ admin: true }) const user1 = await config.createUser({ admin: { global: true } })
const user2 = await config.createUser({ admin: false, builder: true }) const user2 = await config.createUser({
admin: { global: false },
builder: { global: true },
})
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
expect(await rawUserMetadata()).toHaveLength(1) expect(await rawUserMetadata()).toHaveLength(1)
await syncGlobalUsers() await syncGlobalUsers()
@ -51,7 +54,10 @@ describe("syncGlobalUsers", () => {
}) })
it("app users are not synced if not specified", async () => { it("app users are not synced if not specified", async () => {
const user = await config.createUser({ admin: false, builder: false }) const user = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
await syncGlobalUsers() await syncGlobalUsers()
@ -68,8 +74,14 @@ describe("syncGlobalUsers", () => {
it("app users are added when group is assigned to app", async () => { it("app users are added when group is assigned to app", async () => {
await config.doInTenant(async () => { await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup()) const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false }) const user1 = await config.createUser({
const user2 = await config.createUser({ admin: false, builder: false }) admin: { global: false },
builder: { global: false },
})
const user2 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!]) await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
@ -103,8 +115,14 @@ describe("syncGlobalUsers", () => {
it("app users are removed when app is removed from user group", async () => { it("app users are removed when app is removed from user group", async () => {
await config.doInTenant(async () => { await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup()) const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false }) const user1 = await config.createUser({
const user2 = await config.createUser({ admin: false, builder: false }) admin: { global: false },
builder: { global: false },
})
const user2 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await proSdk.groups.updateGroupApps(group.id, { await proSdk.groups.updateGroupApps(group.id, {
appsToAdd: [ appsToAdd: [
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },

View File

@ -49,25 +49,31 @@ import {
AuthToken, AuthToken,
Automation, Automation,
CreateViewRequest, CreateViewRequest,
Ctx,
Datasource, Datasource,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
Layout,
Query,
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipType, RelationshipType,
Row, Row,
Screen,
SearchParams, SearchParams,
SourceName, SourceName,
Table, Table,
TableSourceType, TableSourceType,
User, User,
UserRoles, UserCtx,
View, View,
Webhook,
WithRequired, WithRequired,
} from "@budibase/types" } from "@budibase/types"
import API from "./api" import API from "./api"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import jwt, { Secret } from "jsonwebtoken" import jwt, { Secret } from "jsonwebtoken"
import { Server } from "http"
mocks.licenses.init(pro) mocks.licenses.init(pro)
@ -82,27 +88,23 @@ export interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
} }
export default class TestConfiguration { export default class TestConfiguration {
server: any server?: Server
request: supertest.SuperTest<supertest.Test> | undefined request?: supertest.SuperTest<supertest.Test>
started: boolean started: boolean
appId: string | null appId?: string
allApps: any[] allApps: App[]
app?: App app?: App
prodApp: any prodApp?: App
prodAppId: any prodAppId?: string
user: any user?: User
userMetadataId: any userMetadataId?: string
table?: Table table?: Table
automation: any automation?: Automation
datasource?: Datasource datasource?: Datasource
tenantId?: string tenantId?: string
api: API api: API
csrfToken?: string csrfToken?: string
private get globalUserId() {
return this.user._id
}
constructor(openServer = true) { constructor(openServer = true) {
if (openServer) { if (openServer) {
// use a random port because it doesn't matter // use a random port because it doesn't matter
@ -114,7 +116,7 @@ export default class TestConfiguration {
} else { } else {
this.started = false this.started = false
} }
this.appId = null this.appId = undefined
this.allApps = [] this.allApps = []
this.api = new API(this) this.api = new API(this)
@ -125,46 +127,86 @@ export default class TestConfiguration {
} }
getApp() { getApp() {
if (!this.app) {
throw new Error("app has not been initialised, call config.init() first")
}
return this.app return this.app
} }
getProdApp() { getProdApp() {
if (!this.prodApp) {
throw new Error(
"prodApp has not been initialised, call config.init() first"
)
}
return this.prodApp return this.prodApp
} }
getAppId() { getAppId() {
if (!this.appId) { if (!this.appId) {
throw "appId has not been initialised properly" throw new Error(
"appId has not been initialised, call config.init() first"
)
} }
return this.appId return this.appId
} }
getProdAppId() { getProdAppId() {
if (!this.prodAppId) {
throw new Error(
"prodAppId has not been initialised, call config.init() first"
)
}
return this.prodAppId return this.prodAppId
} }
getUser(): User {
if (!this.user) {
throw new Error("User has not been initialised, call config.init() first")
}
return this.user
}
getUserDetails() { getUserDetails() {
const user = this.getUser()
return { return {
globalId: this.globalUserId, globalId: user._id!,
email: this.user.email, email: user.email,
firstName: this.user.firstName, firstName: user.firstName,
lastName: this.user.lastName, lastName: user.lastName,
} }
} }
getAutomation() {
if (!this.automation) {
throw new Error(
"automation has not been initialised, call config.init() first"
)
}
return this.automation
}
getDatasource() {
if (!this.datasource) {
throw new Error(
"datasource has not been initialised, call config.init() first"
)
}
return this.datasource
}
async doInContext<T>( async doInContext<T>(
appId: string | null, appId: string | undefined,
task: () => Promise<T> task: () => Promise<T>
): Promise<T> { ): Promise<T> {
if (!appId) {
appId = this.appId
}
const tenant = this.getTenantId() const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => { return tenancy.doInTenant(tenant, () => {
if (!appId) {
appId = this.appId
}
// check if already in a context // check if already in a context
if (context.getAppId() == null && appId !== null) { if (context.getAppId() == null && appId) {
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
return task() return task()
}) })
@ -259,7 +301,11 @@ export default class TestConfiguration {
// UTILS // UTILS
_req(body: any, params: any, controlFunc: any) { _req<Req extends Record<string, any> | void, Res>(
handler: (ctx: UserCtx<Req, Res>) => Promise<void>,
body?: Req,
params?: Record<string, string | undefined>
): Promise<Res> {
// create a fake request ctx // create a fake request ctx
const request: any = {} const request: any = {}
const appId = this.appId const appId = this.appId
@ -278,63 +324,48 @@ export default class TestConfiguration {
throw new Error(`Error ${status} - ${message}`) throw new Error(`Error ${status} - ${message}`)
} }
return this.doInContext(appId, async () => { return this.doInContext(appId, async () => {
await controlFunc(request) await handler(request)
return request.body return request.body
}) })
} }
// USER / AUTH // USER / AUTH
async globalUser( async globalUser(config: Partial<User> = {}): Promise<User> {
config: {
id?: string
firstName?: string
lastName?: string
builder?: boolean
admin?: boolean
email?: string
roles?: any
} = {}
): Promise<User> {
const { const {
id = `us_${newid()}`, _id = `us_${newid()}`,
firstName = generator.first(), firstName = generator.first(),
lastName = generator.last(), lastName = generator.last(),
builder = true, builder = { global: true },
admin = false, admin = { global: false },
email = generator.email(), email = generator.email(),
roles, tenantId = this.getTenantId(),
roles = {},
} = config } = config
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
let existing let existing: Partial<User> = {}
try { try {
existing = await db.get<any>(id) existing = await db.get<User>(_id)
} catch (err) { } catch (err) {
existing = { email } // ignore
} }
const user: User = { const user: User = {
_id: id, _id,
...existing, ...existing,
roles: roles || {}, ...config,
tenantId: this.getTenantId(), email,
roles,
tenantId,
firstName, firstName,
lastName, lastName,
builder,
admin,
} }
await sessions.createASession(id, { await sessions.createASession(_id, {
sessionId: "sessionid", sessionId: "sessionid",
tenantId: this.getTenantId(), tenantId: this.getTenantId(),
csrfToken: this.csrfToken, csrfToken: this.csrfToken,
}) })
if (builder) {
user.builder = { global: true }
} else {
user.builder = { global: false }
}
if (admin) {
user.admin = { global: true }
} else {
user.admin = { global: false }
}
const resp = await db.put(user) const resp = await db.put(user)
return { return {
_rev: resp.rev, _rev: resp.rev,
@ -342,38 +373,9 @@ export default class TestConfiguration {
} }
} }
async createUser( async createUser(user: Partial<User> = {}): Promise<User> {
user: { const resp = await this.globalUser(user)
id?: string await cache.user.invalidateUser(resp._id!)
firstName?: string
lastName?: string
email?: string
builder?: boolean
admin?: boolean
roles?: UserRoles
} = {}
): Promise<User> {
const {
id,
firstName = generator.first(),
lastName = generator.last(),
email = generator.email(),
builder = true,
admin,
roles,
} = user
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
const resp = await this.globalUser({
id: globalId,
firstName,
lastName,
email,
builder,
admin,
roles: roles || {},
})
await cache.user.invalidateUser(globalId)
return resp return resp
} }
@ -381,7 +383,7 @@ export default class TestConfiguration {
return context.doInTenant(this.tenantId!, async () => { return context.doInTenant(this.tenantId!, async () => {
const baseGroup = structures.userGroups.userGroup() const baseGroup = structures.userGroups.userGroup()
baseGroup.roles = { baseGroup.roles = {
[this.prodAppId]: roleId, [this.getProdAppId()]: roleId,
} }
const { id, rev } = await pro.sdk.groups.save(baseGroup) const { id, rev } = await pro.sdk.groups.save(baseGroup)
return { return {
@ -404,8 +406,18 @@ export default class TestConfiguration {
}) })
} }
async login({ roleId, userId, builder, prodApp = false }: any = {}) { async login({
const appId = prodApp ? this.prodAppId : this.appId roleId,
userId,
builder,
prodApp,
}: {
roleId?: string
userId: string
builder: boolean
prodApp: boolean
}) {
const appId = prodApp ? this.getProdAppId() : this.getAppId()
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId userId = !userId ? `us_uuid1` : userId
if (!this.request) { if (!this.request) {
@ -414,9 +426,9 @@ export default class TestConfiguration {
// make sure the user exists in the global DB // make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) { if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({ await this.globalUser({
id: userId, _id: userId,
builder, builder: { global: builder },
roles: { [this.prodAppId]: roleId }, roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
}) })
} }
await sessions.createASession(userId, { await sessions.createASession(userId, {
@ -445,8 +457,9 @@ export default class TestConfiguration {
defaultHeaders(extras = {}, prodApp = false) { defaultHeaders(extras = {}, prodApp = false) {
const tenantId = this.getTenantId() const tenantId = this.getTenantId()
const user = this.getUser()
const authObj: AuthToken = { const authObj: AuthToken = {
userId: this.globalUserId, userId: user._id!,
sessionId: "sessionid", sessionId: "sessionid",
tenantId, tenantId,
} }
@ -498,7 +511,7 @@ export default class TestConfiguration {
builder = false, builder = false,
prodApp = true, prodApp = true,
} = {}) { } = {}) {
return this.login({ email, roleId, builder, prodApp }) return this.login({ userId: email, roleId, builder, prodApp })
} }
// TENANCY // TENANCY
@ -521,18 +534,22 @@ export default class TestConfiguration {
this.tenantId = structures.tenant.id() this.tenantId = structures.tenant.id()
this.user = await this.globalUser() this.user = await this.globalUser()
this.userMetadataId = generateUserMetadataID(this.user._id) this.userMetadataId = generateUserMetadataID(this.user._id!)
return this.createApp(appName) return this.createApp(appName)
} }
doInTenant(task: any) { doInTenant<T>(task: () => T) {
return context.doInTenant(this.getTenantId(), task) return context.doInTenant(this.getTenantId(), task)
} }
// API // API
async generateApiKey(userId = this.user._id) { async generateApiKey(userId?: string) {
const user = this.getUser()
if (!userId) {
userId = user._id!
}
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId) const id = dbCore.generateDevInfoID(userId)
let devInfo: any let devInfo: any
@ -552,25 +569,28 @@ export default class TestConfiguration {
async createApp(appName: string): Promise<App> { async createApp(appName: string): Promise<App> {
// create dev app // create dev app
// clear any old app // clear any old app
this.appId = null this.appId = undefined
this.app = await context.doInTenant(this.tenantId!, async () => { this.app = await context.doInTenant(
const app = await this._req({ name: appName }, null, appController.create) this.tenantId!,
this.appId = app.appId! async () =>
return app (await this._req(appController.create, {
}) name: appName,
return await context.doInAppContext(this.getAppId(), async () => { })) as App
)
this.appId = this.app.appId
return await context.doInAppContext(this.app.appId!, async () => {
// create production app // create production app
this.prodApp = await this.publish() this.prodApp = await this.publish()
this.allApps.push(this.prodApp) this.allApps.push(this.prodApp)
this.allApps.push(this.app) this.allApps.push(this.app!)
return this.app! return this.app!
}) })
} }
async publish() { async publish() {
await this._req(null, null, deployController.publishApp) await this._req(deployController.publishApp)
// @ts-ignore // @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "") const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId this.prodAppId = prodAppId
@ -582,13 +602,11 @@ export default class TestConfiguration {
} }
async unpublish() { async unpublish() {
const response = await this._req( const response = await this._req(appController.unpublish, {
null, appId: this.appId,
{ appId: this.appId }, })
appController.unpublish this.prodAppId = undefined
) this.prodApp = undefined
this.prodAppId = null
this.prodApp = null
return response return response
} }
@ -716,8 +734,7 @@ export default class TestConfiguration {
// ROLE // ROLE
async createRole(config?: any) { async createRole(config?: any) {
config = config || basicRole() return this._req(roleController.save, config || basicRole())
return this._req(config, null, roleController.save)
} }
// VIEW // VIEW
@ -730,7 +747,7 @@ export default class TestConfiguration {
tableId: this.table!._id, tableId: this.table!._id,
name: generator.guid(), name: generator.guid(),
} }
return this._req(view, null, viewController.v1.save) return this._req(viewController.v1.save, view)
} }
async createView( async createView(
@ -754,40 +771,38 @@ export default class TestConfiguration {
// AUTOMATION // AUTOMATION
async createAutomation(config?: any) { async createAutomation(config?: Automation) {
config = config || basicAutomation() config = config || basicAutomation()
if (config._rev) { if (config._rev) {
delete config._rev delete config._rev
} }
this.automation = ( const res = await this._req(automationController.create, config)
await this._req(config, null, automationController.create) this.automation = res.automation
).automation
return this.automation return this.automation
} }
async getAllAutomations() { async getAllAutomations() {
return this._req(null, null, automationController.fetch) return this._req(automationController.fetch)
} }
async deleteAutomation(automation?: any) { async deleteAutomation(automation?: Automation) {
automation = automation || this.automation automation = automation || this.automation
if (!automation) { if (!automation) {
return return
} }
return this._req( return this._req(automationController.destroy, undefined, {
null, id: automation._id,
{ id: automation._id, rev: automation._rev }, rev: automation._rev,
automationController.destroy })
)
} }
async createWebhook(config?: any) { async createWebhook(config?: Webhook) {
if (!this.automation) { if (!this.automation) {
throw "Must create an automation before creating webhook." throw "Must create an automation before creating webhook."
} }
config = config || basicWebhook(this.automation._id) config = config || basicWebhook(this.automation._id!)
return (await this._req(config, null, webhookController.save)).webhook return (await this._req(webhookController.save, config)).webhook
} }
// DATASOURCE // DATASOURCE
@ -809,7 +824,7 @@ export default class TestConfiguration {
return { ...this.datasource, _id: this.datasource!._id! } return { ...this.datasource, _id: this.datasource!._id! }
} }
async restDatasource(cfg?: any) { async restDatasource(cfg?: Record<string, any>) {
return this.createDatasource({ return this.createDatasource({
datasource: { datasource: {
...basicDatasource().datasource, ...basicDatasource().datasource,
@ -866,26 +881,25 @@ export default class TestConfiguration {
// QUERY // QUERY
async createQuery(config?: any) { async createQuery(config?: Query) {
if (!this.datasource && !config) { return this._req(
throw "No datasource created for query." queryController.save,
} config || basicQuery(this.getDatasource()._id!)
config = config || basicQuery(this.datasource!._id!) )
return this._req(config, null, queryController.save)
} }
// SCREEN // SCREEN
async createScreen(config?: any) { async createScreen(config?: Screen) {
config = config || basicScreen() config = config || basicScreen()
return this._req(config, null, screenController.save) return this._req(screenController.save, config)
} }
// LAYOUT // LAYOUT
async createLayout(config?: any) { async createLayout(config?: Layout) {
config = config || basicLayout() config = config || basicLayout()
return await this._req(config, null, layoutController.save) return await this._req(layoutController.save, config)
} }
} }

View File

@ -22,6 +22,8 @@ import {
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
TableSourceType, TableSourceType,
Query, Query,
Webhook,
WebhookActionType,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations" import { LoopInput, LoopStepType } from "../../definitions/automations"
@ -407,12 +409,12 @@ export function basicLayout() {
return cloneDeep(EMPTY_LAYOUT) return cloneDeep(EMPTY_LAYOUT)
} }
export function basicWebhook(automationId: string) { export function basicWebhook(automationId: string): Webhook {
return { return {
live: true, live: true,
name: "webhook", name: "webhook",
action: { action: {
type: "automation", type: WebhookActionType.AUTOMATION,
target: automationId, target: automationId,
}, },
} }

View File

@ -0,0 +1,3 @@
import { DocumentDestroyResponse } from "@budibase/nano"
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}

View File

@ -11,3 +11,5 @@ export * from "./global"
export * from "./pagination" export * from "./pagination"
export * from "./searchFilter" export * from "./searchFilter"
export * from "./cookies" export * from "./cookies"
export * from "./automation"
export * from "./layout"

View File

@ -0,0 +1,5 @@
import { Layout } from "../../documents"
export interface SaveLayoutRequest extends Layout {}
export interface SaveLayoutResponse extends Layout {}

View File

@ -1,6 +1,11 @@
import { Document } from "../document" import { Document } from "../document"
export interface Layout extends Document { export interface Layout extends Document {
componentLibraries: string[]
title: string
favicon: string
stylesheets: string[]
props: any props: any
layoutId?: string layoutId?: string
name?: string
} }

View File

@ -22,4 +22,5 @@ export interface Screen extends Document {
routing: ScreenRouting routing: ScreenRouting
props: ScreenProps props: ScreenProps
name?: string name?: string
pluginAdded?: boolean
} }

View File

@ -280,7 +280,7 @@ class TestConfiguration {
const db = context.getGlobalDB() const db = context.getGlobalDB()
const id = dbCore.generateDevInfoID(this.user!._id) const id = dbCore.generateDevInfoID(this.user!._id!)
// TODO: dry // TODO: dry
this.apiKey = encryption.encrypt( this.apiKey = encryption.encrypt(
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`