budibase/packages/server/src/tests/utilities/TestConfiguration.ts

935 lines
22 KiB
TypeScript
Raw Normal View History

2023-02-03 12:28:27 +01:00
import { generator, mocks, structures } from "@budibase/backend-core/tests"
// init the licensing mock
import * as pro from "@budibase/pro"
import { init as dbInit } from "../../db"
import env from "../../environment"
import {
2021-03-03 19:41:49 +01:00
basicAutomation,
basicAutomationResults,
2021-03-04 11:05:50 +01:00
basicDatasource,
basicLayout,
2021-03-04 11:05:50 +01:00
basicQuery,
basicRole,
basicRow,
2021-03-08 15:49:19 +01:00
basicScreen,
basicTable,
basicWebhook,
} from "./structures"
import {
cache,
constants,
context,
db as dbCore,
encryption,
2023-09-06 01:49:54 +02:00
env as coreEnv,
roles,
sessions,
tenancy,
2024-07-11 15:27:48 +02:00
utils,
} from "@budibase/backend-core"
2024-01-26 11:07:06 +01:00
import {
app as appController,
deploy as deployController,
role as roleController,
automation as automationController,
webhook as webhookController,
query as queryController,
screen as screenController,
layout as layoutController,
view as viewController,
} from "./controllers"
import { cleanup } from "../../utilities/fileSystem"
import { generateUserMetadataID } from "../../db/utils"
import { startup } from "../../startup"
2023-01-16 16:35:41 +01:00
import supertest from "supertest"
import {
App,
AuthToken,
Automation,
CreateViewRequest,
Datasource,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
2024-02-28 13:13:13 +01:00
Layout,
Query,
RelationshipFieldMetadata,
RelationshipType,
Row,
2024-02-28 13:13:13 +01:00
Screen,
RowSearchParams,
SourceName,
Table,
TableSourceType,
User,
2024-02-28 12:46:58 +01:00
UserCtx,
2023-09-08 10:43:19 +02:00
View,
2024-02-28 13:13:13 +01:00
Webhook,
2024-01-29 23:25:12 +01:00
WithRequired,
2025-02-25 19:23:29 +01:00
DevInfo,
} from "@budibase/types"
2023-07-18 12:56:24 +02:00
import API from "./api"
import jwt, { Secret } from "jsonwebtoken"
import { Server } from "http"
2023-07-18 12:56:24 +02:00
2024-07-11 15:27:48 +02:00
const newid = utils.newid
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
dbInit()
2023-07-18 12:56:24 +02:00
export interface CreateAppRequest {
appName: string
url?: string
snippets?: any[]
}
2024-02-09 15:27:48 +01:00
export interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
sourceId?: string
sourceType?: TableSourceType
}
2024-02-12 12:34:39 +01:00
export default class TestConfiguration {
server?: Server
request?: supertest.SuperTest<supertest.Test>
started: boolean
appId?: string
2024-02-28 12:46:58 +01:00
allApps: App[]
app?: App
prodApp?: App
prodAppId?: string
user?: User
userMetadataId?: string
2023-07-18 10:14:13 +02:00
table?: Table
2024-02-28 12:46:58 +01:00
automation?: Automation
2023-08-30 17:42:26 +02:00
datasource?: Datasource
tenantId?: string
2023-07-18 12:56:24 +02:00
api: API
csrfToken?: string
2024-10-17 18:15:41 +02:00
temporaryHeaders?: Record<string, string | string[]>
constructor(openServer = true) {
if (openServer) {
// use a random port because it doesn't matter
env.PORT = "0"
this.server = require("../../app").getServer()
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
this.started = true
} else {
this.started = false
}
this.appId = undefined
this.allApps = []
2023-07-18 12:56:24 +02:00
this.api = new API(this)
2023-01-25 18:11:37 +01:00
}
getRequest() {
return this.request
}
2022-02-25 16:55:19 +01:00
getApp() {
2024-02-28 12:46:58 +01:00
if (!this.app) {
throw new Error("app has not been initialised, call config.init() first")
}
2022-02-25 16:55:19 +01:00
return this.app
}
2022-08-10 12:01:54 +02:00
getProdApp() {
2024-02-28 12:46:58 +01:00
if (!this.prodApp) {
throw new Error(
"prodApp has not been initialised, call config.init() first"
)
}
2022-08-10 12:01:54 +02:00
return this.prodApp
}
getAppId() {
2023-12-04 09:23:01 +01:00
if (!this.appId) {
2024-02-28 12:46:58 +01:00
throw new Error(
"appId has not been initialised, call config.init() first"
)
2023-12-04 09:23:01 +01:00
}
return this.appId
}
2022-04-04 16:59:00 +02:00
getProdAppId() {
if (!this.prodAppId) {
throw new Error(
"prodAppId has not been initialised, call config.init() first"
)
}
2022-04-04 16:59:00 +02:00
return this.prodAppId
}
getUser(): User {
if (!this.user) {
throw new Error("User has not been initialised, call config.init() first")
}
return this.user
}
getUserDetails() {
const user = this.getUser()
return {
globalId: user._id!,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
2024-02-21 10:36:17 +01:00
}
}
2024-02-28 12:46:58 +01:00
getAutomation() {
if (!this.automation) {
throw new Error(
"automation has not been initialised, call config.init() first"
)
}
return this.automation
}
2024-02-28 13:13:13 +01:00
getDatasource() {
if (!this.datasource) {
throw new Error(
"datasource has not been initialised, call config.init() first"
)
}
2024-02-28 13:13:13 +01:00
return this.datasource
}
2023-07-13 12:17:24 +02:00
async doInContext<T>(
appId: string | undefined,
2023-07-13 12:17:24 +02:00
task: () => Promise<T>
): Promise<T> {
2023-01-25 18:11:37 +01:00
const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => {
if (!appId) {
appId = this.appId
}
// check if already in a context
if (context.getAppId() == null && appId) {
return context.doInAppContext(appId, async () => {
return task()
})
} else {
return task()
}
})
}
// SETUP / TEARDOWN
// use a new id as the name to avoid name collisions
async init(appName = newid()) {
if (!this.started) {
await startup()
}
return this.newTenant(appName)
}
2023-02-07 13:45:41 +01:00
end() {
if (!this) {
return
}
2024-11-06 18:18:34 +01:00
2024-11-06 23:08:37 +01:00
if (this.server) {
this.server.close()
} else {
require("../../app").getServer().close()
}
2023-02-07 13:45:41 +01:00
if (this.allApps) {
cleanup(this.allApps.map(app => app.appId))
}
}
2025-02-25 19:23:29 +01:00
async withUser<T>(user: User, f: () => Promise<T>): Promise<T> {
2024-03-04 17:42:41 +01:00
const oldUser = this.user
this.user = user
try {
2024-03-05 11:05:05 +01:00
return await f()
2024-03-04 17:42:41 +01:00
} finally {
this.user = oldUser
}
}
2025-01-28 18:43:03 +01:00
async withApp<R>(app: App | string, f: () => Promise<R>) {
const oldAppId = this.appId
this.appId = typeof app === "string" ? app : app.appId
2025-02-17 18:39:38 +01:00
return await context.doInAppContext(this.appId, async () => {
try {
return await f()
} finally {
this.appId = oldAppId
}
})
}
2025-01-28 18:43:03 +01:00
async withProdApp<R>(f: () => Promise<R>) {
return await this.withApp(this.getProdAppId(), f)
}
// UTILS
2024-02-28 13:13:13 +01:00
_req<Req extends Record<string, any> | void, Res>(
2024-02-28 12:46:58 +01:00
handler: (ctx: UserCtx<Req, Res>) => Promise<void>,
body?: Req,
2024-02-28 13:13:13 +01:00
params?: Record<string, string | undefined>
2024-02-28 12:46:58 +01:00
): Promise<Res> {
2022-07-13 14:22:21 +02:00
// create a fake request ctx
const request: any = {}
2022-08-10 12:01:54 +02:00
const appId = this.appId
2022-07-13 14:22:21 +02:00
request.appId = appId
// fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} }
2023-01-25 18:11:37 +01:00
request.user = { appId, tenantId: this.getTenantId() }
request.query = {}
request.request = {
2022-07-13 14:22:21 +02:00
body,
}
2022-07-13 14:22:21 +02:00
if (params) {
request.params = params
}
request.throw = (status: number, message: string) => {
throw new Error(`Error ${status} - ${message}`)
}
2022-07-13 14:22:21 +02:00
return this.doInContext(appId, async () => {
await handler(request)
return request.body
})
}
// USER / AUTH
async globalUser(config: Partial<User> = {}): Promise<User> {
const {
_id = `us_${newid()}`,
firstName = generator.first(),
lastName = generator.last(),
builder = { global: true },
admin = { global: false },
email = generator.email({ domain: "example.com" }),
2024-02-28 12:16:26 +01:00
tenantId = this.getTenantId(),
roles = {},
} = config
const db = tenancy.getTenantDB(this.getTenantId())
2024-02-28 12:16:26 +01:00
let existing: Partial<User> = {}
try {
existing = await db.get<User>(_id)
} catch (err) {
2024-02-28 12:16:26 +01:00
// ignore
}
const user: User = {
2024-02-28 12:16:26 +01:00
_id,
...existing,
2024-02-28 12:16:26 +01:00
...config,
2024-03-05 15:37:06 +01:00
_rev: existing._rev,
2024-02-28 12:16:26 +01:00
email,
roles,
tenantId,
firstName,
lastName,
2024-02-28 12:16:26 +01:00
builder,
admin,
}
await sessions.createASession(_id, {
2024-03-05 15:37:06 +01:00
sessionId: this.sessionIdForUser(_id),
tenantId: this.getTenantId(),
csrfToken: this.csrfToken,
email,
2022-01-25 23:54:50 +01:00
})
const resp = await db.put(user)
2024-03-05 15:37:06 +01:00
await cache.user.invalidateUser(_id)
return {
_rev: resp.rev,
...user,
}
}
async createUser(user: Partial<User> = {}): Promise<User> {
2024-03-05 15:37:06 +01:00
return await this.globalUser(user)
}
2023-09-06 01:49:54 +02:00
async createGroup(roleId: string = roles.BUILTIN_ROLE_IDS.BASIC) {
return context.doInTenant(this.tenantId!, async () => {
const baseGroup = structures.userGroups.userGroup()
baseGroup.roles = {
[this.getProdAppId()]: roleId,
}
const { id, rev } = await pro.sdk.groups.save(baseGroup)
return {
_id: id,
_rev: rev,
...baseGroup,
}
})
}
async addUserToGroup(groupId: string, userId: string) {
return context.doInTenant(this.tenantId!, async () => {
await pro.sdk.groups.addUsers(groupId, [userId])
})
}
async removeUserFromGroup(groupId: string, userId: string) {
return context.doInTenant(this.tenantId!, async () => {
await pro.sdk.groups.removeUsers(groupId, [userId])
})
}
2024-03-05 15:37:06 +01:00
sessionIdForUser(userId: string): string {
return `sessionid-${userId}`
}
async login({
roleId,
userId,
builder,
prodApp,
}: {
2024-02-28 13:13:13 +01:00
roleId?: string
userId: string
builder: boolean
prodApp: boolean
}) {
const appId = prodApp ? this.getProdAppId() : this.getAppId()
return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId
if (!this.request) {
throw "Server has not been opened, cannot login."
}
// make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
const user = await this.globalUser({
_id: userId,
builder: { global: builder },
2024-02-28 13:13:13 +01:00
roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
})
await sessions.createASession(userId, {
sessionId: this.sessionIdForUser(userId),
tenantId: this.getTenantId(),
email: user.email,
})
}
// have to fake this
const authObj = {
userId,
2024-03-05 15:37:06 +01:00
sessionId: this.sessionIdForUser(userId),
2023-01-25 18:11:37 +01:00
tenantId: this.getTenantId(),
}
const authToken = jwt.sign(authObj, coreEnv.JWT_SECRET as Secret)
// returning necessary request headers
await cache.user.invalidateUser(userId)
return {
Accept: "application/json",
Cookie: [`${constants.Cookie.Auth}=${authToken}`],
[constants.Header.APP_ID]: appId,
2024-10-25 11:41:20 +02:00
...this.temporaryHeaders,
}
})
}
// HEADERS
2024-10-14 19:57:46 +02:00
// sets the role for the headers, for the period of a callback
2024-10-17 16:17:36 +02:00
async loginAsRole(roleId: string, cb: () => Promise<unknown>) {
2024-10-14 19:57:46 +02:00
const roleUser = await this.createUser({
roles: {
2024-10-17 17:58:51 +02:00
[this.getProdAppId()]: roleId,
2024-10-14 19:57:46 +02:00
},
builder: { global: false },
admin: { global: false },
})
await this.login({
roleId,
userId: roleUser._id!,
builder: false,
prodApp: true,
})
2024-10-17 17:58:51 +02:00
await this.withUser(roleUser, async () => {
await cb()
})
2024-10-14 19:57:46 +02:00
}
2024-10-17 18:15:41 +02:00
async withHeaders(
headers: Record<string, string | string[]>,
cb: () => Promise<unknown>
) {
this.temporaryHeaders = headers
try {
await cb()
} finally {
this.temporaryHeaders = undefined
}
}
2025-02-25 19:23:29 +01:00
defaultHeaders(
extras: Record<string, string | string[]> = {},
prodApp = false
) {
2023-01-25 18:11:37 +01:00
const tenantId = this.getTenantId()
const user = this.getUser()
2023-01-25 18:11:37 +01:00
const authObj: AuthToken = {
userId: user._id!,
2024-03-05 15:37:06 +01:00
sessionId: this.sessionIdForUser(user._id!),
2023-01-25 18:11:37 +01:00
tenantId,
}
const authToken = jwt.sign(authObj, coreEnv.JWT_SECRET as Secret)
const headers: any = {
Accept: "application/json",
Cookie: [`${constants.Cookie.Auth}=${authToken}`],
[constants.Header.CSRF_TOKEN]: this.csrfToken,
Host: this.tenantHost(),
...extras,
}
2023-01-19 18:23:48 +01:00
2023-05-23 16:55:25 +02:00
if (prodApp) {
headers[constants.Header.APP_ID] = this.prodAppId
} else if (this.appId) {
headers[constants.Header.APP_ID] = this.appId
}
2024-10-17 18:15:41 +02:00
return {
...headers,
...this.temporaryHeaders,
}
}
2025-02-25 19:23:29 +01:00
publicHeaders({
prodApp = true,
extras = {},
}: { prodApp?: boolean; extras?: Record<string, string | string[]> } = {}) {
const appId = prodApp ? this.prodAppId : this.appId
2025-02-25 19:23:29 +01:00
const headers: Record<string, string> = {
Accept: "application/json",
Cookie: "",
}
2021-10-26 17:21:26 +02:00
if (appId) {
headers[constants.Header.APP_ID] = appId
}
2023-01-26 16:16:42 +01:00
headers[constants.Header.TENANT_ID] = this.getTenantId()
2023-01-26 16:16:42 +01:00
2024-10-17 18:15:41 +02:00
return {
...headers,
...this.temporaryHeaders,
2025-02-25 19:23:29 +01:00
...extras,
2024-10-17 18:15:41 +02:00
}
}
async basicRoleHeaders() {
return await this.roleHeaders({
email: generator.email({ domain: "example.com" }),
builder: false,
prodApp: true,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
}
async roleHeaders({
email = generator.email({ domain: "example.com" }),
roleId = roles.BUILTIN_ROLE_IDS.ADMIN,
builder = false,
prodApp = true,
} = {}) {
return this.login({ userId: email, roleId, builder, prodApp })
2021-03-08 15:49:19 +01:00
}
2024-10-25 11:41:20 +02:00
browserUserAgent() {
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
}
// TENANCY
tenantHost() {
const tenantId = this.getTenantId()
const platformHost = new URL(coreEnv.PLATFORM_URL).host.split(":")[0]
return `${tenantId}.${platformHost}`
}
getTenantId() {
if (!this.tenantId) {
throw new Error("no test tenant id - init has not been called")
}
return this.tenantId
}
async newTenant(appName = newid()): Promise<App> {
2024-02-21 10:57:49 +01:00
this.csrfToken = generator.hash()
this.tenantId = structures.tenant.id()
this.user = await this.globalUser()
this.userMetadataId = generateUserMetadataID(this.user._id!)
return this.createApp(appName)
}
2024-02-28 13:19:08 +01:00
doInTenant<T>(task: () => T) {
return context.doInTenant(this.getTenantId(), task)
}
// API
async generateApiKey(userId?: string) {
const user = this.getUser()
if (!userId) {
userId = user._id!
}
const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId)
2025-02-25 19:23:29 +01:00
const devInfo = await db.tryGet<DevInfo>(id)
if (devInfo && devInfo.apiKey) {
return devInfo.apiKey
}
2025-02-25 19:23:29 +01:00
const apiKey = encryption.encrypt(
`${this.getTenantId()}${dbCore.SEPARATOR}${newid()}`
)
2025-02-25 19:23:29 +01:00
const newDevInfo: DevInfo = { _id: id, userId, apiKey }
await db.put(newDevInfo)
return apiKey
}
// APP
async createApp(appName: string, url?: string): Promise<App> {
this.appId = undefined
2024-02-28 12:46:58 +01:00
this.app = await context.doInTenant(
this.tenantId!,
async () =>
(await this._req(appController.create, {
name: appName,
url,
2024-02-28 12:46:58 +01:00
})) as App
)
this.appId = this.app.appId
return await context.doInAppContext(this.app.appId!, async () => {
// create production app
this.prodApp = await this.publish()
this.allApps.push(this.prodApp)
2024-02-28 12:46:58 +01:00
this.allApps.push(this.app!)
2023-11-08 17:17:24 +01:00
return this.app!
})
}
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
async publish() {
await this._req(deployController.publishApp)
// @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "")
2022-07-13 14:22:21 +02:00
this.prodAppId = prodAppId
2022-08-10 12:01:54 +02:00
return context.doInAppContext(prodAppId, async () => {
2022-08-10 12:01:54 +02:00
const db = context.getProdAppDB()
2023-11-08 17:17:24 +01:00
return await db.get<App>(dbCore.DocumentType.APP_METADATA)
})
}
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
async unpublish() {
2024-12-02 16:26:12 +01:00
const response = await this._req(appController.unpublish, undefined, {
appId: this.appId,
})
this.prodAppId = undefined
this.prodApp = undefined
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
return response
}
// TABLE
2024-01-26 10:57:35 +01:00
async upsertTable(
config?: TableToBuild,
2023-07-18 14:34:23 +02:00
{ skipReassigning } = { skipReassigning: false }
): Promise<Table> {
config = config || basicTable()
const response = await this.api.table.save({
...config,
sourceType: config.sourceType || TableSourceType.INTERNAL,
sourceId: config.sourceId || INTERNAL_TABLE_SOURCE_ID,
})
2023-07-18 14:34:23 +02:00
if (!skipReassigning) {
this.table = response
}
return response
}
async createTable(
config?: TableToBuild,
options = { skipReassigning: false }
) {
if (config != null && config._id) {
delete config._id
}
2023-08-30 17:42:26 +02:00
config = config || basicTable()
if (!config.sourceId) {
config.sourceId = INTERNAL_TABLE_SOURCE_ID
2023-08-30 17:42:26 +02:00
}
2024-01-26 10:57:35 +01:00
return this.upsertTable(config, options)
}
2023-08-30 17:42:26 +02:00
async createExternalTable(
config?: TableToBuild,
options = { skipReassigning: false }
) {
if (config != null && config._id) {
delete config._id
}
config = config || basicTable()
if (this.datasource?._id) {
config.sourceId = this.datasource._id
config.sourceType = TableSourceType.EXTERNAL
}
2024-01-26 10:57:35 +01:00
return this.upsertTable(config, options)
}
2023-09-13 12:23:59 +02:00
async getTable(tableId?: string) {
tableId = tableId || this.table!._id!
2024-01-26 10:58:46 +01:00
return this.api.table.get(tableId)
2023-09-13 12:23:59 +02:00
}
2023-09-08 15:54:27 +02:00
async createLinkedTable(
relationshipType = RelationshipType.ONE_TO_MANY,
2023-09-13 14:09:48 +02:00
links: any = ["link"],
config?: TableToBuild
2023-09-08 15:54:27 +02:00
) {
2021-03-04 14:07:33 +01:00
if (!this.table) {
throw "Must have created a table first."
}
2023-09-08 15:54:27 +02:00
const tableConfig = config || basicTable()
if (!tableConfig.sourceId) {
tableConfig.sourceId = INTERNAL_TABLE_SOURCE_ID
}
2021-03-04 14:07:33 +01:00
tableConfig.primaryDisplay = "name"
for (let link of links) {
tableConfig.schema[link] = {
2023-09-08 15:54:27 +02:00
type: FieldType.LINK,
fieldName: link,
2023-10-05 16:14:30 +02:00
tableId: this.table._id!,
name: link,
2023-09-08 15:54:27 +02:00
relationshipType,
2023-10-11 12:55:23 +02:00
} as RelationshipFieldMetadata
2023-09-08 15:54:27 +02:00
}
2023-10-26 17:32:34 +02:00
if (this.datasource?._id) {
2023-09-08 15:54:27 +02:00
tableConfig.sourceId = this.datasource._id
2023-10-26 17:32:34 +02:00
tableConfig.sourceType = TableSourceType.EXTERNAL
}
2023-09-08 15:54:27 +02:00
return await this.createTable(tableConfig)
}
async createAttachmentTable() {
const table: any = basicTable()
table.schema.attachment = {
type: "attachment",
}
return this.createTable(table)
}
// ROW
2023-01-19 12:00:51 +01:00
async createRow(config?: Row): Promise<Row> {
if (!this.table) {
throw "Test requires table to be configured."
}
const tableId = (config && config.tableId) || this.table._id!
2023-07-18 10:14:13 +02:00
config = config || basicRow(tableId!)
return this.api.row.save(tableId, config)
}
async getRows(tableId: string) {
if (!tableId && this.table) {
2023-07-18 10:14:13 +02:00
tableId = this.table._id!
}
2024-01-26 11:01:16 +01:00
return this.api.row.fetch(tableId)
}
async searchRows(tableId: string, searchParams?: RowSearchParams) {
2023-03-21 18:27:31 +01:00
if (!tableId && this.table) {
2023-07-18 10:14:13 +02:00
tableId = this.table._id!
2023-03-21 18:27:31 +01:00
}
2024-01-26 11:24:32 +01:00
return this.api.row.search(tableId, searchParams)
2023-03-21 18:27:31 +01:00
}
// ROLE
async createRole(config?: any) {
return this._req(roleController.save, config || basicRole())
}
// VIEW
2023-09-08 10:43:19 +02:00
async createLegacyView(config?: View) {
if (!this.table && !config) {
throw "Test requires table to be configured."
}
const view = config || {
2023-09-08 10:43:19 +02:00
tableId: this.table!._id,
2023-09-12 20:17:21 +02:00
name: generator.guid(),
}
return this._req(viewController.v1.save, view)
}
2023-09-12 19:39:40 +02:00
async createView(
config?: Omit<CreateViewRequest, "tableId" | "name"> & {
name?: string
tableId?: string
}
) {
if (!this.table && !config?.tableId) {
throw "Test requires table to be configured."
}
const view: CreateViewRequest = {
...config,
tableId: config?.tableId || this.table!._id!,
name: config?.name || generator.word(),
}
return await this.api.viewV2.create(view)
}
// AUTOMATION
2024-02-28 12:46:58 +01:00
async createAutomation(config?: Automation) {
2021-03-03 19:41:49 +01:00
config = config || basicAutomation()
if (config._rev) {
delete config._rev
}
2024-02-28 12:46:58 +01:00
const res = await this._req(automationController.create, config)
this.automation = res.automation
2021-03-03 19:41:49 +01:00
return this.automation
}
async getAllAutomations() {
return this._req(automationController.fetch)
2021-03-03 19:41:49 +01:00
}
2024-02-28 13:13:13 +01:00
async deleteAutomation(automation?: Automation) {
2021-03-03 19:41:49 +01:00
automation = automation || this.automation
if (!automation) {
return
}
2024-02-28 12:16:26 +01:00
return this._req(automationController.destroy, undefined, {
id: automation._id,
rev: automation._rev,
})
2021-03-03 19:41:49 +01:00
}
2024-02-28 13:13:13 +01:00
async createWebhook(config?: Webhook) {
if (!this.automation) {
throw "Must create an automation before creating webhook."
}
2024-02-28 12:46:58 +01:00
config = config || basicWebhook(this.automation._id!)
2024-01-26 11:07:06 +01:00
return (await this._req(webhookController.save, config)).webhook
}
// DATASOURCE
2023-01-20 11:29:11 +01:00
async createDatasource(config?: {
datasource: Datasource
2024-01-29 23:25:12 +01:00
}): Promise<WithRequired<Datasource, "_id">> {
2021-03-04 11:05:50 +01:00
config = config || basicDatasource()
2024-01-26 11:11:54 +01:00
const response = await this.api.datasource.create(config.datasource)
this.datasource = response
2024-01-29 22:57:20 +01:00
return { ...this.datasource, _id: this.datasource!._id! }
2021-03-04 11:05:50 +01:00
}
2024-01-29 23:25:12 +01:00
async updateDatasource(
datasource: Datasource
): Promise<WithRequired<Datasource, "_id">> {
2024-01-26 11:11:54 +01:00
const response = await this.api.datasource.update(datasource)
this.datasource = response
2024-01-29 23:25:12 +01:00
return { ...this.datasource, _id: this.datasource!._id! }
}
2024-02-28 13:13:13 +01:00
async restDatasource(cfg?: Record<string, any>) {
return this.createDatasource({
2023-01-20 11:29:11 +01:00
datasource: {
...basicDatasource().datasource,
source: SourceName.REST,
config: cfg || {},
},
})
}
async dynamicVariableDatasource() {
let datasource = await this.restDatasource()
2024-01-29 23:25:12 +01:00
const basedOnQuery = await this.createQuery({
2023-01-18 11:45:42 +01:00
...basicQuery(datasource._id!),
fields: {
path: "www.google.com",
},
})
datasource = await this.updateDatasource({
...datasource,
config: {
dynamicVariables: [
{
queryId: basedOnQuery._id,
name: "variable3",
value: "{{ data.0.[value] }}",
},
],
},
})
return { datasource, query: basedOnQuery }
}
// AUTOMATION LOG
async createAutomationLog(automation: Automation, appId?: string) {
appId = appId || this.getProdAppId()
return await context.doInAppContext(appId!, async () => {
return await pro.sdk.automations.logs.storeLog(
automation,
2023-06-27 11:01:06 +02:00
basicAutomationResults(automation._id!)
)
})
}
async getAutomationLogs() {
2023-12-04 09:23:01 +01:00
return context.doInAppContext(this.getAppId(), async () => {
2023-06-27 11:01:06 +02:00
const now = new Date()
return await pro.sdk.automations.logs.logSearch({
startDate: new Date(now.getTime() - 100000).toISOString(),
})
})
}
// QUERY
2024-02-28 13:13:13 +01:00
async createQuery(config?: Query) {
return this._req(
queryController.save,
config || basicQuery(this.getDatasource()._id!)
)
2021-03-04 11:05:50 +01:00
}
// SCREEN
2024-02-28 13:13:13 +01:00
async createScreen(config?: Screen) {
2021-03-08 15:49:19 +01:00
config = config || basicScreen()
return this._req(screenController.save, config)
2021-03-08 15:49:19 +01:00
}
// LAYOUT
2024-02-28 13:13:13 +01:00
async createLayout(config?: Layout) {
config = config || basicLayout()
return await this._req(layoutController.save, config)
}
}
2024-02-12 12:50:23 +01:00
module.exports = TestConfiguration