Merge pull request #16129 from Budibase/BUDI-9294/project-apps-crud
New project apps backend crud
This commit is contained in:
commit
b573e9cb2b
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -29,6 +29,7 @@ export interface Screen extends Document {
|
|||
pluginAdded?: boolean
|
||||
onLoad?: EventHandler[]
|
||||
variant?: ScreenVariant
|
||||
workspaceAppId?: string
|
||||
}
|
||||
|
||||
export interface ScreenRoutesViewOutput extends Document {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { Document } from "../document"
|
||||
|
||||
export interface WorkspaceApp extends Document {
|
||||
name: string
|
||||
urlPrefix: string
|
||||
icon: string
|
||||
iconColor?: string
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue