{#if title}
@@ -16,7 +18,7 @@
{@html body}
{:else}
-
+
{@html body}
@@ -24,6 +26,23 @@
diff --git a/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte
deleted file mode 100644
index 9ad41ad652..0000000000
--- a/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
- Update {user.email}'s role for {app.name}.
-
-
diff --git a/packages/builder/src/stores/builder/roles.js b/packages/builder/src/stores/builder/roles.js
index ac395aa232..fd3581f1d4 100644
--- a/packages/builder/src/stores/builder/roles.js
+++ b/packages/builder/src/stores/builder/roles.js
@@ -1,16 +1,34 @@
-import { writable } from "svelte/store"
+import { derived, writable, get } from "svelte/store"
import { API } from "api"
import { RoleUtils } from "@budibase/frontend-core"
export function createRolesStore() {
- const { subscribe, update, set } = writable([])
+ const store = writable([])
+ const enriched = derived(store, $store => {
+ return $store.map(role => ({
+ ...role,
+
+ // Ensure we have new metadata for all roles
+ uiMetadata: {
+ displayName: role.uiMetadata?.displayName || role.name,
+ color:
+ role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
+ description: role.uiMetadata?.description || "Custom role",
+ },
+ }))
+ })
function setRoles(roles) {
- set(
+ store.set(
roles.sort((a, b) => {
const priorityA = RoleUtils.getRolePriority(a._id)
const priorityB = RoleUtils.getRolePriority(b._id)
- return priorityA > priorityB ? -1 : 1
+ if (priorityA !== priorityB) {
+ return priorityA > priorityB ? -1 : 1
+ }
+ const nameA = a.uiMetadata?.displayName || a.name
+ const nameB = b.uiMetadata?.displayName || b.name
+ return nameA < nameB ? -1 : 1
})
)
}
@@ -29,17 +47,43 @@ export function createRolesStore() {
roleId: role?._id,
roleRev: role?._rev,
})
- update(state => state.filter(existing => existing._id !== role._id))
+ await actions.fetch()
},
save: async role => {
const savedRole = await API.saveRole(role)
await actions.fetch()
return savedRole
},
+ replace: (roleId, role) => {
+ // Handles external updates of roles
+ if (!roleId) {
+ return
+ }
+
+ // Handle deletion
+ if (!role) {
+ store.update(state => state.filter(x => x._id !== roleId))
+ return
+ }
+
+ // Add new role
+ const index = get(store).findIndex(x => x._id === role._id)
+ if (index === -1) {
+ store.update(state => [...state, role])
+ }
+
+ // Update existing role
+ else if (role) {
+ store.update(state => {
+ state[index] = role
+ return [...state]
+ })
+ }
+ },
}
return {
- subscribe,
+ subscribe: enriched.subscribe,
...actions,
}
}
diff --git a/packages/builder/src/stores/builder/websocket.js b/packages/builder/src/stores/builder/websocket.js
index 7df5ab9adb..8a0d83abc1 100644
--- a/packages/builder/src/stores/builder/websocket.js
+++ b/packages/builder/src/stores/builder/websocket.js
@@ -9,6 +9,7 @@ import {
snippets,
datasources,
tables,
+ roles,
} from "stores/builder"
import { get } from "svelte/store"
import { auth, appsStore } from "stores/portal"
@@ -56,12 +57,18 @@ export const createBuilderWebsocket = appId => {
datasources.replaceDatasource(id, datasource)
})
+ // Role events
+ socket.onOther(BuilderSocketEvent.RoleChange, ({ id, role }) => {
+ roles.replace(id, role)
+ })
+
// Design section events
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
screenStore.replace(id, screen)
})
+
+ // App events
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
- //Sync app metadata across the stores
appStore.syncMetadata(metadata)
themeStore.syncMetadata(metadata)
navigationStore.syncMetadata(metadata)
@@ -79,7 +86,7 @@ export const createBuilderWebsocket = appId => {
}
)
- // Automations
+ // Automation events
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
automationStore.actions.replace(id, automation)
})
diff --git a/packages/client/src/components/app/DataProvider.svelte b/packages/client/src/components/app/DataProvider.svelte
index 7b5187ffb6..e6629aa3f3 100644
--- a/packages/client/src/components/app/DataProvider.svelte
+++ b/packages/client/src/components/app/DataProvider.svelte
@@ -126,6 +126,9 @@
}
const extendQuery = (defaultQuery, extensions) => {
+ if (!Object.keys(extensions).length) {
+ return defaultQuery
+ }
const extended = {
[LogicalOperator.AND]: {
conditions: [
diff --git a/packages/client/src/components/devtools/DevToolsHeader.svelte b/packages/client/src/components/devtools/DevToolsHeader.svelte
index 55b705e717..eca085a88a 100644
--- a/packages/client/src/components/devtools/DevToolsHeader.svelte
+++ b/packages/client/src/components/devtools/DevToolsHeader.svelte
@@ -1,28 +1,29 @@
diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte
index 5d5f06872d..cd6b61af80 100644
--- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte
@@ -66,7 +66,7 @@
focus: () => api?.focus?.(),
blur: () => api?.blur?.(),
isActive: () => api?.isActive?.() ?? false,
- onKeyDown: (...params) => api?.onKeyDown(...params),
+ onKeyDown: (...params) => api?.onKeyDown?.(...params),
isReadonly: () => readonly,
getType: () => column.schema.type,
getValue: () => row[column.name],
diff --git a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte
index 2acec7d11e..c8ae96ef21 100644
--- a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte
@@ -1,3 +1,7 @@
+
+
-
+
diff --git a/packages/frontend-core/src/components/grid/cells/RoleCell.svelte b/packages/frontend-core/src/components/grid/cells/RoleCell.svelte
new file mode 100644
index 0000000000..82d1e26aa7
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/RoleCell.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ {role?.uiMetadata?.displayName || role?.name || "Unknown role"}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/TextCell.svelte b/packages/frontend-core/src/components/grid/cells/TextCell.svelte
index 0cf0ab2004..9275bca3c6 100644
--- a/packages/frontend-core/src/components/grid/cells/TextCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/TextCell.svelte
@@ -7,11 +7,13 @@
export let type = "text"
export let readonly = false
export let api
+ export let format = null
let input
let active = false
$: editable = focused && !readonly
+ $: displayValue = format?.(value) ?? value ?? ""
const handleChange = e => {
onChange(e.target.value)
@@ -52,7 +54,7 @@
{:else}
- {value ?? ""}
+ {displayValue}
{/if}
diff --git a/packages/frontend-core/src/components/grid/lib/renderers.js b/packages/frontend-core/src/components/grid/lib/renderers.js
index f9e26d920a..70700f9417 100644
--- a/packages/frontend-core/src/components/grid/lib/renderers.js
+++ b/packages/frontend-core/src/components/grid/lib/renderers.js
@@ -16,6 +16,7 @@ import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
import SignatureCell from "../cells/SignatureCell.svelte"
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
+import RoleCell from "../cells/RoleCell.svelte"
const TypeComponentMap = {
[FieldType.STRING]: TextCell,
@@ -35,6 +36,9 @@ const TypeComponentMap = {
[FieldType.JSON]: JSONCell,
[FieldType.BB_REFERENCE]: BBReferenceCell,
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
+
+ // Custom types for UI only
+ role: RoleCell,
}
export const getCellRenderer = column => {
return (
diff --git a/packages/frontend-core/src/utils/roles.js b/packages/frontend-core/src/utils/roles.js
index 1ae9d3ac14..913d452e7c 100644
--- a/packages/frontend-core/src/utils/roles.js
+++ b/packages/frontend-core/src/utils/roles.js
@@ -7,20 +7,7 @@ const RolePriorities = {
[Roles.BASIC]: 2,
[Roles.PUBLIC]: 1,
}
-const RoleColours = {
- [Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)",
- [Roles.CREATOR]: "var(--spectrum-global-color-static-magenta-600)",
- [Roles.POWER]: "var(--spectrum-global-color-static-orange-400)",
- [Roles.BASIC]: "var(--spectrum-global-color-static-green-400)",
- [Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
-}
export const getRolePriority = role => {
return RolePriorities[role] ?? 0
}
-
-export const getRoleColour = roleId => {
- return (
- RoleColours[roleId] ?? "var(--spectrum-global-color-static-magenta-400)"
- )
-}
diff --git a/packages/server/scripts/load/utils.js b/packages/server/scripts/load/utils.js
index 1dabdcec9a..57e03c471c 100644
--- a/packages/server/scripts/load/utils.js
+++ b/packages/server/scripts/load/utils.js
@@ -29,7 +29,7 @@ exports.createApp = async apiKey => {
const body = {
name,
url: `/${name}`,
- useTemplate: "true",
+ useTemplate: true,
templateKey: "app/school-admin-panel",
templateName: "School Admin Panel",
}
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 59f67540fe..d8d9d75336 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -23,6 +23,7 @@ import {
cache,
context,
db as dbCore,
+ docIds,
env as envCore,
ErrorCode,
events,
@@ -35,7 +36,6 @@ import {
import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
-import { stringToReadStream } from "../../utilities"
import { doesUserHaveLock } from "../../utilities/redis"
import { cleanupAutomations } from "../../automations/utils"
import { getUniqueRows } from "../../utilities/usageQuota/rows"
@@ -54,6 +54,11 @@ import {
DuplicateAppResponse,
UpdateAppRequest,
UpdateAppResponse,
+ Database,
+ FieldType,
+ BBReferenceFieldSubType,
+ Row,
+ BBRequest,
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk"
@@ -123,8 +128,7 @@ function checkAppName(
}
interface AppTemplate {
- templateString?: string
- useTemplate?: string
+ useTemplate?: boolean
file?: {
type?: string
path: string
@@ -148,14 +152,7 @@ async function createInstance(appId: string, template: AppTemplate) {
await createRoutingView()
await createAllSearchIndex()
- // replicate the template data to the instance DB
- // this is currently very hard to test, downloading and importing template files
- if (template && template.templateString) {
- const { ok } = await db.load(stringToReadStream(template.templateString))
- if (!ok) {
- throw "Error loading database dump from memory."
- }
- } else if (template && template.useTemplate === "true") {
+ if (template && template.useTemplate) {
await sdk.backups.importApp(appId, db, template)
} else {
// create the users table
@@ -243,14 +240,15 @@ export async function fetchAppPackage(
async function performAppCreate(ctx: UserCtx
) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
- const {
- name,
- url,
- encryptionPassword,
- useTemplate,
- templateKey,
- templateString,
- } = ctx.request.body
+ const { body } = ctx.request
+ const { name, url, encryptionPassword, templateKey } = body
+
+ let useTemplate
+ if (typeof body.useTemplate === "string") {
+ useTemplate = body.useTemplate === "true"
+ } else if (typeof body.useTemplate === "boolean") {
+ useTemplate = body.useTemplate
+ }
checkAppName(ctx, apps, name)
const appUrl = sdk.applications.getAppUrl({ name, url })
@@ -259,16 +257,15 @@ async function performAppCreate(ctx: UserCtx) {
const instanceConfig: AppTemplate = {
useTemplate,
key: templateKey,
- templateString,
}
- if (ctx.request.files && ctx.request.files.templateFile) {
+ if (ctx.request.files && ctx.request.files.fileToImport) {
instanceConfig.file = {
- ...(ctx.request.files.templateFile as any),
+ ...(ctx.request.files.fileToImport as any),
password: encryptionPassword,
}
- } else if (typeof ctx.request.body.file?.path === "string") {
+ } else if (typeof body.file?.path === "string") {
instanceConfig.file = {
- path: ctx.request.body.file?.path,
+ path: body.file?.path,
}
}
@@ -279,6 +276,10 @@ async function performAppCreate(ctx: UserCtx) {
const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB()
+ if (instanceConfig.useTemplate && !instanceConfig.file) {
+ await updateUserColumns(appId, db, ctx.user._id!)
+ }
+
const newApplication: App = {
_id: DocumentType.APP_METADATA,
_rev: undefined,
@@ -375,21 +376,81 @@ async function performAppCreate(ctx: UserCtx) {
})
}
-async function creationEvents(request: any, app: App) {
+async function updateUserColumns(
+ appId: string,
+ db: Database,
+ toUserId: string
+) {
+ await context.doInAppContext(appId, async () => {
+ const allTables = await sdk.tables.getAllTables()
+ const tablesWithUserColumns = []
+ for (const table of allTables) {
+ const userColumns = Object.values(table.schema).filter(
+ f =>
+ (f.type === FieldType.BB_REFERENCE ||
+ f.type === FieldType.BB_REFERENCE_SINGLE) &&
+ f.subtype === BBReferenceFieldSubType.USER
+ )
+ if (!userColumns.length) {
+ continue
+ }
+
+ tablesWithUserColumns.push({
+ tableId: table._id!,
+ columns: userColumns.map(c => c.name),
+ })
+ }
+
+ const docsToUpdate = []
+
+ for (const { tableId, columns } of tablesWithUserColumns) {
+ const docs = await db.allDocs(
+ docIds.getRowParams(tableId, null, { include_docs: true })
+ )
+ const rows = docs.rows.map(d => d.doc!)
+
+ for (const row of rows) {
+ let shouldUpdate = false
+ const updatedColumns = columns.reduce((newColumns, column) => {
+ if (row[column]) {
+ shouldUpdate = true
+ if (Array.isArray(row[column])) {
+ newColumns[column] = row[column]?.map(() => toUserId)
+ } else if (row[column]) {
+ newColumns[column] = toUserId
+ }
+ }
+ return newColumns
+ }, {})
+
+ if (shouldUpdate) {
+ docsToUpdate.push({
+ ...row,
+ ...updatedColumns,
+ })
+ }
+ }
+ }
+
+ await db.bulkDocs(docsToUpdate)
+ })
+}
+
+async function creationEvents(request: BBRequest, app: App) {
let creationFns: ((app: App) => Promise)[] = []
- const body = request.body
- if (body.useTemplate === "true") {
+ const { useTemplate, templateKey, file } = request.body
+ if (useTemplate === "true") {
// from template
- if (body.templateKey && body.templateKey !== "undefined") {
- creationFns.push(a => events.app.templateImported(a, body.templateKey))
+ if (templateKey && templateKey !== "undefined") {
+ creationFns.push(a => events.app.templateImported(a, templateKey))
}
// from file
- else if (request.files?.templateFile) {
+ else if (request.files?.fileToImport) {
creationFns.push(a => events.app.fileImported(a))
}
// from server file path
- else if (request.body.file) {
+ else if (file) {
// explicitly pass in the newly created app id
creationFns.push(a => events.app.duplicated(a, app.appId))
}
@@ -399,16 +460,14 @@ async function creationEvents(request: any, app: App) {
}
}
- if (!request.duplicate) {
- creationFns.push(a => events.app.created(a))
- }
+ creationFns.push(a => events.app.created(a))
for (let fn of creationFns) {
await fn(app)
}
}
-async function appPostCreate(ctx: UserCtx, app: App) {
+async function appPostCreate(ctx: UserCtx, app: App) {
const tenantId = tenancy.getTenantId()
await migrations.backPopulateMigrations({
type: MigrationType.APP,
@@ -419,7 +478,7 @@ async function appPostCreate(ctx: UserCtx, app: App) {
await creationEvents(ctx.request, app)
// app import, template creation and duplication
- if (ctx.request.body.useTemplate === "true") {
+ if (ctx.request.body.useTemplate) {
const { rows } = await getUniqueRows([app.appId])
const rowCount = rows ? rows.length : 0
if (rowCount) {
diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts
index f26b4bae69..1047711983 100644
--- a/packages/server/src/api/controllers/role.ts
+++ b/packages/server/src/api/controllers/role.ts
@@ -18,9 +18,11 @@ import {
UserCtx,
UserMetadata,
DocumentType,
+ PermissionLevel,
} from "@budibase/types"
import { RoleColor, sdk as sharedSdk, helpers } from "@budibase/shared-core"
import sdk from "../../sdk"
+import { builderSocket } from "../../websockets"
const UpdateRolesOptions = {
CREATED: "created",
@@ -34,11 +36,11 @@ async function removeRoleFromOthers(roleId: string) {
let changed = false
if (Array.isArray(role.inherits)) {
const newInherits = role.inherits.filter(
- id => !roles.compareRoleIds(id, roleId)
+ id => !roles.roleIDsAreEqual(id, roleId)
)
changed = role.inherits.length !== newInherits.length
role.inherits = newInherits
- } else if (role.inherits && roles.compareRoleIds(role.inherits, roleId)) {
+ } else if (role.inherits && roles.roleIDsAreEqual(role.inherits, roleId)) {
role.inherits = roles.BUILTIN_ROLE_IDS.PUBLIC
changed = true
}
@@ -124,6 +126,17 @@ export async function save(ctx: UserCtx) {
ctx.throw(400, "Cannot change custom role name")
}
+ // custom roles should always inherit basic - if they don't inherit anything else
+ if (!inherits && roles.validInherits(allRoles, dbRole?.inherits)) {
+ inherits = dbRole?.inherits
+ } else if (!roles.validInherits(allRoles, inherits)) {
+ inherits = [roles.BUILTIN_ROLE_IDS.BASIC]
+ }
+ // assume write permission level for newly created roles
+ if (isCreate && !permissionId) {
+ permissionId = PermissionLevel.WRITE
+ }
+
const role = new roles.Role(_id, name, permissionId, {
displayName: uiMetadata?.displayName || name,
description: uiMetadata?.description || "Custom role",
@@ -177,6 +190,7 @@ export async function save(ctx: UserCtx) {
},
})
}
+ builderSocket?.emitRoleUpdate(ctx, role)
}
export async function destroy(ctx: UserCtx) {
@@ -216,6 +230,7 @@ export async function destroy(ctx: UserCtx) {
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
ctx.status = 200
+ builderSocket?.emitRoleDeletion(ctx, role)
}
export async function accessible(ctx: UserCtx) {
@@ -223,35 +238,23 @@ export async function accessible(ctx: UserCtx) {
if (!roleId) {
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
}
+ // If a custom role is provided in the header, filter out higher level roles
+ const roleHeader = ctx.header[Header.PREVIEW_ROLE]
+ if (Array.isArray(roleHeader)) {
+ ctx.throw(400, `Too many roles specified in ${Header.PREVIEW_ROLE} header`)
+ }
+ const isBuilder = ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)
let roleIds: string[] = []
- if (ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)) {
+ if (!roleHeader && isBuilder) {
const appId = context.getAppId()
if (appId) {
roleIds = await roles.getAllRoleIds(appId)
}
+ } else if (isBuilder && roleHeader) {
+ roleIds = await roles.getUserRoleIdHierarchy(roleHeader)
} else {
roleIds = await roles.getUserRoleIdHierarchy(roleId!)
}
- // If a custom role is provided in the header, filter out higher level roles
- const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string
- if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) {
- const role = await roles.getRole(roleHeader)
- const inherits = role?.inherits
- const orderedRoles = roleIds.reverse()
- let filteredRoles = [roleHeader]
- for (let role of orderedRoles) {
- filteredRoles = [role, ...filteredRoles]
- if (
- (Array.isArray(inherits) && inherits.includes(role)) ||
- role === inherits
- ) {
- break
- }
- }
- filteredRoles.pop()
- roleIds = [roleHeader, ...filteredRoles]
- }
-
ctx.body = roleIds.map(roleId => roles.getExternalRoleID(roleId))
}
diff --git a/packages/server/src/api/routes/tests/application.spec.ts b/packages/server/src/api/routes/tests/application.spec.ts
index fe8250bde5..47b6776610 100644
--- a/packages/server/src/api/routes/tests/application.spec.ts
+++ b/packages/server/src/api/routes/tests/application.spec.ts
@@ -21,6 +21,7 @@ import tk from "timekeeper"
import * as uuid from "uuid"
import { structures } from "@budibase/backend-core/tests"
import nock from "nock"
+import path from "path"
describe("/applications", () => {
let config = setup.getConfig()
@@ -137,11 +138,17 @@ describe("/applications", () => {
})
it("creates app from template", async () => {
+ nock("https://prod-budi-templates.s3-eu-west-1.amazonaws.com")
+ .get(`/templates/app/agency-client-portal.tar.gz`)
+ .replyWithFile(
+ 200,
+ path.resolve(__dirname, "data", "agency-client-portal.tar.gz")
+ )
+
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
- templateKey: "test",
- templateString: "{}",
+ templateKey: "app/agency-client-portal",
})
expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1)
@@ -152,7 +159,7 @@ describe("/applications", () => {
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
- templateFile: "src/api/routes/tests/data/export.txt",
+ fileToImport: "src/api/routes/tests/data/export.txt",
})
expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1)
@@ -172,7 +179,7 @@ describe("/applications", () => {
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
- templateFile: "src/api/routes/tests/data/old-app.txt",
+ fileToImport: "src/api/routes/tests/data/old-app.txt",
})
expect(app._id).toBeDefined()
expect(app.navigation).toBeDefined()
diff --git a/packages/server/src/api/routes/tests/role.spec.ts b/packages/server/src/api/routes/tests/role.spec.ts
index 5668295342..adb83ca793 100644
--- a/packages/server/src/api/routes/tests/role.spec.ts
+++ b/packages/server/src/api/routes/tests/role.spec.ts
@@ -38,6 +38,26 @@ describe("/roles", () => {
_id: dbCore.prefixRoleID(res._id!),
})
})
+
+ it("handle a role with invalid inherits", async () => {
+ const role = basicRole()
+ role.inherits = ["not_real", "some_other_not_real"]
+
+ const res = await config.api.roles.save(role, {
+ status: 200,
+ })
+ expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
+ })
+
+ it("handle a role with no inherits", async () => {
+ const role = basicRole()
+ role.inherits = []
+
+ const res = await config.api.roles.save(role, {
+ status: 200,
+ })
+ expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
+ })
})
describe("update", () => {
@@ -149,6 +169,17 @@ describe("/roles", () => {
{ status: 400, body: { message: LOOP_ERROR } }
)
})
+
+ it("handle updating a role, without its inherits", async () => {
+ const res = await config.api.roles.save({
+ ...basicRole(),
+ inherits: [BUILTIN_ROLE_IDS.ADMIN],
+ })
+ // remove the roles so that it will default back to DB roles, then save again
+ delete res.inherits
+ const updatedRes = await config.api.roles.save(res)
+ expect(updatedRes.inherits).toEqual([BUILTIN_ROLE_IDS.ADMIN])
+ })
})
describe("fetch", () => {
@@ -298,6 +329,23 @@ describe("/roles", () => {
}
)
})
+
+ it("should fetch preview role correctly even without basic specified", async () => {
+ const role = await config.api.roles.save(basicRole())
+ // have to forcefully delete the inherits from DB - technically can't
+ // happen anymore - but good test case
+ await dbCore.getDB(config.appId!).put({
+ ...role,
+ _id: dbCore.prefixRoleID(role._id!),
+ inherits: [],
+ })
+ await config.withHeaders({ "x-budibase-role": role.name }, async () => {
+ const res = await config.api.roles.accessible({
+ status: 200,
+ })
+ expect(res).toEqual([role.name])
+ })
+ })
})
describe("accessible - multi-inheritance", () => {
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index fa288081c7..66e931d0ab 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -2087,7 +2087,6 @@ describe.each([
)
tableId = table._id!
- // @ts-ignore - until AI implemented
const rowValues: Record = {
[FieldType.STRING]: generator.guid(),
[FieldType.LONGFORM]: generator.paragraph(),
diff --git a/packages/server/src/api/routes/tests/screen.spec.ts b/packages/server/src/api/routes/tests/screen.spec.ts
index 5dfe3d2a44..894710ca27 100644
--- a/packages/server/src/api/routes/tests/screen.spec.ts
+++ b/packages/server/src/api/routes/tests/screen.spec.ts
@@ -86,7 +86,6 @@ describe("/screens", () => {
status: 200,
}
)
- // basic and role1 screen
expect(res.screens.length).toEqual(screenIds.length)
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
})
@@ -107,6 +106,25 @@ describe("/screens", () => {
screen2._id!,
])
})
+
+ it("should be able to fetch basic and screen 1 with role1 in role header", async () => {
+ await config.withHeaders(
+ {
+ "x-budibase-role": role1._id!,
+ },
+ async () => {
+ const res = await config.api.application.getDefinition(
+ config.prodAppId!,
+ {
+ status: 200,
+ }
+ )
+ const screenIds = [screen._id!, screen1._id!]
+ expect(res.screens.length).toEqual(screenIds.length)
+ expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
+ }
+ )
+ })
})
describe("save", () => {
diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts
index d9dbd96b16..02a1a2d060 100644
--- a/packages/server/src/api/routes/utils/validators.ts
+++ b/packages/server/src/api/routes/utils/validators.ts
@@ -355,9 +355,7 @@ export function applicationValidator(opts = { isCreate: true }) {
_id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING,
url: OPTIONAL_STRING,
- template: Joi.object({
- templateString: OPTIONAL_STRING,
- }),
+ template: Joi.object({}),
}
const appNameValidator = Joi.string()
@@ -390,9 +388,7 @@ export function applicationValidator(opts = { isCreate: true }) {
_rev: OPTIONAL_STRING,
name: appNameValidator,
url: OPTIONAL_STRING,
- template: Joi.object({
- templateString: OPTIONAL_STRING,
- }).unknown(true),
+ template: Joi.object({}).unknown(true),
snippets: snippetValidator,
}).unknown(true)
)
diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts
index e921e569c9..a8b69fa7d7 100644
--- a/packages/server/src/automations/actions.ts
+++ b/packages/server/src/automations/actions.ts
@@ -18,6 +18,7 @@ import * as loop from "./steps/loop"
import * as collect from "./steps/collect"
import * as branch from "./steps/branch"
import * as triggerAutomationRun from "./steps/triggerAutomationRun"
+import * as openai from "./steps/openai"
import env from "../environment"
import {
PluginType,
@@ -50,6 +51,7 @@ const ACTION_IMPLS: ActionImplType = {
QUERY_ROWS: queryRow.run,
COLLECT: collect.run,
TRIGGER_AUTOMATION_RUN: triggerAutomationRun.run,
+ OPENAI: openai.run,
// these used to be lowercase step IDs, maintain for backwards compat
discord: discord.run,
slack: slack.run,
@@ -89,21 +91,25 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
// ran at all
if (env.SELF_HOSTED) {
const bash = require("./steps/bash")
- const openai = require("./steps/openai")
// @ts-ignore
ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore
BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
- // @ts-ignore
- ACTION_IMPLS.OPENAI = openai.run
- BUILTIN_ACTION_DEFINITIONS.OPENAI = openai.definition
}
export async function getActionDefinitions() {
if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
}
+ if (
+ env.SELF_HOSTED ||
+ (await features.flags.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
+ (await features.flags.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
+ ) {
+ BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
+ }
+
const actionDefinitions = BUILTIN_ACTION_DEFINITIONS
if (env.SELF_HOSTED) {
const plugins = await sdk.plugins.fetch(PluginType.AUTOMATION)
diff --git a/packages/server/src/middleware/currentapp.ts b/packages/server/src/middleware/currentapp.ts
index d616377298..a8ef8bb251 100644
--- a/packages/server/src/middleware/currentapp.ts
+++ b/packages/server/src/middleware/currentapp.ts
@@ -56,22 +56,9 @@ export default async (ctx: UserCtx, next: any) => {
ctx.request &&
(ctx.request.headers[constants.Header.PREVIEW_ROLE] as string)
if (isBuilder && isDevApp && roleHeader) {
- // Ensure the role is valid by ensuring a definition exists
- try {
- if (roleHeader) {
- const role = await roles.getRole(roleHeader)
- if (role) {
- roleId = roleHeader
-
- // Delete admin and builder flags so that the specified role is honoured
- ctx.user = users.removePortalUserPermissions(
- ctx.user
- ) as ContextUser
- }
- }
- } catch (error) {
- // Swallow error and do nothing
- }
+ roleId = roleHeader
+ // Delete admin and builder flags so that the specified role is honoured
+ ctx.user = users.removePortalUserPermissions(ctx.user) as ContextUser
}
}
diff --git a/packages/server/src/tests/utilities/api/application.ts b/packages/server/src/tests/utilities/api/application.ts
index 516af5c973..9dabc8cfe8 100644
--- a/packages/server/src/tests/utilities/api/application.ts
+++ b/packages/server/src/tests/utilities/api/application.ts
@@ -17,8 +17,8 @@ export class ApplicationAPI extends TestAPI {
app: CreateAppRequest,
expectations?: Expectations
): Promise => {
- const files = app.templateFile ? { templateFile: app.templateFile } : {}
- delete app.templateFile
+ const files = app.fileToImport ? { fileToImport: app.fileToImport } : {}
+ delete app.fileToImport
return await this._post("/api/applications", {
fields: app,
files,
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index 1198dc514d..b63d6d1a78 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -8,31 +8,31 @@ import {
} from "../../automations"
import {
AIOperationEnum,
+ AutoFieldSubType,
Automation,
AutomationActionStepId,
+ AutomationEventType,
AutomationResults,
AutomationStatus,
AutomationStep,
AutomationStepType,
AutomationTrigger,
AutomationTriggerStepId,
+ BBReferenceFieldSubType,
+ CreateViewRequest,
Datasource,
+ FieldSchema,
FieldType,
+ INTERNAL_TABLE_SOURCE_ID,
+ JsonFieldSubType,
+ LoopStepType,
+ Query,
+ Role,
SourceName,
Table,
- INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
- Query,
Webhook,
WebhookActionType,
- AutomationEventType,
- LoopStepType,
- FieldSchema,
- BBReferenceFieldSubType,
- JsonFieldSubType,
- AutoFieldSubType,
- Role,
- CreateViewRequest,
} from "@budibase/types"
import { LoopInput } from "../../definitions/automations"
import { merge } from "lodash"
@@ -439,7 +439,7 @@ export function updateRowAutomationWithFilters(
appId: string,
tableId: string
): Automation {
- const automation: Automation = {
+ return {
name: "updateRowWithFilters",
type: "automation",
appId,
@@ -472,7 +472,6 @@ export function updateRowAutomationWithFilters(
},
},
}
- return automation
}
export function basicAutomationResults(
@@ -606,7 +605,6 @@ export function fullSchemaWithoutLinks({
}): {
[type in Exclude]: FieldSchema & { type: type }
} {
- // @ts-ignore - until AI implemented
return {
[FieldType.STRING]: {
name: "string",
diff --git a/packages/server/src/utilities/index.ts b/packages/server/src/utilities/index.ts
index ce6f2345ca..129137a72e 100644
--- a/packages/server/src/utilities/index.ts
+++ b/packages/server/src/utilities/index.ts
@@ -2,9 +2,6 @@ import env from "../environment"
import { context } from "@budibase/backend-core"
import { generateMetadataID } from "../db/utils"
import { Document } from "@budibase/types"
-import stream from "stream"
-
-const Readable = stream.Readable
export function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
@@ -98,15 +95,6 @@ export function escapeDangerousCharacters(string: string) {
.replace(/[\t]/g, "\\t")
}
-export function stringToReadStream(string: string) {
- return new Readable({
- read() {
- this.push(string)
- this.push(null)
- },
- })
-}
-
export function formatBytes(bytes: string) {
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
const byteIncrements = 1024
diff --git a/packages/server/src/websockets/builder.ts b/packages/server/src/websockets/builder.ts
index 702a941095..cf92d68ef3 100644
--- a/packages/server/src/websockets/builder.ts
+++ b/packages/server/src/websockets/builder.ts
@@ -11,6 +11,7 @@ import {
Screen,
App,
Automation,
+ Role,
} from "@budibase/types"
import { gridSocket } from "./index"
import { clearLock, updateLock } from "../utilities/redis"
@@ -100,6 +101,20 @@ export default class BuilderSocket extends BaseSocket {
})
}
+ emitRoleUpdate(ctx: any, role: Role) {
+ this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.RoleChange, {
+ id: role._id,
+ role,
+ })
+ }
+
+ emitRoleDeletion(ctx: any, role: Role) {
+ this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.RoleChange, {
+ id: role._id,
+ role: null,
+ })
+ }
+
emitTableUpdate(ctx: any, table: Table, options?: EmitOptions) {
if (table.sourceId == null || table.sourceId === "") {
throw new Error("Table sourceId is not set")
diff --git a/packages/shared-core/src/constants/index.ts b/packages/shared-core/src/constants/index.ts
index 6c49625937..11bb79c8d3 100644
--- a/packages/shared-core/src/constants/index.ts
+++ b/packages/shared-core/src/constants/index.ts
@@ -97,6 +97,7 @@ export enum BuilderSocketEvent {
SelectResource = "SelectResource",
AppPublishChange = "AppPublishChange",
AutomationChange = "AutomationChange",
+ RoleChange = "RoleChange",
}
export const SocketSessionTTL = 60
diff --git a/packages/types/src/api/web/application.ts b/packages/types/src/api/web/application.ts
index bb4d8c7f72..57422ceabc 100644
--- a/packages/types/src/api/web/application.ts
+++ b/packages/types/src/api/web/application.ts
@@ -7,10 +7,8 @@ export interface CreateAppRequest {
useTemplate?: string
templateName?: string
templateKey?: string
- templateFile?: string
- includeSampleData?: boolean
+ fileToImport?: string
encryptionPassword?: string
- templateString?: string
file?: { path: string }
}
diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts
index 599256694d..b8a19b7b45 100644
--- a/packages/types/src/documents/app/automation/schema.ts
+++ b/packages/types/src/documents/app/automation/schema.ts
@@ -126,16 +126,16 @@ export type ActionImplementations = {
n8nStepInputs,
ExternalAppStepOutputs
>
+ [AutomationActionStepId.OPENAI]: ActionImplementation<
+ OpenAIStepInputs,
+ OpenAIStepOutputs
+ >
} & (T extends "self"
? {
[AutomationActionStepId.EXECUTE_BASH]: ActionImplementation<
BashStepInputs,
BashStepOutputs
>
- [AutomationActionStepId.OPENAI]: ActionImplementation<
- OpenAIStepInputs,
- OpenAIStepOutputs
- >
}
: {})
diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts
index e6e80ff3a5..750b02088c 100644
--- a/packages/worker/src/api/controllers/global/configs.ts
+++ b/packages/worker/src/api/controllers/global/configs.ts
@@ -44,9 +44,7 @@ const getEventFns = async (config: Config, existing?: Config) => {
fns.push(events.email.SMTPCreated)
} else if (isAIConfig(config)) {
fns.push(() => events.ai.AIConfigCreated)
- fns.push(() =>
- pro.quotas.updateCustomAIConfigCount(Object.keys(config.config).length)
- )
+ fns.push(() => pro.quotas.addCustomAIConfig())
} else if (isGoogleConfig(config)) {
fns.push(() => events.auth.SSOCreated(ConfigType.GOOGLE))
if (config.config.activated) {
@@ -85,9 +83,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
fns.push(events.email.SMTPUpdated)
} else if (isAIConfig(config)) {
fns.push(() => events.ai.AIConfigUpdated)
- fns.push(() =>
- pro.quotas.updateCustomAIConfigCount(Object.keys(config.config).length)
- )
} else if (isGoogleConfig(config)) {
fns.push(() => events.auth.SSOUpdated(ConfigType.GOOGLE))
if (!existing.config.activated && config.config.activated) {
@@ -253,7 +248,7 @@ export async function save(ctx: UserCtx) {
if (existingConfig) {
await verifyAIConfig(config, existingConfig)
}
- await pro.quotas.updateCustomAIConfigCount(Object.keys(config).length)
+ await pro.quotas.addCustomAIConfig()
break
}
} catch (err: any) {
@@ -342,29 +337,43 @@ export async function find(ctx: UserCtx) {
let scopedConfig = await configs.getConfig(type)
if (scopedConfig) {
- if (type === ConfigType.OIDC_LOGOS) {
- enrichOIDCLogos(scopedConfig)
- }
-
- if (type === ConfigType.AI) {
- await pro.sdk.ai.enrichAIConfig(scopedConfig)
- // Strip out the API Keys from the response so they don't show in the UI
- for (const key in scopedConfig.config) {
- if (scopedConfig.config[key].apiKey) {
- scopedConfig.config[key].apiKey = PASSWORD_REPLACEMENT
- }
- }
- }
- ctx.body = scopedConfig
+ await handleConfigType(type, scopedConfig)
+ } else if (type === ConfigType.AI) {
+ scopedConfig = { config: {} } as AIConfig
+ await handleAIConfig(scopedConfig)
} else {
- // don't throw an error, there simply is nothing to return
+ // If no config found and not AI type, just return an empty body
ctx.body = {}
+ return
}
+
+ ctx.body = scopedConfig
} catch (err: any) {
ctx.throw(err?.status || 400, err)
}
}
+async function handleConfigType(type: ConfigType, config: Config) {
+ if (type === ConfigType.OIDC_LOGOS) {
+ enrichOIDCLogos(config)
+ } else if (type === ConfigType.AI) {
+ await handleAIConfig(config)
+ }
+}
+
+async function handleAIConfig(config: AIConfig) {
+ await pro.sdk.ai.enrichAIConfig(config)
+ stripApiKeys(config)
+}
+
+function stripApiKeys(config: AIConfig) {
+ for (const key in config?.config) {
+ if (config.config[key].apiKey) {
+ config.config[key].apiKey = PASSWORD_REPLACEMENT
+ }
+ }
+}
+
export async function publicOidc(ctx: Ctx) {
try {
// Find the config with the most granular scope based on context
@@ -508,6 +517,9 @@ export async function destroy(ctx: UserCtx) {
try {
await db.remove(id, rev)
await cache.destroy(cache.CacheKey.CHECKLIST)
+ if (id === configs.generateConfigID(ConfigType.AI)) {
+ await pro.quotas.removeCustomAIConfig()
+ }
ctx.body = { message: "Config deleted successfully" }
} catch (err: any) {
ctx.throw(err.status, err)
diff --git a/packages/worker/src/api/controllers/global/tests/configs.spec.ts b/packages/worker/src/api/controllers/global/tests/configs.spec.ts
index 9091f29247..857f499dcc 100644
--- a/packages/worker/src/api/controllers/global/tests/configs.spec.ts
+++ b/packages/worker/src/api/controllers/global/tests/configs.spec.ts
@@ -13,10 +13,6 @@ describe("Global configs controller", () => {
await config.afterAll()
})
- afterEach(() => {
- jest.resetAllMocks()
- })
-
it("Should strip secrets when pulling AI config", async () => {
const data = structures.configs.ai()
await config.api.configs.saveConfig(data)
diff --git a/yarn.lock b/yarn.lock
index 1198e98ad6..5dfef16e0e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2343,6 +2343,18 @@
enabled "2.0.x"
kuler "^2.0.0"
+"@dagrejs/dagre@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.4.tgz#66f9c0e2b558308f2c268f60e2c28f22ee17e339"
+ integrity sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==
+ dependencies:
+ "@dagrejs/graphlib" "2.2.4"
+
+"@dagrejs/graphlib@2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4"
+ integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==
+
"@datadog/native-appsec@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.0.0.tgz#a380174dd49aef2d9bb613a0ec8ead6dc7822095"
@@ -5093,6 +5105,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.1.tgz#d333fa41909f691c8750b5c15ad9ba029df2248e"
integrity sha512-rX6Iasu9BsFMVgEN0vGRPm9dmSxva+IK/uqQAa9HM0lliwqUiFrJxrFXHHpiAgNuux/U4srEJwbSpGzfF+CegQ==
+"@svelte-put/shortcut@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@svelte-put/shortcut/-/shortcut-3.1.1.tgz#aba4d7407024d5cff38727e12925c8f81e877079"
+ integrity sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==
+
"@sveltejs/vite-plugin-svelte@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz#412a735de489ca731d0c780c2b410f45dd95b392"
@@ -5451,6 +5468,45 @@
dependencies:
"@types/node" "*"
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-drag@^3.0.7":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
+ integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-interpolate@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-selection@*", "@types/d3-selection@^3.0.10":
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
+ integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
+
+"@types/d3-transition@^3.0.8":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f"
+ integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-zoom@^3.0.8":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
+ integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+ dependencies:
+ "@types/d3-interpolate" "*"
+ "@types/d3-selection" "*"
+
"@types/debug@*":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@@ -6578,6 +6634,28 @@
loupe "^3.1.1"
tinyrainbow "^1.2.0"
+"@xyflow/svelte@^0.1.18":
+ version "0.1.18"
+ resolved "https://registry.yarnpkg.com/@xyflow/svelte/-/svelte-0.1.18.tgz#ba2f9f72adc64ff6f71a5ad03cf759af8d7c9748"
+ integrity sha512-P2td3XcvMk36pnhyRUAXtmwfd7sv1KAHVF29YZUNndYlgxG98vwj1UoyyuXwCHIiyu82GgowaTppHCNPXsvNSg==
+ dependencies:
+ "@svelte-put/shortcut" "^3.1.0"
+ "@xyflow/system" "0.0.41"
+ classcat "^5.0.4"
+
+"@xyflow/system@0.0.41":
+ version "0.0.41"
+ resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.41.tgz#6c314b2bbca594aec4d7cdb56efb003be6727d21"
+ integrity sha512-XAjs8AUA0YMfYD91cT6pLGALwbsPS64s2WBHyULqL1m0gTqXqaUSLK1P7qA/Q8HecN0RFbqlM2tPO8bmZXP0YQ==
+ dependencies:
+ "@types/d3-drag" "^3.0.7"
+ "@types/d3-selection" "^3.0.10"
+ "@types/d3-transition" "^3.0.8"
+ "@types/d3-zoom" "^3.0.8"
+ d3-drag "^3.0.0"
+ d3-selection "^3.0.0"
+ d3-zoom "^3.0.0"
+
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
@@ -8244,6 +8322,11 @@ cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.2:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==
+classcat@^5.0.4:
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
+ integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==
+
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@@ -9159,6 +9242,68 @@ curlconverter@3.21.0:
string.prototype.startswith "^1.0.0"
yamljs "^0.3.0"
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+"d3-dispatch@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-ease@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-interpolate@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+"d3-timer@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
dargs@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"