Merge pull request #16129 from Budibase/BUDI-9294/project-apps-crud

New project apps backend crud
This commit is contained in:
Adria Navarro 2025-05-19 10:03:28 +02:00 committed by GitHub
commit b573e9cb2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 645 additions and 254 deletions

View File

@ -210,3 +210,13 @@ export const getOAuth2ConfigParams = (
) => {
return getDocParams(DocumentType.OAUTH2_CONFIG, configId, otherProps)
}
/**
* Gets parameters for retrieving project apps, this is a utility function for the getDocParams function.
*/
export const getWorkspaceAppParams = (
workspaceAppId?: string | null,
otherProps: Partial<DatabaseQueryOpts> = {}
) => {
return getDocParams(DocumentType.WORKSPACE_APP, workspaceAppId, otherProps)
}

View File

@ -53,6 +53,10 @@ export function parseEnvFlags(flags: string): EnvFlagEntry[] {
return result
}
export function getEnvFlags() {
return parseEnvFlags(env.TENANT_FEATURE_FLAGS || "")
}
export class FlagSet<T extends { [name: string]: boolean }> {
// This is used to safely cache flags sets in the current request context.
// Because multiple sets could theoretically exist, we don't want the cache of
@ -89,9 +93,7 @@ export class FlagSet<T extends { [name: string]: boolean }> {
const currentTenantId = context.getTenantId()
const specificallySetFalse = new Set<string>()
for (const { tenantId, key, value } of parseEnvFlags(
env.TENANT_FEATURE_FLAGS || ""
)) {
for (const { tenantId, key, value } of getEnvFlags()) {
if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
continue
}

View File

@ -18,11 +18,13 @@ import {
Component,
ComponentDefinition,
DeleteScreenResponse,
FeatureFlag,
FetchAppPackageResponse,
SaveScreenResponse,
Screen,
ScreenVariant,
} from "@budibase/types"
import { featureFlag } from "@/helpers"
interface ScreenState {
screens: Screen[]
@ -87,9 +89,13 @@ export class ScreenStore extends BudiStore<ScreenState> {
* @param {FetchAppPackageResponse} pkg
*/
syncAppScreens(pkg: FetchAppPackageResponse) {
let screens = [...pkg.screens]
if (featureFlag.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
screens = [...pkg.workspaceApps.flatMap(p => p.screens)]
}
this.update(state => ({
...state,
screens: [...pkg.screens],
screens,
}))
}

View File

@ -26,6 +26,7 @@ import {
docIds,
env as envCore,
events,
features,
objectStore,
roles,
tenancy,
@ -69,6 +70,7 @@ import {
UnpublishAppResponse,
SetRevertableAppVersionResponse,
ErrorCode,
FeatureFlag,
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk"
@ -76,6 +78,7 @@ import { builderSocket } from "../../websockets"
import { DefaultAppTheme, sdk as sharedCoreSDK } from "@budibase/shared-core"
import * as appMigrations from "../../appMigrations"
import { createSampleDataTableScreen } from "../../constants/screens"
import { groupBy } from "lodash/fp"
// utility function, need to do away with this
async function getLayouts() {
@ -181,7 +184,19 @@ async function addSampleDataDocs() {
async function addSampleDataScreen() {
const db = context.getAppDB()
let screen = createSampleDataTableScreen()
let workspaceAppId: string | undefined
if (await features.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
const appMetadata = await sdk.applications.metadata.get()
const workspaceApp = await sdk.workspaceApps.create({
name: appMetadata.name,
urlPrefix: "/",
icon: "Monitoring",
})
workspaceAppId = workspaceApp._id!
}
let screen = await createSampleDataTableScreen(workspaceAppId)
screen._id = generateScreenID()
await db.put(screen)
}
@ -267,6 +282,13 @@ export async function fetchAppPackage(
screens = await accessController.checkScreensAccess(screens, userRoleId)
}
let workspaceApps: FetchAppPackageResponse["workspaceApps"] = []
if (await features.flags.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
workspaceApps = await extractScreensByWorkspaceApp(screens)
screens = []
}
const clientLibPath = objectStore.clientLibraryUrl(
ctx.params.appId,
application.version
@ -275,6 +297,7 @@ export async function fetchAppPackage(
ctx.body = {
application: { ...application, upgradableVersion: envCore.VERSION },
licenseType: license?.plan.type || PlanType.FREE,
workspaceApps,
screens,
layouts,
clientLibPath,
@ -282,6 +305,26 @@ export async function fetchAppPackage(
}
}
async function extractScreensByWorkspaceApp(
screens: Screen[]
): Promise<FetchAppPackageResponse["workspaceApps"]> {
const result: FetchAppPackageResponse["workspaceApps"] = []
const workspaceApps = await sdk.workspaceApps.fetch()
const screensByWorkspaceApp = groupBy(s => s.workspaceAppId, screens)
for (const workspaceAppId of Object.keys(screensByWorkspaceApp)) {
const workspaceApp = workspaceApps.find(p => p._id === workspaceAppId)
result.push({
...workspaceApp!,
screens: screensByWorkspaceApp[workspaceAppId],
})
}
return result
}
async function performAppCreate(
ctx: UserCtx<CreateAppRequest, CreateAppResponse>
) {
@ -426,7 +469,7 @@ async function performAppCreate(
}
const latestMigrationId = appMigrations.getLatestEnabledMigrationId()
if (latestMigrationId) {
if (latestMigrationId && !isImport) {
// Initialise the app migration version as the latest one
await appMigrations.updateAppMigrationMetadata({
appId,

View File

@ -0,0 +1,72 @@
import {
Ctx,
InsertWorkspaceAppRequest,
InsertWorkspaceAppResponse,
WorkspaceApp,
WorkspaceAppResponse,
UpdateWorkspaceAppRequest,
UpdateWorkspaceAppResponse,
} from "@budibase/types"
import sdk from "../../sdk"
function toWorkspaceAppResponse(
workspaceApp: WorkspaceApp
): WorkspaceAppResponse {
return {
_id: workspaceApp._id!,
_rev: workspaceApp._rev!,
name: workspaceApp.name,
urlPrefix: workspaceApp.urlPrefix,
icon: workspaceApp.icon,
iconColor: workspaceApp.iconColor,
}
}
export async function create(
ctx: Ctx<InsertWorkspaceAppRequest, InsertWorkspaceAppResponse>
) {
const { body } = ctx.request
const newWorkspaceApp = {
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const workspaceApp = await sdk.workspaceApps.create(newWorkspaceApp)
ctx.status = 201
ctx.body = {
workspaceApp: toWorkspaceAppResponse(workspaceApp),
}
}
export async function edit(
ctx: Ctx<UpdateWorkspaceAppRequest, UpdateWorkspaceAppResponse>
) {
const { body } = ctx.request
if (ctx.params.id !== body._id) {
ctx.throw("Path and body ids do not match", 400)
}
const toUpdate = {
_id: body._id,
_rev: body._rev,
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const workspaceApp = await sdk.workspaceApps.update(toUpdate)
ctx.body = {
workspaceApp: toWorkspaceAppResponse(workspaceApp),
}
}
export async function remove(ctx: Ctx<void, void>) {
const { id, rev } = ctx.params
await sdk.workspaceApps.remove(id, rev)
ctx.status = 204
}

View File

@ -32,6 +32,7 @@ import rowActionRoutes from "./rowAction"
import oauth2Routes from "./oauth2"
import featuresRoutes from "./features"
import aiRoutes from "./ai"
import workspaceApps from "./workspaceApp"
export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
@ -72,6 +73,7 @@ export const mainRoutes: Router[] = [
rowActionRoutes,
oauth2Routes,
featuresRoutes,
workspaceApps,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -235,9 +235,7 @@ describe("/screens", () => {
viewV2.createRequest(table._id!),
{ status: 201 }
)
const screen = await config.api.screen.save(
createViewScreen("BudibaseDB", view)
)
const screen = await config.api.screen.save(createViewScreen(view))
const usage = await config.api.screen.usage(view.id)
expect(usage.sourceType).toEqual(SourceType.VIEW)
confirmScreen(usage, screen)

View File

@ -0,0 +1,52 @@
import Router from "@koa/router"
import { PermissionType } from "@budibase/types"
import { middleware } from "@budibase/backend-core"
import authorized from "../../middleware/authorized"
import * as controller from "../controllers/workspaceApp"
import Joi from "joi"
const baseSchema = {
name: Joi.string().required(),
urlPrefix: Joi.string().required(),
icon: Joi.string().required(),
iconColor: Joi.string().required(),
}
const insertSchema = Joi.object({
...baseSchema,
})
const updateSchema = Joi.object({
_id: Joi.string().required(),
_rev: Joi.string().required(),
...baseSchema,
})
function workspaceAppValidator(
schema: typeof insertSchema | typeof updateSchema
) {
return middleware.joiValidator.body(schema, { allowUnknown: false })
}
const router: Router = new Router()
router.post(
"/api/workspaceApp",
authorized(PermissionType.BUILDER),
workspaceAppValidator(insertSchema),
controller.create
)
router.put(
"/api/workspaceApp/:id",
authorized(PermissionType.BUILDER),
workspaceAppValidator(updateSchema),
controller.edit
)
router.delete(
"/api/workspaceApp/:id/:rev",
authorized(PermissionType.BUILDER),
controller.remove
)
export default router

View File

@ -1,13 +1,25 @@
// This file should never be manually modified, use `yarn add-app-migration` in order to add a new one
import { features } from "@budibase/backend-core"
import { AppMigration } from "."
import m20240604153647_initial_sqs from "./migrations/20240604153647_initial_sqs"
import m20250514133719_workspace_apps from "./migrations/20250514133719_workspace_apps"
import { FeatureFlag } from "@budibase/types"
// Migrations will be executed sorted by ID
export const MIGRATIONS: AppMigration[] = [
// Migrations will be executed sorted by id
{
id: "20240604153647_initial_sqs",
func: m20240604153647_initial_sqs,
},
{
id: "m20250514133719_workspace_apps",
func: m20250514133719_workspace_apps,
// Disabling it, enabling it via env variables to enable development.
// Using the existing flag system would require async checks and we could run to race conditions, so this keeps is simple
disabled: !features
.getEnvFlags()
.some(f => f.key === FeatureFlag.WORKSPACE_APPS && f.value === true),
},
]

View File

@ -0,0 +1,33 @@
import { Screen } from "@budibase/types"
import sdk from "../../sdk"
import { context } from "@budibase/backend-core"
const migration = async () => {
const screens = await sdk.screens.fetch()
const application = await sdk.applications.metadata.get()
const allWorkspaceApps = await sdk.workspaceApps.fetch()
let workspaceAppId = allWorkspaceApps.find(
p => p.name === application.name
)?._id
if (!workspaceAppId) {
const workspaceApp = await sdk.workspaceApps.create({
name: application.name,
urlPrefix: "/",
icon: "Monitoring",
})
workspaceAppId = workspaceApp._id
}
const db = context.getAppDB()
await db.bulkDocs(
screens
.filter(s => !s.workspaceAppId)
.map<Screen>(s => ({
...s,
workspaceAppId,
}))
)
}
export default migration

View File

@ -1,241 +1,16 @@
import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
import { Screen, Table, Query, ViewV2, Component } from "@budibase/types"
import { Screen } from "@budibase/types"
export const SAMPLE_DATA_SCREEN_NAME = "sample-data-inventory-screen"
export function createHomeScreen(
config: {
roleId: string
route: string
} = {
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/",
}
export function createSampleDataTableScreen(
workspaceAppId: string | undefined
): Screen {
return {
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_transition: "fade",
_children: [
{
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
_component: "@budibase/standard-components/heading",
_styles: {
hover: {},
active: {},
selected: {},
},
text: "Welcome to your Budibase App 👋",
size: "M",
align: "left",
_instanceName: "Heading",
_children: [],
},
],
_instanceName: "Home",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: config.route,
roleId: config.roleId,
},
name: "home-screen",
}
}
function heading(text: string): Component {
return {
_id: "c1bff24cd821e41d18c894ac77a80ef99",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "Table heading",
_children: [],
text,
}
}
export function createTableScreen(
datasourceName: string,
table: Table
): Screen {
return {
props: {
_id: "cad0a0904cacd4678a2ac094e293db1a5",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
heading("table"),
{
_id: "ca6304be2079147bb9933092c4f8ce6fa",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "table - Table",
_children: [],
table: {
label: table.name,
tableId: table._id!,
type: "table",
datasourceName,
},
},
],
_instanceName: "table - List",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/table",
roleId: "ADMIN",
homeScreen: false,
},
name: "screen-id",
}
}
export function createViewScreen(datasourceName: string, view: ViewV2): Screen {
return {
props: {
_id: "cc359092bbd6c4e10b57827155edb7872",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
heading("view"),
{
_id: "ccb4a9e3734794864b5c65b012a0bdc5a",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "view - Table",
_children: [],
table: {
...view,
name: view.name,
tableId: view.tableId,
id: view.id,
label: view.name,
type: "viewV2",
},
},
],
_instanceName: "view - List",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/view",
roleId: "ADMIN",
homeScreen: false,
},
name: "view-id",
}
}
export function createQueryScreen(datasourceId: string, query: Query): Screen {
return {
props: {
_id: "cc59b217aed264939a6c5249eee39cb25",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
{
_id: "c33a4a6e3cb5343158a08625c06b5cd7c",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "New Table",
table: {
...query,
label: query.name,
_id: query._id!,
name: query.name,
datasourceId: datasourceId,
type: "query",
},
initialSortOrder: "Ascending",
allowAddRows: true,
allowEditRows: true,
allowDeleteRows: true,
stripeRows: false,
quiet: false,
columns: null,
},
],
_instanceName: "Blank screen",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/query",
roleId: "BASIC",
homeScreen: false,
},
name: "screen-id",
}
}
export function createSampleDataTableScreen(): Screen {
return {
showNavigation: true,
width: "Large",
routing: { route: "/inventory", roleId: "BASIC", homeScreen: false },
name: SAMPLE_DATA_SCREEN_NAME,
workspaceAppId,
props: {
_id: "c38f2b9f250fb4c33965ce47e12c02a80",
_component: "@budibase/standard-components/container",

View File

@ -4,6 +4,7 @@ import {
OAuth2Config,
PASSWORD_REPLACEMENT,
SEPARATOR,
WithoutDocMetadata,
WithRequired,
} from "@budibase/types"
@ -34,7 +35,7 @@ export async function fetch(): Promise<CreatedOAuthConfig[]> {
}
export async function create(
config: Omit<OAuth2Config, "_id" | "_rev" | "createdAt" | "updatedAt">
config: WithoutDocMetadata<OAuth2Config>
): Promise<CreatedOAuthConfig> {
const db = context.getAppDB()

View File

@ -1,13 +1,17 @@
import { getScreenParams } from "../../../db/utils"
import { context } from "@budibase/backend-core"
import { Screen } from "@budibase/types"
import { Database, Screen } from "@budibase/types"
import { getScreenParams } from "../../../db/utils"
export async function fetch(db = context.getAppDB()): Promise<Screen[]> {
return (
export async function fetch(
db: Database = context.getAppDB()
): Promise<Screen[]> {
const screens = (
await db.allDocs<Screen>(
getScreenParams(null, {
include_docs: true,
})
)
).rows.map(el => el.doc!)
return screens
}

View File

@ -1,4 +1,4 @@
import { Row, Table } from "@budibase/types"
import { Row, Table, WithoutDocMetadata } from "@budibase/types"
import * as external from "./external"
import * as internal from "./internal"
@ -7,7 +7,7 @@ import { setPermissions } from "../permissions"
import { roles } from "@budibase/backend-core"
export async function create(
table: Omit<Table, "_id" | "_rev">,
table: WithoutDocMetadata<Table>,
rows?: Row[],
userId?: string
): Promise<Table> {

View File

@ -7,6 +7,7 @@ import {
TableRequest,
ViewV2,
AutoFieldSubType,
WithoutDocMetadata,
} from "@budibase/types"
import { context, HTTPError } from "@budibase/backend-core"
import {
@ -102,7 +103,7 @@ function getDatasourceId(table: Table) {
return breakExternalTableId(table._id).datasourceId
}
export async function create(table: Omit<Table, "_id" | "_rev">) {
export async function create(table: WithoutDocMetadata<Table>) {
const datasourceId = getDatasourceId(table)
const tableToCreate = { ...table, created: true }

View File

@ -6,6 +6,7 @@ import {
ViewV2,
Row,
TableSourceType,
WithoutDocMetadata,
} from "@budibase/types"
import {
hasTypeChanged,
@ -25,7 +26,7 @@ import { generateTableID, getRowParams } from "../../../../db/utils"
import { quotas } from "@budibase/pro"
export async function create(
table: Omit<Table, "_id" | "_rev">,
table: WithoutDocMetadata<Table>,
rows?: Row[],
userId?: string
) {

View File

@ -0,0 +1,83 @@
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import {
DocumentType,
WorkspaceApp,
SEPARATOR,
WithoutDocMetadata,
} from "@budibase/types"
async function guardName(name: string, id?: string) {
const existingWorkspaceApps = await fetch()
if (existingWorkspaceApps.find(p => p.name === name && p._id !== id)) {
throw new HTTPError(`App with name '${name}' is already taken.`, 400)
}
}
export async function fetch(): Promise<WorkspaceApp[]> {
const db = context.getAppDB()
const docs = await db.allDocs<WorkspaceApp>(
docIds.getWorkspaceAppParams(null, { include_docs: true })
)
const result = docs.rows.map(r => ({
...r.doc!,
_id: r.doc!._id!,
_rev: r.doc!._rev!,
}))
return result
}
export async function get(id: string): Promise<WorkspaceApp | undefined> {
const db = context.getAppDB()
const workspaceApp = await db.tryGet<WorkspaceApp>(id)
return workspaceApp
}
export async function create(workspaceApp: WithoutDocMetadata<WorkspaceApp>) {
const db = context.getAppDB()
await guardName(workspaceApp.name)
const response = await db.put({
_id: `${DocumentType.WORKSPACE_APP}${SEPARATOR}${utils.newid()}`,
...workspaceApp,
})
return {
_id: response.id!,
_rev: response.rev!,
...workspaceApp,
}
}
export async function update(
workspaceApp: Omit<WorkspaceApp, "createdAt" | "updatedAt">
) {
const db = context.getAppDB()
await guardName(workspaceApp.name, workspaceApp._id)
const response = await db.put(workspaceApp)
return {
_id: response.id!,
_rev: response.rev!,
...workspaceApp,
}
}
export async function remove(
workspaceAppId: string,
_rev: string
): Promise<void> {
const db = context.getAppDB()
try {
await db.remove(workspaceAppId, _rev)
} catch (e: any) {
if (e.status === 404) {
throw new HTTPError(
`Project app with id '${workspaceAppId}' not found.`,
404
)
}
throw e
}
}

View File

@ -15,6 +15,7 @@ import * as screens from "./app/screens"
import * as common from "./app/common"
import * as oauth2 from "./app/oauth2"
import * as ai from "./app/ai"
import * as workspaceApps from "./app/workspaceApps"
const sdk = {
backups,
@ -34,6 +35,7 @@ const sdk = {
common,
oauth2,
ai,
workspaceApps,
}
// default export for TS

View File

@ -1,6 +1,5 @@
import { roles, utils } from "@budibase/backend-core"
import { createHomeScreen } from "../../constants/screens"
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { BASE_LAYOUT_PROP_IDS, EMPTY_LAYOUT } from "../../constants/layouts"
import { cloneDeep } from "lodash/fp"
import {
BUILTIN_ACTION_DEFINITIONS,
@ -38,6 +37,7 @@ import {
FilterCondition,
AutomationTriggerResult,
CreateEnvironmentVariableRequest,
Screen,
} from "@budibase/types"
import { LoopInput } from "../../definitions/automations"
import { merge } from "lodash"
@ -46,7 +46,7 @@ export {
createTableScreen,
createQueryScreen,
createViewScreen,
} from "../../constants/screens"
} from "./structures/screens"
const { BUILTIN_ROLE_IDS } = roles
@ -538,6 +538,59 @@ export function basicUser(role: string) {
}
}
function createHomeScreen(
config: {
roleId: string
route: string
} = {
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/",
}
): Screen {
return {
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_transition: "fade",
_children: [
{
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
_component: "@budibase/standard-components/heading",
_styles: {
hover: {},
active: {},
selected: {},
},
text: "Welcome to your Budibase App 👋",
size: "M",
align: "left",
_instanceName: "Heading",
_children: [],
},
],
_instanceName: "Home",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: config.route,
roleId: config.roleId,
},
name: "home-screen",
workspaceAppId: "workspaceAppId",
}
}
export function basicScreen(route = "/") {
return createHomeScreen({
roleId: BUILTIN_ROLE_IDS.BASIC,

View File

@ -0,0 +1,178 @@
import { Component, Query, Screen, Table, ViewV2 } from "@budibase/types"
function heading(text: string): Component {
return {
_id: "c1bff24cd821e41d18c894ac77a80ef99",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "Table heading",
_children: [],
text,
}
}
export function createTableScreen(
datasourceName: string,
table: Table
): Screen {
return {
props: {
_id: "cad0a0904cacd4678a2ac094e293db1a5",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
heading("table"),
{
_id: "ca6304be2079147bb9933092c4f8ce6fa",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "table - Table",
_children: [],
table: {
label: table.name,
tableId: table._id!,
type: "table",
datasourceName,
},
},
],
_instanceName: "table - List",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/table",
roleId: "ADMIN",
homeScreen: false,
},
name: "screen-id",
workspaceAppId: "workspaceAppId",
}
}
export function createViewScreen(view: ViewV2): Screen {
return {
props: {
_id: "cc359092bbd6c4e10b57827155edb7872",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
heading("view"),
{
_id: "ccb4a9e3734794864b5c65b012a0bdc5a",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_instanceName: "view - Table",
_children: [],
table: {
...view,
name: view.name,
tableId: view.tableId,
id: view.id,
label: view.name,
type: "viewV2",
},
},
],
_instanceName: "view - List",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/view",
roleId: "ADMIN",
homeScreen: false,
},
name: "view-id",
workspaceAppId: "workspaceAppId",
}
}
export function createQueryScreen(datasourceId: string, query: Query): Screen {
return {
props: {
_id: "cc59b217aed264939a6c5249eee39cb25",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
{
_id: "c33a4a6e3cb5343158a08625c06b5cd7c",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "New Table",
table: {
...query,
label: query.name,
_id: query._id!,
name: query.name,
datasourceId: datasourceId,
type: "query",
},
initialSortOrder: "Ascending",
allowAddRows: true,
allowEditRows: true,
allowDeleteRows: true,
stripeRows: false,
quiet: false,
columns: null,
},
],
_instanceName: "Blank screen",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/query",
roleId: "BASIC",
homeScreen: false,
},
name: "screen-id",
workspaceAppId: "workspaceAppId",
}
}

View File

@ -1,5 +1,5 @@
import type { PlanType } from "../../../sdk"
import type { Layout, App, Screen } from "../../../documents"
import type { Layout, App, Screen, WorkspaceApp } from "../../../documents"
import { ReadStream } from "fs"
export interface SyncAppResponse {
@ -35,10 +35,15 @@ export interface FetchAppDefinitionResponse {
libraries: string[]
}
interface WorkspaceAppResponse extends WorkspaceApp {
screens: Screen[]
}
export interface FetchAppPackageResponse {
application: App
licenseType: PlanType
screens: Screen[]
workspaceApps: WorkspaceAppResponse[]
layouts: Layout[]
clientLibPath: string
hasLock: boolean

View File

@ -11,6 +11,7 @@ export * from "./layout"
export * from "./metadata"
export * from "./oauth2"
export * from "./permission"
export * from "./workspaceApp"
export * from "./query"
export * from "./role"
export * from "./rowAction"

View File

@ -0,0 +1,32 @@
export interface WorkspaceAppResponse {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor?: string
}
export interface InsertWorkspaceAppRequest {
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface InsertWorkspaceAppResponse {
workspaceApp: WorkspaceAppResponse
}
export interface UpdateWorkspaceAppRequest {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface UpdateWorkspaceAppResponse {
workspaceApp: WorkspaceAppResponse
}

View File

@ -9,6 +9,7 @@ export * from "./layout"
export * from "./links"
export * from "./metadata"
export * from "./oauth2"
export * from "./workspaceApp"
export * from "./query"
export * from "./role"
export * from "./row"

View File

@ -29,6 +29,7 @@ export interface Screen extends Document {
pluginAdded?: boolean
onLoad?: EventHandler[]
variant?: ScreenVariant
workspaceAppId?: string
}
export interface ScreenRoutesViewOutput extends Document {

View File

@ -0,0 +1,8 @@
import { Document } from "../document"
export interface WorkspaceApp extends Document {
name: string
urlPrefix: string
icon: string
iconColor?: string
}

View File

@ -43,6 +43,7 @@ export enum DocumentType {
OAUTH2_CONFIG = "oauth2",
OAUTH2_CONFIG_LOG = "oauth2log",
AGENT_CHAT = "agentchat",
WORKSPACE_APP = "workspace_app",
}
// Because DocumentTypes can overlap, we need to make sure that we search

View File

@ -4,6 +4,7 @@ export enum FeatureFlag {
AI_JS_GENERATION = "AI_JS_GENERATION",
AI_TABLE_GENERATION = "AI_TABLE_GENERATION",
AI_AGENTS = "AI_AGENTS",
WORKSPACE_APPS = "WORKSPACE_APPS",
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
@ -14,6 +15,7 @@ export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.AI_JS_GENERATION]: false,
[FeatureFlag.AI_TABLE_GENERATION]: false,
[FeatureFlag.AI_AGENTS]: false,
[FeatureFlag.WORKSPACE_APPS]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,

View File

@ -1,3 +1,5 @@
import { Document } from "../documents"
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
@ -32,3 +34,8 @@ export type RequiredKeys<T> = {
}
export type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
export type WithoutDocMetadata<T extends Document> = Omit<
T,
"_id" | "_rev" | "createdAt" | "updatedAt"
>

View File

@ -1,6 +1,11 @@
import { structures, TestConfiguration } from "../../../../tests"
import { context, db, roles } from "@budibase/backend-core"
import { App, Database, BuiltinPermissionID } from "@budibase/types"
import {
App,
Database,
BuiltinPermissionID,
WithoutDocMetadata,
} from "@budibase/types"
jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core")
@ -30,7 +35,7 @@ async function addAppMetadata() {
})
}
async function updateAppMetadata(update: Partial<Omit<App, "_id" | "_rev">>) {
async function updateAppMetadata(update: Partial<WithoutDocMetadata<App>>) {
const app = await appDb.get("app_metadata")
await appDb.put({
...app,